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

View File

@ -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 "チケットフロー '{}'が存在しない"

View File

@ -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 "工单流程 `{}` 不存在"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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>