Commit ebe129b3 authored by ibuler's avatar ibuler

[Update] 修改工单

parent 18f88647
...@@ -12,7 +12,7 @@ from ..models import LoginConfirmSetting ...@@ -12,7 +12,7 @@ from ..models import LoginConfirmSetting
from ..serializers import LoginConfirmSettingSerializer from ..serializers import LoginConfirmSettingSerializer
from .. import errors, mixins from .. import errors, mixins
__all__ = ['LoginConfirmSettingUpdateApi', 'LoginConfirmTicketStatusApi'] __all__ = ['LoginConfirmSettingUpdateApi', 'TicketStatusApi']
logger = get_logger(__name__) logger = get_logger(__name__)
...@@ -31,17 +31,17 @@ class LoginConfirmSettingUpdateApi(UpdateAPIView): ...@@ -31,17 +31,17 @@ class LoginConfirmSettingUpdateApi(UpdateAPIView):
return s return s
class LoginConfirmTicketStatusApi(mixins.AuthMixin, APIView): class TicketStatusApi(mixins.AuthMixin, APIView):
permission_classes = () permission_classes = ()
def get_ticket(self): def get_ticket(self):
from tickets.models import LoginConfirmTicket from tickets.models import Ticket
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))
if not ticket_id: if not ticket_id:
ticket = None ticket = None
else: else:
ticket = get_object_or_none(LoginConfirmTicket, pk=ticket_id) ticket = get_object_or_none(Ticket, pk=ticket_id)
return ticket return ticket
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
......
...@@ -104,13 +104,13 @@ class AuthMixin: ...@@ -104,13 +104,13 @@ class AuthMixin:
raise errors.MFAFailedError(username=user.username, request=self.request) raise errors.MFAFailedError(username=user.username, request=self.request)
def get_ticket(self): def get_ticket(self):
from tickets.models import LoginConfirmTicket from tickets.models import Ticket
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))
if not ticket_id: if not ticket_id:
ticket = None ticket = None
else: else:
ticket = get_object_or_none(LoginConfirmTicket, pk=ticket_id) ticket = get_object_or_none(Ticket, pk=ticket_id)
return ticket return ticket
def get_ticket_or_create(self, confirm_setting): def get_ticket_or_create(self, confirm_setting):
......
...@@ -49,23 +49,25 @@ class LoginConfirmSetting(CommonModelMixin): ...@@ -49,23 +49,25 @@ class LoginConfirmSetting(CommonModelMixin):
return get_object_or_none(cls, user=user) return get_object_or_none(cls, user=user)
def create_confirm_ticket(self, request=None): def create_confirm_ticket(self, request=None):
from tickets.models import LoginConfirmTicket from tickets.models import Ticket
title = _('User login confirm: {}').format(self.user) title = '[' + __('Login confirm') + ']: {}'.format(self.user)
if request: if request:
remote_addr = get_request_ip(request) remote_addr = get_request_ip(request)
city = get_ip_city(remote_addr) city = get_ip_city(remote_addr)
body = _("User: {}\nIP: {}\nCity: {}\nDate: {}\n").format( body = __("{user_key}: {username}<br>"
self.user, remote_addr, city, timezone.now() "IP: {ip}<br>"
"{city_key}: {city}<br>"
"{date_key}: {date}<br>").format(
user_key=__("User"), username=self.user,
ip=remote_addr, city_key=_("City"), city=city,
date_key=__("Datetime"), date=timezone.now()
) )
else: else:
city = 'Localhost'
remote_addr = '127.0.0.1'
body = '' body = ''
reviewer = self.reviewers.all() reviewer = self.reviewers.all()
ticket = LoginConfirmTicket.objects.create( ticket = Ticket.objects.create(
user=self.user, title=title, body=body, user=self.user, title=title, body=body,
city=city, ip=remote_addr, type=Ticket.TYPE_LOGIN_CONFIRM,
type=LoginConfirmTicket.TYPE_LOGIN_CONFIRM,
) )
ticket.assignees.set(reviewer) ticket.assignees.set(reviewer)
return ticket return ticket
......
...@@ -126,7 +126,7 @@ function handleProgressBar() { ...@@ -126,7 +126,7 @@ function handleProgressBar() {
progressBarRef.attr('aria-valuenow', offset); progressBarRef.attr('aria-valuenow', offset);
} }
function cancelLoginConfirmTicket() { function cancelTicket() {
requestApi({ requestApi({
url: url, url: url,
method: "DELETE", method: "DELETE",
...@@ -144,7 +144,7 @@ function setCloseConfirm() { ...@@ -144,7 +144,7 @@ function setCloseConfirm() {
return 'Confirm'; return 'Confirm';
}; };
window.onunload = function (e) { window.onunload = function (e) {
cancelLoginConfirmTicket(); cancelTicket();
} }
} }
......
...@@ -18,7 +18,7 @@ urlpatterns = [ ...@@ -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('login-confirm-ticket/status/', api.LoginConfirmTicketStatusApi.as_view(), name='login-confirm-ticket-status'), path('login-confirm-ticket/status/', api.TicketStatusApi.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')
] ]
......
...@@ -144,16 +144,16 @@ class UserLoginWaitConfirmView(TemplateView): ...@@ -144,16 +144,16 @@ class UserLoginWaitConfirmView(TemplateView):
template_name = 'authentication/login_wait_confirm.html' template_name = 'authentication/login_wait_confirm.html'
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
from tickets.models import LoginConfirmTicket from tickets.models import Ticket
ticket_id = self.request.session.get("auth_ticket_id") ticket_id = self.request.session.get("auth_ticket_id")
if not ticket_id: if not ticket_id:
ticket = None ticket = None
else: else:
ticket = get_object_or_none(LoginConfirmTicket, pk=ticket_id) ticket = get_object_or_none(Ticket, pk=ticket_id)
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
if ticket: if ticket:
timestamp_created = datetime.datetime.timestamp(ticket.date_created) timestamp_created = datetime.datetime.timestamp(ticket.date_created)
ticket_detail_url = reverse('tickets:login-confirm-ticket-detail', kwargs={'pk': ticket_id}) ticket_detail_url = reverse('tickets:ticket-detail', kwargs={'pk': ticket_id})
msg = _("""Wait for <b>{}</b> confirm, You also can copy link to her/him <br/> msg = _("""Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>
Don't close this page""").format(ticket.assignees_display) Don't close this page""").format(ticket.assignees_display)
else: else:
......
This diff is collapsed.
...@@ -123,12 +123,10 @@ ...@@ -123,12 +123,10 @@
{% if request.user.can_admin_current_org and LOGIN_CONFIRM_ENABLE %} {% if request.user.can_admin_current_org and LOGIN_CONFIRM_ENABLE %}
<li id="tickets"> <li id="tickets">
<a> <a href="{% url 'tickets:ticket-list' %}">
<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>
</a> </a>
<ul class="nav nav-second-level">
<li id="login-confirm-tickets"><a href="{% url 'tickets:login-confirm-ticket-list' %}">{% trans 'Login confirm' %}</a></li>
</ul>
</li> </li>
{% endif %} {% endif %}
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .base import * from .ticket import *
from .login_confirm import *
# -*- coding: utf-8 -*-
#
from rest_framework_bulk import BulkModelViewSet
from common.permissions import IsValidUser
from common.mixins import CommonApiMixin
from .. import serializers, mixins
from ..models import LoginConfirmTicket
class LoginConfirmTicketViewSet(CommonApiMixin, mixins.TicketMixin, BulkModelViewSet):
serializer_class = serializers.LoginConfirmTicketSerializer
permission_classes = (IsValidUser,)
queryset = LoginConfirmTicket.objects.all()
filter_fields = ['status', 'title', 'action', 'ip']
search_fields = ['user_display', 'title', 'ip', 'city']
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
from rest_framework import viewsets from rest_framework import viewsets
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from common.permissions import IsValidUser
from common.utils import lazyproperty from common.utils import lazyproperty
from .. import serializers, models, mixins from .. import serializers, models, mixins
...@@ -11,14 +12,19 @@ from .. import serializers, models, mixins ...@@ -11,14 +12,19 @@ from .. import serializers, models, mixins
class TicketViewSet(mixins.TicketMixin, viewsets.ModelViewSet): class TicketViewSet(mixins.TicketMixin, viewsets.ModelViewSet):
serializer_class = serializers.TicketSerializer serializer_class = serializers.TicketSerializer
queryset = models.Ticket.objects.all() queryset = models.Ticket.objects.all()
permission_classes = (IsValidUser,)
filter_fields = ['status', 'title', 'action']
search_fields = ['user_display', 'title']
class TicketCommentViewSet(viewsets.ModelViewSet): class TicketCommentViewSet(viewsets.ModelViewSet):
serializer_class = serializers.CommentSerializer serializer_class = serializers.CommentSerializer
http_method_names = ['get', 'post']
def check_permissions(self, request): def check_permissions(self, request):
ticket = self.ticket ticket = self.ticket
if request.user == ticket.user or request.user in ticket.assignees.all(): if request.user == ticket.user or \
request.user in ticket.assignees.all():
return True return True
return False return False
......
# Generated by Django 2.2.5 on 2019-11-07 08:02 # Generated by Django 2.2.5 on 2019-11-15 06:57
import common.fields.model
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
...@@ -25,10 +26,12 @@ class Migration(migrations.Migration): ...@@ -25,10 +26,12 @@ class Migration(migrations.Migration):
('user_display', models.CharField(max_length=128, verbose_name='User display name')), ('user_display', models.CharField(max_length=128, verbose_name='User display name')),
('title', models.CharField(max_length=256, verbose_name='Title')), ('title', models.CharField(max_length=256, verbose_name='Title')),
('body', models.TextField(verbose_name='Body')), ('body', models.TextField(verbose_name='Body')),
('meta', common.fields.model.JsonDictTextField(default='{}', verbose_name='Meta')),
('assignee_display', models.CharField(blank=True, max_length=128, null=True, verbose_name='Assignee display name')), ('assignee_display', models.CharField(blank=True, max_length=128, null=True, verbose_name='Assignee display name')),
('assignees_display', models.CharField(blank=True, max_length=128, verbose_name='Assignees display name')), ('assignees_display', models.CharField(blank=True, max_length=128, verbose_name='Assignees display name')),
('type', models.CharField(default='general', max_length=16, verbose_name='Type')), ('type', models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm')], default='general', max_length=16, verbose_name='Type')),
('status', models.CharField(choices=[('open', 'Open'), ('closed', 'Closed')], default='open', max_length=16)), ('status', models.CharField(choices=[('open', 'Open'), ('closed', 'Closed')], default='open', max_length=16)),
('action', models.CharField(blank=True, choices=[('approve', 'Approve'), ('reject', 'Reject')], default='', max_length=16)),
('assignee', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_handled', to=settings.AUTH_USER_MODEL, verbose_name='Assignee')), ('assignee', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_handled', to=settings.AUTH_USER_MODEL, verbose_name='Assignee')),
('assignees', models.ManyToManyField(related_name='ticket_assigned', to=settings.AUTH_USER_MODEL, verbose_name='Assignees')), ('assignees', models.ManyToManyField(related_name='ticket_assigned', to=settings.AUTH_USER_MODEL, verbose_name='Assignees')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_requested', to=settings.AUTH_USER_MODEL, verbose_name='User')), ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ticket_requested', to=settings.AUTH_USER_MODEL, verbose_name='User')),
...@@ -37,19 +40,6 @@ class Migration(migrations.Migration): ...@@ -37,19 +40,6 @@ class Migration(migrations.Migration):
'ordering': ('-date_created',), 'ordering': ('-date_created',),
}, },
), ),
migrations.CreateModel(
name='LoginConfirmTicket',
fields=[
('ticket_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='tickets.Ticket')),
('ip', models.GenericIPAddressField(blank=True, null=True)),
('city', models.CharField(blank=True, default='', max_length=16)),
('action', models.CharField(blank=True, choices=[('approve', 'Approve'), ('reject', 'Reject')], default='', max_length=16)),
],
options={
'abstract': False,
},
bases=('tickets.ticket',),
),
migrations.CreateModel( migrations.CreateModel(
name='Comment', name='Comment',
fields=[ fields=[
...@@ -59,7 +49,7 @@ class Migration(migrations.Migration): ...@@ -59,7 +49,7 @@ class Migration(migrations.Migration):
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')), ('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('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')),
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tickets.Ticket')), ('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='tickets.Ticket')),
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='comments', to=settings.AUTH_USER_MODEL, verbose_name='User')), ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='comments', to=settings.AUTH_USER_MODEL, verbose_name='User')),
], ],
options={ options={
......
# Generated by Django 2.2.5 on 2019-11-14 03:05
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tickets', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='comment',
name='ticket',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='tickets.Ticket'),
),
migrations.AlterField(
model_name='ticket',
name='type',
field=models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm')], default='general', max_length=16, verbose_name='Type'),
),
]
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .base import * from .ticket import *
from .login_confirm import *
# -*- coding: utf-8 -*-
#
from django.db import models
from django.utils.translation import ugettext_lazy as _
from .base import Ticket
__all__ = ['LoginConfirmTicket']
class LoginConfirmTicket(Ticket):
ACTION_APPROVE = 'approve'
ACTION_REJECT = 'reject'
ACTION_CHOICES = (
(ACTION_APPROVE, _('Approve')),
(ACTION_REJECT, _('Reject')),
)
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()
...@@ -5,6 +5,7 @@ from django.db import models ...@@ -5,6 +5,7 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from common.mixins.models import CommonModelMixin from common.mixins.models import CommonModelMixin
from common.fields.model import JsonDictTextField
__all__ = ['Ticket', 'Comment'] __all__ = ['Ticket', 'Comment']
...@@ -22,17 +23,25 @@ class Ticket(CommonModelMixin): ...@@ -22,17 +23,25 @@ class Ticket(CommonModelMixin):
(TYPE_GENERAL, _("General")), (TYPE_GENERAL, _("General")),
(TYPE_LOGIN_CONFIRM, _("Login confirm")) (TYPE_LOGIN_CONFIRM, _("Login confirm"))
) )
ACTION_APPROVE = 'approve'
ACTION_REJECT = 'reject'
ACTION_CHOICES = (
(ACTION_APPROVE, _('Approve')),
(ACTION_REJECT, _('Reject')),
)
user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_requested', verbose_name=_("User")) user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_requested', verbose_name=_("User"))
user_display = models.CharField(max_length=128, verbose_name=_("User display name")) user_display = models.CharField(max_length=128, verbose_name=_("User display name"))
title = models.CharField(max_length=256, verbose_name=_("Title")) title = models.CharField(max_length=256, verbose_name=_("Title"))
body = models.TextField(verbose_name=_("Body")) body = models.TextField(verbose_name=_("Body"))
meta = JsonDictTextField(verbose_name=_("Meta"), default='{}')
assignee = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_handled', verbose_name=_("Assignee")) assignee = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_handled', verbose_name=_("Assignee"))
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, choices=TYPE_CHOICES, default=TYPE_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')
action = models.CharField(choices=ACTION_CHOICES, max_length=16, default='', blank=True)
def __str__(self): def __str__(self):
return '{}: {}'.format(self.user_display, self.title) return '{}: {}'.format(self.user_display, self.title)
...@@ -45,6 +54,14 @@ class Ticket(CommonModelMixin): ...@@ -45,6 +54,14 @@ class Ticket(CommonModelMixin):
def status_display(self): def status_display(self):
return self.get_status_display() return self.get_status_display()
@property
def type_display(self):
return self.get_type_display()
@property
def action_display(self):
return self.get_action_display()
def create_status_comment(self, status, user): def create_status_comment(self, status, user):
if status == self.STATUS_CLOSED: if status == self.STATUS_CLOSED:
action = _("Close") action = _("Close")
...@@ -59,6 +76,19 @@ class Ticket(CommonModelMixin): ...@@ -59,6 +76,19 @@ class Ticket(CommonModelMixin):
self.status = status self.status = status
self.save() self.save()
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()
class Meta: class Meta:
ordering = ('-date_created',) ordering = ('-date_created',)
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .base import * from .ticket import *
from .login_confirm import *
# -*- coding: utf-8 -*-
#
from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer
from common.mixins.serializers import BulkSerializerMixin
from .base import TicketSerializer
from ..models import LoginConfirmTicket
__all__ = ['LoginConfirmTicketSerializer', 'LoginConfirmTicketActionSerializer']
class LoginConfirmTicketSerializer(BulkSerializerMixin, serializers.ModelSerializer):
class Meta:
list_serializer_class = AdaptedBulkListSerializer
model = LoginConfirmTicket
fields = TicketSerializer.Meta.fields + [
'ip', 'city', 'action'
]
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)
if instance.status == instance.STATUS_CLOSED:
validated_data.pop('action')
instance = super().update(instance, validated_data)
if not instance.status == instance.STATUS_CLOSED:
instance.perform_action(action, user)
return instance
class LoginConfirmTicketActionSerializer(serializers.ModelSerializer):
comment = serializers.CharField(allow_blank=True)
class Meta:
model = LoginConfirmTicket
fields = ['action']
def update(self, instance, validated_data):
pass
def create(self, validated_data):
pass
...@@ -14,12 +14,31 @@ class TicketSerializer(serializers.ModelSerializer): ...@@ -14,12 +14,31 @@ class TicketSerializer(serializers.ModelSerializer):
'id', 'user', 'user_display', 'title', 'body', 'id', 'user', 'user_display', 'title', 'body',
'assignees', 'assignees_display', 'assignees', 'assignees_display',
'status', 'date_created', 'date_updated', 'status', 'date_created', 'date_updated',
'type_display', 'action_display',
] ]
read_only_fields = [ read_only_fields = [
'user_display', 'assignees_display', 'user_display', 'assignees_display',
'date_created', 'date_updated', 'date_created', 'date_updated',
] ]
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)
if instance.status == instance.STATUS_CLOSED:
validated_data.pop('action')
instance = super().update(instance, validated_data)
if not instance.status == instance.STATUS_CLOSED and action:
instance.perform_action(action, user)
return instance
class CurrentTicket(object): class CurrentTicket(object):
ticket = None ticket = None
......
...@@ -4,24 +4,24 @@ from django.dispatch import receiver ...@@ -4,24 +4,24 @@ from django.dispatch import receiver
from django.db.models.signals import m2m_changed, post_save, pre_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, Ticket, Comment from .models import Ticket, Comment
from .utils import ( from .utils import (
send_login_confirm_ticket_mail_to_assignees, send_new_ticket_mail_to_assignees,
send_login_confirm_action_mail_to_user send_ticket_action_mail_to_user
) )
logger = get_logger(__name__) logger = get_logger(__name__)
@receiver(m2m_changed, sender=LoginConfirmTicket.assignees.through) @receiver(m2m_changed, sender=Ticket.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,
reverse=False, model=None, reverse=False, model=None,
pk_set=None, **kwargs): 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_new_ticket_mail_to_assignees(instance, assignees)
if action.startswith('post') and not reverse: if action.startswith('post') and not reverse:
instance.assignees_display = ', '.join([ instance.assignees_display = ', '.join([
str(u) for u in instance.assignees.all() str(u) for u in instance.assignees.all()
...@@ -29,15 +29,15 @@ def on_login_confirm_ticket_assignees_set(sender, instance=None, action=None, ...@@ -29,15 +29,15 @@ def on_login_confirm_ticket_assignees_set(sender, instance=None, action=None,
instance.save() instance.save()
@receiver(post_save, sender=LoginConfirmTicket) @receiver(post_save, sender=Ticket)
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 == "open": 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_ticket_action_mail_to_user(instance)
@receiver(pre_save, sender=LoginConfirmTicket) @receiver(pre_save, sender=Ticket)
def on_ticket_create(sender, instance=None, **kwargs): def on_ticket_create(sender, instance=None, **kwargs):
instance.user_display = str(instance.user) instance.user_display = str(instance.user)
if instance.assignee: if instance.assignee:
......
{% extends 'tickets/ticket_detail.html' %}
{% load static %}
{% load i18n %}
{% 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 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 %}
{% extends '_base_list.html' %}
{% load i18n static %}
{% block table_search %}
{% endblock %}
{% block custom_head_css_js %}
{% endblock %}
{% block table_container %}
<table class="table table-striped table-bordered table-hover " id="login_confirm_ticket_list_table" >
<thead>
<tr>
<th class="text-center">
<input id="" type="checkbox" class="ipt_check_all">
</th>
<th class="text-center">{% trans 'Title' %}</th>
<th class="text-center">{% trans 'User' %}</th>
<th class="text-center">{% trans 'Status' %}</th>
<th class="text-center">{% trans 'Datetime' %}</th>
<th class="text-center">{% trans 'Action' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div id="actions" class="hide">
<div class="input-group">
<select class="form-control m-b" style="width: auto" id="slct_bulk_update">
<option value="approve">{% trans 'Approve selected' %}</option>
<option value="reject">{% trans 'Reject selected' %}</option>
</select>
<div class="input-group-btn pull-left" style="padding-left: 5px;">
<button id='btn_bulk_update' style="height: 32px;" class="btn btn-sm btn-primary">
{% trans 'Submit' %}
</button>
</div>
</div>
</div>
{% include '_filter_dropdown.html' %}
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
<script>
var ticketTable = 0;
function initTable() {
var options = {
ele: $('#login_confirm_ticket_list_table'),
oSearch: {sSearch: "status:open"},
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detailBtn = '<a href="{% url "tickets:login-confirm-ticket-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detailBtn.replace("{{ DEFAULT_PK }}", rowData.id));
}},
{targets: 3, createdCell: function (td, cellData, rowData) {
if (cellData === "approve") {
$(td).html('<i class="fa fa-check text-navy"></i>')
} else if (cellData === "reject") {
$(td).html('<i class="fa fa-times text-danger"></i>')
} else if (cellData === "open") {
$(td).html('<i class="fa fa-spinner text-info"></i>')
} else {
$(td).html('<i class="fa fa-circle text-info"></i>')
}
}},
{targets: 4, createdCell: function (td, cellData) {
var d = toSafeLocalDateStr(cellData);
$(td).html(d)
}},
{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);
var acceptBtnRef = $(acceptBtn);
var rejectBtnRef = $(rejectBtn);
if (rowData.action !== "") {
acceptBtnRef.attr('disabled', 'disabled');
rejectBtnRef.attr('disabled', 'disabled');
}
var btn = acceptBtnRef.prop('outerHTML') + ' ' + rejectBtnRef.prop('outerHTML');
$(td).html(btn)
}}],
ajax_url: '{% url "api-tickets:login-confirm-ticket-list" %}',
columns: [
{data: "id"}, {data: "title"},
{data: "user_display"},
{data: "action", width: "40px"},
{data: "date_created", width: "120px"},
{data: "id", orderable: false}
],
op_html: $('#actions').html()
};
ticketTable = jumpserver.initServerSideDataTable(options);
return ticketTable
}
$(document).ready(function(){
initTable();
var menu = [
{title: "IP", value: "ip"},
{title: "{% trans 'Title' %}", value: "title"},
{title: "{% trans 'User' %}", value: "user_display"},
{title: "{% trans 'Status' %}", value: "status", submenu: [
{title: "{% trans 'Open' %}", value: "open"},
{title: "{% trans 'Closed' %}", value: "closed"},
]},
{title: "{% trans 'Action' %}", value: "action", submenu: [
{title: "{% trans 'Approve' %}", value: "approve"},
{title: "{% trans 'Reject' %}", value: "reject"},
]},
];
initTableFilterDropdown('#login_confirm_ticket_list_table_filter input', menu)
}).on('click', '.btn-action', function () {
var ticketId = $(this).data("uid");
var action = $(this).data('action');
var ticketDetailUrl = "{% url 'api-tickets:login-confirm-ticket-detail' pk=DEFAULT_PK %}";
ticketDetailUrl = ticketDetailUrl.replace("{{ DEFAULT_PK }}", ticketId);
var data = {
url: ticketDetailUrl,
body: JSON.stringify({action: action}),
method: "PATCH",
success: reloadPage
};
requestApi(data);
}).on('click', '#btn_bulk_update', function () {
var action = $('#slct_bulk_update').val();
var idList = ticketTable.selected;
if (idList.length === 0) {
return false;
}
var theUrl = "{% url 'api-tickets:login-confirm-ticket-list' %}";
function doAction(action) {
var data = [];
$.each(idList, function(index, object_id) {
var obj = {
"pk": object_id, "action": action
};
data.push(obj);
});
requestApi({
url: theUrl,
method: 'PATCH',
body: JSON.stringify(data),
success: function (){
$(".ipt_check_all").prop("checked", false)
ticketTable.ajax.reload();
}
});
}
doAction(action)
})
</script>
{% endblock %}
...@@ -72,7 +72,6 @@ ...@@ -72,7 +72,6 @@
</div> </div>
</div> </div>
{% for comment in object.comments.all %} {% for comment in object.comments.all %}
<div class="feed-element"> <div class="feed-element">
<a href="#" class="pull-left"> <a href="#" class="pull-left">
<img alt="image" class="img-circle" src="{% static 'img/avatar/user.png'%}" > <img alt="image" class="img-circle" src="{% static 'img/avatar/user.png'%}" >
...@@ -97,14 +96,12 @@ ...@@ -97,14 +96,12 @@
</div> </div>
</div> </div>
<div class="text-right"> <div class="text-right">
{% block action %} {% if object.type == object.TYPE_LOGIN_CONFIRM %}
{% endblock %} <a class="btn btn-sm btn-primary btn-update btn-action" data-action="approve"><i class="fa fa-check"></i> {% trans 'Approve' %}</a>
{% block status %} <a class="btn btn-sm btn-warning btn-update btn-action" data-action="reject"><i class="fa fa-ban"></i> {% trans 'Reject' %}</a>
<a class="btn btn-sm btn-danger btn-update btn-status" data-uid="close"><i class="fa fa-times"></i> {% trans 'Close' %}</a> {% endif %}
{% endblock %} <a class="btn btn-sm btn-danger btn-update btn-status" data-uid="closed"><i class="fa fa-times"></i> {% trans 'Close' %}</a>
{% 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> <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>
...@@ -127,6 +124,7 @@ var ticketId = "{{ object.id }}"; ...@@ -127,6 +124,7 @@ var ticketId = "{{ object.id }}";
var status = "{{ object.status }}"; var status = "{{ object.status }}";
var commentUrl = "{% url 'api-tickets:ticket-comment-list' ticket_id=object.id %}"; var commentUrl = "{% url 'api-tickets:ticket-comment-list' ticket_id=object.id %}";
var ticketDetailUrl = "{% url 'api-tickets:ticket-detail' pk=object.id %}";
function createComment(successCallback) { function createComment(successCallback) {
var commentText = $("#comment").val(); var commentText = $("#comment").val();
...@@ -158,5 +156,26 @@ $(document).ready(function () { ...@@ -158,5 +156,26 @@ $(document).ready(function () {
.on('click', '.btn-comment', function () { .on('click', '.btn-comment', function () {
createComment(); createComment();
}) })
.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);
})
.on('click', '.btn-status', function () {
var status = $(this).data('uid');
var data = {
url: ticketDetailUrl,
body: JSON.stringify({status: status}),
method: "PATCH",
success: reloadPage
};
requestApi(data);
})
</script> </script>
{% endblock %} {% endblock %}
{% extends 'base.html' %}
{% load i18n static %}
{% block content %}
<div class="wrapper wrapper-content animated fadeIn">
<div class="col-lg-12">
<div class="tabs-container">
<ul class="nav nav-tabs">
<li {% if not assign %} class="active" {% endif %}><a href="{% url 'tickets:ticket-list' %}"> {% trans 'My tickets' %}</a></li>
<li {% if assign %}class="active" {% endif %}><a href="{% url 'tickets:ticket-list' %}?assign=1">{% trans 'Assigned me' %}</a></li>
</ul>
<div class="tab-content">
<div id="my-tickets" class="tab-pane active">
<div class="panel-body">
<table class="table table-striped table-bordered table-hover" id="ticket-list-table" >
<thead>
<tr>
<th class="text-center">
<input id="" type="checkbox" class="ipt_check_all">
</th>
<th class="text-center">{% trans 'Title' %}</th>
<th class="text-center">{% trans 'User' %}</th>
<th class="text-center">{% trans 'Type' %}</th>
<th class="text-center">{% trans 'Status' %}</th>
<th class="text-center">{% trans 'Datetime' %}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% include '_filter_dropdown.html' %}
{% endblock %}
{% block content_bottom_left %}{% endblock %}
{% block custom_foot_js %}
<script>
var assignedTable, myTable, listUrl;
{% if assign %}
listUrl = '{% url "api-tickets:ticket-list" %}?assign=1';
{% else %}
listUrl = '{% url "api-tickets:ticket-list" %}?assign=0';
{% endif %}
function initTable() {
var options = {
ele: $('#ticket-list-table'),
oSearch: {sSearch: "status:open"},
columnDefs: [
{targets: 1, createdCell: function (td, cellData, rowData) {
cellData = htmlEscape(cellData);
var detailBtn = '<a href="{% url "tickets:ticket-detail" pk=DEFAULT_PK %}">' + cellData + '</a>';
$(td).html(detailBtn.replace("{{ DEFAULT_PK }}", rowData.id));
}},
{targets: 4, createdCell: function (td, cellData) {
if (cellData === "open") {
$(td).html('<i class="fa fa-check-circle-o text-navy"></i>');
} else {
$(td).html('<i class="fa fa-times-circle-o text-danger"></i>')
}
}},
{targets: 4, createdCell: function (td, cellData) {
var d = toSafeLocalDateStr(cellData);
$(td).html(d)
}},
],
ajax_url: listUrl,
columns: [
{data: "id"}, {data: "title"},
{data: "user_display"}, {data: "type_display"},
{data: "status", width: "40px"},
{data: "date_created"},
],
op_html: $('#actions').html()
};
myTable = jumpserver.initServerSideDataTable(options);
return myTable
}
$(document).ready(function(){
initTable();
var menu = [
{title: "IP", value: "ip"},
{title: "{% trans 'Title' %}", value: "title"},
{title: "{% trans 'User' %}", value: "user_display"},
{title: "{% trans 'Status' %}", value: "status", submenu: [
{title: "{% trans 'Open' %}", value: "open"},
{title: "{% trans 'Closed' %}", value: "closed"},
]},
{title: "{% trans 'Action' %}", value: "action", submenu: [
{title: "{% trans 'Approve' %}", value: "approve"},
{title: "{% trans 'Reject' %}", value: "reject"},
]},
];
initTableFilterDropdown('#assigned-ticket-list-table input', menu)
}).on('click', '.btn-action', function () {
var ticketId = $(this).data("uid");
var action = $(this).data('action');
var ticketDetailUrl = "{% url 'api-tickets:ticket-detail' pk=DEFAULT_PK %}";
ticketDetailUrl = ticketDetailUrl.replace("{{ DEFAULT_PK }}", ticketId);
var data = {
url: ticketDetailUrl,
body: JSON.stringify({action: action}),
method: "PATCH",
success: reloadPage
};
requestApi(data);
})
</script>
{% endblock %}
...@@ -9,7 +9,6 @@ router = BulkRouter() ...@@ -9,7 +9,6 @@ router = BulkRouter()
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('tickets/(?P<ticket_id>[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment')
router.register('login-confirm-tickets', api.LoginConfirmTicketViewSet, 'login-confirm-ticket')
urlpatterns = [ urlpatterns = [
......
...@@ -6,6 +6,6 @@ from .. import views ...@@ -6,6 +6,6 @@ from .. import views
app_name = 'tickets' app_name = 'tickets'
urlpatterns = [ urlpatterns = [
path('login-confirm-tickets/', views.LoginConfirmTicketListView.as_view(), name='login-confirm-ticket-list'), path('tickets/', views.TicketListView.as_view(), name='ticket-list'),
path('login-confirm-tickets/<uuid:pk>/', views.LoginConfirmTicketDetailView.as_view(), name='login-confirm-ticket-detail') path('tickets/<uuid:pk>/', views.TicketDetailView.as_view(), name='ticket-detail'),
] ]
...@@ -9,37 +9,29 @@ from common.tasks import send_mail_async ...@@ -9,37 +9,29 @@ from common.tasks import send_mail_async
logger = get_logger(__name__) logger = get_logger(__name__)
def send_login_confirm_ticket_mail_to_assignees(ticket, assignees): def send_new_ticket_mail_to_assignees(ticket, assignees):
recipient_list = [user.email for user in assignees] recipient_list = [user.email for user in assignees]
user = ticket.user user = ticket.user
if not recipient_list: if not recipient_list:
logger.error("Ticket not has assignees: {}".format(ticket.id)) logger.error("Ticket not has assignees: {}".format(ticket.id))
return return
subject = '{}: {}'.format(_("New ticket"), ticket.title) subject = '{}: {}'.format(_("New ticket"), ticket.title)
detail_url = reverse('tickets:login-confirm-ticket-detail', detail_url = reverse('tickets:ticket-detail',
kwargs={'pk': ticket.id}, external=True) kwargs={'pk': ticket.id}, external=True)
message = _(""" message = _("""
<div> <div>
<p>Your has a new ticket</p> <p>Your has a new ticket</p>
<div> <div>
<b>Title:</b> {ticket.title} {body}
<br/>
<b>User:</b> {user}
<br/>
<b>Assignees:</b> {ticket.assignees_display}
<br/>
<b>City:</b> {ticket.city}
<br/>
<b>IP:</b> {ticket.ip}
<br/> <br/>
<a href={url}>click here to review</a> <a href={url}>click here to review</a>
</div> </div>
</div> </div>
""").format(ticket=ticket, user=user, url=detail_url) """).format(body=ticket.body, user=user, url=detail_url)
send_mail_async.delay(subject, message, recipient_list, html_message=message) send_mail_async.delay(subject, message, recipient_list, html_message=message)
def send_login_confirm_action_mail_to_user(ticket): def send_ticket_action_mail_to_user(ticket):
if not ticket.user: if not ticket.user:
logger.error("Ticket not has user: {}".format(ticket.id)) logger.error("Ticket not has user: {}".format(ticket.id))
return return
......
...@@ -2,32 +2,34 @@ from django.views.generic import TemplateView, DetailView ...@@ -2,32 +2,34 @@ 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, IsValidUser from common.permissions import PermissionsMixin, IsValidUser
from .models import LoginConfirmTicket from .models import Ticket
from . import mixins from . import mixins
class LoginConfirmTicketListView(PermissionsMixin, TemplateView): class TicketListView(PermissionsMixin, TemplateView):
template_name = 'tickets/login_confirm_ticket_list.html' template_name = 'tickets/ticket_list.html'
permission_classes = (IsValidUser,) permission_classes = (IsValidUser,)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
assign = self.request.GET.get('assign', '0') == '1'
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context.update({ context.update({
'app': _("Tickets"), 'app': _("Tickets"),
'action': _("Login confirm ticket list") 'action': _("Ticket list"),
'assign': assign,
}) })
return context return context
class LoginConfirmTicketDetailView(PermissionsMixin, mixins.TicketMixin, DetailView): class TicketDetailView(PermissionsMixin, mixins.TicketMixin, DetailView):
template_name = 'tickets/login_confirm_ticket_detail.html' template_name = 'tickets/ticket_detail.html'
queryset = LoginConfirmTicket.objects.all()
permission_classes = (IsValidUser,) permission_classes = (IsValidUser,)
queryset = Ticket.objects.all()
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context.update({ context.update({
'app': _("Tickets"), 'app': _("Tickets"),
'action': _("Login confirm ticket detail") 'action': _("Ticket detail")
}) })
return context return context
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment