Commit ebe129b3 authored by ibuler's avatar ibuler

[Update] 修改工单

parent 18f88647
......@@ -12,7 +12,7 @@ from ..models import LoginConfirmSetting
from ..serializers import LoginConfirmSettingSerializer
from .. import errors, mixins
__all__ = ['LoginConfirmSettingUpdateApi', 'LoginConfirmTicketStatusApi']
__all__ = ['LoginConfirmSettingUpdateApi', 'TicketStatusApi']
logger = get_logger(__name__)
......@@ -31,17 +31,17 @@ class LoginConfirmSettingUpdateApi(UpdateAPIView):
return s
class LoginConfirmTicketStatusApi(mixins.AuthMixin, APIView):
class TicketStatusApi(mixins.AuthMixin, APIView):
permission_classes = ()
def get_ticket(self):
from tickets.models import LoginConfirmTicket
from tickets.models import Ticket
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)
ticket = get_object_or_none(Ticket, pk=ticket_id)
return ticket
def get(self, request, *args, **kwargs):
......
......@@ -104,13 +104,13 @@ class AuthMixin:
raise errors.MFAFailedError(username=user.username, request=self.request)
def get_ticket(self):
from tickets.models import LoginConfirmTicket
from tickets.models import Ticket
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)
ticket = get_object_or_none(Ticket, pk=ticket_id)
return ticket
def get_ticket_or_create(self, confirm_setting):
......
......@@ -49,23 +49,25 @@ class LoginConfirmSetting(CommonModelMixin):
return get_object_or_none(cls, user=user)
def create_confirm_ticket(self, request=None):
from tickets.models import LoginConfirmTicket
title = _('User login confirm: {}').format(self.user)
from tickets.models import Ticket
title = '[' + __('Login confirm') + ']: {}'.format(self.user)
if request:
remote_addr = get_request_ip(request)
city = get_ip_city(remote_addr)
body = _("User: {}\nIP: {}\nCity: {}\nDate: {}\n").format(
self.user, remote_addr, city, timezone.now()
body = __("{user_key}: {username}<br>"
"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:
city = 'Localhost'
remote_addr = '127.0.0.1'
body = ''
reviewer = self.reviewers.all()
ticket = LoginConfirmTicket.objects.create(
ticket = Ticket.objects.create(
user=self.user, title=title, body=body,
city=city, ip=remote_addr,
type=LoginConfirmTicket.TYPE_LOGIN_CONFIRM,
type=Ticket.TYPE_LOGIN_CONFIRM,
)
ticket.assignees.set(reviewer)
return ticket
......
......@@ -126,7 +126,7 @@ function handleProgressBar() {
progressBarRef.attr('aria-valuenow', offset);
}
function cancelLoginConfirmTicket() {
function cancelTicket() {
requestApi({
url: url,
method: "DELETE",
......@@ -144,7 +144,7 @@ function setCloseConfirm() {
return 'Confirm';
};
window.onunload = function (e) {
cancelLoginConfirmTicket();
cancelTicket();
}
}
......
......@@ -18,7 +18,7 @@ urlpatterns = [
path('connection-token/',
api.UserConnectionTokenApi.as_view(), name='connection-token'),
path('otp/verify/', api.UserOtpVerifyApi.as_view(), name='user-otp-verify'),
path('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')
]
......
......@@ -144,16 +144,16 @@ class UserLoginWaitConfirmView(TemplateView):
template_name = 'authentication/login_wait_confirm.html'
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")
if not ticket_id:
ticket = None
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)
if ticket:
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/>
Don't close this page""").format(ticket.assignees_display)
else:
......
This diff is collapsed.
......@@ -123,12 +123,10 @@
{% if request.user.can_admin_current_org and LOGIN_CONFIRM_ENABLE %}
<li id="tickets">
<a>
<i class="fa fa-check-square-o" style="width: 14px"></i> <span class="nav-label">{% trans 'Tickets' %}</span><span class="fa arrow"></span>
<a href="{% url 'tickets:ticket-list' %}">
<i class="fa fa-check-square-o" style="width: 14px"></i>
<span class="nav-label">{% trans 'Tickets' %}</span>
</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>
{% endif %}
......
# -*- coding: utf-8 -*-
#
from .base import *
from .login_confirm import *
from .ticket 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 @@
from rest_framework import viewsets
from django.shortcuts import get_object_or_404
from common.permissions import IsValidUser
from common.utils import lazyproperty
from .. import serializers, models, mixins
......@@ -11,14 +12,19 @@ from .. import serializers, models, mixins
class TicketViewSet(mixins.TicketMixin, viewsets.ModelViewSet):
serializer_class = serializers.TicketSerializer
queryset = models.Ticket.objects.all()
permission_classes = (IsValidUser,)
filter_fields = ['status', 'title', 'action']
search_fields = ['user_display', 'title']
class TicketCommentViewSet(viewsets.ModelViewSet):
serializer_class = serializers.CommentSerializer
http_method_names = ['get', 'post']
def check_permissions(self, request):
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 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.db import migrations, models
import django.db.models.deletion
......@@ -25,10 +26,12 @@ class Migration(migrations.Migration):
('user_display', models.CharField(max_length=128, verbose_name='User display name')),
('title', models.CharField(max_length=256, verbose_name='Title')),
('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')),
('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)),
('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')),
('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')),
......@@ -37,19 +40,6 @@ class Migration(migrations.Migration):
'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(
name='Comment',
fields=[
......@@ -59,7 +49,7 @@ class Migration(migrations.Migration):
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('user_display', models.CharField(max_length=128, verbose_name='User display name')),
('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')),
],
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 -*-
#
from .base import *
from .login_confirm import *
from .ticket 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
from django.utils.translation import ugettext_lazy as _
from common.mixins.models import CommonModelMixin
from common.fields.model import JsonDictTextField
__all__ = ['Ticket', 'Comment']
......@@ -22,17 +23,25 @@ class Ticket(CommonModelMixin):
(TYPE_GENERAL, _("General")),
(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_display = models.CharField(max_length=128, verbose_name=_("User display name"))
title = models.CharField(max_length=256, verbose_name=_("Title"))
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_display = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Assignee display name"))
assignees = models.ManyToManyField('users.User', related_name='%(class)s_assigned', verbose_name=_("Assignees"))
assignees_display = models.CharField(max_length=128, verbose_name=_("Assignees display name"), blank=True)
type = models.CharField(max_length=16, choices=TYPE_CHOICES, default=TYPE_GENERAL, verbose_name=_("Type"))
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):
return '{}: {}'.format(self.user_display, self.title)
......@@ -45,6 +54,14 @@ class Ticket(CommonModelMixin):
def status_display(self):
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):
if status == self.STATUS_CLOSED:
action = _("Close")
......@@ -59,6 +76,19 @@ class Ticket(CommonModelMixin):
self.status = status
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:
ordering = ('-date_created',)
......
# -*- coding: utf-8 -*-
#
from .base import *
from .login_confirm import *
from .ticket 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):
'id', 'user', 'user_display', 'title', 'body',
'assignees', 'assignees_display',
'status', 'date_created', 'date_updated',
'type_display', 'action_display',
]
read_only_fields = [
'user_display', 'assignees_display',
'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):
ticket = None
......
......@@ -4,24 +4,24 @@ from django.dispatch import receiver
from django.db.models.signals import m2m_changed, post_save, pre_save
from common.utils import get_logger
from .models import LoginConfirmTicket, Ticket, Comment
from .models import Ticket, Comment
from .utils import (
send_login_confirm_ticket_mail_to_assignees,
send_login_confirm_action_mail_to_user
send_new_ticket_mail_to_assignees,
send_ticket_action_mail_to_user
)
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,
reverse=False, model=None,
pk_set=None, **kwargs):
if action == 'post_add':
logger.debug('New ticket create, send mail: {}'.format(instance.id))
assignees = model.objects.filter(pk__in=pk_set)
send_login_confirm_ticket_mail_to_assignees(instance, assignees)
send_new_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()
......@@ -29,15 +29,15 @@ def on_login_confirm_ticket_assignees_set(sender, instance=None, action=None,
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):
if created or instance.status == "open":
return
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):
instance.user_display = str(instance.user)
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 @@
</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'%}" >
......@@ -97,14 +96,12 @@
</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 %}
{% if object.type == object.TYPE_LOGIN_CONFIRM %}
<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-warning btn-update btn-action" data-action="reject"><i class="fa fa-ban"></i> {% trans 'Reject' %}</a>
{% endif %}
<a class="btn btn-sm btn-danger btn-update btn-status" data-uid="closed"><i class="fa fa-times"></i> {% trans 'Close' %}</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>
......@@ -127,6 +124,7 @@ var ticketId = "{{ object.id }}";
var status = "{{ object.status }}";
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) {
var commentText = $("#comment").val();
......@@ -158,5 +156,26 @@ $(document).ready(function () {
.on('click', '.btn-comment', function () {
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>
{% 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()
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')
urlpatterns = [
......
......@@ -6,6 +6,6 @@ from .. import views
app_name = 'tickets'
urlpatterns = [
path('login-confirm-tickets/', views.LoginConfirmTicketListView.as_view(), name='login-confirm-ticket-list'),
path('login-confirm-tickets/<uuid:pk>/', views.LoginConfirmTicketDetailView.as_view(), name='login-confirm-ticket-detail')
path('tickets/', views.TicketListView.as_view(), name='ticket-list'),
path('tickets/<uuid:pk>/', views.TicketDetailView.as_view(), name='ticket-detail'),
]
......@@ -9,37 +9,29 @@ from common.tasks import send_mail_async
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]
user = ticket.user
if not recipient_list:
logger.error("Ticket not has assignees: {}".format(ticket.id))
return
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)
message = _("""
<div>
<p>Your has a new ticket</p>
<div>
<b>Title:</b> {ticket.title}
<br/>
<b>User:</b> {user}
<br/>
<b>Assignees:</b> {ticket.assignees_display}
<br/>
<b>City:</b> {ticket.city}
<br/>
<b>IP:</b> {ticket.ip}
{body}
<br/>
<a href={url}>click here to review</a>
</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)
def send_login_confirm_action_mail_to_user(ticket):
def send_ticket_action_mail_to_user(ticket):
if not ticket.user:
logger.error("Ticket not has user: {}".format(ticket.id))
return
......
......@@ -2,32 +2,34 @@ from django.views.generic import TemplateView, DetailView
from django.utils.translation import ugettext as _
from common.permissions import PermissionsMixin, IsValidUser
from .models import LoginConfirmTicket
from .models import Ticket
from . import mixins
class LoginConfirmTicketListView(PermissionsMixin, TemplateView):
template_name = 'tickets/login_confirm_ticket_list.html'
class TicketListView(PermissionsMixin, TemplateView):
template_name = 'tickets/ticket_list.html'
permission_classes = (IsValidUser,)
def get_context_data(self, **kwargs):
assign = self.request.GET.get('assign', '0') == '1'
context = super().get_context_data(**kwargs)
context.update({
'app': _("Tickets"),
'action': _("Login confirm ticket list")
'action': _("Ticket list"),
'assign': assign,
})
return context
class LoginConfirmTicketDetailView(PermissionsMixin, mixins.TicketMixin, DetailView):
template_name = 'tickets/login_confirm_ticket_detail.html'
queryset = LoginConfirmTicket.objects.all()
class TicketDetailView(PermissionsMixin, mixins.TicketMixin, DetailView):
template_name = 'tickets/ticket_detail.html'
permission_classes = (IsValidUser,)
queryset = Ticket.objects.all()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({
'app': _("Tickets"),
'action': _("Login confirm ticket detail")
'action': _("Ticket detail")
})
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