feat: 工单支持审批时修改资产 (#8549)

Co-authored-by: feng626 <1304903146@qq.com>
Co-authored-by: feng626 <57284900+feng626@users.noreply.github.com>
pull/8574/head
fit2bot 2022-07-12 15:28:42 +08:00 committed by GitHub
parent b64727e04c
commit b5cfc6831b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 274 additions and 122 deletions

View File

@ -4,6 +4,7 @@ import logging
from datetime import datetime from datetime import datetime
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils import timezone as dj_timezone
from django.db import models from django.db import models
from django.conf import settings from django.conf import settings
@ -18,6 +19,7 @@ class ModelJSONFieldEncoder(json.JSONEncoder):
if isinstance(obj, str_cls): if isinstance(obj, str_cls):
return str(obj) return str(obj)
elif isinstance(obj, datetime): elif isinstance(obj, datetime):
obj = dj_timezone.localtime(obj)
return obj.strftime(settings.DATETIME_DISPLAY_FORMAT) return obj.strftime(settings.DATETIME_DISPLAY_FORMAT)
elif isinstance(obj, (list, tuple)) and len(obj) > 0 \ elif isinstance(obj, (list, tuple)) and len(obj) > 0 \
and isinstance(obj[0], models.Model): and isinstance(obj[0], models.Model):

View File

@ -63,7 +63,7 @@ msgstr "アクティブ"
#: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:34 #: 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/endpoint.py:21 terminal/models/endpoint.py:92
#: terminal/models/storage.py:29 terminal/models/terminal.py:114 #: 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 #: users/models/group.py:16 users/models/user.py:698
#: xpack/plugins/change_auth_plan/models/base.py:44 #: xpack/plugins/change_auth_plan/models/base.py:44
#: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:116 #: 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 #: assets/models/cmd_filter.py:82 assets/models/user.py:250
#: authentication/models.py:69 perms/models/application_permission.py:24 #: authentication/models.py:69 perms/models/application_permission.py:24
#: perms/serializers/application/user_permission.py:34 #: 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/comment.py:26 tickets/models/flow.py:57
#: tickets/models/ticket/apply_application.py:17 #: 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:28
#: xpack/plugins/change_auth_plan/models/app.py:153 #: xpack/plugins/change_auth_plan/models/app.py:153
msgid "Type" msgid "Type"
@ -416,6 +416,8 @@ msgstr "アプリケーションパス"
#: applications/serializers/attrs/application_category/remote_app.py:44 #: applications/serializers/attrs/application_category/remote_app.py:44
#: assets/serializers/system_user.py:167 #: 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:67
#: xpack/plugins/change_auth_plan/serializers/asset.py:70 #: xpack/plugins/change_auth_plan/serializers/asset.py:70
#: xpack/plugins/change_auth_plan/serializers/asset.py:73 #: xpack/plugins/change_auth_plan/serializers/asset.py:73
@ -503,7 +505,7 @@ msgid "Charset"
msgstr "シャーセット" msgstr "シャーセット"
#: assets/models/asset.py:141 assets/serializers/asset.py:176 #: assets/models/asset.py:141 assets/serializers/asset.py:176
#: tickets/models/ticket/general.py:288 #: tickets/models/ticket/general.py:298
msgid "Meta" msgid "Meta"
msgstr "メタ" msgstr "メタ"
@ -525,7 +527,7 @@ msgstr "ベンダー"
msgid "Model" msgid "Model"
msgstr "モデル" 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" msgid "Serial number"
msgstr "シリアル番号" msgstr "シリアル番号"
@ -1525,7 +1527,7 @@ msgid "MFA"
msgstr "MFA" msgstr "MFA"
#: audits/models.py:128 terminal/models/status.py:33 #: 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 #: xpack/plugins/cloud/models.py:227
msgid "Status" msgid "Status"
msgstr "ステータス" msgstr "ステータス"
@ -2581,7 +2583,7 @@ msgstr "%(name)s が正常に作成されました"
msgid "%(name)s was updated successfully" msgid "%(name)s was updated successfully"
msgstr "%(name)s は正常に更新されました" msgstr "%(name)s は正常に更新されました"
#: common/db/encoder.py:10 #: common/db/encoder.py:11
msgid "ugettext_lazy" msgid "ugettext_lazy"
msgstr "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/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 #: orgs/models.py:211 rbac/const.py:7 rbac/models/rolebinding.py:48
#: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62 #: 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" msgid "Organization"
msgstr "組織" msgstr "組織"
@ -3417,7 +3419,7 @@ msgstr "マイアプリ"
msgid "Ticket comment" msgid "Ticket comment"
msgstr "チケットコメント" msgstr "チケットコメント"
#: rbac/tree.py:115 tickets/models/ticket/general.py:295 #: rbac/tree.py:115 tickets/models/ticket/general.py:305
msgid "Ticket" msgid "Ticket"
msgstr "チケット" msgstr "チケット"
@ -4939,7 +4941,7 @@ msgstr "リンク期限切れ"
msgid "User not allowed to join" msgid "User not allowed to join"
msgstr "IPは許可されていません" msgstr "IPは許可されていません"
#: terminal/models/sharing.py:85 terminal/serializers/sharing.py:58 #: terminal/models/sharing.py:85 terminal/serializers/sharing.py:59
msgid "Joiner" msgid "Joiner"
msgstr "ジョイナー" msgstr "ジョイナー"
@ -4996,11 +4998,11 @@ msgstr "ブート時間"
msgid "Default storage" msgid "Default storage"
msgstr "デフォルトのストレージ" msgstr "デフォルトのストレージ"
#: terminal/models/storage.py:136 terminal/models/terminal.py:108 #: terminal/models/storage.py:137 terminal/models/terminal.py:108
msgid "Command storage" msgid "Command storage"
msgstr "コマンドストレージ" msgstr "コマンドストレージ"
#: terminal/models/storage.py:196 terminal/models/terminal.py:109 #: terminal/models/storage.py:197 terminal/models/terminal.py:109
msgid "Replay storage" msgid "Replay storage"
msgstr "再生ストレージ" msgstr "再生ストレージ"
@ -5253,7 +5255,19 @@ msgstr ""
"チケットのタイトル: {} チケット申請者: {} チケットプロセッサ: {} チケットID: " "チケットのタイトル: {} チケット申請者: {} チケットプロセッサ: {} チケット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" msgid "{} {} the ticket"
msgstr "{} {} チケット" msgstr "{} {} チケット"
@ -5269,8 +5283,8 @@ msgstr "応用ログイン都市"
msgid "Applied login datetime" msgid "Applied login datetime"
msgstr "適用されたログインの日付時間" msgstr "適用されたログインの日付時間"
#: tickets/models/comment.py:13 tickets/models/ticket/general.py:39 #: tickets/models/comment.py:13 tickets/models/ticket/general.py:41
#: tickets/models/ticket/general.py:267 #: tickets/models/ticket/general.py:277
msgid "State" msgid "State"
msgstr "状態" msgstr "状態"
@ -5287,7 +5301,7 @@ msgid "Body"
msgstr "ボディ" msgstr "ボディ"
#: tickets/models/flow.py:20 tickets/models/flow.py:62 #: 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" msgid "Approve level"
msgstr "レベルを承認する" msgstr "レベルを承認する"
@ -5313,8 +5327,8 @@ msgstr "チケットセッションの関係"
#: tickets/models/ticket/apply_application.py:11 #: tickets/models/ticket/apply_application.py:11
#: tickets/models/ticket/apply_asset.py:13 #: tickets/models/ticket/apply_asset.py:13
msgid "Apply name" msgid "Permission name"
msgstr "名前を適用" msgstr "認可ルール名"
#: tickets/models/ticket/apply_application.py:20 #: tickets/models/ticket/apply_application.py:20
msgid "Apply applications" msgid "Apply applications"
@ -5362,39 +5376,39 @@ msgstr "コマンドフィルタ規則から"
msgid "From cmd filter rule" msgid "From cmd filter rule"
msgstr "コマンドフィルタ規則から" msgstr "コマンドフィルタ規則から"
#: tickets/models/ticket/general.py:70 #: tickets/models/ticket/general.py:72
msgid "Ticket step" msgid "Ticket step"
msgstr "チケットステップ" msgstr "チケットステップ"
#: tickets/models/ticket/general.py:88 #: tickets/models/ticket/general.py:90
msgid "Ticket assignee" msgid "Ticket assignee"
msgstr "割り当てられたチケット" msgstr "割り当てられたチケット"
#: tickets/models/ticket/general.py:260 #: tickets/models/ticket/general.py:270
msgid "Title" msgid "Title"
msgstr "タイトル" msgstr "タイトル"
#: tickets/models/ticket/general.py:276 #: tickets/models/ticket/general.py:286
msgid "Applicant" msgid "Applicant"
msgstr "応募者" msgstr "応募者"
#: tickets/models/ticket/general.py:281 #: tickets/models/ticket/general.py:291
msgid "TicketFlow" msgid "TicketFlow"
msgstr "作業指示プロセス" msgstr "作業指示プロセス"
#: tickets/models/ticket/general.py:284 #: tickets/models/ticket/general.py:294
msgid "Approval step" msgid "Approval step"
msgstr "承認ステップ" msgstr "承認ステップ"
#: tickets/models/ticket/general.py:287 #: tickets/models/ticket/general.py:297
msgid "Relation snapshot" msgid "Relation snapshot"
msgstr "製造オーダスナップショット" msgstr "製造オーダスナップショット"
#: tickets/models/ticket/general.py:380 #: tickets/models/ticket/general.py:390
msgid "Please try again" msgid "Please try again"
msgstr "もう一度お試しください" msgstr "もう一度お試しください"
#: tickets/models/ticket/general.py:387 #: tickets/models/ticket/general.py:421
msgid "Super ticket" msgid "Super ticket"
msgstr "スーパーチケット" msgstr "スーパーチケット"
@ -5414,27 +5428,27 @@ msgstr "ログインシステムユーザー"
msgid "Login datetime" msgid "Login datetime"
msgstr "ログイン日時" msgstr "ログイン日時"
#: tickets/notifications.py:64 #: tickets/notifications.py:63
msgid "Ticket basic info" msgid "Ticket basic info"
msgstr "チケット基本情報" msgstr "チケット基本情報"
#: tickets/notifications.py:65 #: tickets/notifications.py:64
msgid "Ticket applied info" msgid "Ticket applied info"
msgstr "チケット適用情報" msgstr "チケット適用情報"
#: tickets/notifications.py:116 #: tickets/notifications.py:109
msgid "Your has a new ticket" msgid "Your has a new ticket, applicant - {}"
msgstr "新しいチケットがあります- {}" msgstr "新しいチケットがあります- {}"
#: tickets/notifications.py:120 #: tickets/notifications.py:113
msgid "{}: New Ticket - {} ({})" msgid "{}: New Ticket - {} ({})"
msgstr "新しいチケット- {} ({})" msgstr "新しいチケット- {} ({})"
#: tickets/notifications.py:164 #: tickets/notifications.py:157
msgid "Your ticket has been processed, processor - {}" msgid "Your ticket has been processed, processor - {}"
msgstr "チケットが処理されました。プロセッサー- {}" msgstr "チケットが処理されました。プロセッサー- {}"
#: tickets/notifications.py:168 #: tickets/notifications.py:161
msgid "Ticket has processed - {} ({})" msgid "Ticket has processed - {} ({})"
msgstr "チケットが処理済み- {} ({})" msgstr "チケットが処理済み- {} ({})"
@ -5455,19 +5469,19 @@ msgid "Processor"
msgstr "プロセッサ" msgstr "プロセッサ"
#: tickets/serializers/ticket/common.py:16 #: tickets/serializers/ticket/common.py:16
#: tickets/serializers/ticket/common.py:68 #: tickets/serializers/ticket/common.py:79
msgid "Created by ticket ({}-{})" msgid "Created by ticket ({}-{})"
msgstr "チケットで作成 ({}-{})" msgstr "チケットで作成 ({}-{})"
#: tickets/serializers/ticket/common.py:58 #: tickets/serializers/ticket/common.py:69
msgid "The expiration date should be greater than the start date" msgid "The expiration date should be greater than the start date"
msgstr "有効期限は開始日より大きくする必要があります" msgstr "有効期限は開始日より大きくする必要があります"
#: tickets/serializers/ticket/common.py:74 #: tickets/serializers/ticket/common.py:85
msgid "Permission named `{}` already exists" msgid "Permission named `{}` already exists"
msgstr "'{}'という名前の権限は既に存在します" msgstr "'{}'という名前の権限は既に存在します"
#: tickets/serializers/ticket/ticket.py:89 #: tickets/serializers/ticket/ticket.py:92
msgid "The ticket flow `{}` does not exist" msgid "The ticket flow `{}` does not exist"
msgstr "チケットフロー '{}'が存在しない" msgstr "チケットフロー '{}'が存在しない"

View File

@ -62,7 +62,7 @@ msgstr "激活中"
#: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:34 #: 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/endpoint.py:21 terminal/models/endpoint.py:92
#: terminal/models/storage.py:29 terminal/models/terminal.py:114 #: 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 #: users/models/group.py:16 users/models/user.py:698
#: xpack/plugins/change_auth_plan/models/base.py:44 #: xpack/plugins/change_auth_plan/models/base.py:44
#: xpack/plugins/cloud/models.py:35 xpack/plugins/cloud/models.py:116 #: 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 #: assets/models/cmd_filter.py:82 assets/models/user.py:250
#: authentication/models.py:69 perms/models/application_permission.py:24 #: authentication/models.py:69 perms/models/application_permission.py:24
#: perms/serializers/application/user_permission.py:34 #: 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/comment.py:26 tickets/models/flow.py:57
#: tickets/models/ticket/apply_application.py:17 #: 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:28
#: xpack/plugins/change_auth_plan/models/app.py:153 #: xpack/plugins/change_auth_plan/models/app.py:153
msgid "Type" msgid "Type"
@ -411,6 +411,8 @@ msgstr "应用路径"
#: applications/serializers/attrs/application_category/remote_app.py:44 #: applications/serializers/attrs/application_category/remote_app.py:44
#: assets/serializers/system_user.py:167 #: 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:67
#: xpack/plugins/change_auth_plan/serializers/asset.py:70 #: xpack/plugins/change_auth_plan/serializers/asset.py:70
#: xpack/plugins/change_auth_plan/serializers/asset.py:73 #: xpack/plugins/change_auth_plan/serializers/asset.py:73
@ -498,7 +500,7 @@ msgid "Charset"
msgstr "编码" msgstr "编码"
#: assets/models/asset.py:141 assets/serializers/asset.py:176 #: assets/models/asset.py:141 assets/serializers/asset.py:176
#: tickets/models/ticket/general.py:288 #: tickets/models/ticket/general.py:298
msgid "Meta" msgid "Meta"
msgstr "元数据" msgstr "元数据"
@ -520,7 +522,7 @@ msgstr "制造商"
msgid "Model" msgid "Model"
msgstr "型号" 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" msgid "Serial number"
msgstr "序列号" msgstr "序列号"
@ -1513,7 +1515,7 @@ msgid "MFA"
msgstr "MFA" msgstr "MFA"
#: audits/models.py:128 terminal/models/status.py:33 #: 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 #: xpack/plugins/cloud/models.py:227
msgid "Status" msgid "Status"
msgstr "状态" msgstr "状态"
@ -2547,7 +2549,7 @@ msgstr "%(name)s 创建成功"
msgid "%(name)s was updated successfully" msgid "%(name)s was updated successfully"
msgstr "%(name)s 更新成功" msgstr "%(name)s 更新成功"
#: common/db/encoder.py:10 #: common/db/encoder.py:11
msgid "ugettext_lazy" msgid "ugettext_lazy"
msgstr "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/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 #: orgs/models.py:211 rbac/const.py:7 rbac/models/rolebinding.py:48
#: rbac/serializers/rolebinding.py:40 settings/serializers/auth/ldap.py:62 #: 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" msgid "Organization"
msgstr "组织" msgstr "组织"
@ -3374,7 +3376,7 @@ msgstr "我的应用"
msgid "Ticket comment" msgid "Ticket comment"
msgstr "工单评论" msgstr "工单评论"
#: rbac/tree.py:115 tickets/models/ticket/general.py:295 #: rbac/tree.py:115 tickets/models/ticket/general.py:305
msgid "Ticket" msgid "Ticket"
msgstr "工单管理" msgstr "工单管理"
@ -4863,7 +4865,7 @@ msgstr "链接过期"
msgid "User not allowed to join" msgid "User not allowed to join"
msgstr "来源 IP 不被允许登录" msgstr "来源 IP 不被允许登录"
#: terminal/models/sharing.py:85 terminal/serializers/sharing.py:58 #: terminal/models/sharing.py:85 terminal/serializers/sharing.py:59
msgid "Joiner" msgid "Joiner"
msgstr "加入者" msgstr "加入者"
@ -4920,11 +4922,11 @@ msgstr "运行时间"
msgid "Default storage" msgid "Default storage"
msgstr "默认存储" msgstr "默认存储"
#: terminal/models/storage.py:136 terminal/models/terminal.py:108 #: terminal/models/storage.py:137 terminal/models/terminal.py:108
msgid "Command storage" msgid "Command storage"
msgstr "命令存储" msgstr "命令存储"
#: terminal/models/storage.py:196 terminal/models/terminal.py:109 #: terminal/models/storage.py:197 terminal/models/terminal.py:109
msgid "Replay storage" msgid "Replay storage"
msgstr "录像存储" msgstr "录像存储"
@ -5173,7 +5175,19 @@ msgid ""
msgstr "" msgstr ""
"通过工单创建, 工单标题: {}, 工单申请人: {}, 工单处理人: {}, 工单 ID: {}" "通过工单创建, 工单标题: {}, 工单申请人: {}, 工单处理人: {}, 工单 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" msgid "{} {} the ticket"
msgstr "{} {} 工单" msgstr "{} {} 工单"
@ -5189,8 +5203,8 @@ msgstr "申请登录的城市"
msgid "Applied login datetime" msgid "Applied login datetime"
msgstr "申请登录的日期" msgstr "申请登录的日期"
#: tickets/models/comment.py:13 tickets/models/ticket/general.py:39 #: tickets/models/comment.py:13 tickets/models/ticket/general.py:41
#: tickets/models/ticket/general.py:267 #: tickets/models/ticket/general.py:277
msgid "State" msgid "State"
msgstr "状态" msgstr "状态"
@ -5207,7 +5221,7 @@ msgid "Body"
msgstr "内容" msgstr "内容"
#: tickets/models/flow.py:20 tickets/models/flow.py:62 #: 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" msgid "Approve level"
msgstr "审批级别" msgstr "审批级别"
@ -5233,8 +5247,8 @@ msgstr "工单会话"
#: tickets/models/ticket/apply_application.py:11 #: tickets/models/ticket/apply_application.py:11
#: tickets/models/ticket/apply_asset.py:13 #: tickets/models/ticket/apply_asset.py:13
msgid "Apply name" msgid "Permission name"
msgstr "应用名称" msgstr "授权规则名称"
#: tickets/models/ticket/apply_application.py:20 #: tickets/models/ticket/apply_application.py:20
msgid "Apply applications" msgid "Apply applications"
@ -5282,39 +5296,39 @@ msgstr "来自命令过滤规则"
msgid "From cmd filter rule" msgid "From cmd filter rule"
msgstr "来自命令过滤规则" msgstr "来自命令过滤规则"
#: tickets/models/ticket/general.py:70 #: tickets/models/ticket/general.py:72
msgid "Ticket step" msgid "Ticket step"
msgstr "工单步骤" msgstr "工单步骤"
#: tickets/models/ticket/general.py:88 #: tickets/models/ticket/general.py:90
msgid "Ticket assignee" msgid "Ticket assignee"
msgstr "工单受理人" msgstr "工单受理人"
#: tickets/models/ticket/general.py:260 #: tickets/models/ticket/general.py:270
msgid "Title" msgid "Title"
msgstr "标题" msgstr "标题"
#: tickets/models/ticket/general.py:276 #: tickets/models/ticket/general.py:286
msgid "Applicant" msgid "Applicant"
msgstr "申请人" msgstr "申请人"
#: tickets/models/ticket/general.py:281 #: tickets/models/ticket/general.py:291
msgid "TicketFlow" msgid "TicketFlow"
msgstr "工单流程" msgstr "工单流程"
#: tickets/models/ticket/general.py:284 #: tickets/models/ticket/general.py:294
msgid "Approval step" msgid "Approval step"
msgstr "审批步骤" msgstr "审批步骤"
#: tickets/models/ticket/general.py:287 #: tickets/models/ticket/general.py:297
msgid "Relation snapshot" msgid "Relation snapshot"
msgstr "工单快照" msgstr "工单快照"
#: tickets/models/ticket/general.py:380 #: tickets/models/ticket/general.py:390
msgid "Please try again" msgid "Please try again"
msgstr "请再次尝试" msgstr "请再次尝试"
#: tickets/models/ticket/general.py:387 #: tickets/models/ticket/general.py:421
msgid "Super ticket" msgid "Super ticket"
msgstr "超级工单" msgstr "超级工单"
@ -5334,27 +5348,27 @@ msgstr "登录系统用户"
msgid "Login datetime" msgid "Login datetime"
msgstr "登录日期" msgstr "登录日期"
#: tickets/notifications.py:64 #: tickets/notifications.py:63
msgid "Ticket basic info" msgid "Ticket basic info"
msgstr "工单基本信息" msgstr "工单基本信息"
#: tickets/notifications.py:65 #: tickets/notifications.py:64
msgid "Ticket applied info" msgid "Ticket applied info"
msgstr "工单申请信息" msgstr "工单申请信息"
#: tickets/notifications.py:116 #: tickets/notifications.py:109
msgid "Your has a new ticket" msgid "Your has a new ticket, applicant - {}"
msgstr "你有一个新的工单, 申请人 - {}" msgstr "你有一个新的工单, 申请人 - {}"
#: tickets/notifications.py:120 #: tickets/notifications.py:113
msgid "{}: New Ticket - {} ({})" msgid "{}: New Ticket - {} ({})"
msgstr "新工单 - {} ({})" msgstr "新工单 - {} ({})"
#: tickets/notifications.py:164 #: tickets/notifications.py:157
msgid "Your ticket has been processed, processor - {}" msgid "Your ticket has been processed, processor - {}"
msgstr "你的工单已被处理, 处理人 - {}" msgstr "你的工单已被处理, 处理人 - {}"
#: tickets/notifications.py:168 #: tickets/notifications.py:161
msgid "Ticket has processed - {} ({})" msgid "Ticket has processed - {} ({})"
msgstr "你的工单已被处理, 处理人 - {} ({})" msgstr "你的工单已被处理, 处理人 - {} ({})"
@ -5375,19 +5389,19 @@ msgid "Processor"
msgstr "处理人" msgstr "处理人"
#: tickets/serializers/ticket/common.py:16 #: tickets/serializers/ticket/common.py:16
#: tickets/serializers/ticket/common.py:68 #: tickets/serializers/ticket/common.py:79
msgid "Created by ticket ({}-{})" msgid "Created by ticket ({}-{})"
msgstr "通过工单创建 ({}-{})" msgstr "通过工单创建 ({}-{})"
#: tickets/serializers/ticket/common.py:58 #: tickets/serializers/ticket/common.py:69
msgid "The expiration date should be greater than the start date" msgid "The expiration date should be greater than the start date"
msgstr "过期时间要大于开始时间" msgstr "过期时间要大于开始时间"
#: tickets/serializers/ticket/common.py:74 #: tickets/serializers/ticket/common.py:85
msgid "Permission named `{}` already exists" msgid "Permission named `{}` already exists"
msgstr "授权名称 `{}` 已存在" msgstr "授权名称 `{}` 已存在"
#: tickets/serializers/ticket/ticket.py:89 #: tickets/serializers/ticket/ticket.py:92
msgid "The ticket flow `{}` does not exist" msgid "The ticket flow `{}` does not exist"
msgstr "工单流程 `{}` 不存在" msgstr "工单流程 `{}` 不存在"

View File

@ -21,8 +21,12 @@ class ActionsField(serializers.MultipleChoiceField):
return Action.value_to_choices(value) return Action.value_to_choices(value)
def to_internal_value(self, data): 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 data
return Action.choices_to_value(data) return Action.choices_to_value(data)

View File

@ -5,7 +5,7 @@ from rest_framework.decorators import action
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.exceptions import MethodNotAllowed 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 common.mixins.api import CommonApiMixin
from orgs.utils import tmp_to_root_org from orgs.utils import tmp_to_root_org
@ -71,32 +71,34 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
with tmp_to_root_org(): with tmp_to_root_org():
return super().create(request, *args, **kwargs) 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): def approve(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object() 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) instance.approve(processor=request.user)
return Response(serializer.data) return Response('ok')
@action(detail=True, methods=[PUT], permission_classes=[IsAssignee, ]) @action(detail=True, methods=[PUT], permission_classes=[IsAssignee, ])
def reject(self, request, *args, **kwargs): def reject(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
serializer = self.get_serializer(instance)
instance.reject(processor=request.user) instance.reject(processor=request.user)
return Response(serializer.data) return Response('ok')
@action(detail=True, methods=[PUT], permission_classes=[IsApplicant, ]) @action(detail=True, methods=[PUT], permission_classes=[IsApplicant, ])
def close(self, request, *args, **kwargs): def close(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
serializer = self.get_serializer(instance)
instance.close() instance.close()
return Response(serializer.data) return Response('ok')
class ApplyAssetTicketViewSet(TicketViewSet): class ApplyAssetTicketViewSet(TicketViewSet):
serializer_class = serializers.ApplyAssetDisplaySerializer serializer_class = serializers.ApplyAssetDisplaySerializer
serializer_classes = { serializer_classes = {
'open': serializers.ApplyAssetSerializer 'open': serializers.ApplyAssetSerializer,
'approve': serializers.ApproveAssetSerializer
} }
model = ApplyAssetTicket model = ApplyAssetTicket
filterset_class = filters.ApplyAssetTicketFilter filterset_class = filters.ApplyAssetTicketFilter
@ -105,7 +107,8 @@ class ApplyAssetTicketViewSet(TicketViewSet):
class ApplyApplicationTicketViewSet(TicketViewSet): class ApplyApplicationTicketViewSet(TicketViewSet):
serializer_class = serializers.ApplyApplicationDisplaySerializer serializer_class = serializers.ApplyApplicationDisplaySerializer
serializer_classes = { serializer_classes = {
'open': serializers.ApplyApplicationSerializer 'open': serializers.ApplyApplicationSerializer,
'approve': serializers.ApproveApplicationSerializer
} }
model = ApplyApplicationTicket model = ApplyApplicationTicket
filterset_class = filters.ApplyApplicationTicketFilter filterset_class = filters.ApplyApplicationTicketFilter

View File

@ -1,11 +1,12 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.template.loader import render_to_string
from common.utils import get_logger from common.utils import get_logger
from tickets.utils import ( from tickets.utils import (
send_ticket_processed_mail_to_applicant, send_ticket_processed_mail_to_applicant,
send_ticket_applied_mail_to_assignees send_ticket_applied_mail_to_assignees
) )
from tickets.const import TicketState, TicketStatus from tickets.const import TicketState, TicketType
logger = get_logger(__name__) logger = get_logger(__name__)
@ -59,6 +60,25 @@ class BaseHandler:
logger.debug('Send processed mail to applicant: {}'.format(applicant)) logger.debug('Send processed mail to applicant: {}'.format(applicant))
send_ticket_processed_mail_to_applicant(self.ticket, processor) 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): def _create_state_change_comment(self, state):
# 打开或关闭工单,备注显示是自己,其他是受理人 # 打开或关闭工单,备注显示是自己,其他是受理人
if state in [TicketState.reopen, TicketState.pending, TicketState.closed]: if state in [TicketState.reopen, TicketState.pending, TicketState.closed]:
@ -68,8 +88,11 @@ class BaseHandler:
user_display = str(user) user_display = str(user)
state_display = getattr(TicketState, state).label 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 = { data = {
'body': _('{} {} the ticket').format(user_display, state_display), 'body': render_to_string('tickets/ticket_approve_diff.html', context),
'user': user, 'user': user,
'user_display': str(user), 'user_display': str(user),
'type': 'state', 'type': 'state',

View File

@ -102,7 +102,7 @@ def apply_asset_migrate(apps, *args):
'applicant': instance.applicant_display, 'applicant': instance.applicant_display,
'apply_nodes': meta.get('apply_nodes_display', []), 'apply_nodes': meta.get('apply_nodes_display', []),
'apply_assets': meta.get('apply_assets_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.rel_snapshot = rel_snapshot
instance.save(update_fields=['rel_snapshot']) instance.save(update_fields=['rel_snapshot'])
@ -140,7 +140,7 @@ def apply_application_migrate(apps, *args):
rel_snapshot = { rel_snapshot = {
'applicant': instance.applicant_display, 'applicant': instance.applicant_display,
'apply_applications': meta.get('apply_applications_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.rel_snapshot = rel_snapshot
instance.save(update_fields=['rel_snapshot']) instance.save(update_fields=['rel_snapshot'])

View File

@ -8,7 +8,7 @@ __all__ = ['ApplyApplicationTicket']
class ApplyApplicationTicket(Ticket): 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( apply_category = models.CharField(
max_length=16, choices=AppCategory.choices, verbose_name=_('Category') max_length=16, choices=AppCategory.choices, verbose_name=_('Category')

View File

@ -10,7 +10,7 @@ asset_or_node_help_text = _("Select at least one asset or node")
class ApplyAssetTicket(Ticket): 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_nodes = models.ManyToManyField('assets.Node', verbose_name=_('Apply nodes'))
# 申请信息 # 申请信息
apply_assets = models.ManyToManyField('assets.Asset', verbose_name=_('Apply assets')) apply_assets = models.ManyToManyField('assets.Asset', verbose_name=_('Apply assets'))
@ -26,3 +26,6 @@ class ApplyAssetTicket(Ticket):
@property @property
def apply_actions_display(self): def apply_actions_display(self):
return Action.value_to_choices_display(self.apply_actions) return Action.value_to_choices_display(self.apply_actions)
def get_apply_actions_display(self):
return ', '.join(self.apply_actions_display)

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import json
from typing import Callable from typing import Callable
from django.db import models 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.utils.translation import ugettext_lazy as _
from django.db.utils import IntegrityError from django.db.utils import IntegrityError
from django.db.models.fields import related from django.db.models.fields import related
from django.forms import model_to_dict
from common.exceptions import JMSException from common.exceptions import JMSException
from common.utils.timezone import as_current_tz from common.utils.timezone import as_current_tz
@ -97,6 +99,7 @@ class StatusMixin:
state: str state: str
status: str status: str
applicant_id: str
applicant: models.ForeignKey applicant: models.ForeignKey
current_step: TicketStep current_step: TicketStep
save: Callable save: Callable
@ -130,6 +133,7 @@ class StatusMixin:
self._open() self._open()
def approve(self, processor): def approve(self, processor):
self.set_rel_snapshot()
self._change_state(StepState.approved, processor) self._change_state(StepState.approved, processor)
def reject(self, processor): def reject(self, processor):
@ -178,28 +182,34 @@ class StatusMixin:
@property @property
def process_map(self): def process_map(self):
process_map = [] process_map = []
steps = self.ticket_steps.all() for step in self.ticket_steps.all():
for step in steps: processor_id = ''
assignee_ids = [] assignee_ids = []
processor_display = ''
assignees_display = [] assignees_display = []
ticket_assignees = step.ticket_assignees.all()
processor = None
state = step.state state = step.state
for i in ticket_assignees: for i in step.ticket_assignees.all().prefetch_related('assignee'):
assignee_ids.append(i.assignee_id) assignee_id = i.assignee_id
assignees_display.append(str(i.assignee)) assignee_display = str(i.assignee)
if state != StepState.pending and state == i.state: if state != StepState.pending and state == i.state:
processor = i.assignee processor_id = assignee_id
processor_display = assignee_display
if state == StepState.closed: 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 = { step_info = {
'state': state, 'state': state,
'approval_level': step.level, 'approval_level': step.level,
'assignees': assignee_ids, 'assignees': assignee_ids,
'assignees_display': assignees_display, 'assignees_display': assignees_display,
'approval_date': str(step.date_updated), 'approval_date': str(step.date_updated),
'processor': processor.id if processor else '', 'processor': processor_id,
'processor_display': str(processor) if processor else '' 'processor_display': processor_display
} }
process_map.append(step_info) process_map.append(step_info)
return process_map return process_map
@ -380,6 +390,30 @@ class Ticket(StatusMixin, CommonModelMixin):
raise JMSException(detail=_('Please try again'), code='please_try_again') raise JMSException(detail=_('Please try again'), code='please_try_again')
raise e 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 SuperTicket(Ticket):
class Meta: class Meta:

View File

@ -4,7 +4,6 @@ import json
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.shortcuts import reverse from django.shortcuts import reverse
from django.db.models.fields import related
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.forms import model_to_dict from django.forms import model_to_dict
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -75,13 +74,7 @@ class BaseTicketMessage(UserMessage):
for name in item_names: for name in item_names:
field = fields[name] field = fields[name]
item = {'name': name, 'title': field.verbose_name} item = {'name': name, 'title': field.verbose_name}
value = data.get(name) value = self.ticket.get_field_display(name, field, data)
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])
item['value'] = value item['value'] = value
items.append(item) items.append(item)
return items return items
@ -113,7 +106,7 @@ class TicketAppliedToAssigneeMessage(BaseTicketMessage):
@property @property
def content_title(self): def content_title(self):
return _('Your has a new ticket') return _('Your has a new ticket, applicant - {}').format(self.ticket.applicant)
@property @property
def subject(self): def subject(self):

View File

@ -1,3 +1,4 @@
from django.utils.translation import ugettext as _
from rest_framework import serializers from rest_framework import serializers
from perms.models import ApplicationPermission from perms.models import ApplicationPermission
@ -7,7 +8,7 @@ from tickets.models import ApplyApplicationTicket
from .ticket import TicketApplySerializer from .ticket import TicketApplySerializer
from .common import BaseApplyAssetApplicationSerializer from .common import BaseApplyAssetApplicationSerializer
__all__ = ['ApplyApplicationSerializer', 'ApplyApplicationDisplaySerializer'] __all__ = ['ApplyApplicationSerializer', 'ApplyApplicationDisplaySerializer', 'ApproveApplicationSerializer']
class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer): class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer):
@ -24,15 +25,23 @@ class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketAppl
read_only_fields = list(set(fields) - set(writeable_fields)) read_only_fields = list(set(fields) - set(writeable_fields))
ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs
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) extra_kwargs.update(ticket_extra_kwargs)
def validate_apply_applications(self, applications): 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') tp = self.initial_data.get('apply_type')
return self.filter_many_to_many_field(Application, applications, type=tp) 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): class ApplyApplicationDisplaySerializer(ApplyApplicationSerializer):
apply_applications = serializers.SerializerMethodField() apply_applications = serializers.SerializerMethodField()
apply_system_users = serializers.SerializerMethodField() apply_system_users = serializers.SerializerMethodField()

View File

@ -10,13 +10,13 @@ from tickets.models import ApplyAssetTicket
from .ticket import TicketApplySerializer from .ticket import TicketApplySerializer
from .common import BaseApplyAssetApplicationSerializer from .common import BaseApplyAssetApplicationSerializer
__all__ = ['ApplyAssetSerializer', 'ApplyAssetDisplaySerializer'] __all__ = ['ApplyAssetSerializer', 'ApplyAssetDisplaySerializer', 'ApproveAssetSerializer']
asset_or_node_help_text = _("Select at least one asset or node") asset_or_node_help_text = _("Select at least one asset or node")
class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer): class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer):
apply_actions = ActionsField(required=True, allow_null=True) apply_actions = ActionsField(required=True, allow_empty=False)
permission_model = AssetPermission permission_model = AssetPermission
class Meta: class Meta:
@ -30,9 +30,9 @@ class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySeria
read_only_fields = list(set(fields) - set(writeable_fields)) read_only_fields = list(set(fields) - set(writeable_fields))
ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs
extra_kwargs = { extra_kwargs = {
'apply_nodes': {'required': False, 'help_text': asset_or_node_help_text}, 'apply_nodes': {'required': False, 'allow_empty': True},
'apply_assets': {'required': False, 'help_text': asset_or_node_help_text}, 'apply_assets': {'required': False, 'allow_empty': True},
'apply_system_users': {'required': True}, 'apply_system_users': {'required': False, 'allow_empty': True},
} }
extra_kwargs.update(ticket_extra_kwargs) extra_kwargs.update(ticket_extra_kwargs)
@ -44,14 +44,22 @@ class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySeria
def validate(self, attrs): def validate(self, attrs):
attrs = super().validate(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({ raise serializers.ValidationError({
'apply_nodes': asset_or_node_help_text, 'apply_nodes': asset_or_node_help_text,
'apply_assets': asset_or_node_help_text, 'apply_assets': asset_or_node_help_text,
}) })
return attrs return attrs
class ApproveAssetSerializer(ApplyAssetSerializer):
class Meta(ApplyAssetSerializer.Meta):
read_only_fields = ApplyAssetSerializer.Meta.read_only_fields + ['title', 'type']
class ApplyAssetDisplaySerializer(ApplyAssetSerializer): class ApplyAssetDisplaySerializer(ApplyAssetSerializer):
apply_nodes = serializers.SerializerMethodField() apply_nodes = serializers.SerializerMethodField()
apply_assets = serializers.SerializerMethodField() apply_assets = serializers.SerializerMethodField()

View File

@ -38,14 +38,25 @@ class DefaultPermissionName(object):
class BaseApplyAssetApplicationSerializer(serializers.Serializer): class BaseApplyAssetApplicationSerializer(serializers.Serializer):
permission_model: Model 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): 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] ids = [instance.id for instance in values]
with tmp_to_org(org_id): with tmp_to_org(org_id):
qs = model.objects.filter(id__in=ids, **kwargs).values_list('id', flat=True) qs = model.objects.filter(id__in=ids, **kwargs).values_list('id', flat=True)
return list(qs) return list(qs)
def validate_apply_system_users(self, system_users): 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) return self.filter_many_to_many_field(SystemUser, system_users)
def validate(self, attrs): def validate(self, attrs):
@ -72,3 +83,10 @@ class BaseApplyAssetApplicationSerializer(serializers.Serializer):
instance.save() instance.save()
return instance return instance
raise serializers.ValidationError(_('Permission named `{}` already exists'.format(name))) 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

View File

@ -80,6 +80,9 @@ class TicketApplySerializer(TicketSerializer):
return org_id return org_id
def validate(self, attrs): def validate(self, attrs):
if self.instance:
return attrs
ticket_type = attrs.get('type') ticket_type = attrs.get('type')
org_id = attrs.get('org_id') org_id = attrs.get('org_id')
flow = TicketFlow.get_org_related_flows(org_id=org_id).filter(type=ticket_type).first() flow = TicketFlow.get_org_related_flows(org_id=org_id).filter(type=ticket_type).first()

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<body>
<p> {{ approve_info }}</p>
<br>
<div style="width:100%; overflow-x:scroll;">
<table style="width:1000px; text-align:left">
<tr>
{% for item in headers %}
<th> {{ item }} </th>
{% endfor %}
</tr>
{% for item in content %}
<tr>
{% for child in item %}
<td>{{ child }}</td>
{% endfor %}
</tr>
{% endfor %}
</table>
</div>
</body>
</html>