[Update] 修改登录工单

pull/3428/head
ibuler 2019-11-08 15:48:01 +08:00
parent 08775551c2
commit f9e41d71dc
27 changed files with 784 additions and 599 deletions

View File

@ -4,6 +4,7 @@ from rest_framework.generics import UpdateAPIView
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from django.shortcuts import get_object_or_404 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.utils import get_logger, get_object_or_none
from common.permissions import IsOrgAdmin from common.permissions import IsOrgAdmin
@ -11,7 +12,7 @@ from ..models import LoginConfirmSetting
from ..serializers import LoginConfirmSettingSerializer from ..serializers import LoginConfirmSettingSerializer
from .. import errors from .. import errors
__all__ = ['LoginConfirmSettingUpdateApi', 'UserTicketAcceptAuthApi'] __all__ = ['LoginConfirmSettingUpdateApi', 'LoginConfirmTicketStatusApi']
logger = get_logger(__name__) logger = get_logger(__name__)
@ -30,10 +31,10 @@ class LoginConfirmSettingUpdateApi(UpdateAPIView):
return s return s
class UserTicketAcceptAuthApi(APIView): class LoginConfirmTicketStatusApi(APIView):
permission_classes = () permission_classes = ()
def get(self, request, *args, **kwargs): def get_ticket(self):
from tickets.models import LoginConfirmTicket from tickets.models import LoginConfirmTicket
ticket_id = self.request.session.get("auth_ticket_id") ticket_id = self.request.session.get("auth_ticket_id")
logger.debug('Login confirm ticket id: {}'.format(ticket_id)) logger.debug('Login confirm ticket id: {}'.format(ticket_id))
@ -41,31 +42,32 @@ class UserTicketAcceptAuthApi(APIView):
ticket = None ticket = None
else: else:
ticket = get_object_or_none(LoginConfirmTicket, pk=ticket_id) 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: try:
if not ticket: if not ticket:
raise errors.LoginConfirmTicketNotFound(ticket_id) raise errors.LoginConfirmOtherError(ticket_id, _("not found"))
if ticket.action == LoginConfirmTicket.ACTION_APPROVE: if ticket.status == 'open':
raise errors.LoginConfirmWaitError(ticket_id)
elif ticket.action == ticket.ACTION_APPROVE:
self.request.session["auth_confirm"] = "1" self.request.session["auth_confirm"] = "1"
return Response({"msg": "ok"}) return Response({"msg": "ok"})
elif ticket.action == LoginConfirmTicket.ACTION_REJECT: elif ticket.action == ticket.ACTION_REJECT:
raise errors.LoginConfirmRejectedError(ticket_id) raise errors.LoginConfirmOtherError(
ticket_id, ticket.get_action_display()
)
else: else:
raise errors.LoginConfirmWaitError(ticket_id) raise errors.LoginConfirmOtherError(
ticket_id, ticket.get_status_display()
)
except errors.AuthFailedError as e: except errors.AuthFailedError as e:
data = e.as_data() return Response(e.as_data(), status=400)
return Response(data, status=400)
def delete(self, request, *args, **kwargs):
class UserTicketCancelAuthApi(APIView): ticket = self.get_ticket()
permission_classes = () if ticket:
ticket.perform_status('closed', request.user)
def get(self, request, *args, **kwargs): return Response('', status=200)
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"

View File

@ -48,8 +48,7 @@ mfa_failed_msg = _("MFA code invalid, or ntp sync server time")
mfa_required_msg = _("MFA required") mfa_required_msg = _("MFA required")
login_confirm_required_msg = _("Login confirm required") login_confirm_required_msg = _("Login confirm required")
login_confirm_wait_msg = _("Wait login confirm ticket for accept") login_confirm_wait_msg = _("Wait login confirm ticket for accept")
login_confirm_rejected_msg = _("Login confirm ticket was rejected") login_confirm_error_msg = _("Login confirm ticket was {}")
login_confirm_ticket_not_found_msg = _("Ticket not found")
class AuthFailedNeedLogMixin: class AuthFailedNeedLogMixin:
@ -174,11 +173,9 @@ class LoginConfirmWaitError(LoginConfirmError):
error = 'login_confirm_wait' error = 'login_confirm_wait'
class LoginConfirmRejectedError(LoginConfirmError): class LoginConfirmOtherError(LoginConfirmError):
msg = login_confirm_rejected_msg error = 'login_confirm_error'
error = 'login_confirm_rejected'
def __init__(self, ticket_id, status):
class LoginConfirmTicketNotFound(LoginConfirmError): msg = login_confirm_error_msg.format(status)
msg = login_confirm_ticket_not_found_msg super().__init__(ticket_id=ticket_id, msg=msg)
error = 'login_confirm_ticket_not_found'

View File

@ -106,7 +106,7 @@ class AuthMixin:
if ticket.status == "accepted": if ticket.status == "accepted":
return return
elif ticket.status == "rejected": elif ticket.status == "rejected":
raise errors.LoginConfirmRejectedError(ticket.id) raise errors.LoginConfirmOtherError(ticket.id)
else: else:
raise errors.LoginConfirmWaitError(ticket.id) raise errors.LoginConfirmWaitError(ticket.id)

View File

@ -62,12 +62,9 @@ class LoginConfirmSetting(CommonModelMixin):
remote_addr = '127.0.0.1' remote_addr = '127.0.0.1'
body = '' body = ''
reviewer = self.reviewers.all() reviewer = self.reviewers.all()
reviewer_names = ','.join([u.name for u in reviewer])
ticket = LoginConfirmTicket.objects.create( ticket = LoginConfirmTicket.objects.create(
user=self.user, user_display=str(self.user), user=self.user, title=title, body=body,
title=title, body=body,
city=city, ip=remote_addr, city=city, ip=remote_addr,
assignees_display=reviewer_names,
type=LoginConfirmTicket.TYPE_LOGIN_CONFIRM, type=LoginConfirmTicket.TYPE_LOGIN_CONFIRM,
) )
ticket.assignees.set(reviewer) ticket.assignees.set(reviewer)

View File

@ -1,10 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.core.cache import cache
from rest_framework import serializers from rest_framework import serializers
from common.utils import get_object_or_none from common.utils import get_object_or_none
from users.models import User from users.models import User
from users.serializers import UserProfileSerializer
from .models import AccessKey, LoginConfirmSetting from .models import AccessKey, LoginConfirmSetting
@ -26,14 +26,15 @@ class OtpVerifySerializer(serializers.Serializer):
class BearerTokenSerializer(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, 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, public_key = serializers.CharField(write_only=True, allow_null=True,
required=False) allow_blank=True, required=False)
token = serializers.CharField(read_only=True) token = serializers.CharField(read_only=True)
keyword = serializers.SerializerMethodField() keyword = serializers.SerializerMethodField()
date_expired = serializers.DateTimeField(read_only=True) date_expired = serializers.DateTimeField(read_only=True)
user = UserProfileSerializer(read_only=True)
@staticmethod @staticmethod
def get_keyword(obj): def get_keyword(obj):
@ -52,9 +53,9 @@ class BearerTokenSerializer(serializers.Serializer):
) )
token, date_expired = user.create_bearer_token(request) token, date_expired = user.create_bearer_token(request)
instance = { instance = {
"username": user.username,
"token": token, "token": token,
"date_expired": date_expired, "date_expired": date_expired,
"user": user
} }
return instance return instance

View File

@ -73,7 +73,7 @@ var infoMsgRef = $(".info-messages");
var timestamp = '{{ timestamp }}'; var timestamp = '{{ timestamp }}';
var progressBarRef = $(".progress-bar"); var progressBarRef = $(".progress-bar");
var interval, checkInterval; 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' %}"; var successUrl = "{% url 'authentication:login-guard' %}";
function doRequestAuth() { function doRequestAuth() {

View File

@ -18,7 +18,7 @@ urlpatterns = [
path('connection-token/', path('connection-token/',
api.UserConnectionTokenApi.as_view(), name='connection-token'), api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'), 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') 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

View File

@ -1317,3 +1317,7 @@ function initDateRangePicker(selector, options) {
options = Object.assign(defaultOption, options); options = Object.assign(defaultOption, options);
return $(selector).daterangepicker(options); return $(selector).daterangepicker(options);
} }
function reloadPage() {
window.location.reload();
}

View File

@ -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> <i class="fa fa-check-square-o" style="width: 14px"></i> <span class="nav-label">{% trans 'Tickets' %}</span><span class="fa arrow"></span>
</a> </a>
<ul class="nav nav-second-level"> <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> </ul>
</li> </li>
{% endif %} {% endif %}

View File

@ -1,21 +1,38 @@
# -*- coding: utf-8 -*- # -*- 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 serializer_class = serializers.TicketSerializer
queryset = models.Ticket.objects.all()
def get_queryset(self):
queryset = models.Ticket.objects.all().none()
return queryset
class CommentViewSet(viewsets.ModelViewSet): class TicketCommentViewSet(viewsets.ModelViewSet):
serializer_class = serializers.CommentSerializer 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): def get_queryset(self):
queryset = models.Comment.objects.none() queryset = self.ticket.comments.all()
return queryset return queryset

View File

@ -1,39 +1,30 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework import viewsets, generics from rest_framework import viewsets, generics
from rest_framework.serializers import ValidationError
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from common.permissions import IsValidUser from common.permissions import IsValidUser
from common.mixins import CommonApiMixin from common.mixins import CommonApiMixin
from .. import serializers from .. import serializers, mixins
from ..models import LoginConfirmTicket from ..models import LoginConfirmTicket
class LoginConfirmTicketViewSet(CommonApiMixin, viewsets.ModelViewSet): class LoginConfirmTicketViewSet(CommonApiMixin, mixins.TicketMixin, viewsets.ModelViewSet):
serializer_class = serializers.LoginConfirmTicketSerializer serializer_class = serializers.LoginConfirmTicketSerializer
permission_classes = (IsValidUser,) permission_classes = (IsValidUser,)
filter_fields = ['status', 'title'] queryset = LoginConfirmTicket.objects.all()
filter_fields = ['status', 'title', 'action', 'ip']
search_fields = ['user_display', 'title', 'ip', 'city'] search_fields = ['user_display', 'title', 'ip', 'city']
def get_queryset(self): # def check_update_permission(self, serializer):
queryset = LoginConfirmTicket.objects.all()\ # data = serializer.validated_data
.filter(assignees=self.request.user) # action = data.get("action")
return queryset # user = self.request.user
# instance = serializer.instance
# if action and user not in instance.assignees.all():
class LoginConfirmTicketsCreateActionApi(generics.CreateAPIView): # error = {"action": "Only assignees can update"}
permission_classes = (IsValidUser,) # raise ValidationError(error)
serializer_class = serializers.LoginConfirmTicketActionSerializer #
# def perform_update(self, serializer):
def get_ticket(self): # self.check_update_permission(serializer)
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

View File

@ -3,3 +3,7 @@ from django.apps import AppConfig
class TicketsConfig(AppConfig): class TicketsConfig(AppConfig):
name = 'tickets' name = 'tickets'
def ready(self):
from . import signals_handler
return super().ready()

18
apps/tickets/mixins.py Normal file
View File

@ -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

View File

@ -31,16 +31,12 @@ class Ticket(CommonModelMixin):
assignee_display = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Assignee display name")) 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 = 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) 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') status = models.CharField(choices=STATUS_CHOICES, max_length=16, default='open')
def __str__(self): def __str__(self):
return '{}: {}'.format(self.user_display, self.title) return '{}: {}'.format(self.user_display, self.title)
@property
def comments(self):
return Comment.objects.filter(order_id=self.id)
@property @property
def body_as_html(self): def body_as_html(self):
return self.body.replace('\n', '<br/>') return self.body.replace('\n', '<br/>')
@ -49,17 +45,29 @@ class Ticket(CommonModelMixin):
def status_display(self): def status_display(self):
return self.get_status_display() 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: class Meta:
ordering = ('-date_created',) ordering = ('-date_created',)
class Comment(CommonModelMixin): 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 = 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")) user_display = models.CharField(max_length=128, verbose_name=_("User display name"))
body = models.TextField(verbose_name=_("Body")) body = models.TextField(verbose_name=_("Body"))
class Meta: class Meta:
ordering = ('date_created', ) ordering = ('date_created', )

View File

@ -18,3 +18,16 @@ class LoginConfirmTicket(Ticket):
ip = models.GenericIPAddressField(blank=True, null=True) ip = models.GenericIPAddressField(blank=True, null=True)
city = models.CharField(max_length=16, blank=True, default='') city = models.CharField(max_length=16, blank=True, default='')
action = models.CharField(choices=ACTION_CHOICES, max_length=16, default='', blank=True) 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()

View File

@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
#
from rest_framework.permissions import BasePermission

View File

@ -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): class CommentSerializer(serializers.ModelSerializer):
user = serializers.HiddenField(
default=serializers.CurrentUserDefault(),
)
ticket = serializers.HiddenField(
default=CurrentTicket()
)
class Meta: class Meta:
model = models.Comment model = models.Comment
fields = [ fields = [

View File

@ -17,13 +17,28 @@ class LoginConfirmTicketSerializer(serializers.ModelSerializer):
] ]
read_only_fields = TicketSerializer.Meta.read_only_fields 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): class LoginConfirmTicketActionSerializer(serializers.ModelSerializer):
comment = serializers.CharField(allow_blank=True) comment = serializers.CharField(allow_blank=True)
class Meta: class Meta:
model = LoginConfirmTicket model = LoginConfirmTicket
fields = ['action', 'comment'] fields = ['action']
def update(self, instance, validated_data): def update(self, instance, validated_data):
pass pass

View File

@ -1,10 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.dispatch import receiver 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 common.utils import get_logger
from .models import LoginConfirmTicket from .models import LoginConfirmTicket, Ticket, Comment
from .utils import ( from .utils import (
send_login_confirm_ticket_mail_to_assignees, send_login_confirm_ticket_mail_to_assignees,
send_login_confirm_action_mail_to_user send_login_confirm_action_mail_to_user
@ -16,16 +16,34 @@ logger = get_logger(__name__)
@receiver(m2m_changed, sender=LoginConfirmTicket.assignees.through) @receiver(m2m_changed, sender=LoginConfirmTicket.assignees.through)
def on_login_confirm_ticket_assignees_set(sender, instance=None, action=None, 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': if action == 'post_add':
logger.debug('New ticket create, send mail: {}'.format(instance.id)) logger.debug('New ticket create, send mail: {}'.format(instance.id))
assignees = model.objects.filter(pk__in=pk_set) assignees = model.objects.filter(pk__in=pk_set)
send_login_confirm_ticket_mail_to_assignees(instance, assignees) 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) @receiver(post_save, sender=LoginConfirmTicket)
def on_login_confirm_ticket_status_change(sender, instance=None, created=False, **kwargs): 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 return
logger.debug('Ticket changed, send mail: {}'.format(instance.id)) logger.debug('Ticket changed, send mail: {}'.format(instance.id))
send_login_confirm_action_mail_to_user(instance) 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)

View File

@ -1,137 +1,34 @@
{% extends 'base.html' %} {% extends 'tickets/ticket_detail.html' %}
{% load static %} {% load static %}
{% load i18n %} {% load i18n %}
{% block content %} {% block status %}
<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>
{% endblock %} {% 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 custom_foot_js %}
{{ block.super }}
<script> <script>
var ticketId = "{{ object.id }}"; var ticketDetailUrl = "{% url 'api-tickets:login-confirm-ticket-detail' pk=object.id %}";
var status = "{{ object.status }}"; $(document).ready(function () {
var actionCreateUrl = "{% url 'api-tickets:login-confirm-ticket-create-action' pk=object.id %}"; }).on('click', '.btn-action', function () {
$(document).ready(function () { createComment(function () {
if (status !== "pending") { });
$('.btn-update').attr('disabled', '1') var action = $(this).data('action');
} var data = {
}) url: ticketDetailUrl,
.on('click', '.btn-action', function () { body: JSON.stringify({action: action}),
var action = $(this).data('action'); method: "PATCH",
var comment = $("#comment").val(); success: reloadPage
var data = { };
url: actionCreateUrl, requestApi(data);
method: 'POST', })
body: JSON.stringify({action: action, comment: comment}),
success: function () {
window.location.reload();
}
};
requestApi(data);
})
</script> </script>
{% endblock %} {% endblock %}

View File

@ -13,7 +13,6 @@
</th> </th>
<th class="text-center">{% trans 'Title' %}</th> <th class="text-center">{% trans 'Title' %}</th>
<th class="text-center">{% trans 'User' %}</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 'Status' %}</th>
<th class="text-center">{% trans 'Datetime' %}</th> <th class="text-center">{% trans 'Datetime' %}</th>
<th class="text-center">{% trans 'Action' %}</th> <th class="text-center">{% trans 'Action' %}</th>
@ -39,10 +38,6 @@ function initTable() {
$(td).html(detailBtn.replace("{{ DEFAULT_PK }}", rowData.id)); $(td).html(detailBtn.replace("{{ DEFAULT_PK }}", rowData.id));
}}, }},
{targets: 3, createdCell: function (td, cellData, rowData) { {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") { if (cellData === "approval") {
$(td).html('<i class="fa fa-check text-navy"></i>') $(td).html('<i class="fa fa-check text-navy"></i>')
} else if (cellData === "rejected") { } else if (cellData === "rejected") {
@ -53,12 +48,12 @@ function initTable() {
$(td).html('<i class="fa fa-circle text-info"></i>') $(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); var d = toSafeLocalDateStr(cellData);
$(td).html(d) $(td).html(d)
}}, }},
{targets: 6, createdCell: function (td, cellData, rowData) { {targets: 5, 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> '; 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>'; 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); acceptBtn = acceptBtn.replace('{{ DEFAULT_PK }}', cellData);
rejectBtn = rejectBtn.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" %}', ajax_url: '{% url "api-tickets:login-confirm-ticket-list" %}',
columns: [ columns: [
{data: "id"}, {data: "title"}, {data: "id"}, {data: "title"},
{data: "user_display"}, {data: "ip"}, {data: "user_display"},
{data: "status", ticketable: false}, {data: "status", ticketable: false},
{data: "date_created", width: "120px"}, {data: "date_created", width: "120px"},
{data: "id", ticketable: false} {data: "id", ticketable: false}
@ -101,18 +96,15 @@ $(document).ready(function(){
]; ];
initTableFilterDropdown('#login_confirm_ticket_list_table_filter input', menu) initTableFilterDropdown('#login_confirm_ticket_list_table_filter input', menu)
}).on('click', '.btn-action', function () { }).on('click', '.btn-action', function () {
var actionCreateUrl = "{% url 'api-tickets:login-confirm-ticket-create-action' pk=DEFAULT_PK %}"; var ticketId = $(this).data("uid");
var ticketId = $(this).data('uid');
actionCreateUrl = actionCreateUrl.replace("{{ DEFAULT_PK }}", ticketId);
var action = $(this).data('action'); 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 = { var data = {
url: actionCreateUrl, url: ticketDetailUrl,
method: 'POST', body: JSON.stringify({action: action}),
body: JSON.stringify({action: action, comment: comment}), method: "PATCH",
success: function () { success: reloadPage
window.location.reload();
}
}; };
requestApi(data); requestApi(data);
}) })

View File

@ -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 %}

View File

@ -9,15 +9,11 @@ app_name = 'tickets'
router = DefaultRouter() router = DefaultRouter()
router.register('tickets', api.TicketViewSet, 'ticket') 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('login-confirm-tickets', api.LoginConfirmTicketViewSet, 'login-confirm-ticket')
router.register('tickets/<uuid:ticket_id>/comments/', api.CommentViewSet, 'ticket-comment')
urlpatterns = [ urlpatterns = [
path('login-confirm-tickets/<uuid:pk>/actions/',
api.LoginConfirmTicketsCreateActionApi.as_view(),
name='login-confirm-ticket-create-action'
),
] ]
urlpatterns += router.urls urlpatterns += router.urls

View File

@ -1,13 +1,14 @@
from django.views.generic import TemplateView, DetailView from django.views.generic import TemplateView, DetailView
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from common.permissions import PermissionsMixin, IsOrgAdmin from common.permissions import PermissionsMixin, IsValidUser
from .models import LoginConfirmTicket from .models import LoginConfirmTicket
from . import mixins
class LoginConfirmTicketListView(PermissionsMixin, TemplateView): class LoginConfirmTicketListView(PermissionsMixin, TemplateView):
template_name = 'tickets/login_confirm_ticket_list.html' template_name = 'tickets/login_confirm_ticket_list.html'
permission_classes = (IsOrgAdmin,) permission_classes = (IsValidUser,)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -18,12 +19,10 @@ class LoginConfirmTicketListView(PermissionsMixin, TemplateView):
return context return context
class LoginConfirmTicketDetailView(PermissionsMixin, DetailView): class LoginConfirmTicketDetailView(PermissionsMixin, mixins.TicketMixin, DetailView):
template_name = 'tickets/login_confirm_ticket_detail.html' template_name = 'tickets/login_confirm_ticket_detail.html'
permission_classes = (IsOrgAdmin,) queryset = LoginConfirmTicket.objects.all()
permission_classes = (IsValidUser,)
def get_queryset(self):
return LoginConfirmTicket.objects.filter(assignees=self.request.user)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)

View File

@ -13,11 +13,11 @@ from ..models import User, UserGroup
__all__ = [ __all__ = [
'UserSerializer', 'UserPKUpdateSerializer', 'UserUpdateGroupSerializer', 'UserSerializer', 'UserPKUpdateSerializer', 'UserUpdateGroupSerializer',
'ChangeUserPasswordSerializer', 'ResetOTPSerializer', 'ChangeUserPasswordSerializer', 'ResetOTPSerializer',
'UserProfileSerializer',
] ]
class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
can_update = serializers.SerializerMethodField() can_update = serializers.SerializerMethodField()
can_delete = serializers.SerializerMethodField() can_delete = serializers.SerializerMethodField()
@ -135,3 +135,11 @@ class ResetOTPSerializer(serializers.Serializer):
def update(self, instance, validated_data): def update(self, instance, validated_data):
pass pass
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
'id', 'username', 'name', 'role', 'email'
]