[Update] 修改工单

pull/3454/head
ibuler 2019-11-15 18:55:35 +08:00
parent 18f8864720
commit ebe129b3c2
29 changed files with 433 additions and 547 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Jumpserver 0.3.3\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-14 19:29+0800\n"
"POT-Creation-Date: 2019-11-15 17:39+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: ibuler <ibuler@qq.com>\n"
"Language-Team: Jumpserver team<ibuler@qq.com>\n"
@ -354,7 +354,6 @@ msgstr "重置"
#: terminal/templates/terminal/command_list.html:47
#: terminal/templates/terminal/session_list.html:52
#: terminal/templates/terminal/terminal_update.html:46
#: tickets/templates/tickets/login_confirm_ticket_list.html:32
#: users/templates/users/_user.html:52
#: users/templates/users/forgot_password.html:42
#: users/templates/users/user_bulk_update.html:24
@ -528,8 +527,7 @@ msgstr "创建远程应用"
#: settings/templates/settings/terminal_setting.html:107
#: terminal/templates/terminal/session_list.html:36
#: terminal/templates/terminal/terminal_list.html:36
#: tickets/templates/tickets/login_confirm_ticket_list.html:18
#: tickets/templates/tickets/login_confirm_ticket_list.html:106
#: tickets/templates/tickets/ticket_list.html:93
#: users/templates/users/_granted_assets.html:34
#: users/templates/users/user_group_list.html:38
#: users/templates/users/user_list.html:41
@ -1046,7 +1044,8 @@ msgstr "过滤器"
#: settings/templates/settings/replay_storage_create.html:31
#: settings/templates/settings/terminal_setting.html:84
#: settings/templates/settings/terminal_setting.html:106
#: tickets/models/base.py:34 tickets/templates/tickets/ticket_detail.html:33
#: tickets/models/ticket.py:42 tickets/templates/tickets/ticket_detail.html:33
#: tickets/templates/tickets/ticket_list.html:23
msgid "Type"
msgstr "类型"
@ -1127,11 +1126,10 @@ msgstr "默认资产组"
#: terminal/models.py:156 terminal/templates/terminal/command_list.html:29
#: terminal/templates/terminal/command_list.html:65
#: terminal/templates/terminal/session_list.html:27
#: terminal/templates/terminal/session_list.html:71 tickets/models/base.py:25
#: tickets/models/base.py:68
#: tickets/templates/tickets/login_confirm_ticket_list.html:15
#: tickets/templates/tickets/login_confirm_ticket_list.html:101
#: tickets/templates/tickets/ticket_detail.html:32 users/forms.py:339
#: terminal/templates/terminal/session_list.html:71 tickets/models/ticket.py:32
#: tickets/models/ticket.py:85 tickets/templates/tickets/ticket_detail.html:32
#: tickets/templates/tickets/ticket_list.html:22
#: tickets/templates/tickets/ticket_list.html:88 users/forms.py:339
#: users/models/user.py:149 users/models/user.py:165 users/models/user.py:537
#: users/serializers/group.py:21
#: users/templates/users/user_group_detail.html:78
@ -1506,7 +1504,7 @@ msgstr "获取认证信息错误"
#: authentication/templates/authentication/_access_key_modal.html:142
#: authentication/templates/authentication/_mfa_confirm_modal.html:53
#: settings/templates/settings/_ldap_list_users_modal.html:171
#: templates/_modal.html:22 tickets/models/base.py:50
#: templates/_modal.html:22 tickets/models/ticket.py:67
#: tickets/templates/tickets/ticket_detail.html:103
msgid "Close"
msgstr "关闭"
@ -1517,7 +1515,7 @@ msgstr "关闭"
#: ops/templates/ops/task_adhoc.html:63
#: terminal/templates/terminal/command_list.html:33
#: terminal/templates/terminal/session_detail.html:50
#: tickets/templates/tickets/login_confirm_ticket_list.html:17
#: tickets/templates/tickets/ticket_list.html:25
msgid "Datetime"
msgstr "日期"
@ -1717,7 +1715,7 @@ msgstr "Jumpserver 使用该用户来 `推送系统用户`、`获取资产硬件
#: users/templates/users/user_group_list.html:10
#: users/templates/users/user_list.html:10
#: xpack/plugins/gathered_user/templates/gathered_user/gathered_user_list.html:49
#: xpack/plugins/vault/templates/vault/vault.html:55
#: xpack/plugins/vault/templates/vault/vault.html:47
msgid "Export"
msgstr "导出"
@ -1728,7 +1726,7 @@ msgstr "导出"
#: users/templates/users/user_group_list.html:15
#: users/templates/users/user_list.html:15
#: xpack/plugins/license/templates/license/license_detail.html:110
#: xpack/plugins/vault/templates/vault/vault.html:60
#: xpack/plugins/vault/templates/vault/vault.html:52
msgid "Import"
msgstr "导入"
@ -1747,7 +1745,7 @@ msgstr "创建管理用户"
#: users/templates/users/user_group_list.html:195
#: users/templates/users/user_list.html:165
#: users/templates/users/user_list.html:197
#: xpack/plugins/vault/templates/vault/vault.html:224
#: xpack/plugins/vault/templates/vault/vault.html:200
msgid "Please select file"
msgstr "选择文件"
@ -2239,7 +2237,7 @@ msgstr "成功"
#: audits/models.py:33
#: authentication/templates/authentication/_access_key_modal.html:22
#: xpack/plugins/vault/templates/vault/vault.html:46
#: xpack/plugins/vault/templates/vault/vault.html:38
msgid "Create"
msgstr "创建"
@ -2306,9 +2304,9 @@ msgid "Reason"
msgstr "原因"
#: audits/models.py:88 audits/templates/audits/login_log_list.html:64
#: tickets/templates/tickets/login_confirm_ticket_list.html:16
#: tickets/templates/tickets/login_confirm_ticket_list.html:102
#: tickets/templates/tickets/ticket_detail.html:34
#: tickets/templates/tickets/ticket_list.html:24
#: tickets/templates/tickets/ticket_list.html:89
#: xpack/plugins/cloud/models.py:275 xpack/plugins/cloud/models.py:310
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_history.html:70
#: xpack/plugins/cloud/templates/cloud/sync_instance_task_instance.html:65
@ -2368,7 +2366,7 @@ msgstr "ID"
msgid "UA"
msgstr "Agent"
#: audits/templates/audits/login_log_list.html:61
#: audits/templates/audits/login_log_list.html:61 authentication/models.py:62
msgid "City"
msgstr "城市"
@ -2379,23 +2377,23 @@ msgid "Date"
msgstr "日期"
#: audits/views.py:86 audits/views.py:130 audits/views.py:167
#: audits/views.py:212 audits/views.py:244 templates/_nav.html:139
#: audits/views.py:212 audits/views.py:244 templates/_nav.html:137
msgid "Audits"
msgstr "日志审计"
#: audits/views.py:87 templates/_nav.html:143
#: audits/views.py:87 templates/_nav.html:141
msgid "FTP log"
msgstr "FTP日志"
#: audits/views.py:131 templates/_nav.html:144
#: audits/views.py:131 templates/_nav.html:142
msgid "Operate log"
msgstr "操作日志"
#: audits/views.py:168 templates/_nav.html:145
#: audits/views.py:168 templates/_nav.html:143
msgid "Password change log"
msgstr "改密日志"
#: audits/views.py:213 templates/_nav.html:142
#: audits/views.py:213 templates/_nav.html:140
msgid "Login log"
msgstr "登录日志"
@ -2530,22 +2528,6 @@ msgstr "ssh密钥"
msgid "Reviewers"
msgstr "审批人"
#: authentication/models.py:53
msgid "User login confirm: {}"
msgstr "用户登录复核: {}"
#: authentication/models.py:57
msgid ""
"User: {}\n"
"IP: {}\n"
"City: {}\n"
"Date: {}\n"
msgstr ""
"用户: {}\n"
"IP: {}\n"
"城市: {}\n"
"日期: {}\n"
#: authentication/templates/authentication/_access_key_modal.html:6
msgid "API key list"
msgstr "API Key列表"
@ -4025,7 +4007,7 @@ msgstr "在ou:{}中没有匹配条目"
#: settings/views.py:20 settings/views.py:47 settings/views.py:74
#: settings/views.py:105 settings/views.py:133 settings/views.py:146
#: settings/views.py:160 settings/views.py:187 templates/_nav.html:180
#: settings/views.py:160 settings/views.py:187 templates/_nav.html:178
msgid "Settings"
msgstr "系统设置"
@ -4218,7 +4200,7 @@ msgstr "终端管理"
msgid "Job Center"
msgstr "作业中心"
#: templates/_nav.html:116 templates/_nav.html:146
#: templates/_nav.html:116 templates/_nav.html:144
msgid "Batch command"
msgstr "批量命令"
@ -4226,24 +4208,19 @@ msgstr "批量命令"
msgid "Task monitor"
msgstr "任务监控"
#: templates/_nav.html:127 tickets/views.py:16 tickets/views.py:30
#: templates/_nav.html:128 tickets/views.py:17 tickets/views.py:32
msgid "Tickets"
msgstr "工单管理"
#: templates/_nav.html:130 tickets/models/base.py:23
#: users/templates/users/user_detail.html:257
msgid "Login confirm"
msgstr "登录复核"
#: templates/_nav.html:156
#: templates/_nav.html:154
msgid "XPack"
msgstr ""
#: templates/_nav.html:164 xpack/plugins/cloud/views.py:28
#: templates/_nav.html:162 xpack/plugins/cloud/views.py:28
msgid "Account list"
msgstr "账户列表"
#: templates/_nav.html:165
#: templates/_nav.html:163
msgid "Sync instance"
msgstr "同步实例"
@ -4585,10 +4562,7 @@ msgid "Accept"
msgstr "接受"
#: terminal/templates/terminal/terminal_list.html:80
#: tickets/models/login_confirm.py:16
#: tickets/templates/tickets/login_confirm_ticket_detail.html:10
#: tickets/templates/tickets/login_confirm_ticket_list.html:70
#: tickets/templates/tickets/login_confirm_ticket_list.html:108
#: tickets/models/ticket.py:30 tickets/templates/tickets/ticket_list.html:95
msgid "Reject"
msgstr "拒绝"
@ -4625,78 +4599,77 @@ msgid ""
"You should use your ssh client tools connect terminal: {} <br /> <br />{}"
msgstr "你可以使用ssh客户端工具连接终端"
#: tickets/models/base.py:16 tickets/models/base.py:52
#: tickets/templates/tickets/login_confirm_ticket_list.html:103
#: tickets/models/ticket.py:17 tickets/models/ticket.py:69
#: tickets/templates/tickets/ticket_list.html:90
msgid "Open"
msgstr "开启"
#: tickets/models/base.py:17
#: tickets/templates/tickets/login_confirm_ticket_list.html:104
#: tickets/models/ticket.py:18 tickets/templates/tickets/ticket_list.html:91
msgid "Closed"
msgstr "关闭"
#: tickets/models/base.py:22
#: tickets/models/ticket.py:23
msgid "General"
msgstr "一般"
#: tickets/models/base.py:26 tickets/models/base.py:69
msgid "User display name"
msgstr "用户显示名称"
#: tickets/models/ticket.py:24 users/templates/users/user_detail.html:257
msgid "Login confirm"
msgstr "登录复核"
#: tickets/models/base.py:28
#: tickets/templates/tickets/login_confirm_ticket_list.html:14
#: tickets/templates/tickets/login_confirm_ticket_list.html:100
msgid "Title"
msgstr "标题"
#: tickets/models/base.py:29 tickets/models/base.py:70
msgid "Body"
msgstr "内容"
#: tickets/models/base.py:30 tickets/templates/tickets/ticket_detail.html:51
msgid "Assignee"
msgstr "处理人"
#: tickets/models/base.py:31
msgid "Assignee display name"
msgstr "处理人名称"
#: tickets/models/base.py:32 tickets/templates/tickets/ticket_detail.html:50
msgid "Assignees"
msgstr "待处理人"
#: tickets/models/base.py:33
msgid "Assignees display name"
msgstr "待处理人名称"
#: tickets/models/base.py:53
msgid "{} {} this ticket"
msgstr "{} {} 这个工单"
#: tickets/models/login_confirm.py:15
#: tickets/templates/tickets/login_confirm_ticket_detail.html:9
#: tickets/templates/tickets/login_confirm_ticket_list.html:69
#: tickets/templates/tickets/login_confirm_ticket_list.html:107
#: tickets/models/ticket.py:29 tickets/templates/tickets/ticket_list.html:94
msgid "Approve"
msgstr "同意"
#: tickets/models/login_confirm.py:24
msgid "this order"
msgstr "这个工单"
#: tickets/models/ticket.py:33 tickets/models/ticket.py:86
msgid "User display name"
msgstr "用户显示名称"
#: tickets/templates/tickets/login_confirm_ticket_list.html:27
msgid "Approve selected"
msgstr "同意所选"
#: tickets/models/ticket.py:35 tickets/templates/tickets/ticket_list.html:21
#: tickets/templates/tickets/ticket_list.html:87
msgid "Title"
msgstr "标题"
#: tickets/templates/tickets/login_confirm_ticket_list.html:28
msgid "Reject selected"
msgstr "拒绝所选"
#: tickets/models/ticket.py:36 tickets/models/ticket.py:87
msgid "Body"
msgstr "内容"
#: tickets/models/ticket.py:37
msgid "Meta"
msgstr ""
#: tickets/models/ticket.py:38 tickets/templates/tickets/ticket_detail.html:51
msgid "Assignee"
msgstr "处理人"
#: tickets/models/ticket.py:39
msgid "Assignee display name"
msgstr "处理人名称"
#: tickets/models/ticket.py:40 tickets/templates/tickets/ticket_detail.html:50
msgid "Assignees"
msgstr "待处理人"
#: tickets/models/ticket.py:41
msgid "Assignees display name"
msgstr "待处理人名称"
#: tickets/models/ticket.py:70
msgid "{} {} this ticket"
msgstr "{} {} 这个工单"
#: tickets/templates/tickets/ticket_detail.html:66
#: tickets/templates/tickets/ticket_detail.html:81
msgid "ago"
msgstr "前"
#: tickets/templates/tickets/ticket_list.html:9
msgid "My tickets"
msgstr "我的工单"
#: tickets/templates/tickets/ticket_list.html:10
msgid "Assigned me"
msgstr "待处理"
#: tickets/utils.py:18
msgid "New ticket"
msgstr "新工单"
@ -4708,15 +4681,7 @@ msgid ""
" <div>\n"
" <p>Your has a new ticket</p>\n"
" <div>\n"
" <b>Title:</b> {ticket.title}\n"
" <br/>\n"
" <b>User:</b> {user}\n"
" <br/>\n"
" <b>Assignees:</b> {ticket.assignees_display}\n"
" <br/>\n"
" <b>City:</b> {ticket.city}\n"
" <br/>\n"
" <b>IP:</b> {ticket.ip}\n"
" {body}\n"
" <br/>\n"
" <a href={url}>click here to review</a> \n"
" </div>\n"
@ -4725,28 +4690,20 @@ msgid ""
msgstr ""
"\n"
" <div>\n"
" <p>有一个新工单</p>\n"
" <p>有一个新工单</p>\n"
" <div>\n"
" <b>标题:</b> {ticket.title}\n"
" {body}\n"
" <br/>\n"
" <b>用户:</b> {user}\n"
" <br/>\n"
" <b>待处理人:</b> {ticket.assignees_display}\n"
" <br/>\n"
" <b>城市:</b> {ticket.city}\n"
" <br/>\n"
" <b>IP:</b> {ticket.ip}\n"
" <br/>\n"
" <a href={url}>点我查看</a> \n"
" <a href={url}>点击我查看</a> \n"
" </div>\n"
" </div>\n"
" "
#: tickets/utils.py:48
#: tickets/utils.py:40
msgid "Ticket has been reply"
msgstr "工单已被回复"
#: tickets/utils.py:49
#: tickets/utils.py:41
#, python-brace-format
msgid ""
"\n"
@ -4777,13 +4734,13 @@ msgstr ""
" </div>\n"
" "
#: tickets/views.py:17
msgid "Login confirm ticket list"
msgstr "登录复核工单列表"
#: tickets/views.py:18
msgid "Ticket list"
msgstr "工单列表"
#: tickets/views.py:31
msgid "Login confirm ticket detail"
msgstr "登录复核工单详情"
#: tickets/views.py:33
msgid "Ticket detail"
msgstr "工单详情"
#: users/api/user.py:173
msgid "Could not reset self otp, use profile reset instead"
@ -6455,6 +6412,80 @@ msgstr "密码匣子"
msgid "vault create"
msgstr "创建"
#~ msgid "Assigned ticket"
#~ msgstr "处理人"
#~ msgid "My ticket"
#~ msgstr "我的工单"
#~ msgid "User login confirm: {}"
#~ msgstr "用户登录复核: {}"
#~ msgid ""
#~ "User: {}\n"
#~ "IP: {}\n"
#~ "City: {}\n"
#~ "Date: {}\n"
#~ msgstr ""
#~ "用户: {}\n"
#~ "IP: {}\n"
#~ "城市: {}\n"
#~ "日期: {}\n"
#~ msgid "this order"
#~ msgstr "这个工单"
#~ msgid "Approve selected"
#~ msgstr "同意所选"
#~ msgid "Reject selected"
#~ msgstr "拒绝所选"
#~ msgid ""
#~ "\n"
#~ " <div>\n"
#~ " <p>Your has a new ticket</p>\n"
#~ " <div>\n"
#~ " <b>Title:</b> {ticket.title}\n"
#~ " <br/>\n"
#~ " <b>User:</b> {user}\n"
#~ " <br/>\n"
#~ " <b>Assignees:</b> {ticket.assignees_display}\n"
#~ " <br/>\n"
#~ " <b>City:</b> {ticket.city}\n"
#~ " <br/>\n"
#~ " <b>IP:</b> {ticket.ip}\n"
#~ " <br/>\n"
#~ " <a href={url}>click here to review</a> \n"
#~ " </div>\n"
#~ " </div>\n"
#~ " "
#~ msgstr ""
#~ "\n"
#~ " <div>\n"
#~ " <p>您有一个新工单</p>\n"
#~ " <div>\n"
#~ " <b>标题:</b> {ticket.title}\n"
#~ " <br/>\n"
#~ " <b>用户:</b> {user}\n"
#~ " <br/>\n"
#~ " <b>待处理人:</b> {ticket.assignees_display}\n"
#~ " <br/>\n"
#~ " <b>城市:</b> {ticket.city}\n"
#~ " <br/>\n"
#~ " <b>IP:</b> {ticket.ip}\n"
#~ " <br/>\n"
#~ " <a href={url}>点我查看</a> \n"
#~ " </div>\n"
#~ " </div>\n"
#~ " "
#~ msgid "Login confirm ticket list"
#~ msgstr "登录复核工单列表"
#~ msgid "Login confirm ticket detail"
#~ msgstr "登录复核工单详情"
#, fuzzy
#~| msgid "Login"
#~ msgid "Login IP"

View File

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

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
from .base import *
from .login_confirm import *
from .ticket import *

View File

@ -1,16 +0,0 @@
# -*- 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']

View File

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

View File

@ -1,5 +1,6 @@
# 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={

View File

@ -1,24 +0,0 @@
# 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'),
),
]

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
from .base import *
from .login_confirm import *
from .ticket import *

View File

@ -1,33 +0,0 @@
# -*- 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()

View File

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

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
from .base import *
from .login_confirm import *
from .ticket import *

View File

@ -1,53 +0,0 @@
# -*- 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

View File

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

View File

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

View File

@ -1,34 +0,0 @@
{% 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 %}

View File

@ -1,154 +0,0 @@
{% 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 %}

View File

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

View File

@ -0,0 +1,115 @@
{% 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 %}

View File

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

View File

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

View File

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

View File

@ -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': _("Ticket list"),
'assign': assign,
})
return context
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 list")
})
return context
class LoginConfirmTicketDetailView(PermissionsMixin, mixins.TicketMixin, DetailView):
template_name = 'tickets/login_confirm_ticket_detail.html'
queryset = LoginConfirmTicket.objects.all()
permission_classes = (IsValidUser,)
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