From ebe129b3c2d8c55a444b068ad85cc71617959754 Mon Sep 17 00:00:00 2001 From: ibuler Date: Fri, 15 Nov 2019 18:55:35 +0800 Subject: [PATCH] =?UTF-8?q?[Update]=20=E4=BF=AE=E6=94=B9=E5=B7=A5=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/authentication/api/login_confirm.py | 8 +- apps/authentication/mixins.py | 4 +- apps/authentication/models.py | 20 +- .../authentication/login_wait_confirm.html | 4 +- apps/authentication/urls/api_urls.py | 2 +- apps/authentication/views/login.py | 6 +- apps/locale/zh/LC_MESSAGES/django.mo | Bin 84558 -> 83668 bytes apps/locale/zh/LC_MESSAGES/django.po | 299 ++++++++++-------- apps/templates/_nav.html | 8 +- apps/tickets/api/__init__.py | 3 +- apps/tickets/api/login_confirm.py | 16 - apps/tickets/api/{base.py => ticket.py} | 8 +- apps/tickets/migrations/0001_initial.py | 22 +- .../migrations/0002_auto_20191114_1105.py | 24 -- apps/tickets/models/__init__.py | 3 +- apps/tickets/models/login_confirm.py | 33 -- apps/tickets/models/{base.py => ticket.py} | 30 ++ apps/tickets/serializers/__init__.py | 3 +- apps/tickets/serializers/login_confirm.py | 53 ---- .../serializers/{base.py => ticket.py} | 19 ++ apps/tickets/signals_handler.py | 16 +- .../tickets/login_confirm_ticket_detail.html | 34 -- .../tickets/login_confirm_ticket_list.html | 154 --------- .../templates/tickets/ticket_detail.html | 35 +- .../templates/tickets/ticket_list.html | 115 +++++++ apps/tickets/urls/api_urls.py | 1 - apps/tickets/urls/views_urls.py | 4 +- apps/tickets/utils.py | 18 +- apps/tickets/views.py | 38 +-- 29 files changed, 433 insertions(+), 547 deletions(-) delete mode 100644 apps/tickets/api/login_confirm.py rename apps/tickets/api/{base.py => ticket.py} (75%) delete mode 100644 apps/tickets/migrations/0002_auto_20191114_1105.py delete mode 100644 apps/tickets/models/login_confirm.py rename apps/tickets/models/{base.py => ticket.py} (72%) delete mode 100644 apps/tickets/serializers/login_confirm.py rename apps/tickets/serializers/{base.py => ticket.py} (61%) delete mode 100644 apps/tickets/templates/tickets/login_confirm_ticket_detail.html delete mode 100644 apps/tickets/templates/tickets/login_confirm_ticket_list.html create mode 100644 apps/tickets/templates/tickets/ticket_list.html diff --git a/apps/authentication/api/login_confirm.py b/apps/authentication/api/login_confirm.py index 5232dd753..986488f2b 100644 --- a/apps/authentication/api/login_confirm.py +++ b/apps/authentication/api/login_confirm.py @@ -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): diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py index ceb54f592..80f54dbc9 100644 --- a/apps/authentication/mixins.py +++ b/apps/authentication/mixins.py @@ -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): diff --git a/apps/authentication/models.py b/apps/authentication/models.py index cb4f97e4d..614906c73 100644 --- a/apps/authentication/models.py +++ b/apps/authentication/models.py @@ -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}
" + "IP: {ip}
" + "{city_key}: {city}
" + "{date_key}: {date}
").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 diff --git a/apps/authentication/templates/authentication/login_wait_confirm.html b/apps/authentication/templates/authentication/login_wait_confirm.html index 28f84baf9..8a67a501e 100644 --- a/apps/authentication/templates/authentication/login_wait_confirm.html +++ b/apps/authentication/templates/authentication/login_wait_confirm.html @@ -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(); } } diff --git a/apps/authentication/urls/api_urls.py b/apps/authentication/urls/api_urls.py index 2da89a19f..da59711c4 100644 --- a/apps/authentication/urls/api_urls.py +++ b/apps/authentication/urls/api_urls.py @@ -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//', api.LoginConfirmSettingUpdateApi.as_view(), name='login-confirm-setting-update') ] diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py index 873057169..d6bb2af6c 100644 --- a/apps/authentication/views/login.py +++ b/apps/authentication/views/login.py @@ -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 {} confirm, You also can copy link to her/him
Don't close this page""").format(ticket.assignees_display) else: diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index 720b8a9cf7f6eba4de797be1da3858b34eddb9e4..83f364a18af06aa7f630844ce5858cc1d5fafa79 100644 GIT binary patch delta 24587 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 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index cfc940bec..fcdd6011c 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -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 \n" "Language-Team: Jumpserver team\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: {}

{}" 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 "" "
\n" "

Your has a new ticket

\n" "
\n" @@ -4725,28 +4690,20 @@ msgid "" msgstr "" "\n" "
\n" -"

您有一个新工单

\n" +"

你有一个新工单

\n" "
\n" -" 标题: {ticket.title}\n" +" {body}\n" "
\n" -" 用户: {user}\n" -"
\n" -" 待处理人: {ticket.assignees_display}\n" -"
\n" -" 城市: {ticket.city}\n" -"
\n" -" IP: {ticket.ip}\n" -"
\n" -" 点我查看 \n" +" 点击我查看 \n" "
\n" "
\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 "" "
\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" +#~ "
\n" +#~ "

Your has a new ticket

\n" +#~ "
\n" +#~ " Title: {ticket.title}\n" +#~ "
\n" +#~ " User: {user}\n" +#~ "
\n" +#~ " Assignees: {ticket.assignees_display}\n" +#~ "
\n" +#~ " City: {ticket.city}\n" +#~ "
\n" +#~ " IP: {ticket.ip}\n" +#~ "
\n" +#~ " click here to review \n" +#~ "
\n" +#~ "
\n" +#~ " " +#~ msgstr "" +#~ "\n" +#~ "
\n" +#~ "

您有一个新工单

\n" +#~ "
\n" +#~ " 标题: {ticket.title}\n" +#~ "
\n" +#~ " 用户: {user}\n" +#~ "
\n" +#~ " 待处理人: {ticket.assignees_display}\n" +#~ "
\n" +#~ " 城市: {ticket.city}\n" +#~ "
\n" +#~ " IP: {ticket.ip}\n" +#~ "
\n" +#~ " 点我查看 \n" +#~ "
\n" +#~ "
\n" +#~ " " + +#~ msgid "Login confirm ticket list" +#~ msgstr "登录复核工单列表" + +#~ msgid "Login confirm ticket detail" +#~ msgstr "登录复核工单详情" + #, fuzzy #~| msgid "Login" #~ msgid "Login IP" diff --git a/apps/templates/_nav.html b/apps/templates/_nav.html index 794d9b243..eb2df90ff 100644 --- a/apps/templates/_nav.html +++ b/apps/templates/_nav.html @@ -123,12 +123,10 @@ {% if request.user.can_admin_current_org and LOGIN_CONFIRM_ENABLE %}
  • - - {% trans 'Tickets' %} + + + {% trans 'Tickets' %} -
  • {% endif %} diff --git a/apps/tickets/api/__init__.py b/apps/tickets/api/__init__.py index 99396f9f3..0b5dd0e7d 100644 --- a/apps/tickets/api/__init__.py +++ b/apps/tickets/api/__init__.py @@ -1,4 +1,3 @@ # -*- coding: utf-8 -*- # -from .base import * -from .login_confirm import * +from .ticket import * diff --git a/apps/tickets/api/login_confirm.py b/apps/tickets/api/login_confirm.py deleted file mode 100644 index e0c6f9cf5..000000000 --- a/apps/tickets/api/login_confirm.py +++ /dev/null @@ -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'] diff --git a/apps/tickets/api/base.py b/apps/tickets/api/ticket.py similarity index 75% rename from apps/tickets/api/base.py rename to apps/tickets/api/ticket.py index 266f9f855..1706c38a7 100644 --- a/apps/tickets/api/base.py +++ b/apps/tickets/api/ticket.py @@ -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 diff --git a/apps/tickets/migrations/0001_initial.py b/apps/tickets/migrations/0001_initial.py index e86fc44ad..9b2b5d54e 100644 --- a/apps/tickets/migrations/0001_initial.py +++ b/apps/tickets/migrations/0001_initial.py @@ -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={ diff --git a/apps/tickets/migrations/0002_auto_20191114_1105.py b/apps/tickets/migrations/0002_auto_20191114_1105.py deleted file mode 100644 index 87cec4056..000000000 --- a/apps/tickets/migrations/0002_auto_20191114_1105.py +++ /dev/null @@ -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'), - ), - ] diff --git a/apps/tickets/models/__init__.py b/apps/tickets/models/__init__.py index 99396f9f3..0b5dd0e7d 100644 --- a/apps/tickets/models/__init__.py +++ b/apps/tickets/models/__init__.py @@ -1,4 +1,3 @@ # -*- coding: utf-8 -*- # -from .base import * -from .login_confirm import * +from .ticket import * diff --git a/apps/tickets/models/login_confirm.py b/apps/tickets/models/login_confirm.py deleted file mode 100644 index 87baaefee..000000000 --- a/apps/tickets/models/login_confirm.py +++ /dev/null @@ -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() diff --git a/apps/tickets/models/base.py b/apps/tickets/models/ticket.py similarity index 72% rename from apps/tickets/models/base.py rename to apps/tickets/models/ticket.py index d369e534f..98712da1e 100644 --- a/apps/tickets/models/base.py +++ b/apps/tickets/models/ticket.py @@ -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',) diff --git a/apps/tickets/serializers/__init__.py b/apps/tickets/serializers/__init__.py index 99396f9f3..0b5dd0e7d 100644 --- a/apps/tickets/serializers/__init__.py +++ b/apps/tickets/serializers/__init__.py @@ -1,4 +1,3 @@ # -*- coding: utf-8 -*- # -from .base import * -from .login_confirm import * +from .ticket import * diff --git a/apps/tickets/serializers/login_confirm.py b/apps/tickets/serializers/login_confirm.py deleted file mode 100644 index cdd450d6d..000000000 --- a/apps/tickets/serializers/login_confirm.py +++ /dev/null @@ -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 diff --git a/apps/tickets/serializers/base.py b/apps/tickets/serializers/ticket.py similarity index 61% rename from apps/tickets/serializers/base.py rename to apps/tickets/serializers/ticket.py index e465fa691..9eb2d777b 100644 --- a/apps/tickets/serializers/base.py +++ b/apps/tickets/serializers/ticket.py @@ -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 diff --git a/apps/tickets/signals_handler.py b/apps/tickets/signals_handler.py index 212892d0c..0e4298a61 100644 --- a/apps/tickets/signals_handler.py +++ b/apps/tickets/signals_handler.py @@ -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: diff --git a/apps/tickets/templates/tickets/login_confirm_ticket_detail.html b/apps/tickets/templates/tickets/login_confirm_ticket_detail.html deleted file mode 100644 index e0cc25b0d..000000000 --- a/apps/tickets/templates/tickets/login_confirm_ticket_detail.html +++ /dev/null @@ -1,34 +0,0 @@ -{% extends 'tickets/ticket_detail.html' %} -{% load static %} -{% load i18n %} - -{% block status %} -{% endblock %} - -{% block action %} - {% trans 'Approve' %} - {% trans 'Reject' %} -{% endblock %} - -{% block custom_foot_js %} -{{ block.super }} - -{% endblock %} - - - diff --git a/apps/tickets/templates/tickets/login_confirm_ticket_list.html b/apps/tickets/templates/tickets/login_confirm_ticket_list.html deleted file mode 100644 index 267af970b..000000000 --- a/apps/tickets/templates/tickets/login_confirm_ticket_list.html +++ /dev/null @@ -1,154 +0,0 @@ -{% extends '_base_list.html' %} -{% load i18n static %} -{% block table_search %} -{% endblock %} -{% block custom_head_css_js %} -{% endblock %} -{% block table_container %} - - - - - - - - - - - - - -
    - - {% trans 'Title' %}{% trans 'User' %}{% trans 'Status' %}{% trans 'Datetime' %}{% trans 'Action' %}
    -
    -
    - -
    - -
    -
    -
    -{% include '_filter_dropdown.html' %} -{% endblock %} -{% block content_bottom_left %}{% endblock %} -{% block custom_foot_js %} - -{% endblock %} - diff --git a/apps/tickets/templates/tickets/ticket_detail.html b/apps/tickets/templates/tickets/ticket_detail.html index 320010398..ec25f50e7 100644 --- a/apps/tickets/templates/tickets/ticket_detail.html +++ b/apps/tickets/templates/tickets/ticket_detail.html @@ -72,7 +72,6 @@ {% for comment in object.comments.all %} -
    - {% block action %} - {% endblock %} - {% block status %} - {% trans 'Close' %} - {% endblock %} - {% block comment %} + {% if object.type == object.TYPE_LOGIN_CONFIRM %} + {% trans 'Approve' %} + {% trans 'Reject' %} + {% endif %} + {% trans 'Close' %} {% trans 'Comment' %} - {% endblock %}
    @@ -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); +}) {% endblock %} diff --git a/apps/tickets/templates/tickets/ticket_list.html b/apps/tickets/templates/tickets/ticket_list.html new file mode 100644 index 000000000..ba515997b --- /dev/null +++ b/apps/tickets/templates/tickets/ticket_list.html @@ -0,0 +1,115 @@ +{% extends 'base.html' %} +{% load i18n static %} + +{% block content %} +
    +
    +
    + +
    +
    +
    + + + + + + + + + + + + + +
    + + {% trans 'Title' %}{% trans 'User' %}{% trans 'Type' %}{% trans 'Status' %}{% trans 'Datetime' %}
    +
    +
    +
    +
    +
    +
    +{% include '_filter_dropdown.html' %} +{% endblock %} +{% block content_bottom_left %}{% endblock %} +{% block custom_foot_js %} + +{% endblock %} + diff --git a/apps/tickets/urls/api_urls.py b/apps/tickets/urls/api_urls.py index d160235ab..33cb5a216 100644 --- a/apps/tickets/urls/api_urls.py +++ b/apps/tickets/urls/api_urls.py @@ -9,7 +9,6 @@ router = BulkRouter() router.register('tickets', api.TicketViewSet, 'ticket') router.register('tickets/(?P[0-9a-zA-Z\-]{36})/comments', api.TicketCommentViewSet, 'ticket-comment') -router.register('login-confirm-tickets', api.LoginConfirmTicketViewSet, 'login-confirm-ticket') urlpatterns = [ diff --git a/apps/tickets/urls/views_urls.py b/apps/tickets/urls/views_urls.py index f0b19c6f5..46e15437e 100644 --- a/apps/tickets/urls/views_urls.py +++ b/apps/tickets/urls/views_urls.py @@ -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//', views.LoginConfirmTicketDetailView.as_view(), name='login-confirm-ticket-detail') + path('tickets/', views.TicketListView.as_view(), name='ticket-list'), + path('tickets//', views.TicketDetailView.as_view(), name='ticket-detail'), ] diff --git a/apps/tickets/utils.py b/apps/tickets/utils.py index e2901ca3d..13727a77d 100644 --- a/apps/tickets/utils.py +++ b/apps/tickets/utils.py @@ -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 = _("""

    Your has a new ticket

    - Title: {ticket.title} -
    - User: {user} -
    - Assignees: {ticket.assignees_display} -
    - City: {ticket.city} -
    - IP: {ticket.ip} + {body}
    click here to review
    - """).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 diff --git a/apps/tickets/views.py b/apps/tickets/views.py index aac6b136c..0432ec6bc 100644 --- a/apps/tickets/views.py +++ b/apps/tickets/views.py @@ -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