From 7e2f81a418c3141b387e27be827076d2a48a503e Mon Sep 17 00:00:00 2001
From: fit2bot <68588906+fit2bot@users.noreply.github.com>
Date: Thu, 23 Jun 2022 13:52:28 +0800
Subject: [PATCH] =?UTF-8?q?perf:=20=E9=87=8D=E6=9E=84=20ticket=20(#8281)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* perf: 重构 ticket
* perf: 优化 tickets
* perf: 暂存
* perf: 建立 ticket model
* perf: 暂存一下
* perf: 修改 tickets
* perf: 修改 import
* perf: 修改model
* perf: 暂存一波
* perf: 修改...
* del process_map field
* 工单重构
* 资产 应用对接前端
* perf: 修改 ticket
* fix: bug
* 修改迁移文件
* 添加其他api
* 去掉process_map
* perf: 优化去掉 signal
* perf: 修改这里
* 修改一点
* perf: 修改工单
* perf: 修改状态
* perf: 修改工单流转
* step 状态切换
* perf: 修改 ticket open
* perf: 修改流程
* perf: stash it
* 改又改
* stash it
* perf: stash
* stash
* migrate
* perf migrate
* 调整一下
* 修复bug
* 修改一点
* 修改一点
* 优化一波
* perf: ticket migrations
Co-authored-by: ibuler
Co-authored-by: feng626 <1304903146@qq.com>
---
apps/acls/api/login_asset_check.py | 4 +-
apps/acls/models/login_acl.py | 35 +-
apps/acls/models/login_asset_acl.py | 19 +-
apps/assets/api/cmd_filter.py | 2 +-
apps/assets/models/cmd_filter.py | 25 +-
apps/authentication/api/login_confirm.py | 2 +-
apps/authentication/mixins.py | 15 +-
.../serializers/connect_token.py | 8 +-
apps/authentication/views/login.py | 3 +-
apps/common/db/encoder.py | 24 +-
apps/common/db/models.py | 27 ++
apps/common/utils/common.py | 4 +
apps/terminal/utils.py | 2 +-
apps/tickets/api/__init__.py | 1 +
apps/tickets/api/flow.py | 31 ++
apps/tickets/api/ticket.py | 83 ++--
apps/tickets/const.py | 27 +-
apps/tickets/filters.py | 38 +-
apps/tickets/handler/apply_application.py | 108 -----
apps/tickets/handler/apply_asset.py | 106 -----
apps/tickets/handler/base.py | 135 ------
apps/tickets/handler/command_confirm.py | 33 --
apps/tickets/handler/login_asset_confirm.py | 21 -
.../tickets/{handler => handlers}/__init__.py | 2 +-
apps/tickets/handlers/apply_application.py | 63 +++
apps/tickets/handlers/apply_asset.py | 64 +++
apps/tickets/handlers/base.py | 81 ++++
apps/tickets/handlers/command_confirm.py | 6 +
apps/tickets/{handler => handlers}/general.py | 0
apps/tickets/handlers/login_asset_confirm.py | 6 +
.../{handler => handlers}/login_confirm.py | 9 +-
.../migrations/0007_auto_20201224_1821.py | 6 +-
.../migrations/0016_auto_20220609_1758.py | 189 +++++++++
.../migrations/0017_auto_20220623_1027.py | 339 +++++++++++++++
apps/tickets/models/comment.py | 8 +
apps/tickets/models/flow.py | 9 +-
apps/tickets/models/ticket.py | 328 ---------------
apps/tickets/models/ticket/__init__.py | 6 +
.../models/ticket/apply_application.py | 34 ++
apps/tickets/models/ticket/apply_asset.py | 28 ++
apps/tickets/models/ticket/command_confirm.py | 33 ++
apps/tickets/models/ticket/general.py | 389 ++++++++++++++++++
.../models/ticket/login_asset_confirm.py | 21 +
apps/tickets/models/ticket/login_confirm.py | 12 +
apps/tickets/notifications.py | 71 +++-
apps/tickets/serializers/__init__.py | 3 +-
apps/tickets/serializers/flow.py | 103 +++++
apps/tickets/serializers/super_ticket.py | 6 +-
apps/tickets/serializers/ticket/__init__.py | 7 +-
.../serializers/ticket/apply_application.py | 57 +++
.../tickets/serializers/ticket/apply_asset.py | 70 ++++
.../serializers/ticket/command_confirm.py | 16 +
apps/tickets/serializers/ticket/common.py | 63 +++
.../serializers/ticket/login_asset_confirm.py | 14 +
.../serializers/ticket/login_confirm.py | 14 +
.../serializers/ticket/meta/__init__.py | 1 -
apps/tickets/serializers/ticket/meta/meta.py | 43 --
.../ticket/meta/ticket_type/__init__.py | 0
.../meta/ticket_type/apply_application.py | 93 -----
.../ticket/meta/ticket_type/apply_asset.py | 92 -----
.../meta/ticket_type/command_confirm.py | 26 --
.../ticket/meta/ticket_type/common.py | 30 --
.../meta/ticket_type/login_asset_confirm.py | 21 -
.../ticket/meta/ticket_type/login_confirm.py | 25 --
apps/tickets/serializers/ticket/ticket.py | 184 +--------
apps/tickets/signal_handlers/ticket.py | 25 +-
apps/tickets/signals.py | 3 +-
.../templates/tickets/_base_ticket_body.html | 20 -
.../templates/tickets/_msg_ticket.html | 10 +-
.../tickets/approve_check_password.html | 12 +-
apps/tickets/urls/api_urls.py | 5 +
apps/tickets/utils.py | 17 +-
apps/tickets/views/approve.py | 4 +-
73 files changed, 2004 insertions(+), 1417 deletions(-)
create mode 100644 apps/tickets/api/flow.py
delete mode 100644 apps/tickets/handler/apply_application.py
delete mode 100644 apps/tickets/handler/apply_asset.py
delete mode 100644 apps/tickets/handler/base.py
delete mode 100644 apps/tickets/handler/command_confirm.py
delete mode 100644 apps/tickets/handler/login_asset_confirm.py
rename apps/tickets/{handler => handlers}/__init__.py (70%)
create mode 100644 apps/tickets/handlers/apply_application.py
create mode 100644 apps/tickets/handlers/apply_asset.py
create mode 100644 apps/tickets/handlers/base.py
create mode 100644 apps/tickets/handlers/command_confirm.py
rename apps/tickets/{handler => handlers}/general.py (100%)
create mode 100644 apps/tickets/handlers/login_asset_confirm.py
rename apps/tickets/{handler => handlers}/login_confirm.py (65%)
create mode 100644 apps/tickets/migrations/0016_auto_20220609_1758.py
create mode 100644 apps/tickets/migrations/0017_auto_20220623_1027.py
delete mode 100644 apps/tickets/models/ticket.py
create mode 100644 apps/tickets/models/ticket/__init__.py
create mode 100644 apps/tickets/models/ticket/apply_application.py
create mode 100644 apps/tickets/models/ticket/apply_asset.py
create mode 100644 apps/tickets/models/ticket/command_confirm.py
create mode 100644 apps/tickets/models/ticket/general.py
create mode 100644 apps/tickets/models/ticket/login_asset_confirm.py
create mode 100644 apps/tickets/models/ticket/login_confirm.py
create mode 100644 apps/tickets/serializers/flow.py
create mode 100644 apps/tickets/serializers/ticket/apply_application.py
create mode 100644 apps/tickets/serializers/ticket/apply_asset.py
create mode 100644 apps/tickets/serializers/ticket/command_confirm.py
create mode 100644 apps/tickets/serializers/ticket/common.py
create mode 100644 apps/tickets/serializers/ticket/login_asset_confirm.py
create mode 100644 apps/tickets/serializers/ticket/login_confirm.py
delete mode 100644 apps/tickets/serializers/ticket/meta/__init__.py
delete mode 100644 apps/tickets/serializers/ticket/meta/meta.py
delete mode 100644 apps/tickets/serializers/ticket/meta/ticket_type/__init__.py
delete mode 100644 apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py
delete mode 100644 apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py
delete mode 100644 apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py
delete mode 100644 apps/tickets/serializers/ticket/meta/ticket_type/common.py
delete mode 100644 apps/tickets/serializers/ticket/meta/ticket_type/login_asset_confirm.py
delete mode 100644 apps/tickets/serializers/ticket/meta/ticket_type/login_confirm.py
delete mode 100644 apps/tickets/templates/tickets/_base_ticket_body.html
diff --git a/apps/acls/api/login_asset_check.py b/apps/acls/api/login_asset_check.py
index fc5f4157f..a7e8990c7 100644
--- a/apps/acls/api/login_asset_check.py
+++ b/apps/acls/api/login_asset_check.py
@@ -47,7 +47,7 @@ class LoginAssetCheckAPI(CreateAPIView):
asset=self.serializer.asset,
system_user=self.serializer.system_user,
assignees=acl.reviewers.all(),
- org_id=self.serializer.org.id
+ org_id=self.serializer.org.id,
)
confirm_status_url = reverse(
view_name='api-tickets:super-ticket-status',
@@ -59,7 +59,7 @@ class LoginAssetCheckAPI(CreateAPIView):
external=True, api_to_ui=True
)
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
- ticket_assignees = ticket.current_node.first().ticket_assignees.all()
+ ticket_assignees = ticket.current_step.ticket_assignees.all()
data = {
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
diff --git a/apps/acls/models/login_acl.py b/apps/acls/models/login_acl.py
index f9b24e426..1887ecd33 100644
--- a/apps/acls/models/login_acl.py
+++ b/apps/acls/models/login_acl.py
@@ -97,34 +97,25 @@ class LoginACL(BaseACL):
return allow, reject_type
- @staticmethod
- def construct_confirm_ticket_meta(request=None):
+ def create_confirm_ticket(self, request):
+ from tickets import const
+ from tickets.models import ApplyLoginTicket
+ from orgs.models import Organization
+ title = _('Login confirm') + ' {}'.format(self.user)
login_ip = get_request_ip(request) if request else ''
login_ip = login_ip or '0.0.0.0'
login_city = get_ip_city(login_ip)
login_datetime = local_now_display()
- ticket_meta = {
- 'apply_login_ip': login_ip,
- 'apply_login_city': login_city,
- 'apply_login_datetime': login_datetime,
- }
- return ticket_meta
-
- def create_confirm_ticket(self, request=None):
- from tickets import const
- from tickets.models import Ticket
- from orgs.models import Organization
- ticket_title = _('Login confirm') + ' {}'.format(self.user)
- ticket_meta = self.construct_confirm_ticket_meta(request)
data = {
- 'title': ticket_title,
- 'type': const.TicketType.login_confirm.value,
- 'meta': ticket_meta,
+ 'title': title,
+ 'type': const.TicketType.login_confirm,
+ 'applicant': self.user,
+ 'apply_login_city': login_city,
+ 'apply_login_ip': login_ip,
+ 'apply_login_datetime': login_datetime,
'org_id': Organization.ROOT_ID,
}
- ticket = Ticket.objects.create(**data)
- applicant = self.user
+ ticket = ApplyLoginTicket.objects.create(**data)
assignees = self.reviewers.all()
- ticket.create_process_map_and_node(assignees, applicant)
- ticket.open(applicant)
+ ticket.open_by_system(assignees)
return ticket
diff --git a/apps/acls/models/login_asset_acl.py b/apps/acls/models/login_asset_acl.py
index dda9d97c1..9cf7989f9 100644
--- a/apps/acls/models/login_asset_acl.py
+++ b/apps/acls/models/login_asset_acl.py
@@ -85,19 +85,18 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
@classmethod
def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id):
from tickets.const import TicketType
- from tickets.models import Ticket
+ from tickets.models import ApplyLoginAssetTicket
+ title = _('Login asset confirm') + ' ({})'.format(user)
data = {
- 'title': _('Login asset confirm') + ' ({})'.format(user),
+ 'title': title,
'type': TicketType.login_asset_confirm,
- 'meta': {
- 'apply_login_user': str(user),
- 'apply_login_asset': str(asset),
- 'apply_login_system_user': str(system_user),
- },
+ 'applicant': user,
+ 'apply_login_user': user,
+ 'apply_login_asset': asset,
+ 'apply_login_system_user': system_user,
'org_id': org_id,
}
- ticket = Ticket.objects.create(**data)
- ticket.create_process_map_and_node(assignees, user)
- ticket.open(applicant=user)
+ ticket = ApplyLoginAssetTicket.objects.create(**data)
+ ticket.open_by_system(assignees)
return ticket
diff --git a/apps/assets/api/cmd_filter.py b/apps/assets/api/cmd_filter.py
index dcb2d77c9..0e09d5c73 100644
--- a/apps/assets/api/cmd_filter.py
+++ b/apps/assets/api/cmd_filter.py
@@ -69,7 +69,7 @@ class CommandConfirmAPI(CreateAPIView):
external=True, api_to_ui=True
)
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
- ticket_assignees = ticket.current_node.first().ticket_assignees.all()
+ ticket_assignees = ticket.current_step.ticket_assignees.all()
return {
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py
index c92a25109..235a4f331 100644
--- a/apps/assets/models/cmd_filter.py
+++ b/apps/assets/models/cmd_filter.py
@@ -165,26 +165,23 @@ class CommandFilterRule(OrgModelMixin):
def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
from tickets.const import TicketType
- from tickets.models import Ticket
+ from tickets.models import ApplyCommandTicket
data = {
'title': _('Command confirm') + ' ({})'.format(session.user),
'type': TicketType.command_confirm,
- 'meta': {
- 'apply_run_user': session.user,
- 'apply_run_asset': session.asset,
- 'apply_run_system_user': session.system_user,
- 'apply_run_command': run_command,
- 'apply_from_session_id': str(session.id),
- 'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
- 'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id)
- },
+ 'applicant': session.user_obj,
+ 'apply_run_user_id': session.user_id,
+ 'apply_run_asset_id': session.asset_id,
+ 'apply_run_system_user_id': session.system_user_id,
+ 'apply_run_command': run_command,
+ 'apply_from_session_id': str(session.id),
+ 'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
+ 'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id),
'org_id': org_id,
}
- ticket = Ticket.objects.create(**data)
- applicant = session.user_obj
+ ticket = ApplyCommandTicket.objects.create(**data)
assignees = self.reviewers.all()
- ticket.create_process_map_and_node(assignees, applicant)
- ticket.open(applicant)
+ ticket.open_by_system(assignees)
return ticket
@classmethod
diff --git a/apps/authentication/api/login_confirm.py b/apps/authentication/api/login_confirm.py
index 9adedb1e0..22594b88e 100644
--- a/apps/authentication/api/login_confirm.py
+++ b/apps/authentication/api/login_confirm.py
@@ -25,5 +25,5 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
ticket = self.get_ticket()
if ticket:
request.session.pop('auth_ticket_id', '')
- ticket.close(processor=self.get_user_from_session())
+ ticket.close()
return Response('', status=200)
diff --git a/apps/authentication/mixins.py b/apps/authentication/mixins.py
index 2f4f1d6e0..f1989b181 100644
--- a/apps/authentication/mixins.py
+++ b/apps/authentication/mixins.py
@@ -337,18 +337,18 @@ class AuthACLMixin:
raise errors.TimePeriodNotAllowed(username=user.username, request=self.request)
def get_ticket(self):
- from tickets.models import Ticket
+ from tickets.models import ApplyLoginTicket
ticket_id = self.request.session.get("auth_ticket_id")
logger.debug('Login confirm ticket id: {}'.format(ticket_id))
if not ticket_id:
ticket = None
else:
- ticket = Ticket.all().filter(id=ticket_id).first()
+ ticket = ApplyLoginTicket.all().filter(id=ticket_id).first()
return ticket
def get_ticket_or_create(self, confirm_setting):
ticket = self.get_ticket()
- if not ticket or ticket.status_closed:
+ if not ticket or ticket.is_status(ticket.Status.closed):
ticket = confirm_setting.create_confirm_ticket(self.request)
self.request.session['auth_ticket_id'] = str(ticket.id)
return ticket
@@ -357,16 +357,17 @@ class AuthACLMixin:
ticket = self.get_ticket()
if not ticket:
raise errors.LoginConfirmOtherError('', "Not found")
- if ticket.status_open:
+
+ if ticket.is_status(ticket.Status.open):
raise errors.LoginConfirmWaitError(ticket.id)
- elif ticket.state_approve:
+ elif ticket.is_state(ticket.State.approved):
self.request.session["auth_confirm"] = "1"
return
- elif ticket.state_reject:
+ elif ticket.is_state(ticket.State.rejected):
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_state_display()
)
- elif ticket.state_close:
+ elif ticket.is_state(ticket.State.closed):
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_state_display()
)
diff --git a/apps/authentication/serializers/connect_token.py b/apps/authentication/serializers/connect_token.py
index d9694b5bd..36fc024b1 100644
--- a/apps/authentication/serializers/connect_token.py
+++ b/apps/authentication/serializers/connect_token.py
@@ -86,7 +86,10 @@ class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
class Meta:
model = SystemUser
- fields = ['id', 'name', 'username', 'password', 'private_key', 'protocol', 'ad_domain', 'org_id']
+ fields = [
+ 'id', 'name', 'username', 'password', 'private_key',
+ 'protocol', 'ad_domain', 'org_id'
+ ]
class ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
@@ -122,8 +125,7 @@ class ConnectionTokenFilterRuleSerializer(serializers.ModelSerializer):
model = CommandFilterRule
fields = [
'id', 'type', 'content', 'ignore_case', 'pattern',
- 'priority', 'action',
- 'date_created',
+ 'priority', 'action', 'date_created',
]
diff --git a/apps/authentication/views/login.py b/apps/authentication/views/login.py
index 3b8f2c60c..e6fe6b6cf 100644
--- a/apps/authentication/views/login.py
+++ b/apps/authentication/views/login.py
@@ -282,8 +282,7 @@ class UserLoginWaitConfirmView(TemplateView):
if ticket:
timestamp_created = datetime.datetime.timestamp(ticket.date_created)
ticket_detail_url = TICKET_DETAIL_URL.format(id=ticket_id, type=ticket.type)
- assignees = ticket.current_node.first().ticket_assignees.all()
- assignees_display = ', '.join([str(i.assignee) for i in assignees])
+ assignees_display = ', '.join([str(assignee) for assignee in ticket.current_assignees])
msg = _("""Wait for {} confirm, You also can copy link to her/him
Don't close this page""").format(assignees_display)
else:
diff --git a/apps/common/db/encoder.py b/apps/common/db/encoder.py
index 314ea071d..673b8aeca 100644
--- a/apps/common/db/encoder.py
+++ b/apps/common/db/encoder.py
@@ -1,20 +1,30 @@
import json
-from datetime import datetime
import uuid
+import logging
+from datetime import datetime
from django.utils.translation import ugettext_lazy as _
+from django.db import models
from django.conf import settings
+lazy_type = type(_('ugettext_lazy'))
+
class ModelJSONFieldEncoder(json.JSONEncoder):
""" 解决一些类型的字段不能序列化的问题 """
def default(self, obj):
- if isinstance(obj, datetime):
+ str_cls = (models.Model, lazy_type, models.ImageField, uuid.UUID)
+ if isinstance(obj, str_cls):
+ return str(obj)
+ elif isinstance(obj, datetime):
return obj.strftime(settings.DATETIME_DISPLAY_FORMAT)
- if isinstance(obj, uuid.UUID):
- return str(obj)
- if isinstance(obj, type(_("ugettext_lazy"))):
- return str(obj)
+ elif isinstance(obj, (list, tuple)) and len(obj) > 0 \
+ and isinstance(obj[0], models.Model):
+ return [str(i) for i in obj]
else:
- return super().default(obj)
+ try:
+ return super().default(obj)
+ except TypeError:
+ logging.error('Type error: ', type(obj))
+ return str(obj)
diff --git a/apps/common/db/models.py b/apps/common/db/models.py
index 2989f734e..92b6c4803 100644
--- a/apps/common/db/models.py
+++ b/apps/common/db/models.py
@@ -13,6 +13,7 @@ import uuid
from functools import reduce, partial
import inspect
+from django.db import transaction
from django.db.models import *
from django.db.models import QuerySet
from django.db.models.functions import Concat
@@ -211,3 +212,29 @@ class UnionQuerySet(QuerySet):
qs = cls(assets1, assets2)
return qs
+
+
+class MultiTableChildQueryset(QuerySet):
+
+ def bulk_create(self, objs, batch_size=None):
+ assert batch_size is None or batch_size > 0
+ if not objs:
+ return objs
+
+ self._for_write = True
+ objs = list(objs)
+ parent_model = self.model._meta.pk.related_model
+
+ parent_objs = []
+ for obj in objs:
+ parent_values = {}
+ for field in [f for f in parent_model._meta.fields if hasattr(obj, f.name)]:
+ parent_values[field.name] = getattr(obj, field.name)
+ parent_objs.append(parent_model(**parent_values))
+ setattr(obj, self.model._meta.pk.attname, obj.id)
+ parent_model.objects.bulk_create(parent_objs, batch_size=batch_size)
+
+ with transaction.atomic(using=self.db, savepoint=False):
+ self._batched_insert(objs, self.model._meta.local_fields, batch_size)
+
+ return objs
diff --git a/apps/common/utils/common.py b/apps/common/utils/common.py
index 3982b1349..5b2180ec0 100644
--- a/apps/common/utils/common.py
+++ b/apps/common/utils/common.py
@@ -361,3 +361,7 @@ def pretty_string(data: str, max_length=128, ellipsis_str='...'):
end = data[-half:]
data = f'{start}{ellipsis_str}{end}'
return data
+
+
+def group_by_count(it, count):
+ return [it[i:i+count] for i in range(0, len(it), count)]
diff --git a/apps/terminal/utils.py b/apps/terminal/utils.py
index e6fa17068..abdfbd738 100644
--- a/apps/terminal/utils.py
+++ b/apps/terminal/utils.py
@@ -12,7 +12,7 @@ from common.utils import get_logger
from . import const
from .models import ReplayStorage
from tickets.models import TicketSession, TicketStep, TicketAssignee
-from tickets.const import ProcessStatus
+from tickets.const import StepState
logger = get_logger(__name__)
diff --git a/apps/tickets/api/__init__.py b/apps/tickets/api/__init__.py
index 0342771e0..bec6f21d1 100644
--- a/apps/tickets/api/__init__.py
+++ b/apps/tickets/api/__init__.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
#
from .ticket import *
+from .flow import *
from .comment import *
from .super_ticket import *
from .relation import *
diff --git a/apps/tickets/api/flow.py b/apps/tickets/api/flow.py
new file mode 100644
index 000000000..b45479187
--- /dev/null
+++ b/apps/tickets/api/flow.py
@@ -0,0 +1,31 @@
+from rest_framework.exceptions import MethodNotAllowed
+
+from tickets import serializers
+from tickets.models import TicketFlow
+from common.drf.api import JMSBulkModelViewSet
+
+__all__ = ['TicketFlowViewSet']
+
+
+class TicketFlowViewSet(JMSBulkModelViewSet):
+ serializer_class = serializers.TicketFlowSerializer
+
+ filterset_fields = ['id', 'type']
+ search_fields = ['id', 'type']
+
+ def destroy(self, request, *args, **kwargs):
+ raise MethodNotAllowed(self.action)
+
+ def get_queryset(self):
+ queryset = TicketFlow.get_org_related_flows()
+ return queryset
+
+ def perform_create_or_update(self, serializer):
+ instance = serializer.save()
+ instance.save()
+
+ def perform_create(self, serializer):
+ self.perform_create_or_update(serializer)
+
+ def perform_update(self, serializer):
+ self.perform_create_or_update(serializer)
diff --git a/apps/tickets/api/ticket.py b/apps/tickets/api/ticket.py
index 00e30f899..747066b0c 100644
--- a/apps/tickets/api/ticket.py
+++ b/apps/tickets/api/ticket.py
@@ -2,36 +2,43 @@
#
from rest_framework import viewsets
from rest_framework.decorators import action
-from rest_framework.exceptions import MethodNotAllowed
from rest_framework.response import Response
+from rest_framework.exceptions import MethodNotAllowed
from common.const.http import POST, PUT
from common.mixins.api import CommonApiMixin
-from common.drf.api import JMSBulkModelViewSet
+from orgs.utils import tmp_to_root_org
from rbac.permissions import RBACPermission
from tickets import serializers
-from tickets.models import Ticket, TicketFlow
-from tickets.filters import TicketFilter
+from tickets import filters
from tickets.permissions.ticket import IsAssignee, IsApplicant
+from tickets.models import (
+ Ticket, ApplyAssetTicket, ApplyApplicationTicket,
+ ApplyLoginTicket, ApplyLoginAssetTicket, ApplyCommandTicket
+)
-__all__ = ['TicketViewSet', 'TicketFlowViewSet']
+__all__ = [
+ 'TicketViewSet', 'ApplyAssetTicketViewSet', 'ApplyApplicationTicketViewSet',
+ 'ApplyLoginTicketViewSet', 'ApplyLoginAssetTicketViewSet', 'ApplyCommandTicketViewSet'
+]
class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
serializer_class = serializers.TicketDisplaySerializer
serializer_classes = {
- 'open': serializers.TicketApplySerializer,
- 'approve': serializers.TicketApproveSerializer,
+ 'list': serializers.TicketListSerializer,
+ 'open': serializers.TicketApplySerializer
}
- filterset_class = TicketFilter
+ model = Ticket
+ filterset_class = filters.TicketFilter
search_fields = [
'title', 'type', 'status', 'applicant_display'
]
ordering_fields = (
- 'title', 'applicant_display', 'status', 'state', 'action_display',
- 'date_created', 'serial_num',
+ 'title', 'applicant_display', 'status', 'state',
+ 'action_display', 'date_created', 'serial_num',
)
ordering = ('-date_created',)
rbac_perms = {
@@ -48,15 +55,15 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
raise MethodNotAllowed(self.action)
def get_queryset(self):
- queryset = Ticket.get_user_related_tickets(self.request.user)
+ with tmp_to_root_org():
+ queryset = self.model.get_user_related_tickets(self.request.user)
return queryset
def perform_create(self, serializer):
instance = serializer.save()
- applicant = self.request.user
- instance.create_related_node(applicant)
- instance.process_map = instance.create_process_map(applicant)
- instance.open(applicant)
+ instance.applicant = self.request.user
+ instance.save(update_fields=['applicant'])
+ instance.open()
@action(detail=False, methods=[POST], permission_classes=[RBACPermission, ])
def open(self, request, *args, **kwargs):
@@ -80,29 +87,41 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
def close(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
- instance.close(processor=request.user)
+ instance.close()
return Response(serializer.data)
-class TicketFlowViewSet(JMSBulkModelViewSet):
- serializer_class = serializers.TicketFlowSerializer
+class ApplyAssetTicketViewSet(TicketViewSet):
+ serializer_class = serializers.ApplyAssetDisplaySerializer
+ serializer_classes = {
+ 'open': serializers.ApplyAssetSerializer
+ }
+ model = ApplyAssetTicket
+ filterset_class = filters.ApplyAssetTicketFilter
- filterset_fields = ['id', 'type']
- search_fields = ['id', 'type']
- def destroy(self, request, *args, **kwargs):
- raise MethodNotAllowed(self.action)
+class ApplyApplicationTicketViewSet(TicketViewSet):
+ serializer_class = serializers.ApplyApplicationDisplaySerializer
+ serializer_classes = {
+ 'open': serializers.ApplyApplicationSerializer
+ }
+ model = ApplyApplicationTicket
+ filterset_class = filters.ApplyApplicationTicketFilter
- def get_queryset(self):
- queryset = TicketFlow.get_org_related_flows()
- return queryset
- def perform_create_or_update(self, serializer):
- instance = serializer.save()
- instance.save()
+class ApplyLoginTicketViewSet(TicketViewSet):
+ serializer_class = serializers.LoginConfirmSerializer
+ model = ApplyLoginTicket
+ filterset_class = filters.ApplyLoginTicketFilter
- def perform_create(self, serializer):
- self.perform_create_or_update(serializer)
- def perform_update(self, serializer):
- self.perform_create_or_update(serializer)
+class ApplyLoginAssetTicketViewSet(TicketViewSet):
+ serializer_class = serializers.LoginAssetConfirmSerializer
+ model = ApplyLoginAssetTicket
+ filterset_class = filters.ApplyLoginAssetTicketFilter
+
+
+class ApplyCommandTicketViewSet(TicketViewSet):
+ serializer_class = serializers.ApplyCommandConfirmSerializer
+ model = ApplyCommandTicket
+ filterset_class = filters.ApplyCommandTicketFilter
diff --git a/apps/tickets/const.py b/apps/tickets/const.py
index 9bfaf795e..d97e6716e 100644
--- a/apps/tickets/const.py
+++ b/apps/tickets/const.py
@@ -14,20 +14,29 @@ class TicketType(TextChoices):
class TicketState(TextChoices):
- open = 'open', _('Open')
- approved = 'approved', _('Approved')
- rejected = 'rejected', _('Rejected')
- closed = 'closed', _('Closed')
-
-
-class ProcessStatus(TextChoices):
- notified = 'notified', _('Notified')
+ pending = 'pending', _('Open')
approved = 'approved', _('Approved')
rejected = 'rejected', _('Rejected')
+ closed = 'closed', _("Cancel")
+ reopen = 'reopen', _("Reopen")
class TicketStatus(TextChoices):
open = 'open', _("Open")
+ closed = 'closed', _("Finished")
+
+
+class StepState(TextChoices):
+ pending = 'pending', _('Pending')
+ approved = 'approved', _('Approved')
+ rejected = 'rejected', _('Rejected')
+ closed = 'closed', _("Closed")
+ reopen = 'reopen', _("Reopen")
+
+
+class StepStatus(TextChoices):
+ pending = 'pending', _('Pending')
+ active = 'active', _('Active')
closed = 'closed', _("Closed")
@@ -38,7 +47,7 @@ class TicketAction(TextChoices):
reject = 'reject', _('Reject')
-class TicketApprovalLevel(IntegerChoices):
+class TicketLevel(IntegerChoices):
one = 1, _("One level")
two = 2, _("Two level")
diff --git a/apps/tickets/filters.py b/apps/tickets/filters.py
index 3c795f0df..255908305 100644
--- a/apps/tickets/filters.py
+++ b/apps/tickets/filters.py
@@ -1,7 +1,10 @@
from django_filters import rest_framework as filters
from common.drf.filters import BaseFilterSet
-from tickets.models import Ticket
+from tickets.models import (
+ Ticket, ApplyAssetTicket, ApplyApplicationTicket,
+ ApplyLoginTicket, ApplyLoginAssetTicket, ApplyCommandTicket
+)
class TicketFilter(BaseFilterSet):
@@ -10,9 +13,38 @@ class TicketFilter(BaseFilterSet):
class Meta:
model = Ticket
fields = (
- 'id', 'title', 'type', 'status', 'state', 'applicant', 'assignees__id',
- 'applicant_display',
+ 'id', 'title', 'type', 'status', 'state', 'applicant', 'assignees__id'
)
def filter_assignees_id(self, queryset, name, value):
return queryset.filter(ticket_steps__ticket_assignees__assignee__id=value)
+
+
+class ApplyAssetTicketFilter(BaseFilterSet):
+ class Meta:
+ model = ApplyAssetTicket
+ fields = ('id',)
+
+
+class ApplyApplicationTicketFilter(BaseFilterSet):
+ class Meta:
+ model = ApplyApplicationTicket
+ fields = ('id',)
+
+
+class ApplyLoginTicketFilter(BaseFilterSet):
+ class Meta:
+ model = ApplyLoginTicket
+ fields = ('id',)
+
+
+class ApplyLoginAssetTicketFilter(BaseFilterSet):
+ class Meta:
+ model = ApplyLoginAssetTicket
+ fields = ('id',)
+
+
+class ApplyCommandTicketFilter(BaseFilterSet):
+ class Meta:
+ model = ApplyCommandTicket
+ fields = ('id',)
diff --git a/apps/tickets/handler/apply_application.py b/apps/tickets/handler/apply_application.py
deleted file mode 100644
index 5e20e89f9..000000000
--- a/apps/tickets/handler/apply_application.py
+++ /dev/null
@@ -1,108 +0,0 @@
-from django.utils.translation import ugettext as _
-
-from orgs.utils import tmp_to_org, tmp_to_root_org
-from applications.const import AppCategory, AppType
-from applications.models import Application
-from perms.models import ApplicationPermission
-from assets.models import SystemUser
-
-from .base import BaseHandler
-
-
-class Handler(BaseHandler):
-
- def _on_approve(self):
- is_finished = super()._on_approve()
- if is_finished:
- self._create_application_permission()
-
- # display
- def _construct_meta_display_of_open(self):
- meta_display_fields = ['apply_category_display', 'apply_type_display']
- apply_category = self.ticket.meta.get('apply_category')
- apply_category_display = AppCategory.get_label(apply_category)
- apply_type = self.ticket.meta.get('apply_type')
- apply_type_display = AppType.get_label(apply_type)
- meta_display_values = [apply_category_display, apply_type_display]
- meta_display = dict(zip(meta_display_fields, meta_display_values))
- apply_system_users = self.ticket.meta.get('apply_system_users')
- apply_applications = self.ticket.meta.get('apply_applications')
- with tmp_to_org(self.ticket.org_id):
- meta_display.update({
- 'apply_system_users_display': [str(i) for i in SystemUser.objects.filter(id__in=apply_system_users)],
- 'apply_applications_display': [str(i) for i in Application.objects.filter(id__in=apply_applications)]
- })
-
- return meta_display
-
- # body
- def _construct_meta_body_of_open(self):
- apply_category_display = self.ticket.meta.get('apply_category_display')
- apply_type_display = self.ticket.meta.get('apply_type_display')
- apply_applications = self.ticket.meta.get('apply_applications_display', [])
- apply_system_users = self.ticket.meta.get('apply_system_users_display', [])
- apply_date_start = self.ticket.meta.get('apply_date_start')
- apply_date_expired = self.ticket.meta.get('apply_date_expired')
- applied_body = '''{}: {},
- {}: {}
- {}: {}
- {}: {}
- {}: {}
- {}: {}
- '''.format(
- _('Applied category'), apply_category_display,
- _('Applied type'), apply_type_display,
- _('Applied application group'), ','.join(apply_applications),
- _('Applied system user group'), ','.join(apply_system_users),
- _('Applied date start'), apply_date_start,
- _('Applied date expired'), apply_date_expired,
- )
- return applied_body
-
- # permission
- def _create_application_permission(self):
- with tmp_to_root_org():
- application_permission = ApplicationPermission.objects.filter(id=self.ticket.id).first()
- if application_permission:
- return application_permission
-
- apply_category = self.ticket.meta.get('apply_category')
- apply_type = self.ticket.meta.get('apply_type')
- apply_permission_name = self.ticket.meta.get('apply_permission_name', '')
- apply_applications = self.ticket.meta.get('apply_applications', [])
- apply_system_users = self.ticket.meta.get('apply_system_users', [])
- apply_date_start = self.ticket.meta.get('apply_date_start')
- apply_date_expired = self.ticket.meta.get('apply_date_expired')
- permission_created_by = '{}:{}'.format(
- str(self.ticket.__class__.__name__), str(self.ticket.id)
- )
- permission_comment = _(
- 'Created by the ticket, '
- 'ticket title: {}, '
- 'ticket applicant: {}, '
- 'ticket processor: {}, '
- 'ticket ID: {}'
- ).format(
- self.ticket.title,
- self.ticket.applicant_display,
- ','.join([i['processor_display'] for i in self.ticket.process_map]),
- str(self.ticket.id)
- )
- permissions_data = {
- 'id': self.ticket.id,
- 'name': apply_permission_name,
- 'from_ticket': True,
- 'category': apply_category,
- 'type': apply_type,
- 'comment': str(permission_comment),
- 'created_by': permission_created_by,
- 'date_start': apply_date_start,
- 'date_expired': apply_date_expired,
- }
- with tmp_to_org(self.ticket.org_id):
- application_permission = ApplicationPermission.objects.create(**permissions_data)
- application_permission.users.add(self.ticket.applicant)
- application_permission.applications.set(apply_applications)
- application_permission.system_users.set(apply_system_users)
-
- return application_permission
diff --git a/apps/tickets/handler/apply_asset.py b/apps/tickets/handler/apply_asset.py
deleted file mode 100644
index a13f6e008..000000000
--- a/apps/tickets/handler/apply_asset.py
+++ /dev/null
@@ -1,106 +0,0 @@
-from django.utils.translation import ugettext as _
-
-from assets.models import Node, Asset, SystemUser
-from perms.models import AssetPermission, Action
-from orgs.utils import tmp_to_org, tmp_to_root_org
-from .base import BaseHandler
-
-
-class Handler(BaseHandler):
-
- def _on_approve(self):
- is_finished = super()._on_approve()
- if is_finished:
- self._create_asset_permission()
-
- # display
- def _construct_meta_display_of_open(self):
- meta_display_fields = ['apply_actions_display']
- apply_actions = self.ticket.meta.get('apply_actions', Action.NONE)
- apply_actions_display = Action.value_to_choices_display(apply_actions)
- meta_display_values = [apply_actions_display]
- meta_display = dict(zip(meta_display_fields, meta_display_values))
- apply_nodes = self.ticket.meta.get('apply_nodes', [])
- apply_assets = self.ticket.meta.get('apply_assets', [])
- apply_system_users = self.ticket.meta.get('apply_system_users')
- with tmp_to_org(self.ticket.org_id):
- meta_display.update({
- 'apply_nodes_display': [str(i) for i in Node.objects.filter(id__in=apply_nodes)],
- 'apply_assets_display': [str(i) for i in Asset.objects.filter(id__in=apply_assets)],
- 'apply_system_users_display': [
- str(i) for i in SystemUser.objects.filter(id__in=apply_system_users)
- ]
- })
- return meta_display
-
- # body
- def _construct_meta_body_of_open(self):
- apply_nodes = self.ticket.meta.get('apply_nodes_display', [])
- apply_assets = self.ticket.meta.get('apply_assets_display', [])
- apply_system_users = self.ticket.meta.get('apply_system_users_display', [])
- apply_actions_display = self.ticket.meta.get('apply_actions_display', [])
- apply_date_start = self.ticket.meta.get('apply_date_start')
- apply_date_expired = self.ticket.meta.get('apply_date_expired')
- applied_body = '''{}: {},
- {}: {},
- {}: {},
- {}: {},
- {}: {}
- '''.format(
- _("Applied node group"), ','.join(apply_nodes),
- _("Applied hostname group"), ','.join(apply_assets),
- _("Applied system user group"), ','.join(apply_system_users),
- _("Applied actions"), ','.join(apply_actions_display),
- _('Applied date start'), apply_date_start,
- _('Applied date expired'), apply_date_expired,
- )
- return applied_body
-
- # permission
- def _create_asset_permission(self):
- with tmp_to_root_org():
- asset_permission = AssetPermission.objects.filter(id=self.ticket.id).first()
- if asset_permission:
- return asset_permission
-
- apply_permission_name = self.ticket.meta.get('apply_permission_name', )
- apply_nodes = self.ticket.meta.get('apply_nodes', [])
- apply_assets = self.ticket.meta.get('apply_assets', [])
- apply_system_users = self.ticket.meta.get('apply_system_users', [])
- apply_actions = self.ticket.meta.get('apply_actions', Action.NONE)
- apply_date_start = self.ticket.meta.get('apply_date_start')
- apply_date_expired = self.ticket.meta.get('apply_date_expired')
- permission_created_by = '{}:{}'.format(
- str(self.ticket.__class__.__name__), str(self.ticket.id)
- )
- permission_comment = _(
- 'Created by the ticket '
- 'ticket title: {} '
- 'ticket applicant: {} '
- 'ticket processor: {} '
- 'ticket ID: {}'
- ).format(
- self.ticket.title,
- self.ticket.applicant_display,
- ','.join([i['processor_display'] for i in self.ticket.process_map]),
- str(self.ticket.id)
- )
-
- permission_data = {
- 'id': self.ticket.id,
- 'name': apply_permission_name,
- 'from_ticket': True,
- 'comment': str(permission_comment),
- 'created_by': permission_created_by,
- 'actions': apply_actions,
- 'date_start': apply_date_start,
- 'date_expired': apply_date_expired,
- }
- with tmp_to_org(self.ticket.org_id):
- asset_permission = AssetPermission.objects.create(**permission_data)
- asset_permission.users.add(self.ticket.applicant)
- asset_permission.nodes.set(apply_nodes)
- asset_permission.assets.set(apply_assets)
- asset_permission.system_users.set(apply_system_users)
-
- return asset_permission
diff --git a/apps/tickets/handler/base.py b/apps/tickets/handler/base.py
deleted file mode 100644
index 1855a2f6c..000000000
--- a/apps/tickets/handler/base.py
+++ /dev/null
@@ -1,135 +0,0 @@
-from django.utils.translation import ugettext as _
-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 TicketAction
-
-logger = get_logger(__name__)
-
-
-class BaseHandler(object):
-
- def __init__(self, ticket):
- self.ticket = ticket
-
- # on action
- def _on_open(self):
- self.ticket.applicant_display = str(self.ticket.applicant)
- meta_display = getattr(self, '_construct_meta_display_of_open', lambda: {})()
- self.ticket.meta.update(meta_display)
- self.ticket.save()
- self._send_applied_mail_to_assignees()
-
- def _on_approve(self):
- if self.ticket.approval_step != len(self.ticket.process_map):
- self._send_processed_mail_to_applicant(self.ticket.processor)
- self.ticket.approval_step += 1
- self.ticket.create_related_node()
- self._send_applied_mail_to_assignees()
- is_finished = False
- else:
- self.ticket.set_state_approve()
- self.ticket.set_status_closed()
- self._send_processed_mail_to_applicant(self.ticket.processor)
- is_finished = True
-
- self.ticket.save()
- return is_finished
-
- def _on_reject(self):
- self.ticket.set_state_reject()
- self.ticket.set_status_closed()
- self.__on_process(self.ticket.processor)
-
- def _on_close(self):
- self.ticket.set_state_closed()
- self.ticket.set_status_closed()
- self.__on_process(self.ticket.processor)
-
- def __on_process(self, processor):
- self._send_processed_mail_to_applicant(processor)
- self.ticket.save()
-
- def dispatch(self, action):
- processor = self.ticket.processor
- current_node = self.ticket.current_node.first()
- self.ticket.process_map[self.ticket.approval_step - 1].update({
- 'approval_date': str(current_node.date_updated),
- 'state': current_node.state,
- 'processor': processor.id if processor else '',
- 'processor_display': str(processor) if processor else '',
- })
- self.ticket.save()
- self._create_comment_on_action(action)
- method = getattr(self, f'_on_{action}', lambda: None)
- return method()
-
- # email
- def _send_applied_mail_to_assignees(self):
- assignees = self.ticket.current_node.first().ticket_assignees.all()
- assignees_display = ', '.join([str(i.assignee) for i in assignees])
- logger.debug('Send applied email to assignees: {}'.format(assignees_display))
- send_ticket_applied_mail_to_assignees(self.ticket)
-
- def _send_processed_mail_to_applicant(self, processor):
- logger.debug('Send processed mail to applicant: {}'.format(self.ticket.applicant_display))
- send_ticket_processed_mail_to_applicant(self.ticket, processor)
-
- # comments
- def _create_comment_on_action(self, action):
- user = self.ticket.processor
- # 打开或关闭工单,备注显示是自己,其他是受理人
- if action == TicketAction.open or action == TicketAction.close:
- user = self.ticket.applicant
- user_display = str(user)
- action_display = getattr(TicketAction, action).label
- data = {
- 'body': _('{} {} the ticket').format(user_display, action_display),
- 'user': user,
- 'user_display': user_display
- }
- return self.ticket.comments.create(**data)
-
- # body
- body_html_format = '''
- {}:
- {}
- '''
-
- def get_body(self):
- old_body = self.ticket.meta.get('body')
- if old_body:
- # 之前版本的body
- return old_body
- basic_body = self._construct_basic_body()
- meta_body = self._construct_meta_body()
- return basic_body + meta_body
-
- def _construct_basic_body(self):
- basic_body = '''
- {}: {}
- {}: {}
- {}: {}
- {}: {}
- '''.format(
- _('Ticket title'), self.ticket.title,
- _('Ticket type'), self.ticket.get_type_display(),
- _('Ticket status'), self.ticket.get_status_display(),
- _('Ticket applicant'), self.ticket.applicant_display,
- ).strip()
- body = self.body_html_format.format(_("Ticket basic info"), basic_body)
- return body
-
- def _construct_meta_body(self):
- body = ''
- open_body = self._base_construct_meta_body_of_open().strip()
- body += open_body
- return body
-
- def _base_construct_meta_body_of_open(self):
- meta_body_of_open = getattr(
- self, '_construct_meta_body_of_open', lambda: _('No content')
- )().strip()
- body = self.body_html_format.format(_('Ticket applied info'), meta_body_of_open)
- return body
diff --git a/apps/tickets/handler/command_confirm.py b/apps/tickets/handler/command_confirm.py
deleted file mode 100644
index 6fb27dcfe..000000000
--- a/apps/tickets/handler/command_confirm.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from django.utils.translation import ugettext as _
-from .base import BaseHandler
-
-
-class Handler(BaseHandler):
-
- # body
- def _construct_meta_body_of_open(self):
- apply_run_user = self.ticket.meta.get('apply_run_user')
- apply_run_asset = self.ticket.meta.get('apply_run_asset')
- apply_run_system_user = self.ticket.meta.get('apply_run_system_user')
- apply_run_command = self.ticket.meta.get('apply_run_command')
- apply_from_session_id = self.ticket.meta.get('apply_from_session_id')
- apply_from_cmd_filter_rule_id = self.ticket.meta.get('apply_from_cmd_filter_rule_id')
- apply_from_cmd_filter_id = self.ticket.meta.get('apply_from_cmd_filter_id')
-
- applied_body = '''
- {}: {}
- {}: {}
- {}: {}
- {}: {}
- {}: {}
- {}: {}
- '''.format(
- _("Applied run user"), apply_run_user,
- _("Applied run asset"), apply_run_asset,
- _("Applied run system user"), apply_run_system_user,
- _("Applied run command"), apply_run_command,
- _("Applied from session"), apply_from_session_id,
- _("Applied from command filter rules"), apply_from_cmd_filter_rule_id,
- _("Applied from command filter"), apply_from_cmd_filter_id,
- )
- return applied_body
diff --git a/apps/tickets/handler/login_asset_confirm.py b/apps/tickets/handler/login_asset_confirm.py
deleted file mode 100644
index 039f7ce50..000000000
--- a/apps/tickets/handler/login_asset_confirm.py
+++ /dev/null
@@ -1,21 +0,0 @@
-from django.utils.translation import ugettext as _
-from .base import BaseHandler
-
-
-class Handler(BaseHandler):
-
- # body
- def _construct_meta_body_of_open(self):
- apply_login_user = self.ticket.meta.get('apply_login_user')
- apply_login_asset = self.ticket.meta.get('apply_login_asset')
- apply_login_system_user = self.ticket.meta.get('apply_login_system_user')
- applied_body = '''
- {}: {}
- {}: {}
- {}: {}
- '''.format(
- _("Applied login user"), apply_login_user,
- _("Applied login asset"), apply_login_asset,
- _("Applied login system user"), apply_login_system_user,
- )
- return applied_body
diff --git a/apps/tickets/handler/__init__.py b/apps/tickets/handlers/__init__.py
similarity index 70%
rename from apps/tickets/handler/__init__.py
rename to apps/tickets/handlers/__init__.py
index 42ef0d871..4400d043d 100644
--- a/apps/tickets/handler/__init__.py
+++ b/apps/tickets/handlers/__init__.py
@@ -2,6 +2,6 @@ from django.utils.module_loading import import_string
def get_ticket_handler(ticket):
- handler_class_path = 'tickets.handler.{}.Handler'.format(ticket.type)
+ handler_class_path = 'tickets.handlers.{}.Handler'.format(ticket.type)
handler_class = import_string(handler_class_path)
return handler_class(ticket=ticket)
diff --git a/apps/tickets/handlers/apply_application.py b/apps/tickets/handlers/apply_application.py
new file mode 100644
index 000000000..15c76c7e5
--- /dev/null
+++ b/apps/tickets/handlers/apply_application.py
@@ -0,0 +1,63 @@
+from django.utils.translation import ugettext as _
+
+from orgs.utils import tmp_to_org, tmp_to_root_org
+from perms.models import ApplicationPermission
+from tickets.models import ApplyApplicationTicket
+from .base import BaseHandler
+
+
+class Handler(BaseHandler):
+ ticket: ApplyApplicationTicket
+
+ def _on_step_approved(self, step):
+ is_finished = super()._on_step_approved(step)
+ if is_finished:
+ self._create_application_permission()
+
+ # permission
+ def _create_application_permission(self):
+ with tmp_to_root_org():
+ application_permission = ApplicationPermission.objects.filter(id=self.ticket.id).first()
+ if application_permission:
+ return application_permission
+
+ apply_permission_name = self.ticket.apply_permission_name
+ apply_category = self.ticket.apply_category
+ apply_type = self.ticket.apply_type
+ apply_applications = self.ticket.apply_applications.all()
+ apply_system_users = self.ticket.apply_system_users.all()
+ apply_date_start = self.ticket.apply_date_start
+ apply_date_expired = self.ticket.apply_date_expired
+ permission_created_by = '{}:{}'.format(
+ str(self.ticket.__class__.__name__), str(self.ticket.id)
+ )
+ permission_comment = _(
+ 'Created by the ticket, '
+ 'ticket title: {}, '
+ 'ticket applicant: {}, '
+ 'ticket processor: {}, '
+ 'ticket ID: {}'
+ ).format(
+ self.ticket.title,
+ self.ticket.applicant,
+ ','.join([i['processor_display'] for i in self.ticket.process_map]),
+ str(self.ticket.id)
+ )
+ permissions_data = {
+ 'id': self.ticket.id,
+ 'name': apply_permission_name,
+ 'from_ticket': True,
+ 'category': apply_category,
+ 'type': apply_type,
+ 'comment': str(permission_comment),
+ 'created_by': permission_created_by,
+ 'date_start': apply_date_start,
+ 'date_expired': apply_date_expired,
+ }
+ with tmp_to_org(self.ticket.org_id):
+ application_permission = ApplicationPermission.objects.create(**permissions_data)
+ application_permission.users.add(self.ticket.applicant)
+ application_permission.applications.set(apply_applications)
+ application_permission.system_users.set(apply_system_users)
+
+ return application_permission
diff --git a/apps/tickets/handlers/apply_asset.py b/apps/tickets/handlers/apply_asset.py
new file mode 100644
index 000000000..136347782
--- /dev/null
+++ b/apps/tickets/handlers/apply_asset.py
@@ -0,0 +1,64 @@
+from django.utils.translation import ugettext as _
+
+from perms.models import AssetPermission
+from orgs.utils import tmp_to_org, tmp_to_root_org
+from tickets.models import ApplyAssetTicket
+from .base import BaseHandler
+
+
+class Handler(BaseHandler):
+ ticket: ApplyAssetTicket
+
+ def _on_step_approved(self, step):
+ is_finished = super()._on_step_approved(step)
+ if is_finished:
+ self._create_asset_permission()
+
+ # permission
+ def _create_asset_permission(self):
+ with tmp_to_root_org():
+ asset_permission = AssetPermission.objects.filter(id=self.ticket.id).first()
+ if asset_permission:
+ return asset_permission
+
+ apply_permission_name = self.ticket.apply_permission_name
+ apply_nodes = self.ticket.apply_nodes.all()
+ apply_assets = self.ticket.apply_assets.all()
+ apply_system_users = self.ticket.apply_system_users.all()
+ apply_actions = self.ticket.apply_actions
+ apply_date_start = self.ticket.apply_date_start
+ apply_date_expired = self.ticket.apply_date_expired
+ permission_created_by = '{}:{}'.format(
+ str(self.ticket.__class__.__name__), str(self.ticket.id)
+ )
+ permission_comment = _(
+ 'Created by the ticket '
+ 'ticket title: {} '
+ 'ticket applicant: {} '
+ 'ticket processor: {} '
+ 'ticket ID: {}'
+ ).format(
+ self.ticket.title,
+ self.ticket.applicant,
+ ','.join([i['processor_display'] for i in self.ticket.process_map]),
+ str(self.ticket.id)
+ )
+
+ permission_data = {
+ 'id': self.ticket.id,
+ 'name': apply_permission_name,
+ 'from_ticket': True,
+ 'comment': str(permission_comment),
+ 'created_by': permission_created_by,
+ 'actions': apply_actions,
+ 'date_start': apply_date_start,
+ 'date_expired': apply_date_expired,
+ }
+ with tmp_to_org(self.ticket.org_id):
+ asset_permission = AssetPermission.objects.create(**permission_data)
+ asset_permission.users.add(self.ticket.applicant)
+ asset_permission.nodes.set(apply_nodes)
+ asset_permission.assets.set(apply_assets)
+ asset_permission.system_users.set(apply_system_users)
+
+ return asset_permission
diff --git a/apps/tickets/handlers/base.py b/apps/tickets/handlers/base.py
new file mode 100644
index 000000000..308f9f914
--- /dev/null
+++ b/apps/tickets/handlers/base.py
@@ -0,0 +1,81 @@
+from django.utils.translation import ugettext as _
+
+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
+
+logger = get_logger(__name__)
+
+
+class BaseHandler:
+
+ def __init__(self, ticket):
+ self.ticket = ticket
+
+ def on_change_state(self, state):
+ self._create_state_change_comment(state)
+ handler = getattr(self, f'_on_{state}', lambda: None)
+ return handler()
+
+ def _on_pending(self):
+ self._send_applied_mail_to_assignees()
+
+ def on_step_state_change(self, step, state):
+ self._create_state_change_comment(state)
+ handler = getattr(self, f'_on_step_{state}', lambda: None)
+ return handler(step)
+
+ def _on_step_approved(self, step):
+ next_step = step.next()
+ is_finished = not next_step
+ if is_finished:
+ self._send_processed_mail_to_applicant(step)
+ else:
+ self._send_processed_mail_to_applicant(step)
+ self._send_applied_mail_to_assignees(next_step)
+ return is_finished
+
+ def _on_step_rejected(self, step):
+ self._send_processed_mail_to_applicant(step)
+
+ def _on_step_closed(self, step):
+ self._send_processed_mail_to_applicant()
+
+ def _send_applied_mail_to_assignees(self, step=None):
+ if step:
+ assignees = [step.assignee for step in step.ticket_assignees.all()]
+ else:
+ assignees = self.ticket.current_assignees
+ assignees_display = ', '.join([str(assignee) for assignee in assignees])
+ logger.debug('Send applied email to assignees: {}'.format(assignees_display))
+ send_ticket_applied_mail_to_assignees(self.ticket, assignees)
+
+ def _send_processed_mail_to_applicant(self, step=None):
+ applicant = self.ticket.applicant
+ if self.ticket.status == TicketStatus.closed:
+ processor = applicant
+ else:
+ processor = step.processor if step else self.ticket.processor
+ logger.debug('Send processed mail to applicant: {}'.format(applicant))
+ send_ticket_processed_mail_to_applicant(self.ticket, processor)
+
+ def _create_state_change_comment(self, state):
+ # 打开或关闭工单,备注显示是自己,其他是受理人
+ if state in [TicketState.reopen, TicketState.pending, TicketState.closed]:
+ user = self.ticket.applicant
+ else:
+ user = self.ticket.processor
+
+ user_display = str(user)
+ state_display = getattr(TicketState, state).label
+ data = {
+ 'body': _('{} {} the ticket').format(user_display, state_display),
+ 'user': user,
+ 'user_display': str(user),
+ 'type': 'state',
+ 'state': state
+ }
+ return self.ticket.comments.create(**data)
diff --git a/apps/tickets/handlers/command_confirm.py b/apps/tickets/handlers/command_confirm.py
new file mode 100644
index 000000000..a34751b9d
--- /dev/null
+++ b/apps/tickets/handlers/command_confirm.py
@@ -0,0 +1,6 @@
+from tickets.models import ApplyCommandTicket
+from .base import BaseHandler
+
+
+class Handler(BaseHandler):
+ ticket: ApplyCommandTicket
diff --git a/apps/tickets/handler/general.py b/apps/tickets/handlers/general.py
similarity index 100%
rename from apps/tickets/handler/general.py
rename to apps/tickets/handlers/general.py
diff --git a/apps/tickets/handlers/login_asset_confirm.py b/apps/tickets/handlers/login_asset_confirm.py
new file mode 100644
index 000000000..16f156d4d
--- /dev/null
+++ b/apps/tickets/handlers/login_asset_confirm.py
@@ -0,0 +1,6 @@
+from tickets.models import ApplyLoginAssetTicket
+from .base import BaseHandler
+
+
+class Handler(BaseHandler):
+ ticket: ApplyLoginAssetTicket
diff --git a/apps/tickets/handler/login_confirm.py b/apps/tickets/handlers/login_confirm.py
similarity index 65%
rename from apps/tickets/handler/login_confirm.py
rename to apps/tickets/handlers/login_confirm.py
index 65c46fb56..ad33ce476 100644
--- a/apps/tickets/handler/login_confirm.py
+++ b/apps/tickets/handlers/login_confirm.py
@@ -1,14 +1,15 @@
from django.utils.translation import ugettext as _
+from tickets.models import ApplyLoginTicket
from .base import BaseHandler
class Handler(BaseHandler):
+ ticket: ApplyLoginTicket
- # body
def _construct_meta_body_of_open(self):
- apply_login_ip = self.ticket.meta.get('apply_login_ip')
- apply_login_city = self.ticket.meta.get('apply_login_city')
- apply_login_datetime = self.ticket.meta.get('apply_login_datetime')
+ apply_login_ip = self.ticket.apply_login_ip
+ apply_login_city = self.ticket.apply_login_city
+ apply_login_datetime = self.ticket.apply_login_datetime
applied_body = '''
{}: {}
{}: {}
diff --git a/apps/tickets/migrations/0007_auto_20201224_1821.py b/apps/tickets/migrations/0007_auto_20201224_1821.py
index a16771ef9..31645b00f 100644
--- a/apps/tickets/migrations/0007_auto_20201224_1821.py
+++ b/apps/tickets/migrations/0007_auto_20201224_1821.py
@@ -3,7 +3,7 @@
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
-import tickets.models.ticket
+import common.db.encoder
TICKET_TYPE_APPLY_ASSET = 'apply_asset'
@@ -116,7 +116,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='ticket',
name='assignees_display_new',
- field=models.JSONField(default=list, encoder=tickets.models.ticket.ModelJSONFieldEncoder, verbose_name='Assignees display'),
+ field=models.JSONField(default=list, encoder=common.db.encoder.ModelJSONFieldEncoder, verbose_name='Assignees display'),
),
migrations.AlterField(
model_name='ticket',
@@ -126,7 +126,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='ticket',
name='meta',
- field=models.JSONField(default=dict, encoder=tickets.models.ticket.ModelJSONFieldEncoder, verbose_name='Meta'),
+ field=models.JSONField(default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, verbose_name='Meta'),
),
migrations.AlterField(
model_name='ticket',
diff --git a/apps/tickets/migrations/0016_auto_20220609_1758.py b/apps/tickets/migrations/0016_auto_20220609_1758.py
new file mode 100644
index 000000000..55d88385e
--- /dev/null
+++ b/apps/tickets/migrations/0016_auto_20220609_1758.py
@@ -0,0 +1,189 @@
+# Generated by Django 3.1.14 on 2022-06-09 09:58
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ('terminal', '0049_endpoint_redis_port'),
+ ('assets', '0090_auto_20220412_1145'),
+ ('applications', '0020_auto_20220316_2028'),
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('tickets', '0015_superticket'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ApplyLoginTicket',
+ fields=[
+ ('ticket_ptr',
+ models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
+ primary_key=True, serialize=False, to='tickets.ticket')),
+ ('apply_login_ip', models.GenericIPAddressField(null=True, verbose_name='Login ip')),
+ ('apply_login_city', models.CharField(max_length=64, null=True, verbose_name='Login city')),
+ ('apply_login_datetime', models.DateTimeField(null=True, verbose_name='Login datetime')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('tickets.ticket',),
+ ),
+ migrations.RemoveField(
+ model_name='ticket',
+ name='process_map',
+ ),
+ migrations.AddField(
+ model_name='comment',
+ name='state',
+ field=models.CharField(max_length=16, null=True),
+ ),
+ migrations.AddField(
+ model_name='comment',
+ name='type',
+ field=models.CharField(choices=[('state', 'State'), ('common', 'common')], default='common', max_length=16,
+ verbose_name='Type'),
+ ),
+ migrations.AddField(
+ model_name='ticket',
+ name='rel_snapshot',
+ field=models.JSONField(default=dict, verbose_name='Relation snapshot'),
+ ),
+ migrations.AddField(
+ model_name='ticketstep',
+ name='status',
+ field=models.CharField(choices=[('pending', 'Pending'), ('active', 'Active'), ('closed', 'Closed')],
+ default='pending', max_length=16),
+ ),
+ migrations.AlterField(
+ model_name='ticket',
+ name='state',
+ field=models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected'),
+ ('closed', 'Cancel'), ('reopen', 'Reopen')], default='pending',
+ max_length=16, verbose_name='State'),
+ ),
+ migrations.AlterField(
+ model_name='ticket',
+ name='status',
+ field=models.CharField(choices=[('open', 'Open'), ('closed', 'Finished')], default='open', max_length=16,
+ verbose_name='Status'),
+ ),
+ migrations.AlterField(
+ model_name='ticketassignee',
+ name='state',
+ field=models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected'),
+ ('closed', 'Cancel'), ('reopen', 'Reopen')], default='pending',
+ max_length=64),
+ ),
+ migrations.AlterField(
+ model_name='ticketstep',
+ name='state',
+ field=models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected'),
+ ('closed', 'Closed')], default='pending', max_length=64,
+ verbose_name='State'),
+ ),
+ migrations.CreateModel(
+ name='ApplyLoginAssetTicket',
+ fields=[
+ ('ticket_ptr',
+ models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
+ primary_key=True, serialize=False, to='tickets.ticket')),
+ ('apply_login_asset',
+ models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.asset',
+ verbose_name='Login asset')),
+ ('apply_login_system_user',
+ models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.systemuser',
+ verbose_name='Login system user')),
+ ('apply_login_user',
+ models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL,
+ verbose_name='Login user')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('tickets.ticket',),
+ ),
+ migrations.CreateModel(
+ name='ApplyCommandTicket',
+ fields=[
+ ('ticket_ptr',
+ models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
+ primary_key=True, serialize=False, to='tickets.ticket')),
+ ('apply_run_command', models.CharField(max_length=4096, verbose_name='Run command')),
+ ('apply_from_cmd_filter',
+ models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.commandfilter',
+ verbose_name='From cmd filter')),
+ ('apply_from_cmd_filter_rule',
+ models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL,
+ to='assets.commandfilterrule', verbose_name='From cmd filter rule')),
+ ('apply_from_session',
+ models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='terminal.session',
+ verbose_name='Session')),
+ ('apply_run_asset',
+ models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.asset',
+ verbose_name='Run asset')),
+ ('apply_run_system_user',
+ models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.systemuser',
+ verbose_name='Run system user')),
+ ('apply_run_user',
+ models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL,
+ verbose_name='Run user')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('tickets.ticket',),
+ ),
+ migrations.CreateModel(
+ name='ApplyAssetTicket',
+ fields=[
+ ('ticket_ptr',
+ models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
+ primary_key=True, serialize=False, to='tickets.ticket')),
+ ('apply_permission_name', models.CharField(max_length=128, verbose_name='Apply name')),
+ ('apply_actions', models.IntegerField(
+ choices=[(255, 'All'), (1, 'Connect'), (2, 'Upload file'), (4, 'Download file'),
+ (6, 'Upload download'), (8, 'Clipboard copy'), (16, 'Clipboard paste'),
+ (24, 'Clipboard copy paste')], default=255, verbose_name='Actions')),
+ ('apply_date_start', models.DateTimeField(null=True, verbose_name='Date start')),
+ ('apply_date_expired', models.DateTimeField(null=True, verbose_name='Date expired')),
+ ('apply_assets', models.ManyToManyField(to='assets.Asset', verbose_name='Apply assets')),
+ ('apply_nodes', models.ManyToManyField(to='assets.Node', verbose_name='Apply nodes')),
+ ('apply_system_users',
+ models.ManyToManyField(to='assets.SystemUser', verbose_name='Apply system users')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('tickets.ticket',),
+ ),
+ migrations.CreateModel(
+ name='ApplyApplicationTicket',
+ fields=[
+ ('ticket_ptr',
+ models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
+ primary_key=True, serialize=False, to='tickets.ticket')),
+ ('apply_permission_name', models.CharField(max_length=128, verbose_name='Apply name')),
+ ('apply_category',
+ models.CharField(choices=[('db', 'Database'), ('remote_app', 'Remote app'), ('cloud', 'Cloud')],
+ max_length=16, verbose_name='Category')),
+ ('apply_type', models.CharField(
+ choices=[('mysql', 'MySQL'), ('mariadb', 'MariaDB'), ('oracle', 'Oracle'),
+ ('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'),
+ ('mongodb', 'MongoDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'),
+ ('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')],
+ max_length=16, verbose_name='Type')),
+ ('apply_date_start', models.DateTimeField(null=True, verbose_name='Date start')),
+ ('apply_date_expired', models.DateTimeField(null=True, verbose_name='Date expired')),
+ ('apply_applications',
+ models.ManyToManyField(to='applications.Application', verbose_name='Apply applications')),
+ ('apply_system_users',
+ models.ManyToManyField(to='assets.SystemUser', verbose_name='Apply system users')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('tickets.ticket',),
+ ),
+ ]
diff --git a/apps/tickets/migrations/0017_auto_20220623_1027.py b/apps/tickets/migrations/0017_auto_20220623_1027.py
new file mode 100644
index 000000000..7ac1c9eb2
--- /dev/null
+++ b/apps/tickets/migrations/0017_auto_20220623_1027.py
@@ -0,0 +1,339 @@
+# Generated by Django 3.1.14 on 2022-06-23 02:27
+
+import re
+from datetime import datetime
+from collections import defaultdict
+
+from django.utils import timezone as dj_timezone
+from django.db import migrations
+
+from perms.models import Action
+from tickets.const import TicketType
+
+pt = re.compile(r'(\w+)\((\w+)\)')
+
+
+def time_conversion(t):
+ if not t:
+ return
+ try:
+ return datetime.strptime(t, '%Y-%m-%d %H:%M:%S'). \
+ astimezone(dj_timezone.get_current_timezone())
+ except Exception:
+ return
+
+
+nodes_dict = defaultdict(set)
+assets_dict = defaultdict(set)
+system_users_dict = defaultdict(set)
+apps_dict = defaultdict(set)
+global_inited = {}
+
+
+def init_global_dict(apps):
+ if global_inited:
+ return
+ node_model = apps.get_model('assets', 'Node')
+ asset_model = apps.get_model('assets', 'Asset')
+ system_user_model = apps.get_model('assets', 'SystemUser')
+ application_model = apps.get_model('applications', 'Application')
+
+ node_qs = node_model.objects.values('id', 'org_id')
+ asset_qs = asset_model.objects.values('id', 'org_id')
+ system_user_qs = system_user_model.objects.values('id', 'org_id')
+ app_qs = application_model.objects.values('id', 'org_id')
+
+ for d, qs in [
+ (nodes_dict, node_qs),
+ (assets_dict, asset_qs),
+ (system_users_dict, system_user_qs),
+ (apps_dict, app_qs)
+ ]:
+ for i in qs:
+ _id = str(i['id'])
+ org_id = str(i['org_id'])
+ d[org_id].add(_id)
+ global_inited['inited'] = True
+
+
+def apply_asset_migrate(apps, *args):
+ init_global_dict(apps)
+
+ ticket_model = apps.get_model('tickets', 'Ticket')
+ tickets = ticket_model.objects.filter(type=TicketType.apply_asset)
+ ticket_apply_asset_model = apps.get_model('tickets', 'ApplyAssetTicket')
+
+ for instance in tickets:
+ meta = instance.meta
+ org_id = instance.org_id
+ apply_actions = meta.get('apply_actions')
+
+ # 数据库中数据结构有问题,可能是 list,可能是 int
+ if isinstance(apply_actions, list):
+ apply_actions = Action.choices_to_value(value=apply_actions)
+ elif isinstance(apply_actions, int):
+ apply_actions = apply_actions
+ else:
+ apply_actions = 0
+
+ data = {
+ 'ticket_ptr_id': instance.pk,
+ 'apply_permission_name': meta.get('apply_permission_name', ''),
+ 'apply_date_start': time_conversion(meta.get('apply_date_start')),
+ 'apply_date_expired': time_conversion(meta.get('apply_date_expired')),
+ 'apply_actions': apply_actions,
+ }
+
+ child = ticket_apply_asset_model(**data)
+ child.__dict__.update(instance.__dict__)
+ child.save()
+
+ apply_nodes = list(set(meta.get('apply_nodes', [])) & nodes_dict[org_id])
+ apply_assets = list(set(meta.get('apply_assets', [])) & assets_dict[org_id])
+ apply_system_users = list(set(meta.get('apply_system_users', [])) & system_users_dict[org_id])
+ child.apply_nodes.set(apply_nodes)
+ child.apply_assets.set(apply_assets)
+ child.apply_system_users.set(apply_system_users)
+
+ if (not apply_nodes and not apply_assets) or not apply_system_users:
+ continue
+
+ rel_snapshot = {
+ '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', []),
+ }
+ instance.rel_snapshot = rel_snapshot
+ instance.save(update_fields=['rel_snapshot'])
+
+
+def apply_application_migrate(apps, *args):
+ init_global_dict(apps)
+
+ ticket_model = apps.get_model('tickets', 'Ticket')
+ tickets = ticket_model.objects.filter(type=TicketType.apply_application)
+ ticket_apply_app_model = apps.get_model('tickets', 'ApplyApplicationTicket')
+
+ for instance in tickets:
+ meta = instance.meta
+ org_id = instance.org_id
+ data = {
+ 'ticket_ptr_id': instance.pk,
+ 'apply_permission_name': meta.get('apply_permission_name', ''),
+ 'apply_category': meta.get('apply_category'),
+ 'apply_type': meta.get('apply_type'),
+ 'apply_date_start': time_conversion(meta.get('apply_date_start')),
+ 'apply_date_expired': time_conversion(meta.get('apply_date_expired')),
+ }
+ child = ticket_apply_app_model(**data)
+ child.__dict__.update(instance.__dict__)
+ child.save()
+
+ apply_applications = list(set(meta.get('apply_applications', [])) & apps_dict[org_id])
+ apply_system_users = list(set(meta.get('apply_system_users', [])) & system_users_dict[org_id])
+ if not apply_applications or not apply_system_users:
+ continue
+ child.apply_applications.set(apply_applications)
+ child.apply_system_users.set(apply_system_users)
+
+ rel_snapshot = {
+ 'applicant': instance.applicant_display,
+ 'apply_applications': meta.get('apply_applications_display', []),
+ 'apply_system_users': meta.get('apply_system_users', []),
+ }
+ instance.rel_snapshot = rel_snapshot
+ instance.save(update_fields=['rel_snapshot'])
+
+
+def login_confirm_migrate(apps, *args):
+ ticket_model = apps.get_model('tickets', 'Ticket')
+ tickets = ticket_model.objects.filter(type=TicketType.login_confirm)
+ ticket_apply_login_model = apps.get_model('tickets', 'ApplyLoginTicket')
+
+ for instance in tickets:
+ meta = instance.meta
+ data = {
+ 'ticket_ptr_id': instance.pk,
+ 'apply_login_ip': meta.get('apply_login_ip'),
+ 'apply_login_city': meta.get('apply_login_city'),
+ 'apply_login_datetime': time_conversion(meta.get('apply_login_datetime')),
+ }
+ rel_snapshot = {
+ 'applicant': instance.applicant_display
+ }
+ instance.rel_snapshot = rel_snapshot
+ instance.save(update_fields=['rel_snapshot'])
+ child = ticket_apply_login_model(**data)
+ child.__dict__.update(instance.__dict__)
+ child.save()
+
+
+def analysis_instance_name(name: str):
+ if not name:
+ return None
+ matched = pt.match(name)
+ if not matched:
+ return None
+ return matched.groups()
+
+
+def login_asset_confirm_migrate(apps, *args):
+ user_model = apps.get_model('users', 'User')
+ asset_model = apps.get_model('assets', 'Asset')
+ system_user_model = apps.get_model('assets', 'SystemUser')
+ ticket_model = apps.get_model('tickets', 'Ticket')
+ tickets = ticket_model.objects.filter(type=TicketType.login_asset_confirm)
+ ticket_apply_login_asset_model = apps.get_model('tickets', 'ApplyLoginAssetTicket')
+
+ for instance in tickets:
+ meta = instance.meta
+
+ name_username = analysis_instance_name(meta.get('apply_login_user'))
+ apply_login_user = user_model.objects.filter(
+ name=name_username[0],
+ username=name_username[1]
+ ).first() if name_username else None
+
+ hostname_ip = analysis_instance_name(meta.get('apply_login_asset'))
+ apply_login_asset = asset_model.objects.filter(
+ org_id=instance.org_id,
+ hostname=hostname_ip[0],
+ ip=hostname_ip[1]
+ ).first() if hostname_ip else None
+
+ name_username = analysis_instance_name(meta.get('apply_login_system_user'))
+ apply_login_system_user = system_user_model.objects.filter(
+ org_id=instance.org_id,
+ name=name_username[0],
+ username=name_username[1]
+ ).first() if name_username else None
+
+ data = {
+ 'ticket_ptr_id': instance.pk,
+ 'apply_login_user': apply_login_user,
+ 'apply_login_asset': apply_login_asset,
+ 'apply_login_system_user': apply_login_system_user,
+ }
+ child = ticket_apply_login_asset_model(**data)
+ child.__dict__.update(instance.__dict__)
+ child.save()
+
+ rel_snapshot = {
+ 'applicant': instance.applicant_display,
+ 'apply_login_user': meta.get('apply_login_user', ''),
+ 'apply_login_asset': meta.get('apply_login_asset', ''),
+ 'apply_login_system_user': meta.get('apply_login_system_user', ''),
+ }
+ instance.rel_snapshot = rel_snapshot
+ instance.save(update_fields=['rel_snapshot'])
+
+
+def command_confirm_migrate(apps, *args):
+ user_model = apps.get_model('users', 'User')
+ asset_model = apps.get_model('assets', 'Asset')
+ system_user_model = apps.get_model('assets', 'SystemUser')
+ ticket_model = apps.get_model('tickets', 'Ticket')
+ session_model = apps.get_model('terminal', 'Session')
+ command_filter_model = apps.get_model('assets', 'CommandFilter')
+ command_filter_rule_model = apps.get_model('assets', 'CommandFilterRule')
+
+ tickets = ticket_model.objects.filter(type=TicketType.command_confirm)
+ session_ids = tickets.values_list('meta__apply_from_session_id', flat=True)
+ session_ids = session_model.objects.filter(id__in=session_ids).values_list('id', flat=True)
+
+ command_filter_ids = tickets.values_list('meta__apply_from_cmd_filter_id', flat=True)
+ command_filter_ids = command_filter_model.objects\
+ .filter(id__in=command_filter_ids)\
+ .values_list('id', flat=True)
+
+ command_filter_rule_ids = tickets.values_list('meta__apply_from_cmd_filter_rule_id', flat=True)
+ command_filter_rule_ids = command_filter_rule_model.objects\
+ .filter(id__in=command_filter_rule_ids)\
+ .values_list('id', flat=True)
+ ticket_apply_command_model = apps.get_model('tickets', 'ApplyCommandTicket')
+
+ for instance in tickets:
+ meta = instance.meta
+
+ name_username = analysis_instance_name(meta.get('apply_run_user'))
+ apply_run_user = user_model.objects.filter(
+ name=name_username[0], username=name_username[1]
+ ).first() if name_username else None
+
+ hostname_ip = analysis_instance_name(meta.get('apply_run_asset'))
+ apply_run_asset = asset_model.objects.filter(
+ org_id=instance.org_id, hostname=hostname_ip[0], ip=hostname_ip[1]
+ ).first() if hostname_ip else None
+
+ name_username = analysis_instance_name(meta.get('apply_run_system_user'))
+ apply_run_system_user = system_user_model.objects.filter(
+ org_id=instance.org_id, name=name_username[0], username=name_username[1]
+ ).first() if name_username else None
+
+ apply_from_session_id = meta.get('apply_from_session_id')
+ apply_from_cmd_filter_id = meta.get('apply_from_cmd_filter_id')
+ apply_from_cmd_filter_rule_id = meta.get('apply_from_cmd_filter_rule_id')
+
+ if apply_from_session_id not in session_ids:
+ apply_from_session_id = None
+ if apply_from_cmd_filter_id not in command_filter_ids:
+ apply_from_cmd_filter_id = None
+ if apply_from_cmd_filter_rule_id not in command_filter_rule_ids:
+ apply_from_cmd_filter_rule_id = None
+
+ data = {
+ 'ticket_ptr_id': instance.pk,
+ 'apply_run_user': apply_run_user,
+ 'apply_run_asset': apply_run_asset,
+ 'apply_run_system_user': apply_run_system_user,
+ 'apply_run_command': meta.get('apply_run_command', ''),
+ 'apply_from_session_id': apply_from_session_id,
+ 'apply_from_cmd_filter_id': apply_from_cmd_filter_id,
+ 'apply_from_cmd_filter_rule_id': apply_from_cmd_filter_rule_id,
+ }
+
+ rel_snapshot = {
+ 'applicant': instance.applicant_display,
+ 'apply_run_user': meta.get('apply_run_user', ''),
+ 'apply_run_asset': meta.get('apply_run_asset', ''),
+ 'apply_run_system_user': meta.get('apply_run_system_user', ''),
+ 'apply_from_session': meta.get('apply_from_session_id', ''),
+ 'apply_from_cmd_filter': meta.get('apply_from_cmd_filter_id', ''),
+ 'apply_from_cmd_filter_rule': meta.get('apply_from_cmd_filter_rule_id', ''),
+ }
+ child = ticket_apply_command_model(**data)
+ child.__dict__.update(instance.__dict__)
+ child.save()
+
+ instance.rel_snapshot = rel_snapshot
+ instance.save(update_fields=['rel_snapshot'])
+
+
+def migrate_ticket_state(apps, *args):
+ ticket_model = apps.get_model('tickets', 'Ticket')
+ ticket_step_model = apps.get_model('tickets', 'TicketStep')
+ ticket_assignee_model = apps.get_model('tickets', 'TicketAssignee')
+ ticket_model.objects.filter(state='open').update(state='pending')
+ ticket_step_model.objects.filter(state='notified').update(state='pending')
+ ticket_assignee_model.objects.filter(state='notified').update(state='pending')
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('tickets', '0016_auto_20220609_1758'),
+ ]
+
+ operations = [
+ migrations.RunPython(migrate_ticket_state),
+ migrations.RunPython(apply_asset_migrate),
+ migrations.RunPython(apply_application_migrate),
+ migrations.RunPython(login_confirm_migrate),
+ migrations.RunPython(login_asset_confirm_migrate),
+ migrations.RunPython(command_confirm_migrate),
+ migrations.RemoveField(
+ model_name='ticket',
+ name='applicant_display',
+ ),
+ ]
diff --git a/apps/tickets/models/comment.py b/apps/tickets/models/comment.py
index c08b29dbe..f15057d7e 100644
--- a/apps/tickets/models/comment.py
+++ b/apps/tickets/models/comment.py
@@ -9,6 +9,10 @@ __all__ = ['Comment']
class Comment(CommonModelMixin):
+ class Type(models.TextChoices):
+ state = 'state', _('State')
+ common = 'common', _('common')
+
ticket = models.ForeignKey(
'tickets.Ticket', on_delete=models.CASCADE, related_name='comments'
)
@@ -18,6 +22,10 @@ class Comment(CommonModelMixin):
)
user_display = models.CharField(max_length=256, verbose_name=_("User display name"))
body = models.TextField(verbose_name=_("Body"))
+ type = models.CharField(
+ max_length=16, choices=Type.choices, default=Type.common, verbose_name=_("Type")
+ )
+ state = models.CharField(max_length=16, null=True)
class Meta:
ordering = ('date_created', )
diff --git a/apps/tickets/models/flow.py b/apps/tickets/models/flow.py
index 69aa6c432..4dd468f57 100644
--- a/apps/tickets/models/flow.py
+++ b/apps/tickets/models/flow.py
@@ -5,17 +5,18 @@ from django.utils.translation import ugettext_lazy as _
from users.models import User
from common.mixins.models import CommonModelMixin
+
from orgs.mixins.models import OrgModelMixin
from orgs.models import Organization
from orgs.utils import tmp_to_org, get_current_org_id
-from ..const import TicketType, TicketApprovalLevel, TicketApprovalStrategy
+from ..const import TicketType, TicketLevel, TicketApprovalStrategy
__all__ = ['TicketFlow', 'ApprovalRule']
class ApprovalRule(CommonModelMixin):
level = models.SmallIntegerField(
- default=TicketApprovalLevel.one, choices=TicketApprovalLevel.choices,
+ default=TicketLevel.one, choices=TicketLevel.choices,
verbose_name=_('Approve level')
)
strategy = models.CharField(
@@ -56,8 +57,8 @@ class TicketFlow(CommonModelMixin, OrgModelMixin):
default=TicketType.general, verbose_name=_("Type")
)
approval_level = models.SmallIntegerField(
- default=TicketApprovalLevel.one,
- choices=TicketApprovalLevel.choices,
+ default=TicketLevel.one,
+ choices=TicketLevel.choices,
verbose_name=_('Approve level')
)
rules = models.ManyToManyField(ApprovalRule, related_name='ticket_flows')
diff --git a/apps/tickets/models/ticket.py b/apps/tickets/models/ticket.py
deleted file mode 100644
index c05fa8b3b..000000000
--- a/apps/tickets/models/ticket.py
+++ /dev/null
@@ -1,328 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-from typing import Callable
-
-from django.db import models
-from django.db.models import Q
-from django.utils.translation import ugettext_lazy as _
-from django.db.utils import IntegrityError
-
-from common.exceptions import JMSException
-from common.utils.timezone import as_current_tz
-from common.mixins.models import CommonModelMixin
-from common.db.encoder import ModelJSONFieldEncoder
-from orgs.mixins.models import OrgModelMixin
-from orgs.utils import tmp_to_root_org, tmp_to_org
-from tickets.const import TicketType, TicketStatus, TicketState, TicketApprovalLevel, ProcessStatus, TicketAction
-from tickets.signals import post_change_ticket_action
-from tickets.handler import get_ticket_handler
-from tickets.errors import AlreadyClosed
-
-__all__ = ['Ticket', 'TicketStep', 'TicketAssignee', 'SuperTicket']
-
-
-class TicketStep(CommonModelMixin):
- ticket = models.ForeignKey(
- 'Ticket', related_name='ticket_steps', on_delete=models.CASCADE, verbose_name='Ticket'
- )
- level = models.SmallIntegerField(
- default=TicketApprovalLevel.one, choices=TicketApprovalLevel.choices,
- verbose_name=_('Approve level')
- )
- state = models.CharField(choices=ProcessStatus.choices, max_length=64, default=ProcessStatus.notified)
-
- class Meta:
- verbose_name = _("Ticket step")
-
-
-class TicketAssignee(CommonModelMixin):
- assignee = models.ForeignKey(
- 'users.User', related_name='ticket_assignees', on_delete=models.CASCADE, verbose_name='Assignee'
- )
- state = models.CharField(choices=ProcessStatus.choices, max_length=64, default=ProcessStatus.notified)
- step = models.ForeignKey('tickets.TicketStep', related_name='ticket_assignees', on_delete=models.CASCADE)
-
- class Meta:
- verbose_name = _('Ticket assignee')
-
- def __str__(self):
- return '{0.assignee.name}({0.assignee.username})_{0.step}'.format(self)
-
-
-class StatusMixin:
- state: str
- status: str
- applicant: models.ForeignKey
- current_node: models.Manager
- save: Callable
-
- def set_state_approve(self):
- self.state = TicketState.approved
-
- def set_state_reject(self):
- self.state = TicketState.rejected
-
- def set_state_closed(self):
- self.state = TicketState.closed
-
- def set_status_closed(self):
- self.status = TicketStatus.closed
-
- # status
- @property
- def status_open(self):
- return self.status == TicketStatus.open.value
-
- @property
- def status_closed(self):
- return self.status == TicketStatus.closed.value
-
- @property
- def state_open(self):
- return self.state == TicketState.open.value
-
- @property
- def state_approve(self):
- return self.state == TicketState.approved.value
-
- @property
- def state_reject(self):
- return self.state == TicketState.rejected.value
-
- @property
- def state_close(self):
- return self.state == TicketState.closed.value
-
- # action changed
- def open(self, applicant):
- self.applicant = applicant
- self._change_action(TicketAction.open)
-
- def approve(self, processor):
- self.update_current_step_state_and_assignee(processor, TicketState.approved)
- self._change_action(TicketAction.approve)
-
- def reject(self, processor):
- self.update_current_step_state_and_assignee(processor, TicketState.rejected)
- self._change_action(TicketAction.reject)
-
- def close(self, processor):
- self.update_current_step_state_and_assignee(processor, TicketState.closed)
- self._change_action(TicketAction.close)
-
- def update_current_step_state_and_assignee(self, processor, state):
- if self.status_closed:
- raise AlreadyClosed
- if state != TicketState.approved:
- self.state = state
- current_node = self.current_node
- current_node.update(state=state)
- current_node.first().ticket_assignees.filter(assignee=processor).update(state=state)
-
- def _change_action(self, action):
- self.save()
- post_change_ticket_action.send(sender=self.__class__, ticket=self, action=action)
-
-
-class Ticket(CommonModelMixin, StatusMixin, OrgModelMixin):
- title = models.CharField(max_length=256, verbose_name=_("Title"))
- type = models.CharField(
- max_length=64, choices=TicketType.choices,
- default=TicketType.general, verbose_name=_("Type")
- )
- meta = models.JSONField(encoder=ModelJSONFieldEncoder, default=dict, verbose_name=_("Meta"))
- state = models.CharField(
- max_length=16, choices=TicketState.choices,
- default=TicketState.open, verbose_name=_("State")
- )
- status = models.CharField(
- max_length=16, choices=TicketStatus.choices,
- default=TicketStatus.open, verbose_name=_("Status")
- )
- approval_step = models.SmallIntegerField(
- default=TicketApprovalLevel.one, choices=TicketApprovalLevel.choices,
- verbose_name=_('Approval step')
- )
- # 申请人
- applicant = models.ForeignKey(
- 'users.User', related_name='applied_tickets', on_delete=models.SET_NULL, null=True,
- verbose_name=_("Applicant")
- )
- applicant_display = models.CharField(max_length=256, default='', verbose_name=_("Applicant display"))
- process_map = models.JSONField(encoder=ModelJSONFieldEncoder, default=list, verbose_name=_("Process"))
- # 评论
- comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
- flow = models.ForeignKey(
- 'TicketFlow', related_name='tickets', on_delete=models.SET_NULL, null=True,
- verbose_name=_("TicketFlow")
- )
- serial_num = models.CharField(max_length=128, unique=True, null=True, verbose_name=_('Serial number'))
-
- class Meta:
- ordering = ('-date_created',)
- verbose_name = _('Ticket')
-
- def __str__(self):
- return '{}({})'.format(self.title, self.applicant_display)
-
- # type
- @property
- def type_apply_asset(self):
- return self.type == TicketType.apply_asset.value
-
- @property
- def type_apply_application(self):
- return self.type == TicketType.apply_application.value
-
- @property
- def type_login_confirm(self):
- return self.type == TicketType.login_confirm.value
-
- @property
- def current_node(self):
- return self.ticket_steps.filter(level=self.approval_step)
-
- @property
- def processor(self):
- processor = self.current_node.first().ticket_assignees\
- .exclude(state=ProcessStatus.notified).first()
- return processor.assignee if processor else None
-
- def ignore_applicant(self, assignees, applicant=None):
- applicant = applicant if applicant else self.applicant
- if len(assignees) != 1:
- assignees = set(assignees) - {applicant, }
- return list(assignees)
-
- def create_related_node(self, applicant=None):
- org_id = self.flow.org_id
- approval_rule = self.get_current_ticket_flow_approve()
- ticket_step = TicketStep.objects.create(ticket=self, level=self.approval_step)
- ticket_assignees = []
- assignees = approval_rule.get_assignees(org_id=org_id)
- assignees = self.ignore_applicant(assignees, applicant)
- for assignee in assignees:
- ticket_assignees.append(TicketAssignee(step=ticket_step, assignee=assignee))
- TicketAssignee.objects.bulk_create(ticket_assignees)
-
- def create_process_map(self, applicant=None):
- org_id = self.flow.org_id
- approval_rules = self.flow.rules.order_by('level')
- nodes = list()
- for node in approval_rules:
- assignees = node.get_assignees(org_id=org_id)
- assignees = self.ignore_applicant(assignees, applicant)
- assignee_ids = [assignee.id for assignee in assignees]
- assignees_display = [str(assignee) for assignee in assignees]
- nodes.append(
- {
- 'approval_level': node.level,
- 'state': ProcessStatus.notified,
- 'assignees': assignee_ids,
- 'assignees_display': assignees_display
- }
- )
- return nodes
-
- # TODO 兼容不存在流的工单
- def create_process_map_and_node(self, assignees, applicant):
- assignees = self.ignore_applicant(assignees, applicant)
- self.process_map = [{
- 'approval_level': 1,
- 'state': 'notified',
- 'assignees': [assignee.id for assignee in assignees],
- 'assignees_display': [str(assignee) for assignee in assignees]
- }, ]
- self.save()
- ticket_step = TicketStep.objects.create(ticket=self, level=1)
- ticket_assignees = []
- for assignee in assignees:
- ticket_assignees.append(TicketAssignee(step=ticket_step, assignee=assignee))
- TicketAssignee.objects.bulk_create(ticket_assignees)
-
- def has_current_assignee(self, assignee):
- return self.ticket_steps.filter(ticket_assignees__assignee=assignee, level=self.approval_step).exists()
-
- def has_all_assignee(self, assignee):
- return self.ticket_steps.filter(ticket_assignees__assignee=assignee).exists()
-
- @classmethod
- def get_user_related_tickets(cls, user):
- queries = Q(applicant=user) | Q(ticket_steps__ticket_assignees__assignee=user)
- tickets = cls.all().filter(queries).distinct()
- return tickets
-
- def get_current_ticket_flow_approve(self):
- return self.flow.rules.filter(level=self.approval_step).first()
-
- @classmethod
- def all(cls):
- with tmp_to_root_org():
- return Ticket.objects.all()
-
- def save(self, *args, **kwargs):
- """ 确保保存的org_id的是自身的值 """
- with tmp_to_org(self.org_id):
- return super().save(*args, **kwargs)
-
- @property
- def handler(self):
- return get_ticket_handler(ticket=self)
-
- # body
- @property
- def body(self):
- _body = self.handler.get_body()
- return _body
-
- def get_serial_num_date(self):
- date_created = as_current_tz(self.date_created)
- date = date_created.strftime('%Y%m%d')
- return date
-
- def get_last_serail_num(self):
- date_created = as_current_tz(self.date_created)
- date_prefix = date_created.strftime('%Y%m%d')
-
- ticket = Ticket.all().select_for_update().filter(
- serial_num__startswith=date_prefix
- ).order_by('-date_created').first()
-
- if ticket:
- # 202212010001
- num_str = ticket.serial_num[8:]
- num = int(num_str)
- return num
- return None
-
- def get_next_serail_num(self):
- num = self.get_last_serail_num()
- if num is None:
- num = 0
- return '%04d' % (num + 1)
-
- def construct_serial_num(self):
- date_prefix = self.get_serial_num_date()
- num_suffix = self.get_next_serail_num()
- return date_prefix + num_suffix
-
- def update_serial_num_if_need(self):
- if self.serial_num:
- return
-
- try:
- self.serial_num = self.construct_serial_num()
- self.save(update_fields=('serial_num',))
- except IntegrityError as e:
- if e.args[0] == 1062:
- # 虽然做了 `select_for_update` 但是每天的第一条工单仍可能造成冲突
- # 但概率小,这里只报错,用户重新提交即可
- raise JMSException(detail=_('Please try again'), code='please_try_again')
-
- raise e
-
-
-class SuperTicket(Ticket):
- class Meta:
- proxy = True
- verbose_name = _("Super ticket")
diff --git a/apps/tickets/models/ticket/__init__.py b/apps/tickets/models/ticket/__init__.py
new file mode 100644
index 000000000..c13cea9b1
--- /dev/null
+++ b/apps/tickets/models/ticket/__init__.py
@@ -0,0 +1,6 @@
+from .general import *
+from .apply_asset import *
+from .apply_application import *
+from .command_confirm import *
+from .login_asset_confirm import *
+from .login_confirm import *
diff --git a/apps/tickets/models/ticket/apply_application.py b/apps/tickets/models/ticket/apply_application.py
new file mode 100644
index 000000000..de041c8da
--- /dev/null
+++ b/apps/tickets/models/ticket/apply_application.py
@@ -0,0 +1,34 @@
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from .general import Ticket
+from applications.const import AppCategory, AppType
+
+__all__ = ['ApplyApplicationTicket']
+
+
+class ApplyApplicationTicket(Ticket):
+ apply_permission_name = models.CharField(max_length=128, verbose_name=_('Apply name'))
+ # 申请信息
+ apply_category = models.CharField(
+ max_length=16, choices=AppCategory.choices, verbose_name=_('Category')
+ )
+ apply_type = models.CharField(
+ max_length=16, choices=AppType.choices, verbose_name=_('Type')
+ )
+ apply_applications = models.ManyToManyField(
+ 'applications.Application', verbose_name=_('Apply applications'),
+ )
+ apply_system_users = models.ManyToManyField(
+ 'assets.SystemUser', verbose_name=_('Apply system users'),
+ )
+ apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True)
+ apply_date_expired = models.DateTimeField(verbose_name=_('Date expired'), null=True)
+
+ @property
+ def apply_category_display(self):
+ return AppCategory.get_label(self.apply_category)
+
+ @property
+ def apply_type_display(self):
+ return AppType.get_label(self.apply_type)
diff --git a/apps/tickets/models/ticket/apply_asset.py b/apps/tickets/models/ticket/apply_asset.py
new file mode 100644
index 000000000..45cad4dca
--- /dev/null
+++ b/apps/tickets/models/ticket/apply_asset.py
@@ -0,0 +1,28 @@
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from perms.models import Action
+from .general import Ticket
+
+__all__ = ['ApplyAssetTicket']
+
+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_nodes = models.ManyToManyField('assets.Node', verbose_name=_('Apply nodes'))
+ # 申请信息
+ apply_assets = models.ManyToManyField('assets.Asset', verbose_name=_('Apply assets'))
+ apply_system_users = models.ManyToManyField(
+ 'assets.SystemUser', verbose_name=_('Apply system users')
+ )
+ apply_actions = models.IntegerField(
+ choices=Action.DB_CHOICES, default=Action.ALL, verbose_name=_('Actions')
+ )
+ apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True)
+ apply_date_expired = models.DateTimeField(verbose_name=_('Date expired'), null=True)
+
+ @property
+ def apply_actions_display(self):
+ return Action.value_to_choices_display(self.apply_actions)
diff --git a/apps/tickets/models/ticket/command_confirm.py b/apps/tickets/models/ticket/command_confirm.py
new file mode 100644
index 000000000..94d27fde7
--- /dev/null
+++ b/apps/tickets/models/ticket/command_confirm.py
@@ -0,0 +1,33 @@
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from .general import Ticket
+
+
+class ApplyCommandTicket(Ticket):
+ apply_run_user = models.ForeignKey(
+ 'users.User', on_delete=models.SET_NULL,
+ null=True, verbose_name=_('Run user')
+ )
+ apply_run_asset = models.ForeignKey(
+ 'assets.Asset', on_delete=models.SET_NULL,
+ null=True, verbose_name=_('Run asset')
+ )
+ apply_run_system_user = models.ForeignKey(
+ 'assets.SystemUser', on_delete=models.SET_NULL,
+ null=True, verbose_name=_('Run system user')
+ )
+ apply_run_command = models.CharField(max_length=4096, verbose_name=_('Run command'))
+ apply_from_session = models.ForeignKey(
+ 'terminal.Session', on_delete=models.SET_NULL,
+ null=True, verbose_name=_("Session")
+ )
+ apply_from_cmd_filter = models.ForeignKey(
+ 'assets.CommandFilter', on_delete=models.SET_NULL,
+ null=True, verbose_name=_('From cmd filter')
+ )
+ apply_from_cmd_filter_rule = models.ForeignKey(
+ 'assets.CommandFilterRule', on_delete=models.SET_NULL,
+ null=True, verbose_name=_('From cmd filter rule')
+ )
+
diff --git a/apps/tickets/models/ticket/general.py b/apps/tickets/models/ticket/general.py
new file mode 100644
index 000000000..b8f192a9d
--- /dev/null
+++ b/apps/tickets/models/ticket/general.py
@@ -0,0 +1,389 @@
+# -*- coding: utf-8 -*-
+#
+from typing import Callable
+
+from django.db import models
+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 common.exceptions import JMSException
+from common.utils.timezone import as_current_tz
+from common.mixins.models import CommonModelMixin
+from common.db.encoder import ModelJSONFieldEncoder
+from orgs.models import Organization
+from tickets.const import (
+ TicketType, TicketStatus, TicketState,
+ TicketLevel, StepState, StepStatus
+)
+from tickets.handlers import get_ticket_handler
+from tickets.errors import AlreadyClosed
+from ..flow import TicketFlow
+
+__all__ = ['Ticket', 'TicketStep', 'TicketAssignee', 'SuperTicket', 'SubTicketManager']
+
+
+class TicketStep(CommonModelMixin):
+ ticket = models.ForeignKey(
+ 'Ticket', related_name='ticket_steps',
+ on_delete=models.CASCADE, verbose_name='Ticket'
+ )
+ level = models.SmallIntegerField(
+ default=TicketLevel.one, choices=TicketLevel.choices,
+ verbose_name=_('Approve level')
+ )
+ state = models.CharField(
+ max_length=64, choices=StepState.choices,
+ default=StepState.pending, verbose_name=_("State")
+ )
+ status = models.CharField(
+ max_length=16, choices=StepStatus.choices,
+ default=StepStatus.pending
+ )
+
+ def change_state(self, state, processor):
+ if state != StepState.closed:
+ assignees = self.ticket_assignees.filter(assignee=processor)
+ if not assignees:
+ raise PermissionError('Only assignees can do this')
+ assignees.update(state=state)
+ self.status = StepStatus.closed
+ self.state = state
+ self.save(update_fields=['state', 'status'])
+
+ def set_active(self):
+ self.status = StepStatus.active
+ self.save(update_fields=['status'])
+
+ def next(self):
+ kwargs = dict(ticket=self.ticket, level=self.level + 1, status=StepStatus.pending)
+ return self.__class__.objects.filter(**kwargs).first()
+
+ @property
+ def processor(self):
+ processor = self.ticket_assignees.exclude(state=StepState.pending).first()
+ return processor.assignee if processor else None
+
+ class Meta:
+ verbose_name = _("Ticket step")
+
+
+class TicketAssignee(CommonModelMixin):
+ assignee = models.ForeignKey(
+ 'users.User', related_name='ticket_assignees',
+ on_delete=models.CASCADE, verbose_name='Assignee'
+ )
+ state = models.CharField(
+ choices=TicketState.choices, max_length=64,
+ default=TicketState.pending
+ )
+ step = models.ForeignKey(
+ 'tickets.TicketStep', related_name='ticket_assignees',
+ on_delete=models.CASCADE
+ )
+
+ class Meta:
+ verbose_name = _('Ticket assignee')
+
+ def __str__(self):
+ return '{0.assignee.name}({0.assignee.username})_{0.step}'.format(self)
+
+
+class StatusMixin:
+ State = TicketState
+ Status = TicketStatus
+
+ state: str
+ status: str
+ applicant: models.ForeignKey
+ current_step: TicketStep
+ save: Callable
+ create_process_steps_by_flow: Callable
+ create_process_steps_by_assignees: Callable
+ assignees: Callable
+ set_serial_num: Callable
+ set_rel_snapshot: Callable
+ approval_step: int
+ handler: None
+ flow: TicketFlow
+ ticket_steps: models.Manager
+
+ def is_state(self, state: TicketState):
+ return self.state == state
+
+ def is_status(self, status: TicketStatus):
+ return self.status == status
+
+ def _open(self):
+ self.set_serial_num()
+ self.set_rel_snapshot()
+ self._change_state_by_applicant(TicketState.pending)
+
+ def open(self):
+ self.create_process_steps_by_flow()
+ self._open()
+
+ def open_by_system(self, assignees):
+ self.create_process_steps_by_assignees(assignees)
+ self._open()
+
+ def approve(self, processor):
+ self._change_state(StepState.approved, processor)
+
+ def reject(self, processor):
+ self._change_state(StepState.rejected, processor)
+
+ def reopen(self):
+ self._change_state_by_applicant(TicketState.reopen)
+
+ def close(self):
+ self._change_state(TicketState.closed, self.applicant)
+
+ def _change_state_by_applicant(self, state):
+ if state == TicketState.closed:
+ self.status = TicketStatus.closed
+ elif state in [TicketState.reopen, TicketState.pending]:
+ self.status = TicketStatus.open
+ else:
+ raise ValueError("Not supported state: {}".format(state))
+
+ self.state = state
+ self.save(update_fields=['state', 'status'])
+ self.handler.on_change_state(state)
+
+ def _change_state(self, state, processor):
+ if self.is_status(self.Status.closed):
+ raise AlreadyClosed
+ current_step = self.current_step
+ current_step.change_state(state, processor)
+ self._finish_or_next(current_step, state)
+
+ def _finish_or_next(self, current_step, state):
+ next_step = current_step.next()
+
+ # 提前结束,或者最后一步
+ if state in [TicketState.rejected, TicketState.closed] or not next_step:
+ self.state = state
+ self.status = Ticket.Status.closed
+ self.save(update_fields=['state', 'status'])
+ self.handler.on_step_state_change(current_step, state)
+ else:
+ self.handler.on_step_state_change(current_step, state)
+ next_step.set_active()
+ self.approval_step += 1
+ self.save(update_fields=['approval_step'])
+
+ @property
+ def process_map(self):
+ process_map = []
+ steps = self.ticket_steps.all()
+ for step in steps:
+ assignee_ids = []
+ 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))
+ if state != StepState.pending and state == i.state:
+ processor = i.assignee
+ if state != StepState.closed:
+ processor = self.applicant
+ 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 ''
+ }
+ process_map.append(step_info)
+ return process_map
+
+ def exclude_applicant(self, assignees, applicant=None):
+ applicant = applicant if applicant else self.applicant
+ if len(assignees) != 1:
+ assignees = set(assignees) - {applicant, }
+ return list(assignees)
+
+ def create_process_steps_by_flow(self):
+ org_id = self.flow.org_id
+ flow_rules = self.flow.rules.order_by('level')
+ for rule in flow_rules:
+ step = TicketStep.objects.create(ticket=self, level=rule.level)
+ assignees = rule.get_assignees(org_id=org_id)
+ assignees = self.exclude_applicant(assignees, self.applicant)
+ step_assignees = [TicketAssignee(step=step, assignee=user) for user in assignees]
+ TicketAssignee.objects.bulk_create(step_assignees)
+
+ def create_process_steps_by_assignees(self, assignees):
+ assignees = self.exclude_applicant(assignees, self.applicant)
+ step = TicketStep.objects.create(ticket=self, level=1)
+ ticket_assignees = [TicketAssignee(step=step, assignee=user) for user in assignees]
+ TicketAssignee.objects.bulk_create(ticket_assignees)
+
+ @property
+ def current_step(self):
+ return self.ticket_steps.filter(level=self.approval_step).first()
+
+ @property
+ def current_assignees(self):
+ ticket_assignees = self.current_step.ticket_assignees.all()
+ return [i.assignee for i in ticket_assignees]
+
+ @property
+ def processor(self):
+ processor = self.current_step.ticket_assignees \
+ .exclude(state=StepState.pending) \
+ .first()
+ return processor.assignee if processor else None
+
+ def has_current_assignee(self, assignee):
+ return self.ticket_steps.filter(
+ ticket_assignees__assignee=assignee,
+ level=self.approval_step
+ ).exists()
+
+ def has_all_assignee(self, assignee):
+ return self.ticket_steps.filter(ticket_assignees__assignee=assignee).exists()
+
+ @property
+ def handler(self):
+ return get_ticket_handler(ticket=self)
+
+
+class Ticket(StatusMixin, CommonModelMixin):
+ title = models.CharField(max_length=256, verbose_name=_('Title'))
+ type = models.CharField(
+ max_length=64, choices=TicketType.choices,
+ default=TicketType.general, verbose_name=_('Type')
+ )
+ state = models.CharField(
+ max_length=16, choices=TicketState.choices,
+ default=TicketState.pending, verbose_name=_('State')
+ )
+ status = models.CharField(
+ max_length=16, choices=TicketStatus.choices,
+ default=TicketStatus.open, verbose_name=_('Status')
+ )
+ # 申请人
+ applicant = models.ForeignKey(
+ 'users.User', related_name='applied_tickets', on_delete=models.SET_NULL,
+ null=True, verbose_name=_("Applicant")
+ )
+ comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
+ flow = models.ForeignKey(
+ 'TicketFlow', related_name='tickets', on_delete=models.SET_NULL,
+ null=True, verbose_name=_('TicketFlow')
+ )
+ approval_step = models.SmallIntegerField(
+ default=TicketLevel.one, choices=TicketLevel.choices, verbose_name=_('Approval step')
+ )
+ serial_num = models.CharField(_('Serial number'), max_length=128, unique=True, null=True)
+ rel_snapshot = models.JSONField(verbose_name=_('Relation snapshot'), default=dict)
+ meta = models.JSONField(encoder=ModelJSONFieldEncoder, default=dict, verbose_name=_("Meta"))
+ org_id = models.CharField(
+ max_length=36, blank=True, default='', verbose_name=_('Organization'), db_index=True
+ )
+
+ class Meta:
+ ordering = ('-date_created',)
+ verbose_name = _('Ticket')
+
+ def __str__(self):
+ return '{}({})'.format(self.title, self.applicant)
+
+ @property
+ def spec_ticket(self):
+ attr = self.type.replace('_', '') + 'ticket'
+ return getattr(self, attr)
+
+ # TODO 先单独处理一下
+ @property
+ def org_name(self):
+ org = Organization.get_instance(self.org_id)
+ return org.name
+
+ def is_type(self, tp: TicketType):
+ return self.type == tp
+
+ @classmethod
+ def get_user_related_tickets(cls, user):
+ queries = Q(applicant=user) | Q(ticket_steps__ticket_assignees__assignee=user)
+ tickets = cls.objects.all().filter(queries).distinct()
+ return tickets
+
+ def get_current_ticket_flow_approve(self):
+ return self.flow.rules.filter(level=self.approval_step).first()
+
+ @classmethod
+ def all(cls):
+ return cls.objects.all()
+
+ def set_rel_snapshot(self, save=True):
+ rel_fields = set()
+ m2m_fields = set()
+ excludes = ['ticket_ptr_id', 'ticket_ptr', 'flow_id', 'flow', 'applicant_id']
+ for name, field in self._meta._forward_fields_map.items():
+ if name in excludes:
+ continue
+ if isinstance(field, related.RelatedField):
+ rel_fields.add(name)
+ if isinstance(field, related.ManyToManyField):
+ m2m_fields.add(name)
+
+ snapshot = {}
+ for field in rel_fields:
+ value = getattr(self, field)
+
+ if field in m2m_fields:
+ value = [str(v) for v in value.all()]
+ else:
+ value = str(value) if value else ''
+ snapshot[field] = value
+
+ self.rel_snapshot.update(snapshot)
+ if save:
+ self.save(update_fields=('rel_snapshot',))
+
+ def get_next_serial_num(self):
+ date_created = as_current_tz(self.date_created)
+ date_prefix = date_created.strftime('%Y%m%d')
+
+ ticket = Ticket.objects.all().select_for_update().filter(
+ serial_num__startswith=date_prefix
+ ).order_by('-date_created').first()
+
+ last_num = 0
+ if ticket:
+ last_num = ticket.serial_num[8:]
+ last_num = int(last_num)
+ num = '%04d' % (last_num + 1)
+ return '{}{}'.format(date_prefix, num)
+
+ def set_serial_num(self):
+ if self.serial_num:
+ return
+
+ try:
+ self.serial_num = self.get_next_serial_num()
+ self.save(update_fields=('serial_num',))
+ except IntegrityError as e:
+ if e.args[0] == 1062:
+ # 虽然做了 `select_for_update` 但是每天的第一条工单仍可能造成冲突
+ # 但概率小,这里只报错,用户重新提交即可
+ raise JMSException(detail=_('Please try again'), code='please_try_again')
+ raise e
+
+
+class SuperTicket(Ticket):
+ class Meta:
+ proxy = True
+ verbose_name = _("Super ticket")
+
+
+class SubTicketManager(models.Manager):
+ pass
diff --git a/apps/tickets/models/ticket/login_asset_confirm.py b/apps/tickets/models/ticket/login_asset_confirm.py
new file mode 100644
index 000000000..5e5c53a47
--- /dev/null
+++ b/apps/tickets/models/ticket/login_asset_confirm.py
@@ -0,0 +1,21 @@
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from .general import Ticket
+
+__all__ = ['ApplyLoginAssetTicket']
+
+
+class ApplyLoginAssetTicket(Ticket):
+ apply_login_user = models.ForeignKey(
+ 'users.User', on_delete=models.SET_NULL, null=True,
+ verbose_name=_('Login user'),
+ )
+ apply_login_asset = models.ForeignKey(
+ 'assets.Asset', on_delete=models.SET_NULL, null=True,
+ verbose_name=_('Login asset'),
+ )
+ apply_login_system_user = models.ForeignKey(
+ 'assets.SystemUser', on_delete=models.SET_NULL, null=True,
+ verbose_name=_('Login system user'),
+ )
diff --git a/apps/tickets/models/ticket/login_confirm.py b/apps/tickets/models/ticket/login_confirm.py
new file mode 100644
index 000000000..89fb32e94
--- /dev/null
+++ b/apps/tickets/models/ticket/login_confirm.py
@@ -0,0 +1,12 @@
+from django.db import models
+from django.utils.translation import gettext_lazy as _
+
+from .general import Ticket, SubTicketManager
+
+__all__ = ['ApplyLoginTicket']
+
+
+class ApplyLoginTicket(Ticket):
+ apply_login_ip = models.GenericIPAddressField(verbose_name=_('Login ip'), null=True)
+ apply_login_city = models.CharField(max_length=64, verbose_name=_('Login city'), null=True)
+ apply_login_datetime = models.DateTimeField(verbose_name=_('Login datetime'), null=True)
diff --git a/apps/tickets/notifications.py b/apps/tickets/notifications.py
index c29c8d00e..728510280 100644
--- a/apps/tickets/notifications.py
+++ b/apps/tickets/notifications.py
@@ -1,13 +1,17 @@
from urllib.parse import urljoin
+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 _
from notifications.notifications import UserMessage
from common.utils import get_logger, random_string
+from common.db.encoder import ModelJSONFieldEncoder
from .models import Ticket
from . import const
@@ -41,8 +45,8 @@ class BaseTicketMessage(UserMessage):
def get_html_msg(self) -> dict:
context = dict(
title=self.content_title,
- ticket_detail_url=self.ticket_detail_url,
- body=self.ticket.body.replace('\n', '
'),
+ content=self.content,
+ ticket_detail_url=self.ticket_detail_url
)
message = render_to_string('tickets/_msg_ticket.html', context)
return {
@@ -54,8 +58,48 @@ class BaseTicketMessage(UserMessage):
def gen_test_msg(cls):
return None
+ @property
+ def content(self):
+ content = [
+ {'title': _('Ticket basic info'), 'content': self.basic_items},
+ {'title': _('Ticket applied info'), 'content': self.spec_items},
+ ]
+ return content
-class TicketAppliedToAssignee(BaseTicketMessage):
+ def _get_fields_items(self, item_names):
+ fields = self.ticket._meta._forward_fields_map
+ json_data = json.dumps(model_to_dict(self.ticket), cls=ModelJSONFieldEncoder)
+ data = json.loads(json_data)
+ items = []
+
+ 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])
+ item['value'] = value
+ items.append(item)
+ return items
+
+ @property
+ def basic_items(self):
+ item_names = ['serial_num', 'title', 'type', 'state', 'applicant', 'comment']
+ return self._get_fields_items(item_names)
+
+ @property
+ def spec_items(self):
+ fields = self.ticket._meta.local_fields + self.ticket._meta.local_many_to_many
+ excludes = ['ticket_ptr']
+ item_names = [field.name for field in fields if field.name not in excludes]
+ return self._get_fields_items(item_names)
+
+
+class TicketAppliedToAssigneeMessage(BaseTicketMessage):
def __init__(self, user, ticket):
self.ticket = ticket
super().__init__(user)
@@ -69,14 +113,14 @@ class TicketAppliedToAssignee(BaseTicketMessage):
@property
def content_title(self):
- return _('Your has a new ticket, applicant - {}').format(
- str(self.ticket.applicant_display)
- )
+ return _('Your has a new ticket')
@property
def subject(self):
- title = _('New Ticket - {} ({})').format(
- self.ticket.title, self.ticket.get_type_display()
+ title = _('{}: New Ticket - {} ({})').format(
+ self.ticket.applicant,
+ self.ticket.title,
+ self.ticket.get_type_display()
)
return title
@@ -85,19 +129,16 @@ class TicketAppliedToAssignee(BaseTicketMessage):
return urljoin(settings.SITE_URL, url)
def get_html_msg(self) -> dict:
- body = self.ticket.body.replace('\n', '
')
context = dict(
title=self.content_title,
- ticket_detail_url=self.ticket_detail_url,
- body=body,
+ content=self.content,
+ ticket_detail_url=self.ticket_detail_url
)
ticket_approval_url = self.get_ticket_approval_url()
context.update({'ticket_approval_url': ticket_approval_url})
message = render_to_string('tickets/_msg_ticket.html', context)
- cache.set(self.token, {
- 'body': body, 'ticket_id': self.ticket.id
- }, 3600)
+ cache.set(self.token, {'ticket_id': self.ticket.id, 'content': self.content}, 3600)
return {
'subject': self.subject,
'message': message
@@ -112,7 +153,7 @@ class TicketAppliedToAssignee(BaseTicketMessage):
return cls(user, ticket)
-class TicketProcessedToApplicant(BaseTicketMessage):
+class TicketProcessedToApplicantMessage(BaseTicketMessage):
def __init__(self, user, ticket, processor):
self.ticket = ticket
self.processor = processor
diff --git a/apps/tickets/serializers/__init__.py b/apps/tickets/serializers/__init__.py
index 853feafd1..26a4b9aa6 100644
--- a/apps/tickets/serializers/__init__.py
+++ b/apps/tickets/serializers/__init__.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
#
from .ticket import *
+from .flow import *
from .comment import *
from .relation import *
-from .super_ticket import *
\ No newline at end of file
+from .super_ticket import *
diff --git a/apps/tickets/serializers/flow.py b/apps/tickets/serializers/flow.py
new file mode 100644
index 000000000..42c7bed15
--- /dev/null
+++ b/apps/tickets/serializers/flow.py
@@ -0,0 +1,103 @@
+from django.db.transaction import atomic
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+from orgs.models import Organization
+from orgs.mixins.serializers import OrgResourceModelSerializerMixin
+from tickets.models import TicketFlow, ApprovalRule
+from tickets.const import TicketApprovalStrategy
+
+__all__ = ['TicketFlowSerializer']
+
+
+class TicketFlowApproveSerializer(serializers.ModelSerializer):
+ strategy_display = serializers.ReadOnlyField(source='get_strategy_display', label=_('Approve strategy'))
+ assignees_read_only = serializers.SerializerMethodField(label=_('Assignees'))
+ assignees_display = serializers.SerializerMethodField(label=_('Assignees display'))
+
+ class Meta:
+ model = ApprovalRule
+ fields_small = [
+ 'level', 'strategy', 'assignees_read_only', 'assignees_display', 'strategy_display'
+ ]
+ fields_m2m = ['assignees', ]
+ fields = fields_small + fields_m2m
+ read_only_fields = ['level', 'assignees_display']
+ extra_kwargs = {
+ 'assignees': {'write_only': True, 'allow_empty': True, 'required': False}
+ }
+
+ @staticmethod
+ def get_assignees_display(instance):
+ return [str(assignee) for assignee in instance.get_assignees()]
+
+ @staticmethod
+ def get_assignees_read_only(instance):
+ if instance.strategy == TicketApprovalStrategy.custom_user:
+ return instance.assignees.values_list('id', flat=True)
+ return []
+
+ def validate(self, attrs):
+ if attrs['strategy'] == TicketApprovalStrategy.custom_user and not attrs.get('assignees'):
+ error = _('Please select the Assignees')
+ raise serializers.ValidationError({'assignees': error})
+ return super().validate(attrs)
+
+
+class TicketFlowSerializer(OrgResourceModelSerializerMixin):
+ type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
+ rules = TicketFlowApproveSerializer(many=True, required=True)
+
+ class Meta:
+ model = TicketFlow
+ fields_mini = ['id', ]
+ fields_small = fields_mini + [
+ 'type', 'type_display', 'approval_level', 'created_by', 'date_created', 'date_updated',
+ 'org_id', 'org_name'
+ ]
+ fields = fields_small + ['rules', ]
+ read_only_fields = ['created_by', 'org_id', 'date_created', 'date_updated']
+ extra_kwargs = {
+ 'type': {'required': True},
+ 'approval_level': {'required': True}
+ }
+
+ def validate_type(self, value):
+ if not self.instance or (self.instance and self.instance.type != value):
+ if self.Meta.model.objects.filter(type=value).exists():
+ error = _('The current organization type already exists')
+ raise serializers.ValidationError(error)
+ return value
+
+ def create_or_update(self, action, validated_data, instance=None):
+ related = 'rules'
+ assignees = 'assignees'
+ childs = validated_data.pop(related, [])
+ if not instance:
+ instance = getattr(super(), action)(validated_data)
+ else:
+ instance = getattr(super(), action)(instance, validated_data)
+ getattr(instance, related).all().delete()
+ instance_related = getattr(instance, related)
+ child_instances = []
+ related_model = instance_related.model
+ # Todo: 这个权限的判断
+ for level, data in enumerate(childs, 1):
+ data_m2m = data.pop(assignees, None)
+ child_instance = related_model.objects.create(**data, level=level)
+ getattr(child_instance, assignees).set(data_m2m)
+ child_instances.append(child_instance)
+ instance_related.set(child_instances)
+ return instance
+
+ @atomic
+ def create(self, validated_data):
+ return self.create_or_update('create', validated_data)
+
+ @atomic
+ def update(self, instance, validated_data):
+ if instance.org_id == Organization.ROOT_ID:
+ instance = self.create(validated_data)
+ else:
+ instance = self.create_or_update('update', validated_data, instance)
+ return instance
diff --git a/apps/tickets/serializers/super_ticket.py b/apps/tickets/serializers/super_ticket.py
index 9d84728e4..9200c3f28 100644
--- a/apps/tickets/serializers/super_ticket.py
+++ b/apps/tickets/serializers/super_ticket.py
@@ -15,7 +15,7 @@ class SuperTicketSerializer(serializers.ModelSerializer):
fields = ['id', 'status', 'state', 'processor']
@staticmethod
- def get_processor(ticket):
- if not ticket.processor:
+ def get_processor(instance):
+ if not instance.processor:
return ''
- return str(ticket.processor)
+ return str(instance.processor)
diff --git a/apps/tickets/serializers/ticket/__init__.py b/apps/tickets/serializers/ticket/__init__.py
index bb2ee74d6..698906d3a 100644
--- a/apps/tickets/serializers/ticket/__init__.py
+++ b/apps/tickets/serializers/ticket/__init__.py
@@ -1,2 +1,7 @@
from .ticket import *
-from .meta import *
+from .apply_asset import *
+from .apply_application import *
+from .login_confirm import *
+from .login_asset_confirm import *
+from .command_confirm import *
+from .common import *
diff --git a/apps/tickets/serializers/ticket/apply_application.py b/apps/tickets/serializers/ticket/apply_application.py
new file mode 100644
index 000000000..b2774e420
--- /dev/null
+++ b/apps/tickets/serializers/ticket/apply_application.py
@@ -0,0 +1,57 @@
+from rest_framework import serializers
+
+from perms.models import ApplicationPermission
+from orgs.utils import tmp_to_org
+from applications.models import Application
+from tickets.models import ApplyApplicationTicket
+from .ticket import TicketApplySerializer
+from .common import BaseApplyAssetApplicationSerializer
+
+__all__ = ['ApplyApplicationSerializer', 'ApplyApplicationDisplaySerializer']
+
+
+class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer):
+ permission_model = ApplicationPermission
+
+ class Meta:
+ model = ApplyApplicationTicket
+ writeable_fields = [
+ 'id', 'title', 'type', 'apply_category',
+ 'apply_type', 'apply_applications', 'apply_system_users',
+ 'apply_date_start', 'apply_date_expired', 'org_id'
+ ]
+ fields = TicketApplySerializer.Meta.fields + writeable_fields + ['apply_permission_name']
+ read_only_fields = list(set(fields) - set(writeable_fields))
+ ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs
+ extra_kwargs = {}
+ extra_kwargs.update(ticket_extra_kwargs)
+
+ def validate_apply_applications(self, apply_applications):
+ type = self.initial_data.get('apply_type')
+ org_id = self.initial_data.get('org_id')
+ application_ids = [app.id for app in apply_applications]
+ with tmp_to_org(org_id):
+ applications = Application.objects.filter(
+ id__in=application_ids, type=type
+ ).values_list('id', flat=True)
+ return list(applications)
+
+
+class ApplyApplicationDisplaySerializer(ApplyApplicationSerializer):
+ apply_applications = serializers.SerializerMethodField()
+ apply_system_users = serializers.SerializerMethodField()
+
+ class Meta:
+ model = ApplyApplicationSerializer.Meta.model
+ fields = ApplyApplicationSerializer.Meta.fields
+ read_only_fields = fields
+
+ @staticmethod
+ def get_apply_applications(instance):
+ with tmp_to_org(instance.org_id):
+ return instance.apply_applications.values_list('id', flat=True)
+
+ @staticmethod
+ def get_apply_system_users(instance):
+ with tmp_to_org(instance.org_id):
+ return instance.apply_system_users.values_list('id', flat=True)
diff --git a/apps/tickets/serializers/ticket/apply_asset.py b/apps/tickets/serializers/ticket/apply_asset.py
new file mode 100644
index 000000000..3c8e0aade
--- /dev/null
+++ b/apps/tickets/serializers/ticket/apply_asset.py
@@ -0,0 +1,70 @@
+from django.utils.translation import ugettext_lazy as _
+from rest_framework import serializers
+
+from perms.serializers.base import ActionsField
+from perms.models import AssetPermission
+from orgs.utils import tmp_to_org
+
+from tickets.models import ApplyAssetTicket
+from .ticket import TicketApplySerializer
+from .common import BaseApplyAssetApplicationSerializer
+
+__all__ = ['ApplyAssetSerializer', 'ApplyAssetDisplaySerializer']
+
+asset_or_node_help_text = _("Select at least one asset or node")
+
+
+class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer):
+ apply_actions = ActionsField(required=True, allow_null=True)
+ permission_model = AssetPermission
+
+ class Meta:
+ model = ApplyAssetTicket
+ writeable_fields = [
+ 'id', 'title', 'type', 'apply_nodes', 'apply_assets',
+ 'apply_system_users', 'apply_actions', 'apply_actions_display',
+ 'apply_date_start', 'apply_date_expired', 'org_id'
+ ]
+ fields = TicketApplySerializer.Meta.fields + writeable_fields + ['apply_permission_name']
+ 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}
+ }
+ extra_kwargs.update(ticket_extra_kwargs)
+
+ def validate(self, attrs):
+ attrs = super().validate(attrs)
+ if 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 ApplyAssetDisplaySerializer(ApplyAssetSerializer):
+ apply_nodes = serializers.SerializerMethodField()
+ apply_assets = serializers.SerializerMethodField()
+ apply_system_users = serializers.SerializerMethodField()
+
+ class Meta:
+ model = ApplyAssetSerializer.Meta.model
+ fields = ApplyAssetSerializer.Meta.fields
+ read_only_fields = fields
+
+ @staticmethod
+ def get_apply_nodes(instance):
+ with tmp_to_org(instance.org_id):
+ return instance.apply_nodes.values_list('id', flat=True)
+
+ @staticmethod
+ def get_apply_assets(instance):
+ with tmp_to_org(instance.org_id):
+ return instance.apply_assets.values_list('id', flat=True)
+
+ @staticmethod
+ def get_apply_system_users(instance):
+ with tmp_to_org(instance.org_id):
+ return instance.apply_system_users.values_list('id', flat=True)
diff --git a/apps/tickets/serializers/ticket/command_confirm.py b/apps/tickets/serializers/ticket/command_confirm.py
new file mode 100644
index 000000000..c52bfd153
--- /dev/null
+++ b/apps/tickets/serializers/ticket/command_confirm.py
@@ -0,0 +1,16 @@
+from tickets.models import ApplyCommandTicket
+from .ticket import TicketApplySerializer
+
+__all__ = [
+ 'ApplyCommandConfirmSerializer',
+]
+
+
+class ApplyCommandConfirmSerializer(TicketApplySerializer):
+ class Meta:
+ model = ApplyCommandTicket
+ fields = TicketApplySerializer.Meta.fields + [
+ 'apply_run_user', 'apply_run_asset', 'apply_run_system_user',
+ 'apply_run_command', 'apply_from_session', 'apply_from_cmd_filter',
+ 'apply_from_cmd_filter_rule'
+ ]
diff --git a/apps/tickets/serializers/ticket/common.py b/apps/tickets/serializers/ticket/common.py
new file mode 100644
index 000000000..ef52c14d3
--- /dev/null
+++ b/apps/tickets/serializers/ticket/common.py
@@ -0,0 +1,63 @@
+from django.db.transaction import atomic
+from django.db.models import Model
+from django.utils.translation import ugettext as _
+from rest_framework import serializers
+
+from orgs.utils import tmp_to_org
+from tickets.models import Ticket
+
+__all__ = ['DefaultPermissionName', 'get_default_permission_name', 'BaseApplyAssetApplicationSerializer']
+
+
+def get_default_permission_name(ticket):
+ name = ''
+ if isinstance(ticket, Ticket):
+ name = _('Created by ticket ({}-{})').format(ticket.title, str(ticket.id)[:4])
+ return name
+
+
+class DefaultPermissionName(object):
+ default = None
+
+ @staticmethod
+ def _construct_default_permission_name(serializer_field):
+ permission_name = ''
+ ticket = serializer_field.root.instance
+ if isinstance(ticket, Ticket):
+ permission_name = get_default_permission_name(ticket)
+ return permission_name
+
+ def set_context(self, serializer_field):
+ self.default = self._construct_default_permission_name(serializer_field)
+
+ def __call__(self):
+ return self.default
+
+
+class BaseApplyAssetApplicationSerializer(serializers.Serializer):
+ permission_model: Model
+
+ def validate(self, attrs):
+ attrs = super().validate(attrs)
+
+ apply_date_start = attrs['apply_date_start'].strftime('%Y-%m-%d %H:%M:%S')
+ apply_date_expired = attrs['apply_date_expired'].strftime('%Y-%m-%d %H:%M:%S')
+
+ if apply_date_expired <= apply_date_start:
+ error = _('The expiration date should be greater than the start date')
+ raise serializers.ValidationError({'apply_date_expired': error})
+
+ attrs['apply_date_start'] = apply_date_start
+ attrs['apply_date_expired'] = apply_date_expired
+ return attrs
+
+ @atomic
+ def create(self, validated_data):
+ instance = super().create(validated_data)
+ name = _('Created by ticket ({}-{})').format(instance.title, str(instance.id)[:4])
+ with tmp_to_org(instance.org_id):
+ if not self.permission_model.objects.filter(name=name).exists():
+ instance.apply_permission_name = name
+ instance.save()
+ return instance
+ raise serializers.ValidationError(_('Permission named `{}` already exists'.format(name)))
diff --git a/apps/tickets/serializers/ticket/login_asset_confirm.py b/apps/tickets/serializers/ticket/login_asset_confirm.py
new file mode 100644
index 000000000..9e3076f0f
--- /dev/null
+++ b/apps/tickets/serializers/ticket/login_asset_confirm.py
@@ -0,0 +1,14 @@
+from tickets.models import ApplyLoginAssetTicket
+from .ticket import TicketApplySerializer
+
+__all__ = [
+ 'LoginAssetConfirmSerializer'
+]
+
+
+class LoginAssetConfirmSerializer(TicketApplySerializer):
+ class Meta:
+ model = ApplyLoginAssetTicket
+ fields = TicketApplySerializer.Meta.fields + [
+ 'apply_login_user', 'apply_login_asset', 'apply_login_system_user'
+ ]
diff --git a/apps/tickets/serializers/ticket/login_confirm.py b/apps/tickets/serializers/ticket/login_confirm.py
new file mode 100644
index 000000000..e760c653f
--- /dev/null
+++ b/apps/tickets/serializers/ticket/login_confirm.py
@@ -0,0 +1,14 @@
+from tickets.models import ApplyLoginTicket
+from .ticket import TicketApplySerializer
+
+__all__ = [
+ 'LoginConfirmSerializer'
+]
+
+
+class LoginConfirmSerializer(TicketApplySerializer):
+ class Meta:
+ model = ApplyLoginTicket
+ fields = TicketApplySerializer.Meta.fields + [
+ 'apply_login_ip', 'apply_login_city', 'apply_login_datetime'
+ ]
diff --git a/apps/tickets/serializers/ticket/meta/__init__.py b/apps/tickets/serializers/ticket/meta/__init__.py
deleted file mode 100644
index 7b5fbad28..000000000
--- a/apps/tickets/serializers/ticket/meta/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from .meta import *
diff --git a/apps/tickets/serializers/ticket/meta/meta.py b/apps/tickets/serializers/ticket/meta/meta.py
deleted file mode 100644
index 936977dfc..000000000
--- a/apps/tickets/serializers/ticket/meta/meta.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from tickets import const
-from .ticket_type import (
- apply_asset, apply_application, login_confirm,
- login_asset_confirm, command_confirm
-)
-
-__all__ = [
- 'type_serializer_classes_mapping',
-]
-
-# ticket action
-# -------------
-
-action_open = const.TicketAction.open.value
-action_approve = const.TicketAction.approve.value
-
-
-# defines `meta` field dynamic mapping serializers
-# ------------------------------------------------
-
-type_serializer_classes_mapping = {
- const.TicketType.apply_asset.value: {
- 'default': apply_asset.ApplySerializer
- },
- const.TicketType.apply_application.value: {
- 'default': apply_application.ApplySerializer
- },
- const.TicketType.login_confirm.value: {
- 'default': login_confirm.LoginConfirmSerializer,
- action_open: login_confirm.ApplySerializer,
- action_approve: login_confirm.LoginConfirmSerializer(read_only=True),
- },
- const.TicketType.login_asset_confirm.value: {
- 'default': login_asset_confirm.LoginAssetConfirmSerializer,
- action_open: login_asset_confirm.ApplySerializer,
- action_approve: login_asset_confirm.LoginAssetConfirmSerializer(read_only=True),
- },
- const.TicketType.command_confirm.value: {
- 'default': command_confirm.CommandConfirmSerializer,
- action_open: command_confirm.ApplySerializer,
- action_approve: command_confirm.CommandConfirmSerializer(read_only=True)
- }
-}
diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/__init__.py b/apps/tickets/serializers/ticket/meta/ticket_type/__init__.py
deleted file mode 100644
index e69de29bb..000000000
diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py b/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py
deleted file mode 100644
index a7f60ae82..000000000
--- a/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py
+++ /dev/null
@@ -1,93 +0,0 @@
-from django.utils.translation import ugettext_lazy as _
-from rest_framework import serializers
-
-from perms.models import ApplicationPermission
-from applications.const import AppCategory, AppType
-from orgs.utils import tmp_to_org
-from tickets.models import Ticket
-from applications.models import Application
-from .common import DefaultPermissionName
-
-__all__ = [
- 'ApplySerializer',
-]
-
-
-class ApplySerializer(serializers.Serializer):
- apply_permission_name = serializers.CharField(
- max_length=128, default=DefaultPermissionName(), label=_('Apply name')
- )
- # 申请信息
- apply_category = serializers.ChoiceField(
- required=True, choices=AppCategory.choices, label=_('Category'),
- allow_null=True,
- )
- apply_category_display = serializers.CharField(
- read_only=True, label=_('Category display'), allow_null=True,
- )
- apply_type = serializers.ChoiceField(
- required=True, choices=AppType.choices, label=_('Type'),
- allow_null=True
- )
- apply_type_display = serializers.CharField(
- required=False, read_only=True, label=_('Type display'),
- allow_null=True
- )
- apply_applications = serializers.ListField(
- required=True, child=serializers.UUIDField(), label=_('Apply applications'),
- allow_null=True
- )
- apply_applications_display = serializers.ListField(
- required=False, read_only=True, child=serializers.CharField(),
- label=_('Apply applications display'), allow_null=True,
- default=list
- )
- apply_system_users = serializers.ListField(
- required=True, child=serializers.UUIDField(), label=_('Apply system users'),
- allow_null=True
- )
- apply_system_users_display = serializers.ListField(
- required=False, read_only=True, child=serializers.CharField(),
- label=_('Apply system user display'), allow_null=True,
- default=list
- )
- apply_date_start = serializers.DateTimeField(
- required=True, label=_('Date start'), allow_null=True
- )
- apply_date_expired = serializers.DateTimeField(
- required=True, label=_('Date expired'), allow_null=True
- )
-
- def validate_approve_permission_name(self, permission_name):
- if not isinstance(self.root.instance, Ticket):
- return permission_name
-
- with tmp_to_org(self.root.instance.org_id):
- already_exists = ApplicationPermission.objects.filter(name=permission_name).exists()
- if not already_exists:
- return permission_name
-
- raise serializers.ValidationError(_(
- 'Permission named `{}` already exists'.format(permission_name)
- ))
-
- def validate_apply_applications(self, apply_applications):
- type = self.root.initial_data['meta'].get('apply_type')
- org_id = self.root.initial_data.get('org_id')
- with tmp_to_org(org_id):
- applications = Application.objects.filter(
- id__in=apply_applications, type=type
- ).values_list('id', flat=True)
- return list(applications)
-
- def validate(self, attrs):
- apply_date_start = attrs['apply_date_start'].strftime('%Y-%m-%d %H:%M:%S')
- apply_date_expired = attrs['apply_date_expired'].strftime('%Y-%m-%d %H:%M:%S')
-
- if apply_date_expired <= apply_date_start:
- error = _('The expiration date should be greater than the start date')
- raise serializers.ValidationError({'apply_date_expired': error})
-
- attrs['apply_date_start'] = apply_date_start
- attrs['apply_date_expired'] = apply_date_expired
- return attrs
diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py b/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py
deleted file mode 100644
index ddd77d769..000000000
--- a/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py
+++ /dev/null
@@ -1,92 +0,0 @@
-from django.utils.translation import ugettext_lazy as _
-from rest_framework import serializers
-
-from perms.serializers.base import ActionsField
-from perms.models import AssetPermission
-from orgs.utils import tmp_to_org
-from tickets.models import Ticket
-from .common import DefaultPermissionName
-
-__all__ = [
- 'ApplySerializer',
-]
-
-asset_or_node_help_text = _("Select at least one asset or node")
-
-
-class ApplySerializer(serializers.Serializer):
- apply_permission_name = serializers.CharField(
- max_length=128, default=DefaultPermissionName(), label=_('Apply name')
- )
- apply_nodes = serializers.ListField(
- required=False, allow_null=True, child=serializers.UUIDField(),
- label=_('Apply nodes'), help_text=asset_or_node_help_text,
- default=list
- )
- apply_nodes_display = serializers.ListField(
- child=serializers.CharField(), label=_('Apply nodes display'), required=False
- )
- # 申请信息
- apply_assets = serializers.ListField(
- required=False, allow_null=True, child=serializers.UUIDField(),
- label=_('Apply assets'), help_text=asset_or_node_help_text
- )
- apply_assets_display = serializers.ListField(
- required=False, read_only=True, child=serializers.CharField(),
- label=_('Apply assets display'), allow_null=True,
- default=list
- )
- apply_system_users = serializers.ListField(
- required=True, allow_null=True, child=serializers.UUIDField(),
- label=_('Apply system users')
- )
- apply_system_users_display = serializers.ListField(
- required=False, read_only=True, child=serializers.CharField(),
- label=_('Apply assets display'), allow_null=True,
- default=list,
- )
- apply_actions = ActionsField(
- required=True, allow_null=True
- )
- apply_actions_display = serializers.ListField(
- required=False, read_only=True, child=serializers.CharField(),
- label=_('Apply assets display'), allow_null=True,
- default=list,
- )
- apply_date_start = serializers.DateTimeField(
- required=True, label=_('Date start'), allow_null=True,
- )
- apply_date_expired = serializers.DateTimeField(
- required=True, label=_('Date expired'), allow_null=True,
- )
-
- def validate_approve_permission_name(self, permission_name):
- if not isinstance(self.root.instance, Ticket):
- return permission_name
-
- with tmp_to_org(self.root.instance.org_id):
- already_exists = AssetPermission.objects.filter(name=permission_name).exists()
- if not already_exists:
- return permission_name
-
- raise serializers.ValidationError(_(
- 'Permission named `{}` already exists'.format(permission_name)
- ))
-
- def validate(self, attrs):
- if 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,
- })
-
- apply_date_start = attrs['apply_date_start'].strftime('%Y-%m-%d %H:%M:%S')
- apply_date_expired = attrs['apply_date_expired'].strftime('%Y-%m-%d %H:%M:%S')
-
- if apply_date_expired <= apply_date_start:
- error = _('The expiration date should be greater than the start date')
- raise serializers.ValidationError({'apply_date_expired': error})
-
- attrs['apply_date_start'] = apply_date_start
- attrs['apply_date_expired'] = apply_date_expired
- return attrs
diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py b/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py
deleted file mode 100644
index eb631fe98..000000000
--- a/apps/tickets/serializers/ticket/meta/ticket_type/command_confirm.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from rest_framework import serializers
-from django.utils.translation import ugettext_lazy as _
-
-
-__all__ = [
- 'ApplySerializer', 'CommandConfirmSerializer',
-]
-
-
-class ApplySerializer(serializers.Serializer):
- # 申请信息
- apply_run_user = serializers.CharField(required=True, label=_('Run user'))
- apply_run_asset = serializers.CharField(required=True, label=_('Run asset'))
- apply_run_system_user = serializers.CharField(
- required=True, max_length=64, label=_('Run system user')
- )
- apply_run_command = serializers.CharField(required=True, label=_('Run command'))
- apply_from_session_id = serializers.UUIDField(required=False, label=_('From session'))
- apply_from_cmd_filter_rule_id = serializers.UUIDField(
- required=False, label=_('From cmd filter rule')
- )
- apply_from_cmd_filter_id = serializers.UUIDField(required=False, label=_('From cmd filter'))
-
-
-class CommandConfirmSerializer(ApplySerializer):
- pass
diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/common.py b/apps/tickets/serializers/ticket/meta/ticket_type/common.py
deleted file mode 100644
index 2d43f6a81..000000000
--- a/apps/tickets/serializers/ticket/meta/ticket_type/common.py
+++ /dev/null
@@ -1,30 +0,0 @@
-from django.utils.translation import ugettext as _
-from tickets.models import Ticket
-
-
-__all__ = ['DefaultPermissionName', 'get_default_permission_name']
-
-
-def get_default_permission_name(ticket):
- name = ''
- if isinstance(ticket, Ticket):
- name = _('Created by ticket ({}-{})').format(ticket.title, str(ticket.id)[:4])
- return name
-
-
-class DefaultPermissionName(object):
- default = None
-
- @staticmethod
- def _construct_default_permission_name(serializer_field):
- permission_name = ''
- ticket = serializer_field.root.instance
- if isinstance(ticket, Ticket):
- permission_name = get_default_permission_name(ticket)
- return permission_name
-
- def set_context(self, serializer_field):
- self.default = self._construct_default_permission_name(serializer_field)
-
- def __call__(self):
- return self.default
diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/login_asset_confirm.py b/apps/tickets/serializers/ticket/meta/ticket_type/login_asset_confirm.py
deleted file mode 100644
index 2570e4792..000000000
--- a/apps/tickets/serializers/ticket/meta/ticket_type/login_asset_confirm.py
+++ /dev/null
@@ -1,21 +0,0 @@
-
-from rest_framework import serializers
-from django.utils.translation import ugettext_lazy as _
-
-
-__all__ = [
- 'ApplySerializer', 'LoginAssetConfirmSerializer',
-]
-
-
-class ApplySerializer(serializers.Serializer):
- # 申请信息
- apply_login_user = serializers.CharField(required=True, label=_('Login user'))
- apply_login_asset = serializers.CharField(required=True, label=_('Login asset'))
- apply_login_system_user = serializers.CharField(
- required=True, max_length=64, label=_('Login system user')
- )
-
-
-class LoginAssetConfirmSerializer(ApplySerializer):
- pass
diff --git a/apps/tickets/serializers/ticket/meta/ticket_type/login_confirm.py b/apps/tickets/serializers/ticket/meta/ticket_type/login_confirm.py
deleted file mode 100644
index 9308d0ee2..000000000
--- a/apps/tickets/serializers/ticket/meta/ticket_type/login_confirm.py
+++ /dev/null
@@ -1,25 +0,0 @@
-
-from rest_framework import serializers
-from django.utils.translation import ugettext_lazy as _
-
-
-__all__ = [
- 'ApplySerializer', 'LoginConfirmSerializer',
-]
-
-
-class ApplySerializer(serializers.Serializer):
- # 申请信息
- apply_login_ip = serializers.IPAddressField(
- required=True, label=_('Login ip'), allow_null=True
- )
- apply_login_city = serializers.CharField(
- required=True, max_length=64, label=_('Login city'), allow_null=True
- )
- apply_login_datetime = serializers.DateTimeField(
- required=True, label=_('Login datetime'), allow_null=True
- )
-
-
-class LoginConfirmSerializer(ApplySerializer):
- pass
diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py
index aec89cfae..5b71cb783 100644
--- a/apps/tickets/serializers/ticket/ticket.py
+++ b/apps/tickets/serializers/ticket/ticket.py
@@ -1,68 +1,42 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
-from django.db.transaction import atomic
from rest_framework import serializers
-from common.drf.serializers import MethodSerializer
-from orgs.mixins.serializers import OrgResourceModelSerializerMixin
-from perms.models import AssetPermission
+
from orgs.models import Organization
-from orgs.utils import tmp_to_org
-from tickets.models import Ticket, TicketFlow, ApprovalRule
-from tickets.const import TicketApprovalStrategy
-from .meta import type_serializer_classes_mapping
+from orgs.mixins.serializers import OrgResourceModelSerializerMixin
+from tickets.models import Ticket, TicketFlow
__all__ = [
- 'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketApproveSerializer', 'TicketFlowSerializer'
+ 'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketListSerializer'
]
class TicketSerializer(OrgResourceModelSerializerMixin):
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status display'))
- meta = MethodSerializer()
class Meta:
model = Ticket
fields_mini = ['id', 'title']
fields_small = fields_mini + [
- 'type', 'type_display', 'meta', 'state', 'approval_step',
- 'status', 'status_display', 'applicant_display', 'process_map',
- 'date_created', 'date_updated', 'comment', 'org_id', 'org_name', 'body',
- 'serial_num',
+ 'type', 'type_display', 'status', 'status_display',
+ 'state', 'approval_step', 'rel_snapshot', 'comment',
+ 'date_created', 'date_updated', 'org_id', 'rel_snapshot',
+ 'process_map', 'org_name', 'serial_num'
]
fields_fk = ['applicant', ]
fields = fields_small + fields_fk
- def get_meta_serializer(self):
- default_serializer = serializers.Serializer(read_only=True)
- if isinstance(self.instance, Ticket):
- _type = self.instance.type
- else:
- _type = self.context['request'].query_params.get('type')
- if _type:
- action_serializer_classes_mapping = type_serializer_classes_mapping.get(_type)
- if action_serializer_classes_mapping:
- query_action = self.context['request'].query_params.get('action')
- action = query_action if query_action else self.context['view'].action
- serializer_class = action_serializer_classes_mapping.get(action)
- if not serializer_class:
- serializer_class = action_serializer_classes_mapping.get('default')
- else:
- serializer_class = default_serializer
- else:
- serializer_class = default_serializer
-
- if not serializer_class:
- serializer_class = default_serializer
-
- if isinstance(serializer_class, type):
- serializer = serializer_class()
- else:
- serializer = serializer_class
-
- return serializer
+class TicketListSerializer(TicketSerializer):
+ class Meta:
+ model = Ticket
+ fields = [
+ 'id', 'title', 'serial_num', 'type', 'type_display', 'status',
+ 'state', 'rel_snapshot', 'date_created', 'rel_snapshot'
+ ]
+ read_only_fields = fields
class TicketDisplaySerializer(TicketSerializer):
@@ -80,24 +54,10 @@ class TicketApplySerializer(TicketSerializer):
class Meta:
model = Ticket
fields = TicketSerializer.Meta.fields
- writeable_fields = [
- 'id', 'title', 'type', 'meta', 'comment', 'org_id'
- ]
- read_only_fields = list(set(fields) - set(writeable_fields))
extra_kwargs = {
- 'type': {'required': True},
+ 'type': {'required': True}
}
- def validate_type(self, tp):
- request_type = self.context['request'].query_params.get('type')
- if tp != request_type:
- error = _(
- 'The `type` in the submission data (`{}`) is different from the type '
- 'in the request url (`{}`)'.format(tp, request_type)
- )
- raise serializers.ValidationError(error)
- return tp
-
@staticmethod
def validate_org_id(org_id):
org = Organization.get_instance(org_id)
@@ -116,113 +76,3 @@ class TicketApplySerializer(TicketSerializer):
error = _('The ticket flow `{}` does not exist'.format(ticket_type))
raise serializers.ValidationError(error)
return attrs
-
- @atomic
- def create(self, validated_data):
- instance = super().create(validated_data)
- name = _('Created by ticket ({}-{})').format(instance.title, str(instance.id)[:4])
- with tmp_to_org(instance.org_id):
- if not AssetPermission.objects.filter(name=name).exists():
- instance.meta.update({'apply_permission_name': name})
- return instance
- raise serializers.ValidationError(_('Permission named `{}` already exists'.format(name)))
-
-
-class TicketApproveSerializer(TicketSerializer):
- meta = serializers.ReadOnlyField()
-
- class Meta:
- model = Ticket
- fields = TicketSerializer.Meta.fields
- read_only_fields = fields
-
-
-class TicketFlowApproveSerializer(serializers.ModelSerializer):
- strategy_display = serializers.ReadOnlyField(source='get_strategy_display', label=_('Approve strategy'))
- assignees_read_only = serializers.SerializerMethodField(label=_('Assignees'))
- assignees_display = serializers.SerializerMethodField(label=_('Assignees display'))
-
- class Meta:
- model = ApprovalRule
- fields_small = [
- 'level', 'strategy', 'assignees_read_only', 'assignees_display', 'strategy_display'
- ]
- fields_m2m = ['assignees', ]
- fields = fields_small + fields_m2m
- read_only_fields = ['level', 'assignees_display']
- extra_kwargs = {
- 'assignees': {'write_only': True, 'allow_empty': True, 'required': False}
- }
-
- def get_assignees_display(self, obj):
- return [str(assignee) for assignee in obj.get_assignees()]
-
- def get_assignees_read_only(self, obj):
- if obj.strategy == TicketApprovalStrategy.custom_user:
- return obj.assignees.values_list('id', flat=True)
- return []
-
- def validate(self, attrs):
- if attrs['strategy'] == TicketApprovalStrategy.custom_user and not attrs.get('assignees'):
- error = _('Please select the Assignees')
- raise serializers.ValidationError({'assignees': error})
- return super().validate(attrs)
-
-
-class TicketFlowSerializer(OrgResourceModelSerializerMixin):
- type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
- rules = TicketFlowApproveSerializer(many=True, required=True)
-
- class Meta:
- model = TicketFlow
- fields_mini = ['id', ]
- fields_small = fields_mini + [
- 'type', 'type_display', 'approval_level', 'created_by', 'date_created', 'date_updated',
- 'org_id', 'org_name'
- ]
- fields = fields_small + ['rules', ]
- read_only_fields = ['created_by', 'org_id', 'date_created', 'date_updated']
- extra_kwargs = {
- 'type': {'required': True},
- 'approval_level': {'required': True}
- }
-
- def validate_type(self, value):
- if not self.instance or (self.instance and self.instance.type != value):
- if self.Meta.model.objects.filter(type=value).exists():
- error = _('The current organization type already exists')
- raise serializers.ValidationError(error)
- return value
-
- def create_or_update(self, action, validated_data, instance=None):
- related = 'rules'
- assignees = 'assignees'
- childs = validated_data.pop(related, [])
- if not instance:
- instance = getattr(super(), action)(validated_data)
- else:
- instance = getattr(super(), action)(instance, validated_data)
- getattr(instance, related).all().delete()
- instance_related = getattr(instance, related)
- child_instances = []
- related_model = instance_related.model
- # Todo: 这个权限的判断
- for level, data in enumerate(childs, 1):
- data_m2m = data.pop(assignees, None)
- child_instance = related_model.objects.create(**data, level=level)
- getattr(child_instance, assignees).set(data_m2m)
- child_instances.append(child_instance)
- instance_related.set(child_instances)
- return instance
-
- @atomic
- def create(self, validated_data):
- return self.create_or_update('create', validated_data)
-
- @atomic
- def update(self, instance, validated_data):
- if instance.org_id == Organization.ROOT_ID:
- instance = self.create(validated_data)
- else:
- instance = self.create_or_update('update', validated_data, instance)
- return instance
diff --git a/apps/tickets/signal_handlers/ticket.py b/apps/tickets/signal_handlers/ticket.py
index 1a343b18f..8d9a9ef32 100644
--- a/apps/tickets/signal_handlers/ticket.py
+++ b/apps/tickets/signal_handlers/ticket.py
@@ -1,19 +1,28 @@
# -*- coding: utf-8 -*-
#
from django.dispatch import receiver
-from django.db.models.signals import post_save
+from django.db.models.signals import post_save, m2m_changed
+
+from common.decorator import on_transaction_commit
from common.utils import get_logger
from tickets.models import Ticket
-from ..signals import post_change_ticket_action
logger = get_logger(__name__)
-@receiver(post_change_ticket_action, sender=Ticket)
-def on_post_change_ticket_action(sender, ticket, action, **kwargs):
- ticket.handler.dispatch(action)
+@on_transaction_commit
+def after_save_set_rel_snapshot(sender, instance, update_fields=None, **kwargs):
+ if update_fields and list(update_fields)[0] == 'rel_snapshot':
+ return
+ instance.set_rel_snapshot()
-@receiver(post_save, sender=Ticket)
-def on_pre_save_ensure_serial_num(sender, instance: Ticket, **kwargs):
- instance.update_serial_num_if_need()
+@on_transaction_commit
+def on_m2m_change(sender, action, instance, reverse=False, **kwargs):
+ if action.startswith('post'):
+ instance.set_rel_snapshot()
+
+
+for ticket_cls in Ticket.__subclasses__():
+ post_save.connect(after_save_set_rel_snapshot, sender=ticket_cls)
+ m2m_changed.connect(on_m2m_change, sender=ticket_cls)
diff --git a/apps/tickets/signals.py b/apps/tickets/signals.py
index b626cfa35..6fa73d8d5 100644
--- a/apps/tickets/signals.py
+++ b/apps/tickets/signals.py
@@ -1,5 +1,4 @@
from django.dispatch import Signal
-post_change_ticket_action = Signal()
-
+active_step = Signal()
post_or_update_change_ticket_flow_approval = Signal()
diff --git a/apps/tickets/templates/tickets/_base_ticket_body.html b/apps/tickets/templates/tickets/_base_ticket_body.html
deleted file mode 100644
index 593a1d35e..000000000
--- a/apps/tickets/templates/tickets/_base_ticket_body.html
+++ /dev/null
@@ -1,20 +0,0 @@
-{% load i18n %}
-
-
- {{ ticket_info }}
-
-
-
-
-
-
-
-
- {{ body | safe }}
-
-
-
\ No newline at end of file
diff --git a/apps/tickets/templates/tickets/_msg_ticket.html b/apps/tickets/templates/tickets/_msg_ticket.html
index 5f268592c..75d205080 100644
--- a/apps/tickets/templates/tickets/_msg_ticket.html
+++ b/apps/tickets/templates/tickets/_msg_ticket.html
@@ -4,7 +4,15 @@
{{ title | safe }}
- {{ body | safe }}
+ {% for child in content %}
+
{{ child.title }}
+
+ {% for item in child.content %}
+
+ {{ item.title }}: {{ item.value }}
+
+ {% endfor %}
+ {% endfor %}
diff --git a/apps/tickets/templates/tickets/approve_check_password.html b/apps/tickets/templates/tickets/approve_check_password.html
index d75e9f956..78eadbf6f 100644
--- a/apps/tickets/templates/tickets/approve_check_password.html
+++ b/apps/tickets/templates/tickets/approve_check_password.html
@@ -10,7 +10,17 @@
{% trans 'Ticket information' %}
-
{{ ticket_info | safe }}
+
+ {% for child in content %}
+
{{ child.title }}
+
+ {% for item in child.content %}
+
+ {{ item.title }}: {{ item.value }}
+
+ {% endfor %}
+ {% endfor %}
+
diff --git a/apps/tickets/urls/api_urls.py b/apps/tickets/urls/api_urls.py
index bc6bb7f54..22715c527 100644
--- a/apps/tickets/urls/api_urls.py
+++ b/apps/tickets/urls/api_urls.py
@@ -10,6 +10,11 @@ app_name = 'tickets'
router = BulkRouter()
router.register('tickets', api.TicketViewSet, 'ticket')
+router.register('apply-asset-tickets', api.ApplyAssetTicketViewSet, 'apply-asset-ticket')
+router.register('apply-app-tickets', api.ApplyApplicationTicketViewSet, 'apply-app-ticket')
+router.register('apply-login-tickets', api.ApplyLoginTicketViewSet, 'apply-login-ticket')
+router.register('apply-login-asset-tickets', api.ApplyLoginAssetTicketViewSet, 'apply-login-asset-ticket')
+router.register('apply-command-tickets', api.ApplyCommandTicketViewSet, 'apply-command-ticket')
router.register('flows', api.TicketFlowViewSet, 'flows')
router.register('comments', api.CommentViewSet, 'comment')
router.register('ticket-session-relation', api.TicketSessionRelationViewSet, 'ticket-session-relation')
diff --git a/apps/tickets/utils.py b/apps/tickets/utils.py
index 66edb3828..231881846 100644
--- a/apps/tickets/utils.py
+++ b/apps/tickets/utils.py
@@ -3,21 +3,22 @@
from django.conf import settings
from common.utils import get_logger
-from .notifications import TicketAppliedToAssignee, TicketProcessedToApplicant
+from .notifications import TicketAppliedToAssigneeMessage, TicketProcessedToApplicantMessage
logger = get_logger(__file__)
-def send_ticket_applied_mail_to_assignees(ticket):
- ticket_assignees = ticket.current_node.first().ticket_assignees.all()
- if not ticket_assignees:
+def send_ticket_applied_mail_to_assignees(ticket, assignees):
+ if not assignees:
logger.debug(
- "Not found assignees, ticket: {}({}), assignees: {}".format(ticket, str(ticket.id), ticket_assignees)
+ "Not found assignees, ticket: {}({}), assignees: {}".format(
+ ticket, str(ticket.id), assignees
+ )
)
return
- for ticket_assignee in ticket_assignees:
- instance = TicketAppliedToAssignee(ticket_assignee.assignee, ticket)
+ for user in assignees:
+ instance = TicketAppliedToAssigneeMessage(user, ticket)
if settings.DEBUG:
logger.debug(instance)
instance.publish_async()
@@ -28,7 +29,7 @@ def send_ticket_processed_mail_to_applicant(ticket, processor):
logger.error("Not found applicant: {}({})".format(ticket.title, ticket.id))
return
- instance = TicketProcessedToApplicant(ticket.applicant, ticket, processor)
+ instance = TicketProcessedToApplicantMessage(ticket.applicant, ticket, processor)
if settings.DEBUG:
logger.debug(instance)
instance.publish_async()
diff --git a/apps/tickets/views/approve.py b/apps/tickets/views/approve.py
index 6632dc813..5be230f0a 100644
--- a/apps/tickets/views/approve.py
+++ b/apps/tickets/views/approve.py
@@ -50,13 +50,13 @@ class TicketDirectApproveView(TemplateView):
def get_context_data(self, **kwargs):
# 放入工单信息
token = kwargs.get('token')
- ticket_info = cache.get(token, {}).get('body', '')
+ content = cache.get(token, {}).get('content', [])
if self.request.user.is_authenticated:
prompt_msg = _('Click the button below to approve or reject')
else:
prompt_msg = _('After successful authentication, this ticket can be approved directly')
kwargs.update({
- 'ticket_info': ticket_info, 'prompt_msg': prompt_msg,
+ 'content': content, 'prompt_msg': prompt_msg,
'login_url': '%s&next=%s' % (
self.login_url,
reverse('tickets:direct-approve', kwargs={'token': token})