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