From b5cfc6831b3efc6a255815af6ab5483f02c809e0 Mon Sep 17 00:00:00 2001 From: fit2bot <68588906+fit2bot@users.noreply.github.com> Date: Tue, 12 Jul 2022 15:28:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=B7=A5=E5=8D=95=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=AE=A1=E6=89=B9=E6=97=B6=E4=BF=AE=E6=94=B9=E8=B5=84=E4=BA=A7?= =?UTF-8?q?=20(#8549)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: feng626 <1304903146@qq.com> Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com> --- apps/common/db/encoder.py | 2 + apps/locale/ja/LC_MESSAGES/django.po | 90 +++++++++++-------- apps/locale/zh/LC_MESSAGES/django.po | 90 +++++++++++-------- apps/perms/serializers/base.py | 6 +- apps/tickets/api/ticket.py | 23 ++--- apps/tickets/handlers/base.py | 27 +++++- .../migrations/0017_auto_20220623_1027.py | 4 +- .../models/ticket/apply_application.py | 2 +- apps/tickets/models/ticket/apply_asset.py | 5 +- apps/tickets/models/ticket/general.py | 56 +++++++++--- apps/tickets/notifications.py | 11 +-- .../serializers/ticket/apply_application.py | 13 ++- .../tickets/serializers/ticket/apply_asset.py | 20 +++-- apps/tickets/serializers/ticket/common.py | 20 ++++- apps/tickets/serializers/ticket/ticket.py | 3 + .../tickets/ticket_approve_diff.html | 24 +++++ 16 files changed, 274 insertions(+), 122 deletions(-) create mode 100644 apps/tickets/templates/tickets/ticket_approve_diff.html diff --git a/apps/common/db/encoder.py b/apps/common/db/encoder.py index 673b8aeca..e09ffd60e 100644 --- a/apps/common/db/encoder.py +++ b/apps/common/db/encoder.py @@ -4,6 +4,7 @@ import logging from datetime import datetime from django.utils.translation import ugettext_lazy as _ +from django.utils import timezone as dj_timezone from django.db import models from django.conf import settings @@ -18,6 +19,7 @@ class ModelJSONFieldEncoder(json.JSONEncoder): if isinstance(obj, str_cls): return str(obj) elif isinstance(obj, datetime): + obj = dj_timezone.localtime(obj) return obj.strftime(settings.DATETIME_DISPLAY_FORMAT) elif isinstance(obj, (list, tuple)) and len(obj) > 0 \ and isinstance(obj[0], models.Model): diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index 0f9e3d288..88cb86c95 100644 --- a/apps/locale/ja/LC_MESSAGES/django.po +++ b/apps/locale/ja/LC_MESSAGES/django.po @@ -63,7 +63,7 @@ msgstr "アクティブ" #: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:34 #: terminal/models/endpoint.py:21 terminal/models/endpoint.py:92 #: terminal/models/storage.py:29 terminal/models/terminal.py:114 -#: tickets/models/comment.py:32 tickets/models/ticket/general.py:278 +#: tickets/models/comment.py:32 tickets/models/ticket/general.py:288 #: users/models/group.py:16 users/models/user.py:698 #: xpack/plugins/change_auth_plan/models/base.py:44 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:116 @@ -309,10 +309,10 @@ msgstr "カテゴリ" #: assets/models/cmd_filter.py:82 assets/models/user.py:250 #: authentication/models.py:69 perms/models/application_permission.py:24 #: perms/serializers/application/user_permission.py:34 -#: terminal/models/storage.py:58 terminal/models/storage.py:142 +#: terminal/models/storage.py:58 terminal/models/storage.py:143 #: tickets/models/comment.py:26 tickets/models/flow.py:57 #: tickets/models/ticket/apply_application.py:17 -#: tickets/models/ticket/general.py:263 +#: tickets/models/ticket/general.py:273 #: xpack/plugins/change_auth_plan/models/app.py:28 #: xpack/plugins/change_auth_plan/models/app.py:153 msgid "Type" @@ -416,6 +416,8 @@ msgstr "アプリケーションパス" #: applications/serializers/attrs/application_category/remote_app.py:44 #: assets/serializers/system_user.py:167 +#: tickets/serializers/ticket/apply_application.py:35 +#: tickets/serializers/ticket/common.py:59 #: xpack/plugins/change_auth_plan/serializers/asset.py:67 #: xpack/plugins/change_auth_plan/serializers/asset.py:70 #: xpack/plugins/change_auth_plan/serializers/asset.py:73 @@ -503,7 +505,7 @@ msgid "Charset" msgstr "シャーセット" #: assets/models/asset.py:141 assets/serializers/asset.py:176 -#: tickets/models/ticket/general.py:288 +#: tickets/models/ticket/general.py:298 msgid "Meta" msgstr "メタ" @@ -525,7 +527,7 @@ msgstr "ベンダー" msgid "Model" msgstr "モデル" -#: assets/models/asset.py:170 tickets/models/ticket/general.py:286 +#: assets/models/asset.py:170 tickets/models/ticket/general.py:296 msgid "Serial number" msgstr "シリアル番号" @@ -1525,7 +1527,7 @@ msgid "MFA" msgstr "MFA" #: audits/models.py:128 terminal/models/status.py:33 -#: tickets/models/ticket/general.py:271 xpack/plugins/cloud/models.py:175 +#: tickets/models/ticket/general.py:281 xpack/plugins/cloud/models.py:175 #: xpack/plugins/cloud/models.py:227 msgid "Status" msgstr "ステータス" @@ -2581,7 +2583,7 @@ msgstr "%(name)s が正常に作成されました" msgid "%(name)s was updated successfully" msgstr "%(name)s は正常に更新されました" -#: common/db/encoder.py:10 +#: common/db/encoder.py:11 msgid "ugettext_lazy" msgstr "ugettext_lazy" @@ -3009,7 +3011,7 @@ msgstr "アプリ組織" #: orgs/mixins/models.py:46 orgs/mixins/serializers.py:25 orgs/models.py:80 #: orgs/models.py:211 rbac/const.py:7 rbac/models/rolebinding.py:48 #: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62 -#: tickets/models/ticket/general.py:290 tickets/serializers/ticket/ticket.py:64 +#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:64 msgid "Organization" msgstr "組織" @@ -3417,7 +3419,7 @@ msgstr "マイアプリ" msgid "Ticket comment" msgstr "チケットコメント" -#: rbac/tree.py:115 tickets/models/ticket/general.py:295 +#: rbac/tree.py:115 tickets/models/ticket/general.py:305 msgid "Ticket" msgstr "チケット" @@ -4939,7 +4941,7 @@ msgstr "リンク期限切れ" msgid "User not allowed to join" msgstr "IPは許可されていません" -#: terminal/models/sharing.py:85 terminal/serializers/sharing.py:58 +#: terminal/models/sharing.py:85 terminal/serializers/sharing.py:59 msgid "Joiner" msgstr "ジョイナー" @@ -4996,11 +4998,11 @@ msgstr "ブート時間" msgid "Default storage" msgstr "デフォルトのストレージ" -#: terminal/models/storage.py:136 terminal/models/terminal.py:108 +#: terminal/models/storage.py:137 terminal/models/terminal.py:108 msgid "Command storage" msgstr "コマンドストレージ" -#: terminal/models/storage.py:196 terminal/models/terminal.py:109 +#: terminal/models/storage.py:197 terminal/models/terminal.py:109 msgid "Replay storage" msgstr "再生ストレージ" @@ -5253,7 +5255,19 @@ msgstr "" "チケットのタイトル: {} チケット申請者: {} チケットプロセッサ: {} チケットID: " "{}" -#: tickets/handlers/base.py:72 +#: tickets/handlers/base.py:79 +msgid "Change field" +msgstr "フィールドを変更" + +#: tickets/handlers/base.py:79 +msgid "Before change" +msgstr "変更前" + +#: tickets/handlers/base.py:79 +msgid "After change" +msgstr "変更後" + +#: tickets/handlers/base.py:91 msgid "{} {} the ticket" msgstr "{} {} チケット" @@ -5269,8 +5283,8 @@ msgstr "応用ログイン都市" msgid "Applied login datetime" msgstr "適用されたログインの日付時間" -#: tickets/models/comment.py:13 tickets/models/ticket/general.py:39 -#: tickets/models/ticket/general.py:267 +#: tickets/models/comment.py:13 tickets/models/ticket/general.py:41 +#: tickets/models/ticket/general.py:277 msgid "State" msgstr "状態" @@ -5287,7 +5301,7 @@ msgid "Body" msgstr "ボディ" #: tickets/models/flow.py:20 tickets/models/flow.py:62 -#: tickets/models/ticket/general.py:35 +#: tickets/models/ticket/general.py:37 msgid "Approve level" msgstr "レベルを承認する" @@ -5313,8 +5327,8 @@ msgstr "チケットセッションの関係" #: tickets/models/ticket/apply_application.py:11 #: tickets/models/ticket/apply_asset.py:13 -msgid "Apply name" -msgstr "名前を適用" +msgid "Permission name" +msgstr "認可ルール名" #: tickets/models/ticket/apply_application.py:20 msgid "Apply applications" @@ -5362,39 +5376,39 @@ msgstr "コマンドフィルタ規則から" msgid "From cmd filter rule" msgstr "コマンドフィルタ規則から" -#: tickets/models/ticket/general.py:70 +#: tickets/models/ticket/general.py:72 msgid "Ticket step" msgstr "チケットステップ" -#: tickets/models/ticket/general.py:88 +#: tickets/models/ticket/general.py:90 msgid "Ticket assignee" msgstr "割り当てられたチケット" -#: tickets/models/ticket/general.py:260 +#: tickets/models/ticket/general.py:270 msgid "Title" msgstr "タイトル" -#: tickets/models/ticket/general.py:276 +#: tickets/models/ticket/general.py:286 msgid "Applicant" msgstr "応募者" -#: tickets/models/ticket/general.py:281 +#: tickets/models/ticket/general.py:291 msgid "TicketFlow" msgstr "作業指示プロセス" -#: tickets/models/ticket/general.py:284 +#: tickets/models/ticket/general.py:294 msgid "Approval step" msgstr "承認ステップ" -#: tickets/models/ticket/general.py:287 +#: tickets/models/ticket/general.py:297 msgid "Relation snapshot" msgstr "製造オーダスナップショット" -#: tickets/models/ticket/general.py:380 +#: tickets/models/ticket/general.py:390 msgid "Please try again" msgstr "もう一度お試しください" -#: tickets/models/ticket/general.py:387 +#: tickets/models/ticket/general.py:421 msgid "Super ticket" msgstr "スーパーチケット" @@ -5414,27 +5428,27 @@ msgstr "ログインシステムユーザー" msgid "Login datetime" msgstr "ログイン日時" -#: tickets/notifications.py:64 +#: tickets/notifications.py:63 msgid "Ticket basic info" msgstr "チケット基本情報" -#: tickets/notifications.py:65 +#: tickets/notifications.py:64 msgid "Ticket applied info" msgstr "チケット適用情報" -#: tickets/notifications.py:116 -msgid "Your has a new ticket" +#: tickets/notifications.py:109 +msgid "Your has a new ticket, applicant - {}" msgstr "新しいチケットがあります- {}" -#: tickets/notifications.py:120 +#: tickets/notifications.py:113 msgid "{}: New Ticket - {} ({})" msgstr "新しいチケット- {} ({})" -#: tickets/notifications.py:164 +#: tickets/notifications.py:157 msgid "Your ticket has been processed, processor - {}" msgstr "チケットが処理されました。プロセッサー- {}" -#: tickets/notifications.py:168 +#: tickets/notifications.py:161 msgid "Ticket has processed - {} ({})" msgstr "チケットが処理済み- {} ({})" @@ -5455,19 +5469,19 @@ msgid "Processor" msgstr "プロセッサ" #: tickets/serializers/ticket/common.py:16 -#: tickets/serializers/ticket/common.py:68 +#: tickets/serializers/ticket/common.py:79 msgid "Created by ticket ({}-{})" msgstr "チケットで作成 ({}-{})" -#: tickets/serializers/ticket/common.py:58 +#: tickets/serializers/ticket/common.py:69 msgid "The expiration date should be greater than the start date" msgstr "有効期限は開始日より大きくする必要があります" -#: tickets/serializers/ticket/common.py:74 +#: tickets/serializers/ticket/common.py:85 msgid "Permission named `{}` already exists" msgstr "'{}'という名前の権限は既に存在します" -#: tickets/serializers/ticket/ticket.py:89 +#: tickets/serializers/ticket/ticket.py:92 msgid "The ticket flow `{}` does not exist" msgstr "チケットフロー '{}'が存在しない" diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 89123d879..ab6fe167e 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -62,7 +62,7 @@ msgstr "激活中" #: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:34 #: terminal/models/endpoint.py:21 terminal/models/endpoint.py:92 #: terminal/models/storage.py:29 terminal/models/terminal.py:114 -#: tickets/models/comment.py:32 tickets/models/ticket/general.py:278 +#: tickets/models/comment.py:32 tickets/models/ticket/general.py:288 #: users/models/group.py:16 users/models/user.py:698 #: xpack/plugins/change_auth_plan/models/base.py:44 #: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:116 @@ -304,10 +304,10 @@ msgstr "类别" #: assets/models/cmd_filter.py:82 assets/models/user.py:250 #: authentication/models.py:69 perms/models/application_permission.py:24 #: perms/serializers/application/user_permission.py:34 -#: terminal/models/storage.py:58 terminal/models/storage.py:142 +#: terminal/models/storage.py:58 terminal/models/storage.py:143 #: tickets/models/comment.py:26 tickets/models/flow.py:57 #: tickets/models/ticket/apply_application.py:17 -#: tickets/models/ticket/general.py:263 +#: tickets/models/ticket/general.py:273 #: xpack/plugins/change_auth_plan/models/app.py:28 #: xpack/plugins/change_auth_plan/models/app.py:153 msgid "Type" @@ -411,6 +411,8 @@ msgstr "应用路径" #: applications/serializers/attrs/application_category/remote_app.py:44 #: assets/serializers/system_user.py:167 +#: tickets/serializers/ticket/apply_application.py:35 +#: tickets/serializers/ticket/common.py:59 #: xpack/plugins/change_auth_plan/serializers/asset.py:67 #: xpack/plugins/change_auth_plan/serializers/asset.py:70 #: xpack/plugins/change_auth_plan/serializers/asset.py:73 @@ -498,7 +500,7 @@ msgid "Charset" msgstr "编码" #: assets/models/asset.py:141 assets/serializers/asset.py:176 -#: tickets/models/ticket/general.py:288 +#: tickets/models/ticket/general.py:298 msgid "Meta" msgstr "元数据" @@ -520,7 +522,7 @@ msgstr "制造商" msgid "Model" msgstr "型号" -#: assets/models/asset.py:170 tickets/models/ticket/general.py:286 +#: assets/models/asset.py:170 tickets/models/ticket/general.py:296 msgid "Serial number" msgstr "序列号" @@ -1513,7 +1515,7 @@ msgid "MFA" msgstr "MFA" #: audits/models.py:128 terminal/models/status.py:33 -#: tickets/models/ticket/general.py:271 xpack/plugins/cloud/models.py:175 +#: tickets/models/ticket/general.py:281 xpack/plugins/cloud/models.py:175 #: xpack/plugins/cloud/models.py:227 msgid "Status" msgstr "状态" @@ -2547,7 +2549,7 @@ msgstr "%(name)s 创建成功" msgid "%(name)s was updated successfully" msgstr "%(name)s 更新成功" -#: common/db/encoder.py:10 +#: common/db/encoder.py:11 msgid "ugettext_lazy" msgstr "ugettext_lazy" @@ -2969,7 +2971,7 @@ msgstr "组织管理" #: orgs/mixins/models.py:46 orgs/mixins/serializers.py:25 orgs/models.py:80 #: orgs/models.py:211 rbac/const.py:7 rbac/models/rolebinding.py:48 #: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62 -#: tickets/models/ticket/general.py:290 tickets/serializers/ticket/ticket.py:64 +#: tickets/models/ticket/general.py:300 tickets/serializers/ticket/ticket.py:64 msgid "Organization" msgstr "组织" @@ -3374,7 +3376,7 @@ msgstr "我的应用" msgid "Ticket comment" msgstr "工单评论" -#: rbac/tree.py:115 tickets/models/ticket/general.py:295 +#: rbac/tree.py:115 tickets/models/ticket/general.py:305 msgid "Ticket" msgstr "工单管理" @@ -4863,7 +4865,7 @@ msgstr "链接过期" msgid "User not allowed to join" msgstr "来源 IP 不被允许登录" -#: terminal/models/sharing.py:85 terminal/serializers/sharing.py:58 +#: terminal/models/sharing.py:85 terminal/serializers/sharing.py:59 msgid "Joiner" msgstr "加入者" @@ -4920,11 +4922,11 @@ msgstr "运行时间" msgid "Default storage" msgstr "默认存储" -#: terminal/models/storage.py:136 terminal/models/terminal.py:108 +#: terminal/models/storage.py:137 terminal/models/terminal.py:108 msgid "Command storage" msgstr "命令存储" -#: terminal/models/storage.py:196 terminal/models/terminal.py:109 +#: terminal/models/storage.py:197 terminal/models/terminal.py:109 msgid "Replay storage" msgstr "录像存储" @@ -5173,7 +5175,19 @@ msgid "" msgstr "" "通过工单创建, 工单标题: {}, 工单申请人: {}, 工单处理人: {}, 工单 ID: {}" -#: tickets/handlers/base.py:72 +#: tickets/handlers/base.py:79 +msgid "Change field" +msgstr "变更字段" + +#: tickets/handlers/base.py:79 +msgid "Before change" +msgstr "变更前" + +#: tickets/handlers/base.py:79 +msgid "After change" +msgstr "变更后" + +#: tickets/handlers/base.py:91 msgid "{} {} the ticket" msgstr "{} {} 工单" @@ -5189,8 +5203,8 @@ msgstr "申请登录的城市" msgid "Applied login datetime" msgstr "申请登录的日期" -#: tickets/models/comment.py:13 tickets/models/ticket/general.py:39 -#: tickets/models/ticket/general.py:267 +#: tickets/models/comment.py:13 tickets/models/ticket/general.py:41 +#: tickets/models/ticket/general.py:277 msgid "State" msgstr "状态" @@ -5207,7 +5221,7 @@ msgid "Body" msgstr "内容" #: tickets/models/flow.py:20 tickets/models/flow.py:62 -#: tickets/models/ticket/general.py:35 +#: tickets/models/ticket/general.py:37 msgid "Approve level" msgstr "审批级别" @@ -5233,8 +5247,8 @@ msgstr "工单会话" #: tickets/models/ticket/apply_application.py:11 #: tickets/models/ticket/apply_asset.py:13 -msgid "Apply name" -msgstr "应用名称" +msgid "Permission name" +msgstr "授权规则名称" #: tickets/models/ticket/apply_application.py:20 msgid "Apply applications" @@ -5282,39 +5296,39 @@ msgstr "来自命令过滤规则" msgid "From cmd filter rule" msgstr "来自命令过滤规则" -#: tickets/models/ticket/general.py:70 +#: tickets/models/ticket/general.py:72 msgid "Ticket step" msgstr "工单步骤" -#: tickets/models/ticket/general.py:88 +#: tickets/models/ticket/general.py:90 msgid "Ticket assignee" msgstr "工单受理人" -#: tickets/models/ticket/general.py:260 +#: tickets/models/ticket/general.py:270 msgid "Title" msgstr "标题" -#: tickets/models/ticket/general.py:276 +#: tickets/models/ticket/general.py:286 msgid "Applicant" msgstr "申请人" -#: tickets/models/ticket/general.py:281 +#: tickets/models/ticket/general.py:291 msgid "TicketFlow" msgstr "工单流程" -#: tickets/models/ticket/general.py:284 +#: tickets/models/ticket/general.py:294 msgid "Approval step" msgstr "审批步骤" -#: tickets/models/ticket/general.py:287 +#: tickets/models/ticket/general.py:297 msgid "Relation snapshot" msgstr "工单快照" -#: tickets/models/ticket/general.py:380 +#: tickets/models/ticket/general.py:390 msgid "Please try again" msgstr "请再次尝试" -#: tickets/models/ticket/general.py:387 +#: tickets/models/ticket/general.py:421 msgid "Super ticket" msgstr "超级工单" @@ -5334,27 +5348,27 @@ msgstr "登录系统用户" msgid "Login datetime" msgstr "登录日期" -#: tickets/notifications.py:64 +#: tickets/notifications.py:63 msgid "Ticket basic info" msgstr "工单基本信息" -#: tickets/notifications.py:65 +#: tickets/notifications.py:64 msgid "Ticket applied info" msgstr "工单申请信息" -#: tickets/notifications.py:116 -msgid "Your has a new ticket" +#: tickets/notifications.py:109 +msgid "Your has a new ticket, applicant - {}" msgstr "你有一个新的工单, 申请人 - {}" -#: tickets/notifications.py:120 +#: tickets/notifications.py:113 msgid "{}: New Ticket - {} ({})" msgstr "新工单 - {} ({})" -#: tickets/notifications.py:164 +#: tickets/notifications.py:157 msgid "Your ticket has been processed, processor - {}" msgstr "你的工单已被处理, 处理人 - {}" -#: tickets/notifications.py:168 +#: tickets/notifications.py:161 msgid "Ticket has processed - {} ({})" msgstr "你的工单已被处理, 处理人 - {} ({})" @@ -5375,19 +5389,19 @@ msgid "Processor" msgstr "处理人" #: tickets/serializers/ticket/common.py:16 -#: tickets/serializers/ticket/common.py:68 +#: tickets/serializers/ticket/common.py:79 msgid "Created by ticket ({}-{})" msgstr "通过工单创建 ({}-{})" -#: tickets/serializers/ticket/common.py:58 +#: tickets/serializers/ticket/common.py:69 msgid "The expiration date should be greater than the start date" msgstr "过期时间要大于开始时间" -#: tickets/serializers/ticket/common.py:74 +#: tickets/serializers/ticket/common.py:85 msgid "Permission named `{}` already exists" msgstr "授权名称 `{}` 已存在" -#: tickets/serializers/ticket/ticket.py:89 +#: tickets/serializers/ticket/ticket.py:92 msgid "The ticket flow `{}` does not exist" msgstr "工单流程 `{}` 不存在" diff --git a/apps/perms/serializers/base.py b/apps/perms/serializers/base.py index 3d48447fb..cbed4a2f8 100644 --- a/apps/perms/serializers/base.py +++ b/apps/perms/serializers/base.py @@ -21,8 +21,12 @@ class ActionsField(serializers.MultipleChoiceField): return Action.value_to_choices(value) def to_internal_value(self, data): - if data is None: + if not self.allow_empty and not data: + self.fail('empty') + + if not data: return data + return Action.choices_to_value(data) diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py index 0cf9652a6..ee6b76534 100644 --- a/apps/tickets/api/ticket.py +++ b/apps/tickets/api/ticket.py @@ -5,7 +5,7 @@ from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.exceptions import MethodNotAllowed -from common.const.http import POST, PUT +from common.const.http import POST, PUT, PATCH from common.mixins.api import CommonApiMixin from orgs.utils import tmp_to_root_org @@ -71,32 +71,34 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet): with tmp_to_root_org(): return super().create(request, *args, **kwargs) - @action(detail=True, methods=[PUT], permission_classes=[IsAssignee, ]) + @action(detail=True, methods=[PUT, PATCH], permission_classes=[IsAssignee, ]) def approve(self, request, *args, **kwargs): + partial = kwargs.pop('partial', False) instance = self.get_object() - serializer = self.get_serializer(instance) + serializer = self.get_serializer(instance, data=request.data, partial=partial) + serializer.is_valid(raise_exception=True) + instance = serializer.save() instance.approve(processor=request.user) - return Response(serializer.data) + return Response('ok') @action(detail=True, methods=[PUT], permission_classes=[IsAssignee, ]) def reject(self, request, *args, **kwargs): instance = self.get_object() - serializer = self.get_serializer(instance) instance.reject(processor=request.user) - return Response(serializer.data) + return Response('ok') @action(detail=True, methods=[PUT], permission_classes=[IsApplicant, ]) def close(self, request, *args, **kwargs): instance = self.get_object() - serializer = self.get_serializer(instance) instance.close() - return Response(serializer.data) + return Response('ok') class ApplyAssetTicketViewSet(TicketViewSet): serializer_class = serializers.ApplyAssetDisplaySerializer serializer_classes = { - 'open': serializers.ApplyAssetSerializer + 'open': serializers.ApplyAssetSerializer, + 'approve': serializers.ApproveAssetSerializer } model = ApplyAssetTicket filterset_class = filters.ApplyAssetTicketFilter @@ -105,7 +107,8 @@ class ApplyAssetTicketViewSet(TicketViewSet): class ApplyApplicationTicketViewSet(TicketViewSet): serializer_class = serializers.ApplyApplicationDisplaySerializer serializer_classes = { - 'open': serializers.ApplyApplicationSerializer + 'open': serializers.ApplyApplicationSerializer, + 'approve': serializers.ApproveApplicationSerializer } model = ApplyApplicationTicket filterset_class = filters.ApplyApplicationTicketFilter diff --git a/apps/tickets/handlers/base.py b/apps/tickets/handlers/base.py index 6165d035b..c48ce350b 100644 --- a/apps/tickets/handlers/base.py +++ b/apps/tickets/handlers/base.py @@ -1,11 +1,12 @@ from django.utils.translation import ugettext as _ +from django.template.loader import render_to_string from common.utils import get_logger from tickets.utils import ( send_ticket_processed_mail_to_applicant, send_ticket_applied_mail_to_assignees ) -from tickets.const import TicketState, TicketStatus +from tickets.const import TicketState, TicketType logger = get_logger(__name__) @@ -59,6 +60,25 @@ class BaseHandler: logger.debug('Send processed mail to applicant: {}'.format(applicant)) send_ticket_processed_mail_to_applicant(self.ticket, processor) + def _diff_prev_approve_context(self, state): + diff_context = {} + if state != TicketState.approved: + return diff_context + if self.ticket.type not in [TicketType.apply_asset, TicketType.apply_application]: + return diff_context + + old_rel_snapshot = self.ticket.old_rel_snapshot + current_rel_snapshot = self.ticket.get_local_snapshot() + diff = set(current_rel_snapshot.items()) - set(old_rel_snapshot.items()) + if not diff: + return diff_context + + content = [] + for k, v in sorted(list(diff), reverse=True): + content.append([k, old_rel_snapshot[k], v]) + headers = [_('Change field'), _('Before change'), _('After change')] + return {'headers': headers, 'content': content} + def _create_state_change_comment(self, state): # 打开或关闭工单,备注显示是自己,其他是受理人 if state in [TicketState.reopen, TicketState.pending, TicketState.closed]: @@ -68,8 +88,11 @@ class BaseHandler: user_display = str(user) state_display = getattr(TicketState, state).label + approve_info = _('{} {} the ticket').format(user_display, state_display) + context = self._diff_prev_approve_context(state) + context.update({'approve_info': approve_info}) data = { - 'body': _('{} {} the ticket').format(user_display, state_display), + 'body': render_to_string('tickets/ticket_approve_diff.html', context), 'user': user, 'user_display': str(user), 'type': 'state', diff --git a/apps/tickets/migrations/0017_auto_20220623_1027.py b/apps/tickets/migrations/0017_auto_20220623_1027.py index 3f241d54f..746d267c8 100644 --- a/apps/tickets/migrations/0017_auto_20220623_1027.py +++ b/apps/tickets/migrations/0017_auto_20220623_1027.py @@ -102,7 +102,7 @@ def apply_asset_migrate(apps, *args): 'applicant': instance.applicant_display, 'apply_nodes': meta.get('apply_nodes_display', []), 'apply_assets': meta.get('apply_assets_display', []), - 'apply_system_users': meta.get('apply_system_users', []), + 'apply_system_users': meta.get('apply_system_users_display', []), } instance.rel_snapshot = rel_snapshot instance.save(update_fields=['rel_snapshot']) @@ -140,7 +140,7 @@ def apply_application_migrate(apps, *args): rel_snapshot = { 'applicant': instance.applicant_display, 'apply_applications': meta.get('apply_applications_display', []), - 'apply_system_users': meta.get('apply_system_users', []), + 'apply_system_users': meta.get('apply_system_users_display', []), } instance.rel_snapshot = rel_snapshot instance.save(update_fields=['rel_snapshot']) diff --git a/apps/tickets/models/ticket/apply_application.py b/apps/tickets/models/ticket/apply_application.py index de041c8da..6bd721677 100644 --- a/apps/tickets/models/ticket/apply_application.py +++ b/apps/tickets/models/ticket/apply_application.py @@ -8,7 +8,7 @@ __all__ = ['ApplyApplicationTicket'] class ApplyApplicationTicket(Ticket): - apply_permission_name = models.CharField(max_length=128, verbose_name=_('Apply name')) + apply_permission_name = models.CharField(max_length=128, verbose_name=_('Permission name')) # 申请信息 apply_category = models.CharField( max_length=16, choices=AppCategory.choices, verbose_name=_('Category') diff --git a/apps/tickets/models/ticket/apply_asset.py b/apps/tickets/models/ticket/apply_asset.py index 45cad4dca..c3759dc9a 100644 --- a/apps/tickets/models/ticket/apply_asset.py +++ b/apps/tickets/models/ticket/apply_asset.py @@ -10,7 +10,7 @@ asset_or_node_help_text = _("Select at least one asset or node") class ApplyAssetTicket(Ticket): - apply_permission_name = models.CharField(max_length=128, verbose_name=_('Apply name')) + apply_permission_name = models.CharField(max_length=128, verbose_name=_('Permission name')) apply_nodes = models.ManyToManyField('assets.Node', verbose_name=_('Apply nodes')) # 申请信息 apply_assets = models.ManyToManyField('assets.Asset', verbose_name=_('Apply assets')) @@ -26,3 +26,6 @@ class ApplyAssetTicket(Ticket): @property def apply_actions_display(self): return Action.value_to_choices_display(self.apply_actions) + + def get_apply_actions_display(self): + return ', '.join(self.apply_actions_display) diff --git a/apps/tickets/models/ticket/general.py b/apps/tickets/models/ticket/general.py index c87b3aa7f..e58ffa04a 100644 --- a/apps/tickets/models/ticket/general.py +++ b/apps/tickets/models/ticket/general.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- # +import json from typing import Callable from django.db import models @@ -7,6 +8,7 @@ from django.db.models import Q from django.utils.translation import ugettext_lazy as _ from django.db.utils import IntegrityError from django.db.models.fields import related +from django.forms import model_to_dict from common.exceptions import JMSException from common.utils.timezone import as_current_tz @@ -97,6 +99,7 @@ class StatusMixin: state: str status: str + applicant_id: str applicant: models.ForeignKey current_step: TicketStep save: Callable @@ -130,6 +133,7 @@ class StatusMixin: self._open() def approve(self, processor): + self.set_rel_snapshot() self._change_state(StepState.approved, processor) def reject(self, processor): @@ -178,28 +182,34 @@ class StatusMixin: @property def process_map(self): process_map = [] - steps = self.ticket_steps.all() - for step in steps: + for step in self.ticket_steps.all(): + processor_id = '' assignee_ids = [] + processor_display = '' assignees_display = [] - ticket_assignees = step.ticket_assignees.all() - processor = None state = step.state - for i in ticket_assignees: - assignee_ids.append(i.assignee_id) - assignees_display.append(str(i.assignee)) + for i in step.ticket_assignees.all().prefetch_related('assignee'): + assignee_id = i.assignee_id + assignee_display = str(i.assignee) + if state != StepState.pending and state == i.state: - processor = i.assignee + processor_id = assignee_id + processor_display = assignee_display if state == StepState.closed: - processor = self.applicant + processor_id = self.applicant_id + processor_display = str(self.applicant) + + assignee_ids.append(assignee_id) + assignees_display.append(assignee_display) + step_info = { 'state': state, 'approval_level': step.level, 'assignees': assignee_ids, 'assignees_display': assignees_display, 'approval_date': str(step.date_updated), - 'processor': processor.id if processor else '', - 'processor_display': str(processor) if processor else '' + 'processor': processor_id, + 'processor_display': processor_display } process_map.append(step_info) return process_map @@ -380,6 +390,30 @@ class Ticket(StatusMixin, CommonModelMixin): raise JMSException(detail=_('Please try again'), code='please_try_again') raise e + def get_field_display(self, name, field, data: dict): + value = data.get(name) + if hasattr(self, f'get_{name}_display'): + value = getattr(self, f'get_{name}_display')() + elif isinstance(field, related.ForeignKey): + value = self.rel_snapshot[name] + elif isinstance(field, related.ManyToManyField): + value = ', '.join(self.rel_snapshot[name]) + return value + + def get_local_snapshot(self): + fields = self._meta._forward_fields_map + json_data = json.dumps(model_to_dict(self), cls=ModelJSONFieldEncoder) + data = json.loads(json_data) + snapshot = {} + local_fields = self._meta.local_fields + self._meta.local_many_to_many + excludes = ['ticket_ptr'] + item_names = [field.name for field in local_fields if field.name not in excludes] + for name in item_names: + field = fields[name] + value = self.get_field_display(name, field, data) + snapshot[field.verbose_name] = value + return snapshot + class SuperTicket(Ticket): class Meta: diff --git a/apps/tickets/notifications.py b/apps/tickets/notifications.py index 728510280..3fa0e5a82 100644 --- a/apps/tickets/notifications.py +++ b/apps/tickets/notifications.py @@ -4,7 +4,6 @@ import json from django.conf import settings from django.core.cache import cache from django.shortcuts import reverse -from django.db.models.fields import related from django.template.loader import render_to_string from django.forms import model_to_dict from django.utils.translation import ugettext_lazy as _ @@ -75,13 +74,7 @@ class BaseTicketMessage(UserMessage): for name in item_names: field = fields[name] item = {'name': name, 'title': field.verbose_name} - value = data.get(name) - if hasattr(self.ticket, f'get_{name}_display'): - value = getattr(self.ticket, f'get_{name}_display')() - elif isinstance(field, related.ForeignKey): - value = self.ticket.rel_snapshot[name] - elif isinstance(field, related.ManyToManyField): - value = ', '.join(self.ticket.rel_snapshot[name]) + value = self.ticket.get_field_display(name, field, data) item['value'] = value items.append(item) return items @@ -113,7 +106,7 @@ class TicketAppliedToAssigneeMessage(BaseTicketMessage): @property def content_title(self): - return _('Your has a new ticket') + return _('Your has a new ticket, applicant - {}').format(self.ticket.applicant) @property def subject(self): diff --git a/apps/tickets/serializers/ticket/apply_application.py b/apps/tickets/serializers/ticket/apply_application.py index 700f7b45e..c713f21d6 100644 --- a/apps/tickets/serializers/ticket/apply_application.py +++ b/apps/tickets/serializers/ticket/apply_application.py @@ -1,3 +1,4 @@ +from django.utils.translation import ugettext as _ from rest_framework import serializers from perms.models import ApplicationPermission @@ -7,7 +8,7 @@ from tickets.models import ApplyApplicationTicket from .ticket import TicketApplySerializer from .common import BaseApplyAssetApplicationSerializer -__all__ = ['ApplyApplicationSerializer', 'ApplyApplicationDisplaySerializer'] +__all__ = ['ApplyApplicationSerializer', 'ApplyApplicationDisplaySerializer', 'ApproveApplicationSerializer'] class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer): @@ -24,15 +25,23 @@ class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketAppl read_only_fields = list(set(fields) - set(writeable_fields)) ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs extra_kwargs = { - 'apply_system_users': {'required': True}, + 'apply_applications': {'required': False, 'allow_empty': True}, + 'apply_system_users': {'required': False, 'allow_empty': True}, } extra_kwargs.update(ticket_extra_kwargs) def validate_apply_applications(self, applications): + if self.is_final_approval and not applications: + raise serializers.ValidationError(_('This field is required.')) tp = self.initial_data.get('apply_type') return self.filter_many_to_many_field(Application, applications, type=tp) +class ApproveApplicationSerializer(ApplyApplicationSerializer): + class Meta(ApplyApplicationSerializer.Meta): + read_only_fields = ApplyApplicationSerializer.Meta.read_only_fields + ['title', 'type'] + + class ApplyApplicationDisplaySerializer(ApplyApplicationSerializer): apply_applications = serializers.SerializerMethodField() apply_system_users = serializers.SerializerMethodField() diff --git a/apps/tickets/serializers/ticket/apply_asset.py b/apps/tickets/serializers/ticket/apply_asset.py index c32de52c1..b8a007e8d 100644 --- a/apps/tickets/serializers/ticket/apply_asset.py +++ b/apps/tickets/serializers/ticket/apply_asset.py @@ -10,13 +10,13 @@ from tickets.models import ApplyAssetTicket from .ticket import TicketApplySerializer from .common import BaseApplyAssetApplicationSerializer -__all__ = ['ApplyAssetSerializer', 'ApplyAssetDisplaySerializer'] +__all__ = ['ApplyAssetSerializer', 'ApplyAssetDisplaySerializer', 'ApproveAssetSerializer'] asset_or_node_help_text = _("Select at least one asset or node") class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer): - apply_actions = ActionsField(required=True, allow_null=True) + apply_actions = ActionsField(required=True, allow_empty=False) permission_model = AssetPermission class Meta: @@ -30,9 +30,9 @@ class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySeria read_only_fields = list(set(fields) - set(writeable_fields)) ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs extra_kwargs = { - 'apply_nodes': {'required': False, 'help_text': asset_or_node_help_text}, - 'apply_assets': {'required': False, 'help_text': asset_or_node_help_text}, - 'apply_system_users': {'required': True}, + 'apply_nodes': {'required': False, 'allow_empty': True}, + 'apply_assets': {'required': False, 'allow_empty': True}, + 'apply_system_users': {'required': False, 'allow_empty': True}, } extra_kwargs.update(ticket_extra_kwargs) @@ -44,14 +44,22 @@ class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySeria def validate(self, attrs): attrs = super().validate(attrs) - if not attrs.get('apply_nodes') and not attrs.get('apply_assets'): + if self.is_final_approval and ( + not attrs.get('apply_nodes') and not attrs.get('apply_assets') + ): raise serializers.ValidationError({ 'apply_nodes': asset_or_node_help_text, 'apply_assets': asset_or_node_help_text, }) + return attrs +class ApproveAssetSerializer(ApplyAssetSerializer): + class Meta(ApplyAssetSerializer.Meta): + read_only_fields = ApplyAssetSerializer.Meta.read_only_fields + ['title', 'type'] + + class ApplyAssetDisplaySerializer(ApplyAssetSerializer): apply_nodes = serializers.SerializerMethodField() apply_assets = serializers.SerializerMethodField() diff --git a/apps/tickets/serializers/ticket/common.py b/apps/tickets/serializers/ticket/common.py index 4a375465e..6957da3b0 100644 --- a/apps/tickets/serializers/ticket/common.py +++ b/apps/tickets/serializers/ticket/common.py @@ -38,14 +38,25 @@ class DefaultPermissionName(object): class BaseApplyAssetApplicationSerializer(serializers.Serializer): permission_model: Model + @property + def is_final_approval(self): + instance = self.instance + if not instance: + return False + if instance.approval_step == instance.ticket_steps.count(): + return True + return False + def filter_many_to_many_field(self, model, values: list, **kwargs): - org_id = self.initial_data.get('org_id') + org_id = self.instance.org_id if self.instance else self.initial_data.get('org_id') ids = [instance.id for instance in values] with tmp_to_org(org_id): qs = model.objects.filter(id__in=ids, **kwargs).values_list('id', flat=True) return list(qs) def validate_apply_system_users(self, system_users): + if self.is_final_approval and not system_users: + raise serializers.ValidationError(_('This field is required.')) return self.filter_many_to_many_field(SystemUser, system_users) def validate(self, attrs): @@ -72,3 +83,10 @@ class BaseApplyAssetApplicationSerializer(serializers.Serializer): instance.save() return instance raise serializers.ValidationError(_('Permission named `{}` already exists'.format(name))) + + @atomic + def update(self, instance, validated_data): + old_rel_snapshot = instance.get_local_snapshot() + instance = super().update(instance, validated_data) + instance.old_rel_snapshot = old_rel_snapshot + return instance diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py index 2af3811ca..facf3fcec 100644 --- a/apps/tickets/serializers/ticket/ticket.py +++ b/apps/tickets/serializers/ticket/ticket.py @@ -80,6 +80,9 @@ class TicketApplySerializer(TicketSerializer): return org_id def validate(self, attrs): + if self.instance: + return attrs + ticket_type = attrs.get('type') org_id = attrs.get('org_id') flow = TicketFlow.get_org_related_flows(org_id=org_id).filter(type=ticket_type).first() diff --git a/apps/tickets/templates/tickets/ticket_approve_diff.html b/apps/tickets/templates/tickets/ticket_approve_diff.html new file mode 100644 index 000000000..9fe6b80e0 --- /dev/null +++ b/apps/tickets/templates/tickets/ticket_approve_diff.html @@ -0,0 +1,24 @@ + + +
+{{ approve_info }}
+{{ item }} | + {% endfor %} +
---|
{{ child }} | + {% endfor %} +