mirror of https://github.com/jumpserver/jumpserver
[Update] 修改登录工单
parent
08775551c2
commit
f9e41d71dc
|
@ -4,6 +4,7 @@ from rest_framework.generics import UpdateAPIView
|
|||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from common.utils import get_logger, get_object_or_none
|
||||
from common.permissions import IsOrgAdmin
|
||||
|
@ -11,7 +12,7 @@ from ..models import LoginConfirmSetting
|
|||
from ..serializers import LoginConfirmSettingSerializer
|
||||
from .. import errors
|
||||
|
||||
__all__ = ['LoginConfirmSettingUpdateApi', 'UserTicketAcceptAuthApi']
|
||||
__all__ = ['LoginConfirmSettingUpdateApi', 'LoginConfirmTicketStatusApi']
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
|
@ -30,10 +31,10 @@ class LoginConfirmSettingUpdateApi(UpdateAPIView):
|
|||
return s
|
||||
|
||||
|
||||
class UserTicketAcceptAuthApi(APIView):
|
||||
class LoginConfirmTicketStatusApi(APIView):
|
||||
permission_classes = ()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
def get_ticket(self):
|
||||
from tickets.models import LoginConfirmTicket
|
||||
ticket_id = self.request.session.get("auth_ticket_id")
|
||||
logger.debug('Login confirm ticket id: {}'.format(ticket_id))
|
||||
|
@ -41,31 +42,32 @@ class UserTicketAcceptAuthApi(APIView):
|
|||
ticket = None
|
||||
else:
|
||||
ticket = get_object_or_none(LoginConfirmTicket, pk=ticket_id)
|
||||
return ticket
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
ticket_id = self.request.session.get("auth_ticket_id")
|
||||
ticket = self.get_ticket()
|
||||
try:
|
||||
if not ticket:
|
||||
raise errors.LoginConfirmTicketNotFound(ticket_id)
|
||||
if ticket.action == LoginConfirmTicket.ACTION_APPROVE:
|
||||
raise errors.LoginConfirmOtherError(ticket_id, _("not found"))
|
||||
if ticket.status == 'open':
|
||||
raise errors.LoginConfirmWaitError(ticket_id)
|
||||
elif ticket.action == ticket.ACTION_APPROVE:
|
||||
self.request.session["auth_confirm"] = "1"
|
||||
return Response({"msg": "ok"})
|
||||
elif ticket.action == LoginConfirmTicket.ACTION_REJECT:
|
||||
raise errors.LoginConfirmRejectedError(ticket_id)
|
||||
elif ticket.action == ticket.ACTION_REJECT:
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket_id, ticket.get_action_display()
|
||||
)
|
||||
else:
|
||||
raise errors.LoginConfirmWaitError(ticket_id)
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket_id, ticket.get_status_display()
|
||||
)
|
||||
except errors.AuthFailedError as e:
|
||||
data = e.as_data()
|
||||
return Response(data, status=400)
|
||||
return Response(e.as_data(), status=400)
|
||||
|
||||
|
||||
class UserTicketCancelAuthApi(APIView):
|
||||
permission_classes = ()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
from tickets.models import LoginConfirmTicket
|
||||
ticket_id = self.request.session.get("auth_ticket_id")
|
||||
logger.debug('Login confirm ticket id: {}'.format(ticket_id))
|
||||
if not ticket_id:
|
||||
ticket = None
|
||||
else:
|
||||
ticket = get_object_or_none(LoginConfirmTicket, pk=ticket_id)
|
||||
if not ticket:
|
||||
ticket.status = "close"
|
||||
def delete(self, request, *args, **kwargs):
|
||||
ticket = self.get_ticket()
|
||||
if ticket:
|
||||
ticket.perform_status('closed', request.user)
|
||||
return Response('', status=200)
|
||||
|
|
|
@ -48,8 +48,7 @@ mfa_failed_msg = _("MFA code invalid, or ntp sync server time")
|
|||
mfa_required_msg = _("MFA required")
|
||||
login_confirm_required_msg = _("Login confirm required")
|
||||
login_confirm_wait_msg = _("Wait login confirm ticket for accept")
|
||||
login_confirm_rejected_msg = _("Login confirm ticket was rejected")
|
||||
login_confirm_ticket_not_found_msg = _("Ticket not found")
|
||||
login_confirm_error_msg = _("Login confirm ticket was {}")
|
||||
|
||||
|
||||
class AuthFailedNeedLogMixin:
|
||||
|
@ -174,11 +173,9 @@ class LoginConfirmWaitError(LoginConfirmError):
|
|||
error = 'login_confirm_wait'
|
||||
|
||||
|
||||
class LoginConfirmRejectedError(LoginConfirmError):
|
||||
msg = login_confirm_rejected_msg
|
||||
error = 'login_confirm_rejected'
|
||||
class LoginConfirmOtherError(LoginConfirmError):
|
||||
error = 'login_confirm_error'
|
||||
|
||||
|
||||
class LoginConfirmTicketNotFound(LoginConfirmError):
|
||||
msg = login_confirm_ticket_not_found_msg
|
||||
error = 'login_confirm_ticket_not_found'
|
||||
def __init__(self, ticket_id, status):
|
||||
msg = login_confirm_error_msg.format(status)
|
||||
super().__init__(ticket_id=ticket_id, msg=msg)
|
||||
|
|
|
@ -106,7 +106,7 @@ class AuthMixin:
|
|||
if ticket.status == "accepted":
|
||||
return
|
||||
elif ticket.status == "rejected":
|
||||
raise errors.LoginConfirmRejectedError(ticket.id)
|
||||
raise errors.LoginConfirmOtherError(ticket.id)
|
||||
else:
|
||||
raise errors.LoginConfirmWaitError(ticket.id)
|
||||
|
||||
|
|
|
@ -62,12 +62,9 @@ class LoginConfirmSetting(CommonModelMixin):
|
|||
remote_addr = '127.0.0.1'
|
||||
body = ''
|
||||
reviewer = self.reviewers.all()
|
||||
reviewer_names = ','.join([u.name for u in reviewer])
|
||||
ticket = LoginConfirmTicket.objects.create(
|
||||
user=self.user, user_display=str(self.user),
|
||||
title=title, body=body,
|
||||
user=self.user, title=title, body=body,
|
||||
city=city, ip=remote_addr,
|
||||
assignees_display=reviewer_names,
|
||||
type=LoginConfirmTicket.TYPE_LOGIN_CONFIRM,
|
||||
)
|
||||
ticket.assignees.set(reviewer)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.core.cache import cache
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.utils import get_object_or_none
|
||||
from users.models import User
|
||||
from users.serializers import UserProfileSerializer
|
||||
from .models import AccessKey, LoginConfirmSetting
|
||||
|
||||
|
||||
|
@ -26,14 +26,15 @@ class OtpVerifySerializer(serializers.Serializer):
|
|||
|
||||
|
||||
class BearerTokenSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(allow_null=True, required=False)
|
||||
username = serializers.CharField(allow_null=True, required=False, write_only=True)
|
||||
password = serializers.CharField(write_only=True, allow_null=True,
|
||||
required=False)
|
||||
required=False, allow_blank=True)
|
||||
public_key = serializers.CharField(write_only=True, allow_null=True,
|
||||
required=False)
|
||||
allow_blank=True, required=False)
|
||||
token = serializers.CharField(read_only=True)
|
||||
keyword = serializers.SerializerMethodField()
|
||||
date_expired = serializers.DateTimeField(read_only=True)
|
||||
user = UserProfileSerializer(read_only=True)
|
||||
|
||||
@staticmethod
|
||||
def get_keyword(obj):
|
||||
|
@ -52,9 +53,9 @@ class BearerTokenSerializer(serializers.Serializer):
|
|||
)
|
||||
token, date_expired = user.create_bearer_token(request)
|
||||
instance = {
|
||||
"username": user.username,
|
||||
"token": token,
|
||||
"date_expired": date_expired,
|
||||
"user": user
|
||||
}
|
||||
return instance
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ var infoMsgRef = $(".info-messages");
|
|||
var timestamp = '{{ timestamp }}';
|
||||
var progressBarRef = $(".progress-bar");
|
||||
var interval, checkInterval;
|
||||
var url = "{% url 'api-auth:user-order-auth' %}";
|
||||
var url = "{% url 'api-auth:login-confirm-ticket-status' %}";
|
||||
var successUrl = "{% url 'authentication:login-guard' %}";
|
||||
|
||||
function doRequestAuth() {
|
||||
|
|
|
@ -18,7 +18,7 @@ urlpatterns = [
|
|||
path('connection-token/',
|
||||
api.UserConnectionTokenApi.as_view(), name='connection-token'),
|
||||
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
|
||||
path('order/auth/', api.UserTicketAcceptAuthApi.as_view(), name='user-order-auth'),
|
||||
path('login-confirm-ticket/status/', api.LoginConfirmTicketStatusApi.as_view(), name='login-confirm-ticket-status'),
|
||||
path('login-confirm-settings/<uuid:user_id>/', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update')
|
||||
]
|
||||
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -1317,3 +1317,7 @@ function initDateRangePicker(selector, options) {
|
|||
options = Object.assign(defaultOption, options);
|
||||
return $(selector).daterangepicker(options);
|
||||
}
|
||||
|
||||
function reloadPage() {
|
||||
window.location.reload();
|
||||
}
|
||||
|
|
|
@ -127,7 +127,7 @@
|
|||
<i class="fa fa-check-square-o" style="width: 14px"></i> <span class="nav-label">{% trans 'Tickets' %}</span><span class="fa arrow"></span>
|
||||
</a>
|
||||
<ul class="nav nav-second-level">
|
||||
<li id="login-confirm-orders"><a href="{% url 'tickets:login-confirm-ticket-list' %}">{% trans 'Login confirm' %}</a></li>
|
||||
<li id="login-confirm-tickets"><a href="{% url 'tickets:login-confirm-ticket-list' %}">{% trans 'Login confirm' %}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,21 +1,38 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import viewsets, generics
|
||||
|
||||
from .. import serializers, models
|
||||
from rest_framework import viewsets
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from common.utils import lazyproperty
|
||||
from .. import serializers, models, mixins
|
||||
|
||||
|
||||
class TicketViewSet(viewsets.ModelViewSet):
|
||||
class TicketViewSet(mixins.TicketMixin, viewsets.ModelViewSet):
|
||||
serializer_class = serializers.TicketSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = models.Ticket.objects.all().none()
|
||||
return queryset
|
||||
queryset = models.Ticket.objects.all()
|
||||
|
||||
|
||||
class CommentViewSet(viewsets.ModelViewSet):
|
||||
class TicketCommentViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = serializers.CommentSerializer
|
||||
|
||||
def check_permissions(self, request):
|
||||
ticket = self.ticket
|
||||
if request.user == ticket.user or request.user in ticket.assignees.all():
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context['ticket'] = self.ticket
|
||||
return context
|
||||
|
||||
@lazyproperty
|
||||
def ticket(self):
|
||||
ticket_id = self.kwargs.get('ticket_id')
|
||||
ticket = get_object_or_404(models.Ticket, pk=ticket_id)
|
||||
return ticket
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = models.Comment.objects.none()
|
||||
queryset = self.ticket.comments.all()
|
||||
return queryset
|
||||
|
|
|
@ -1,39 +1,30 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from rest_framework import viewsets, generics
|
||||
from rest_framework.serializers import ValidationError
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from common.permissions import IsValidUser
|
||||
from common.mixins import CommonApiMixin
|
||||
from .. import serializers
|
||||
from .. import serializers, mixins
|
||||
from ..models import LoginConfirmTicket
|
||||
|
||||
|
||||
class LoginConfirmTicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
||||
class LoginConfirmTicketViewSet(CommonApiMixin, mixins.TicketMixin, viewsets.ModelViewSet):
|
||||
serializer_class = serializers.LoginConfirmTicketSerializer
|
||||
permission_classes = (IsValidUser,)
|
||||
filter_fields = ['status', 'title']
|
||||
queryset = LoginConfirmTicket.objects.all()
|
||||
filter_fields = ['status', 'title', 'action', 'ip']
|
||||
search_fields = ['user_display', 'title', 'ip', 'city']
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = LoginConfirmTicket.objects.all()\
|
||||
.filter(assignees=self.request.user)
|
||||
return queryset
|
||||
|
||||
|
||||
class LoginConfirmTicketsCreateActionApi(generics.CreateAPIView):
|
||||
permission_classes = (IsValidUser,)
|
||||
serializer_class = serializers.LoginConfirmTicketActionSerializer
|
||||
|
||||
def get_ticket(self):
|
||||
ticket_id = self.kwargs.get('pk')
|
||||
queryset = LoginConfirmTicket.objects.all()\
|
||||
.filter(assignees=self.request.user)
|
||||
ticket = get_object_or_404(queryset, id=ticket_id)
|
||||
return ticket
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
ticket = self.get_ticket()
|
||||
context['ticket'] = ticket
|
||||
return context
|
||||
# def check_update_permission(self, serializer):
|
||||
# data = serializer.validated_data
|
||||
# action = data.get("action")
|
||||
# user = self.request.user
|
||||
# instance = serializer.instance
|
||||
# if action and user not in instance.assignees.all():
|
||||
# error = {"action": "Only assignees can update"}
|
||||
# raise ValidationError(error)
|
||||
#
|
||||
# def perform_update(self, serializer):
|
||||
# self.check_update_permission(serializer)
|
||||
|
|
|
@ -3,3 +3,7 @@ from django.apps import AppConfig
|
|||
|
||||
class TicketsConfig(AppConfig):
|
||||
name = 'tickets'
|
||||
|
||||
def ready(self):
|
||||
from . import signals_handler
|
||||
return super().ready()
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.db.models import Q
|
||||
|
||||
|
||||
class TicketMixin:
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
assign = self.request.GET.get('assign', None)
|
||||
if assign is None:
|
||||
queryset = queryset.filter(
|
||||
Q(assignees=self.request.user) | Q(user=self.request.user)
|
||||
).distinct()
|
||||
elif assign in ['1']:
|
||||
queryset = queryset.filter(assignees=self.request.user)
|
||||
else:
|
||||
queryset = queryset.filter(user=self.request.user)
|
||||
return queryset
|
|
@ -31,16 +31,12 @@ class Ticket(CommonModelMixin):
|
|||
assignee_display = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Assignee display name"))
|
||||
assignees = models.ManyToManyField('users.User', related_name='%(class)s_assigned', verbose_name=_("Assignees"))
|
||||
assignees_display = models.CharField(max_length=128, verbose_name=_("Assignees display name"), blank=True)
|
||||
type = models.CharField(max_length=16, default='general', verbose_name=_("Type"))
|
||||
type = models.CharField(max_length=16, choices=TYPE_CHOICES, default=TYPE_GENERAL, verbose_name=_("Type"))
|
||||
status = models.CharField(choices=STATUS_CHOICES, max_length=16, default='open')
|
||||
|
||||
def __str__(self):
|
||||
return '{}: {}'.format(self.user_display, self.title)
|
||||
|
||||
@property
|
||||
def comments(self):
|
||||
return Comment.objects.filter(order_id=self.id)
|
||||
|
||||
@property
|
||||
def body_as_html(self):
|
||||
return self.body.replace('\n', '<br/>')
|
||||
|
@ -49,17 +45,29 @@ class Ticket(CommonModelMixin):
|
|||
def status_display(self):
|
||||
return self.get_status_display()
|
||||
|
||||
def create_status_comment(self, status, user):
|
||||
if status == self.STATUS_CLOSED:
|
||||
action = _("Close")
|
||||
else:
|
||||
action = _("Open")
|
||||
body = _('{} {} this ticket').format(self.user, action)
|
||||
self.comments.create(user=user, body=body)
|
||||
|
||||
def perform_status(self, status, user):
|
||||
if self.status == status:
|
||||
return
|
||||
self.status = status
|
||||
self.save()
|
||||
|
||||
class Meta:
|
||||
ordering = ('-date_created',)
|
||||
|
||||
|
||||
class Comment(CommonModelMixin):
|
||||
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE)
|
||||
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name='comments')
|
||||
user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, verbose_name=_("User"), related_name='comments')
|
||||
user_display = models.CharField(max_length=128, verbose_name=_("User display name"))
|
||||
body = models.TextField(verbose_name=_("Body"))
|
||||
|
||||
class Meta:
|
||||
ordering = ('date_created', )
|
||||
|
||||
|
||||
|
|
|
@ -18,3 +18,16 @@ class LoginConfirmTicket(Ticket):
|
|||
ip = models.GenericIPAddressField(blank=True, null=True)
|
||||
city = models.CharField(max_length=16, blank=True, default='')
|
||||
action = models.CharField(choices=ACTION_CHOICES, max_length=16, default='', blank=True)
|
||||
|
||||
def create_action_comment(self, action, user):
|
||||
action_display = dict(self.ACTION_CHOICES).get(action)
|
||||
body = '{} {} {}'.format(user, action_display, _("this order"))
|
||||
self.comments.create(body=body, user=user, user_display=str(user))
|
||||
|
||||
def perform_action(self, action, user):
|
||||
self.create_action_comment(action, user)
|
||||
self.action = action
|
||||
self.status = self.STATUS_CLOSED
|
||||
self.assignee = user
|
||||
self.assignees_display = str(user)
|
||||
self.save()
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from rest_framework.permissions import BasePermission
|
||||
|
||||
|
|
@ -21,7 +21,24 @@ class TicketSerializer(serializers.ModelSerializer):
|
|||
]
|
||||
|
||||
|
||||
class CurrentTicket(object):
|
||||
ticket = None
|
||||
|
||||
def set_context(self, serializer_field):
|
||||
self.ticket = serializer_field.context['ticket']
|
||||
|
||||
def __call__(self):
|
||||
return self.ticket
|
||||
|
||||
|
||||
class CommentSerializer(serializers.ModelSerializer):
|
||||
user = serializers.HiddenField(
|
||||
default=serializers.CurrentUserDefault(),
|
||||
)
|
||||
ticket = serializers.HiddenField(
|
||||
default=CurrentTicket()
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = models.Comment
|
||||
fields = [
|
||||
|
|
|
@ -17,13 +17,28 @@ class LoginConfirmTicketSerializer(serializers.ModelSerializer):
|
|||
]
|
||||
read_only_fields = TicketSerializer.Meta.read_only_fields
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data.pop('action')
|
||||
return super().create(validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
action = validated_data.get("action")
|
||||
user = self.context["request"].user
|
||||
if action and user not in instance.assignees.all():
|
||||
error = {"action": "Only assignees can update"}
|
||||
raise serializers.ValidationError(error)
|
||||
instance = super().update(instance, validated_data)
|
||||
if action:
|
||||
instance.perform_action(action, user)
|
||||
return instance
|
||||
|
||||
|
||||
class LoginConfirmTicketActionSerializer(serializers.ModelSerializer):
|
||||
comment = serializers.CharField(allow_blank=True)
|
||||
|
||||
class Meta:
|
||||
model = LoginConfirmTicket
|
||||
fields = ['action', 'comment']
|
||||
fields = ['action']
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
pass
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import m2m_changed, post_save
|
||||
from django.db.models.signals import m2m_changed, post_save, pre_save
|
||||
|
||||
from common.utils import get_logger
|
||||
from .models import LoginConfirmTicket
|
||||
from .models import LoginConfirmTicket, Ticket, Comment
|
||||
from .utils import (
|
||||
send_login_confirm_ticket_mail_to_assignees,
|
||||
send_login_confirm_action_mail_to_user
|
||||
|
@ -16,16 +16,34 @@ logger = get_logger(__name__)
|
|||
|
||||
@receiver(m2m_changed, sender=LoginConfirmTicket.assignees.through)
|
||||
def on_login_confirm_ticket_assignees_set(sender, instance=None, action=None,
|
||||
model=None, pk_set=None, **kwargs):
|
||||
reverse=False, model=None,
|
||||
pk_set=None, **kwargs):
|
||||
if action == 'post_add':
|
||||
logger.debug('New ticket create, send mail: {}'.format(instance.id))
|
||||
assignees = model.objects.filter(pk__in=pk_set)
|
||||
send_login_confirm_ticket_mail_to_assignees(instance, assignees)
|
||||
if action.startswith('post') and not reverse:
|
||||
instance.assignees_display = ', '.join([
|
||||
str(u) for u in instance.assignees.all()
|
||||
])
|
||||
instance.save()
|
||||
|
||||
|
||||
@receiver(post_save, sender=LoginConfirmTicket)
|
||||
def on_login_confirm_ticket_status_change(sender, instance=None, created=False, **kwargs):
|
||||
if created or instance.status == "pending":
|
||||
if created or instance.status == "open":
|
||||
return
|
||||
logger.debug('Ticket changed, send mail: {}'.format(instance.id))
|
||||
send_login_confirm_action_mail_to_user(instance)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=LoginConfirmTicket)
|
||||
def on_ticket_create(sender, instance=None, **kwargs):
|
||||
instance.user_display = str(instance.user)
|
||||
if instance.assignee:
|
||||
instance.assignee_display = str(instance.assignee)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Comment)
|
||||
def on_comment_create(sender, instance=None, **kwargs):
|
||||
instance.user_display = str(instance.user)
|
||||
|
|
|
@ -1,137 +1,34 @@
|
|||
{% extends 'base.html' %}
|
||||
{% extends 'tickets/ticket_detail.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>
|
||||
{{ object.title }}
|
||||
</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<div class="row">
|
||||
<div class="col-lg-11">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans 'User' %}:</dt> <dd>{{ object.user_display }}</dd>
|
||||
<dt>{% trans 'IP' %}:</dt> <dd>{{ object.ip }}</dd>
|
||||
<dt>{% trans 'Assignees' %}:</dt> <dd> {{ object.assignees_display }}</dd>
|
||||
<dt>{% trans 'Status' %}:</dt>
|
||||
<dd>
|
||||
{% if object.status == "accpeted" %}
|
||||
<span class="label label-primary">
|
||||
{{ object.get_status_display }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if object.status == "rejected" %}
|
||||
<span class="label label-danger">
|
||||
{{ object.get_status_display }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if object.status == "pending" %}
|
||||
<span class="label label-info">
|
||||
{{ object.get_status_display }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<dl class="dl-horizontal">
|
||||
<dt><br></dt><dd></dd>
|
||||
<dt>{% trans 'City' %}:</dt> <dd>{{ object.city }}</dd>
|
||||
<dt>{% trans 'Assignee' %}:</dt> <dd>{{ object.assignee_display | default_if_none:"" }}</dd>
|
||||
<dt>{% trans 'Date created' %}:</dt> <dd> {{ object.date_created }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-t-sm">
|
||||
<div class="col-lg-12">
|
||||
<div class="panel blank-panel">
|
||||
<div class="panel-body">
|
||||
<div class="feed-activity-list">
|
||||
{% for comment in object.comments %}
|
||||
<div class="feed-element">
|
||||
<a href="#" class="pull-left">
|
||||
<img alt="image" class="img-circle" src="{% static 'img/avatar/user.png'%}" >
|
||||
</a>
|
||||
<div class="media-body ">
|
||||
<strong>{{ comment.user_display }}</strong> <small class="text-muted"> {{ comment.date_created|timesince}} {% trans 'ago' %}</small>
|
||||
<br/>
|
||||
<small class="text-muted">{{ comment.date_created }} </small>
|
||||
<div style="padding-top: 10px">
|
||||
{{ comment.body }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="feed-element">
|
||||
<a href="" class="pull-left">
|
||||
<img alt="image" class="img-circle" src="{% static 'img/avatar/user.png'%}" >
|
||||
</a>
|
||||
<div class="media-body">
|
||||
<textarea class="form-control" placeholder="" id="comment"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<a class="btn btn-sm btn-primary btn-action btn-update" data-action="accept"><i class="fa fa-check"></i> {% trans 'Accept' %}</a>
|
||||
<a class="btn btn-sm btn-danger btn-action btn-update" data-action="reject"><i class="fa fa-times"></i> {% trans 'Reject' %}</a>
|
||||
<a class="btn btn-sm btn-info btn-action" data-action="comment"><i class="fa fa-pencil"></i> {% trans 'Comment' %}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block status %}
|
||||
{% endblock %}
|
||||
|
||||
{% block action %}
|
||||
<a class="btn btn-sm btn-primary btn-update btn-action" data-action="approve"><i class="fa fa-check"></i> {% trans 'Approve' %}</a>
|
||||
<a class="btn btn-sm btn-danger btn-update btn-action" data-action="reject"><i class="fa fa-times"></i> {% trans 'Reject' %}</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block custom_foot_js %}
|
||||
{{ block.super }}
|
||||
<script>
|
||||
var ticketId = "{{ object.id }}";
|
||||
var status = "{{ object.status }}";
|
||||
var actionCreateUrl = "{% url 'api-tickets:login-confirm-ticket-create-action' pk=object.id %}";
|
||||
$(document).ready(function () {
|
||||
if (status !== "pending") {
|
||||
$('.btn-update').attr('disabled', '1')
|
||||
}
|
||||
})
|
||||
.on('click', '.btn-action', function () {
|
||||
var action = $(this).data('action');
|
||||
var comment = $("#comment").val();
|
||||
var data = {
|
||||
url: actionCreateUrl,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({action: action, comment: comment}),
|
||||
success: function () {
|
||||
window.location.reload();
|
||||
}
|
||||
};
|
||||
requestApi(data);
|
||||
})
|
||||
var ticketDetailUrl = "{% url 'api-tickets:login-confirm-ticket-detail' pk=object.id %}";
|
||||
$(document).ready(function () {
|
||||
}).on('click', '.btn-action', function () {
|
||||
createComment(function () {
|
||||
});
|
||||
var action = $(this).data('action');
|
||||
var data = {
|
||||
url: ticketDetailUrl,
|
||||
body: JSON.stringify({action: action}),
|
||||
method: "PATCH",
|
||||
success: reloadPage
|
||||
};
|
||||
requestApi(data);
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@
|
|||
</th>
|
||||
<th class="text-center">{% trans 'Title' %}</th>
|
||||
<th class="text-center">{% trans 'User' %}</th>
|
||||
<th class="text-center">{% trans 'IP' %}</th>
|
||||
<th class="text-center">{% trans 'Status' %}</th>
|
||||
<th class="text-center">{% trans 'Datetime' %}</th>
|
||||
<th class="text-center">{% trans 'Action' %}</th>
|
||||
|
@ -39,10 +38,6 @@ function initTable() {
|
|||
$(td).html(detailBtn.replace("{{ DEFAULT_PK }}", rowData.id));
|
||||
}},
|
||||
{targets: 3, createdCell: function (td, cellData, rowData) {
|
||||
var d = cellData + "(" + rowData.city + ")";
|
||||
$(td).html(d)
|
||||
}},
|
||||
{targets: 4, createdCell: function (td, cellData, rowData) {
|
||||
if (cellData === "approval") {
|
||||
$(td).html('<i class="fa fa-check text-navy"></i>')
|
||||
} else if (cellData === "rejected") {
|
||||
|
@ -53,12 +48,12 @@ function initTable() {
|
|||
$(td).html('<i class="fa fa-circle text-info"></i>')
|
||||
}
|
||||
}},
|
||||
{targets: 5, createdCell: function (td, cellData) {
|
||||
{targets: 4, createdCell: function (td, cellData) {
|
||||
var d = toSafeLocalDateStr(cellData);
|
||||
$(td).html(d)
|
||||
}},
|
||||
{targets: 6, createdCell: function (td, cellData, rowData) {
|
||||
var acceptBtn = '<a class="btn btn-xs btn-info btn-action" data-action="accept" data-uid="{{ DEFAULT_PK }}" >{% trans "Accept" %}</a> ';
|
||||
{targets: 5, createdCell: function (td, cellData, rowData) {
|
||||
var acceptBtn = '<a class="btn btn-xs btn-info btn-action" data-action="approve" data-uid="{{ DEFAULT_PK }}" >{% trans "Approve" %}</a> ';
|
||||
var rejectBtn = '<a class="btn btn-xs btn-danger btn-action" data-action="reject" data-uid="{{ DEFAULT_PK }}" >{% trans "Reject" %}</a>';
|
||||
acceptBtn = acceptBtn.replace('{{ DEFAULT_PK }}', cellData);
|
||||
rejectBtn = rejectBtn.replace('{{ DEFAULT_PK }}', cellData);
|
||||
|
@ -74,7 +69,7 @@ function initTable() {
|
|||
ajax_url: '{% url "api-tickets:login-confirm-ticket-list" %}',
|
||||
columns: [
|
||||
{data: "id"}, {data: "title"},
|
||||
{data: "user_display"}, {data: "ip"},
|
||||
{data: "user_display"},
|
||||
{data: "status", ticketable: false},
|
||||
{data: "date_created", width: "120px"},
|
||||
{data: "id", ticketable: false}
|
||||
|
@ -101,18 +96,15 @@ $(document).ready(function(){
|
|||
];
|
||||
initTableFilterDropdown('#login_confirm_ticket_list_table_filter input', menu)
|
||||
}).on('click', '.btn-action', function () {
|
||||
var actionCreateUrl = "{% url 'api-tickets:login-confirm-ticket-create-action' pk=DEFAULT_PK %}";
|
||||
var ticketId = $(this).data('uid');
|
||||
actionCreateUrl = actionCreateUrl.replace("{{ DEFAULT_PK }}", ticketId);
|
||||
var ticketId = $(this).data("uid");
|
||||
var action = $(this).data('action');
|
||||
var comment = '';
|
||||
var ticketDetailUrl = "{% url 'api-tickets:login-confirm-ticket-detail' pk=DEFAULT_PK %}";
|
||||
ticketDetailUrl = ticketDetailUrl.replace("{{ DEFAULT_PK }}", ticketId);
|
||||
var data = {
|
||||
url: actionCreateUrl,
|
||||
method: 'POST',
|
||||
body: JSON.stringify({action: action, comment: comment}),
|
||||
success: function () {
|
||||
window.location.reload();
|
||||
}
|
||||
url: ticketDetailUrl,
|
||||
body: JSON.stringify({action: action}),
|
||||
method: "PATCH",
|
||||
success: reloadPage
|
||||
};
|
||||
requestApi(data);
|
||||
})
|
||||
|
|
|
@ -0,0 +1,162 @@
|
|||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="wrapper wrapper-content animated fadeInRight">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="ibox float-e-margins">
|
||||
<div class="ibox-title">
|
||||
<h5>
|
||||
{{ object.title }}
|
||||
</h5>
|
||||
<div class="ibox-tools">
|
||||
<a class="collapse-link">
|
||||
<i class="fa fa-chevron-up"></i>
|
||||
</a>
|
||||
<a class="dropdown-toggle" data-toggle="dropdown" href="#">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</a>
|
||||
<a class="close-link">
|
||||
<i class="fa fa-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ibox-content">
|
||||
<div class="row">
|
||||
<div class="col-lg-11">
|
||||
<div class="row">
|
||||
<div class="col-lg-6">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans 'User' %}:</dt> <dd>{{ object.user_display }}</dd>
|
||||
<dt>{% trans 'Type' %}:</dt> <dd>{{ object.get_type_display | default_if_none:"" }}</dd>
|
||||
<dt>{% trans 'Status' %}:</dt>
|
||||
<dd>
|
||||
{% if object.status == "open" %}
|
||||
<span class="label label-primary">
|
||||
{{ object.get_status_display }}
|
||||
</span>
|
||||
{% elif object.status == "closed" %}
|
||||
<span class="label label-danger">
|
||||
{{ object.get_status_display }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans 'Assignees' %}:</dt> <dd> {{ object.assignees_display }}</dd>
|
||||
<dt>{% trans 'Assignee' %}:</dt> <dd>{{ object.assignee_display | default_if_none:"" }}</dd>
|
||||
<dt>{% trans 'Date created' %}:</dt> <dd> {{ object.date_created }}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row m-t-sm">
|
||||
<div class="col-lg-12">
|
||||
<div class="panel blank-panel">
|
||||
<div class="panel-body">
|
||||
<div class="feed-activity-list">
|
||||
<div class="feed-element">
|
||||
<a href="#" class="pull-left">
|
||||
<img alt="image" class="img-circle" src="{% static 'img/avatar/user.png'%}" >
|
||||
</a>
|
||||
<div class="media-body ">
|
||||
<strong>{{ object.user_display }}</strong> <small class="text-muted"> {{ object.date_created|timesince}} {% trans 'ago' %}</small>
|
||||
<br/>
|
||||
<small class="text-muted">{{ object.date_created }} </small>
|
||||
<div style="padding-top: 10px">
|
||||
{{ object.body_as_html | safe }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% for comment in object.comments.all %}
|
||||
|
||||
<div class="feed-element">
|
||||
<a href="#" class="pull-left">
|
||||
<img alt="image" class="img-circle" src="{% static 'img/avatar/user.png'%}" >
|
||||
</a>
|
||||
<div class="media-body ">
|
||||
<strong>{{ comment.user_display }}</strong> <small class="text-muted"> {{ comment.date_created|timesince}} {% trans 'ago' %}</small>
|
||||
<br/>
|
||||
<small class="text-muted">{{ comment.date_created }} </small>
|
||||
<div style="padding-top: 10px">
|
||||
{{ comment.body }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="feed-element">
|
||||
<a href="" class="pull-left">
|
||||
<img alt="image" class="img-circle" src="{% static 'img/avatar/user.png'%}" >
|
||||
</a>
|
||||
<div class="media-body">
|
||||
<textarea class="form-control" placeholder="" id="comment"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
{% block action %}
|
||||
{% endblock %}
|
||||
{% block status %}
|
||||
<a class="btn btn-sm btn-danger btn-update btn-status" data-uid="close"><i class="fa fa-times"></i> {% trans 'Close' %}</a>
|
||||
{% endblock %}
|
||||
{% block comment %}
|
||||
<a class="btn btn-sm btn-info btn-update btn-comment" data-uid="comment"><i class="fa fa-pencil"></i> {% trans 'Comment' %}</a>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-1">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% block custom_foot_js %}
|
||||
<script>
|
||||
var ticketId = "{{ object.id }}";
|
||||
var status = "{{ object.status }}";
|
||||
|
||||
var commentUrl = "{% url 'api-tickets:ticket-comment-list' ticket_id=object.id %}";
|
||||
|
||||
function createComment(successCallback) {
|
||||
var commentText = $("#comment").val();
|
||||
if (!commentText) {
|
||||
return
|
||||
}
|
||||
var body = {
|
||||
body: commentText,
|
||||
ticket: ticketId,
|
||||
};
|
||||
var success = function () {
|
||||
window.location.reload();
|
||||
};
|
||||
if (successCallback){
|
||||
success = successCallback;
|
||||
}
|
||||
requestApi({
|
||||
url: commentUrl,
|
||||
data: JSON.stringify(body),
|
||||
method: "POST",
|
||||
success: success
|
||||
})
|
||||
}
|
||||
$(document).ready(function () {
|
||||
if (status !== "open") {
|
||||
$('.btn-update').attr('disabled', '1')
|
||||
}
|
||||
})
|
||||
.on('click', '.btn-comment', function () {
|
||||
createComment();
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -9,15 +9,11 @@ app_name = 'tickets'
|
|||
router = DefaultRouter()
|
||||
|
||||
router.register('tickets', api.TicketViewSet, 'ticket')
|
||||
router.register('tickets/(?P<ticket_id>[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment')
|
||||
router.register('login-confirm-tickets', api.LoginConfirmTicketViewSet, 'login-confirm-ticket')
|
||||
router.register('tickets/<uuid:ticket_id>/comments/', api.CommentViewSet, 'ticket-comment')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('login-confirm-tickets/<uuid:pk>/actions/',
|
||||
api.LoginConfirmTicketsCreateActionApi.as_view(),
|
||||
name='login-confirm-ticket-create-action'
|
||||
),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
from django.views.generic import TemplateView, DetailView
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from common.permissions import PermissionsMixin, IsOrgAdmin
|
||||
from common.permissions import PermissionsMixin, IsValidUser
|
||||
from .models import LoginConfirmTicket
|
||||
from . import mixins
|
||||
|
||||
|
||||
class LoginConfirmTicketListView(PermissionsMixin, TemplateView):
|
||||
template_name = 'tickets/login_confirm_ticket_list.html'
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
permission_classes = (IsValidUser,)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
@ -18,12 +19,10 @@ class LoginConfirmTicketListView(PermissionsMixin, TemplateView):
|
|||
return context
|
||||
|
||||
|
||||
class LoginConfirmTicketDetailView(PermissionsMixin, DetailView):
|
||||
class LoginConfirmTicketDetailView(PermissionsMixin, mixins.TicketMixin, DetailView):
|
||||
template_name = 'tickets/login_confirm_ticket_detail.html'
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
|
||||
def get_queryset(self):
|
||||
return LoginConfirmTicket.objects.filter(assignees=self.request.user)
|
||||
queryset = LoginConfirmTicket.objects.all()
|
||||
permission_classes = (IsValidUser,)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
|
|
@ -13,11 +13,11 @@ from ..models import User, UserGroup
|
|||
__all__ = [
|
||||
'UserSerializer', 'UserPKUpdateSerializer', 'UserUpdateGroupSerializer',
|
||||
'ChangeUserPasswordSerializer', 'ResetOTPSerializer',
|
||||
'UserProfileSerializer',
|
||||
]
|
||||
|
||||
|
||||
class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
|
||||
|
||||
can_update = serializers.SerializerMethodField()
|
||||
can_delete = serializers.SerializerMethodField()
|
||||
|
||||
|
@ -135,3 +135,11 @@ class ResetOTPSerializer(serializers.Serializer):
|
|||
|
||||
def update(self, instance, validated_data):
|
||||
pass
|
||||
|
||||
|
||||
class UserProfileSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
'id', 'username', 'name', 'role', 'email'
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue