[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.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)

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

@ -1317,3 +1317,7 @@ function initDateRangePicker(selector, options) {
options = Object.assign(defaultOption, 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>
</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 %}

View File

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

View File

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

View File

@ -3,3 +3,7 @@ from django.apps import AppConfig
class TicketsConfig(AppConfig):
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"))
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', )

View File

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

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

View File

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

View File

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

View File

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

View File

@ -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);
})

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

View File

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

View File

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