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.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"
|
|
||||||
|
|
|
@ -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'
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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
|
@ -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();
|
||||||
|
}
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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"))
|
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', )
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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):
|
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 = [
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 = 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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'
|
||||||
|
]
|
||||||
|
|
Loading…
Reference in New Issue