perf: 重构工单处理流程 (#5408)

* perf: 重构工单处理流程

* perf: 重构工单处理流程 (1)

* perf: 重构工单处理流程 (2)

* perf: 重构工单处理流程 (3)

* perf: 重构工单处理流程 (4)

* perf: 重构工单处理流程 (5)

* perf: 重构工单处理流程 (6)

Co-authored-by: Bai <bugatti_it@163.com>
pull/5415/head
fit2bot 2021-01-13 17:49:03 +08:00 committed by GitHub
parent 528e251f31
commit a7468a243d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 411 additions and 619 deletions

View File

@ -207,12 +207,13 @@ class SystemUser(BaseUser):
@classmethod @classmethod
def get_protocol_by_application_type(cls, app_type): def get_protocol_by_application_type(cls, app_type):
from applications.const import ApplicationTypeChoices from applications.const import ApplicationTypeChoices
if app_type in ApplicationTypeChoices.remote_app_types(): if app_type in cls.APPLICATION_CATEGORY_PROTOCOLS:
protocol = app_type
elif app_type in ApplicationTypeChoices.remote_app_types():
protocol = cls.PROTOCOL_RDP protocol = cls.PROTOCOL_RDP
else: else:
protocol = app_type protocol = None
if protocol in cls.APPLICATION_CATEGORY_PROTOCOLS: return protocol
return protocol
class Meta: class Meta:
ordering = ['name'] ordering = ['name']

View File

@ -254,152 +254,3 @@ def get_disk_usage():
mount_points = [p.mountpoint for p in partitions] mount_points = [p.mountpoint for p in partitions]
usages = {p: psutil.disk_usage(p) for p in mount_points} usages = {p: psutil.disk_usage(p) for p in mount_points}
return usages return usages
# Verify that `value` is in `choices` and throw an `JMSException`
# ---------------------------------------------------------------
def check_value_in_choices(value, choices, **kwargs):
# get raise parameters from kwargs
raise_exception = kwargs.get('raise_exception', False)
raise_error_msg = kwargs.get('raise_error_msg', None)
raise_reverse = kwargs.get('raise_reverse', False)
def should_raise():
"""
Simplify the following logic:
if raise_exception:
if raise_reverse and value_in_choices:
return True
else:
return False
if not raise_reverse and not value_in_choices:
return True
else:
return False
else:
return False
"""
return raise_exception and raise_reverse == value_in_choices
value_in_choices = True if value in choices else False
if not should_raise():
return value_in_choices
if raise_error_msg is None:
raise_error_msg = _('Value `{}` is not in Choices: `{}`'.format(value, choices))
raise JMSException(raise_error_msg)
# Quick lookup dict
# -----------------
class QuickLookupDict(object):
"""
说明:
dict 类型数据的快速查找
作用:
可根据指定 key 的深度 path 快速查找出对应的 value
依赖:
data-tree==0.0.1
实现:
通过对 data-tree 库的封装来实现
"""
def __init__(self, data, key_delimiter='.'):
self._check_data_type(data, type_choices=(dict, ), error='Expected `data` type is dict')
self.data = data
self.key_delimiter = key_delimiter
self._data_tree = self._get_data_tree(data, key_delimiter)
# Method encapsulated of `data-tree`
# ----------------------------------
@staticmethod
def _get_data_tree(data, key_delimiter):
tree = data_tree.Data_tree_node(
arg_data=data, arg_string_delimiter_for_path=key_delimiter
)
return tree
def _get_data_tree_node(self, path):
return self._data_tree.get(arg_path=path)
@staticmethod
def _get_data_tree_node_original_data(tree_node):
if isinstance(tree_node, data_tree.Data_tree_node):
data = tree_node.get_data_in_format_for_export()
else:
data = tree_node
return data
# Method called internally
# ------------------------
@staticmethod
def _check_data_type(data, type_choices, error=None):
error = error or '`data` type error, {} => {}'.format(type(data), type_choices)
assert isinstance(data, type_choices), error
@staticmethod
def _check_object_callable(_object):
if _object is None:
return False
if not callable(_object):
return False
return True
# Method called externally
# ------------------------
def get(self, key_path, default=None):
error = 'key_path - can be either a list of keys, or a delimited string.'
self._check_data_type(key_path, (list, str,), error=error)
tree_node = self._get_data_tree_node(key_path)
if tree_node is None:
return default
value = self._get_data_tree_node_original_data(tree_node)
return value
def get_many(self, key_paths, default=None):
values = [
self.get(key_path, default=default) for key_path in key_paths
]
return values
def find_one(self, key_paths, default=None, callable_filter=None):
"""
按照 key_paths 顺序查找返回第一个满足 `callable_filter` 规则的值
"""
def get_data_filter():
if self._check_object_callable(callable_filter):
return callable_filter
return self.__default_find_callable_filter
_filter = get_data_filter()
for key_path in key_paths:
value = self.get(key_path=key_path)
if _filter(key_path, value):
return value
return default
# Method default
# --------------
@staticmethod
def __default_find_callable_filter(key_path, value):
return value is not None

View File

@ -0,0 +1,23 @@
# Generated by Django 3.1 on 2021-01-13 05:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('terminal', '0030_terminal_type'),
]
operations = [
migrations.AlterField(
model_name='commandstorage',
name='comment',
field=models.TextField(blank=True, default='', verbose_name='Comment'),
),
migrations.AlterField(
model_name='replaystorage',
name='comment',
field=models.TextField(blank=True, default='', verbose_name='Comment'),
),
]

View File

@ -18,7 +18,7 @@ class CommandStorage(CommonModelMixin):
default=const.CommandStorageTypeChoices.server.value, verbose_name=_('Type'), default=const.CommandStorageTypeChoices.server.value, verbose_name=_('Type'),
) )
meta = EncryptJsonDictTextField(default={}) meta = EncryptJsonDictTextField(default={})
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
def __str__(self): def __str__(self):
return self.name return self.name
@ -58,7 +58,7 @@ class ReplayStorage(CommonModelMixin):
default=const.ReplayStorageTypeChoices.server.value, verbose_name=_('Type') default=const.ReplayStorageTypeChoices.server.value, verbose_name=_('Type')
) )
meta = EncryptJsonDictTextField(default={}) meta = EncryptJsonDictTextField(default={})
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment')) comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
def __str__(self): def __str__(self):
return self.name return self.name

View File

@ -2,7 +2,6 @@
# #
from rest_framework import viewsets, mixins from rest_framework import viewsets, mixins
from django.shortcuts import get_object_or_404
from common.exceptions import JMSException from common.exceptions import JMSException
from common.utils import lazyproperty from common.utils import lazyproperty
from tickets import serializers from tickets import serializers
@ -22,11 +21,10 @@ class CommentViewSet(mixins.CreateModelMixin, viewsets.ReadOnlyModelViewSet):
if getattr(self, 'swagger_fake_view', False): if getattr(self, 'swagger_fake_view', False):
return None return None
ticket_id = self.request.query_params.get('ticket_id') ticket_id = self.request.query_params.get('ticket_id')
try: ticket = Ticket.all().filter(pk=ticket_id).first()
ticket = get_object_or_404(Ticket, pk=ticket_id) if not ticket:
return ticket raise JMSException('Not found Ticket object about `id={}`'.format(ticket_id))
except Exception as e: return ticket
raise JMSException(str(e))
def get_serializer_context(self): def get_serializer_context(self):
context = super().get_serializer_context() context = super().get_serializer_context()

View File

@ -4,6 +4,7 @@
from rest_framework import viewsets from rest_framework import viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import MethodNotAllowed from rest_framework.exceptions import MethodNotAllowed
from rest_framework.response import Response
from common.const.http import POST, PUT from common.const.http import POST, PUT
from common.mixins.api import CommonApiMixin from common.mixins.api import CommonApiMixin
@ -21,12 +22,8 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
permission_classes = (IsValidUser,) permission_classes = (IsValidUser,)
serializer_class = serializers.TicketDisplaySerializer serializer_class = serializers.TicketDisplaySerializer
serializer_classes = { serializer_classes = {
'default': serializers.TicketDisplaySerializer,
'display': serializers.TicketDisplaySerializer,
'open': serializers.TicketApplySerializer, 'open': serializers.TicketApplySerializer,
'approve': serializers.TicketApproveSerializer, 'approve': serializers.TicketApproveSerializer,
'reject': serializers.TicketRejectSerializer,
'close': serializers.TicketCloseSerializer,
} }
filterset_fields = [ filterset_fields = [
'id', 'title', 'type', 'action', 'status', 'applicant', 'applicant_display', 'processor', 'id', 'title', 'type', 'action', 'status', 'applicant', 'applicant_display', 'processor',
@ -49,19 +46,31 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
queryset = Ticket.get_user_related_tickets(self.request.user) queryset = Ticket.get_user_related_tickets(self.request.user)
return queryset return queryset
def perform_create(self, serializer):
instance = serializer.save()
instance.open(applicant=self.request.user)
@action(detail=False, methods=[POST]) @action(detail=False, methods=[POST])
def open(self, request, *args, **kwargs): def open(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs) return super().create(request, *args, **kwargs)
@action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed]) @action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed])
def approve(self, request, *args, **kwargs): def approve(self, request, *args, **kwargs):
return super().update(request, *args, **kwargs) response = super().update(request, *args, **kwargs)
instance = self.get_object()
instance.approve(processor=self.request.user)
return response
@action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed]) @action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed])
def reject(self, request, *args, **kwargs): def reject(self, request, *args, **kwargs):
return super().update(request, *args, **kwargs) instance = self.get_object()
serializer = self.get_serializer(instance)
instance.reject(processor=request.user)
return Response(serializer.data)
@action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed]) @action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed])
def close(self, request, *args, **kwargs): def close(self, request, *args, **kwargs):
return super().update(request, *args, **kwargs) instance = self.get_object()
serializer = self.get_serializer(instance)
instance.close(processor=request.user)
return Response(serializer.data)

View File

@ -0,0 +1,7 @@
from django.utils.module_loading import import_string
def get_ticket_handler(ticket):
handler_class_path = 'tickets.handler.{}.Handler'.format(ticket.type)
handler_class = import_string(handler_class_path)
return handler_class(ticket=ticket)

View File

@ -4,25 +4,32 @@ from applications.models import Application
from applications.const import ApplicationCategoryChoices, ApplicationTypeChoices from applications.const import ApplicationCategoryChoices, ApplicationTypeChoices
from assets.models import SystemUser from assets.models import SystemUser
from perms.models import ApplicationPermission from perms.models import ApplicationPermission
from tickets.utils import convert_model_instance_data_field_name_to_verbose_name from tickets.utils import convert_model_data_field_name_to_verbose_name
from .base import BaseHandler
class ConstructDisplayFieldMixin: class Handler(BaseHandler):
def construct_meta_apply_application_open_fields_display(self):
def _on_approve(self):
super()._on_approve()
self._create_application_permission()
# display
def _construct_meta_display_of_open(self):
meta_display_fields = ['apply_category_display', 'apply_type_display'] meta_display_fields = ['apply_category_display', 'apply_type_display']
apply_category = self.meta.get('apply_category') apply_category = self.ticket.meta.get('apply_category')
apply_category_display = ApplicationCategoryChoices.get_label(apply_category) apply_category_display = ApplicationCategoryChoices.get_label(apply_category)
apply_type = self.meta.get('apply_type') apply_type = self.ticket.meta.get('apply_type')
apply_type_display = ApplicationTypeChoices.get_label(apply_type) apply_type_display = ApplicationTypeChoices.get_label(apply_type)
meta_display_values = [apply_category_display, apply_type_display] meta_display_values = [apply_category_display, apply_type_display]
meta_display = dict(zip(meta_display_fields, meta_display_values)) meta_display = dict(zip(meta_display_fields, meta_display_values))
return meta_display return meta_display
def construct_meta_apply_application_approve_fields_display(self): def _construct_meta_display_of_approve(self):
meta_display_fields = ['approve_applications_snapshot', 'approve_system_users_snapshot'] meta_display_fields = ['approve_applications_snapshot', 'approve_system_users_snapshot']
approve_applications_id = self.meta.get('approve_applications', []) approve_applications_id = self.ticket.meta.get('approve_applications', [])
approve_system_users_id = self.meta.get('approve_system_users', []) approve_system_users_id = self.ticket.meta.get('approve_system_users', [])
with tmp_to_org(self.org_id): with tmp_to_org(self.ticket.org_id):
approve_applications_snapshot = list( approve_applications_snapshot = list(
Application.objects.filter(id__in=approve_applications_id).values( Application.objects.filter(id__in=approve_applications_id).values(
'name', 'category', 'type' 'name', 'category', 'type'
@ -38,16 +45,14 @@ class ConstructDisplayFieldMixin:
meta_display = dict(zip(meta_display_fields, meta_display_values)) meta_display = dict(zip(meta_display_fields, meta_display_values))
return meta_display return meta_display
# body
class ConstructBodyMixin: def _construct_meta_body_of_open(self):
apply_category_display = self.ticket.meta.get('apply_category_display')
def construct_apply_application_applied_body(self): apply_type_display = self.ticket.meta.get('apply_type_display')
apply_category_display = self.meta.get('apply_category_display') apply_application_group = self.ticket.meta.get('apply_application_group', [])
apply_type_display = self.meta.get('apply_type_display') apply_system_user_group = self.ticket.meta.get('apply_system_user_group', [])
apply_application_group = self.meta.get('apply_application_group', []) apply_date_start = self.ticket.meta.get('apply_date_start')
apply_system_user_group = self.meta.get('apply_system_user_group', []) apply_date_expired = self.ticket.meta.get('apply_date_expired')
apply_date_start = self.meta.get('apply_date_start')
apply_date_expired = self.meta.get('apply_date_expired')
applied_body = '''{}: {}, applied_body = '''{}: {},
{}: {}, {}: {},
{}: {}, {}: {},
@ -64,18 +69,18 @@ class ConstructBodyMixin:
) )
return applied_body return applied_body
def construct_apply_application_approved_body(self): def _construct_meta_body_of_approve(self):
# 审批信息 # 审批信息
approve_applications_snapshot = self.meta.get('approve_applications_snapshot', []) approve_applications_snapshot = self.ticket.meta.get('approve_applications_snapshot', [])
approve_applications_snapshot_display = convert_model_instance_data_field_name_to_verbose_name( approve_applications_snapshot_display = convert_model_data_field_name_to_verbose_name(
Application, approve_applications_snapshot model=Application, data=approve_applications_snapshot
) )
approve_system_users_snapshot = self.meta.get('approve_system_users_snapshot', []) approve_system_users_snapshot = self.ticket.meta.get('approve_system_users_snapshot', [])
approve_system_users_snapshot_display = convert_model_instance_data_field_name_to_verbose_name( approve_system_users_snapshot_display = convert_model_data_field_name_to_verbose_name(
SystemUser, approve_system_users_snapshot model=SystemUser, data=approve_system_users_snapshot
) )
approve_date_start = self.meta.get('approve_date_start') approve_date_start = self.ticket.meta.get('approve_date_start')
approve_date_expired = self.meta.get('approve_date_expired') approve_date_expired = self.ticket.meta.get('approve_date_expired')
approved_body = '''{}: {}, approved_body = '''{}: {},
{}: {}, {}: {},
{}: {}, {}: {},
@ -88,23 +93,21 @@ class ConstructBodyMixin:
) )
return approved_body return approved_body
# permission
class CreatePermissionMixin: def _create_application_permission(self):
def create_apply_application_permission(self):
with tmp_to_root_org(): with tmp_to_root_org():
application_permission = ApplicationPermission.objects.filter(id=self.id).first() application_permission = ApplicationPermission.objects.filter(id=self.ticket.id).first()
if application_permission: if application_permission:
return application_permission return application_permission
apply_category = self.meta.get('apply_category') apply_category = self.ticket.meta.get('apply_category')
apply_type = self.meta.get('apply_type') apply_type = self.ticket.meta.get('apply_type')
approved_applications_id = self.meta.get('approve_applications', []) approved_applications_id = self.ticket.meta.get('approve_applications', [])
approve_system_users_id = self.meta.get('approve_system_users', []) approve_system_users_id = self.ticket.meta.get('approve_system_users', [])
approve_date_start = self.meta.get('approve_date_start') approve_date_start = self.ticket.meta.get('approve_date_start')
approve_date_expired = self.meta.get('approve_date_expired') approve_date_expired = self.ticket.meta.get('approve_date_expired')
permission_name = '{}({})'.format( permission_name = '{}({})'.format(
__('Created by ticket ({})'.format(self.title)), str(self.id)[:4] __('Created by ticket ({})'.format(self.ticket.title)), str(self.ticket.id)[:4]
) )
permission_comment = __( permission_comment = __(
'Created by the ticket, ' 'Created by the ticket, '
@ -112,21 +115,24 @@ class CreatePermissionMixin:
'ticket applicant: {}, ' 'ticket applicant: {}, '
'ticket processor: {}, ' 'ticket processor: {}, '
'ticket ID: {}' 'ticket ID: {}'
''.format(self.title, self.applicant_display, self.processor_display, str(self.id)) ''.format(
self.ticket.title, self.ticket.applicant_display,
self.ticket.processor_display, str(self.ticket.id)
)
) )
permissions_data = { permissions_data = {
'id': self.id, 'id': self.ticket.id,
'name': permission_name, 'name': permission_name,
'category': apply_category, 'category': apply_category,
'type': apply_type, 'type': apply_type,
'comment': permission_comment, 'comment': permission_comment,
'created_by': '{}:{}'.format(str(self.__class__.__name__), str(self.id)), 'created_by': '{}:{}'.format(str(self.__class__.__name__), str(self.ticket.id)),
'date_start': approve_date_start, 'date_start': approve_date_start,
'date_expired': approve_date_expired, 'date_expired': approve_date_expired,
} }
with tmp_to_org(self.org_id): with tmp_to_org(self.ticket.org_id):
application_permission = ApplicationPermission.objects.create(**permissions_data) application_permission = ApplicationPermission.objects.create(**permissions_data)
application_permission.users.add(self.applicant) application_permission.users.add(self.ticket.applicant)
application_permission.applications.set(approved_applications_id) application_permission.applications.set(approved_applications_id)
application_permission.system_users.set(approve_system_users_id) application_permission.system_users.set(approve_system_users_id)

View File

@ -1,31 +1,36 @@
from .base import BaseHandler
from django.utils.translation import ugettext as __ from django.utils.translation import ugettext as __
from perms.models import AssetPermission, Action from perms.models import AssetPermission, Action
from assets.models import Asset, SystemUser from assets.models import Asset, SystemUser
from orgs.utils import tmp_to_org, tmp_to_root_org from orgs.utils import tmp_to_org, tmp_to_root_org
from tickets.utils import convert_model_instance_data_field_name_to_verbose_name from tickets.utils import convert_model_data_field_name_to_verbose_name
class ConstructDisplayFieldMixin: class Handler(BaseHandler):
def construct_meta_apply_asset_open_fields_display(self):
def _on_approve(self):
super()._on_approve()
self._create_asset_permission()
# display
def _construct_meta_display_of_open(self):
meta_display_fields = ['apply_actions_display'] meta_display_fields = ['apply_actions_display']
apply_actions = self.ticket.meta.get('apply_actions', Action.NONE)
apply_actions = self.meta.get('apply_actions', Action.NONE)
apply_actions_display = Action.value_to_choices_display(apply_actions) apply_actions_display = Action.value_to_choices_display(apply_actions)
meta_display_values = [apply_actions_display] meta_display_values = [apply_actions_display]
meta_display = dict(zip(meta_display_fields, meta_display_values)) meta_display = dict(zip(meta_display_fields, meta_display_values))
return meta_display return meta_display
def construct_meta_apply_asset_approve_fields_display(self): def _construct_meta_display_of_approve(self):
meta_display_fields = [ meta_display_fields = [
'approve_actions_display', 'approve_assets_snapshot', 'approve_system_users_snapshot' 'approve_actions_display', 'approve_assets_snapshot', 'approve_system_users_snapshot'
] ]
approve_actions = self.meta.get('approve_actions', Action.NONE) approve_actions = self.ticket.meta.get('approve_actions', Action.NONE)
approve_actions_display = Action.value_to_choices_display(approve_actions) approve_actions_display = Action.value_to_choices_display(approve_actions)
approve_assets_id = self.meta.get('approve_assets', []) approve_assets_id = self.ticket.meta.get('approve_assets', [])
approve_system_users_id = self.meta.get('approve_system_users', []) approve_system_users_id = self.ticket.meta.get('approve_system_users', [])
with tmp_to_org(self.org_id): with tmp_to_org(self.ticket.org_id):
approve_assets_snapshot = list( approve_assets_snapshot = list(
Asset.objects.filter(id__in=approve_assets_id).values( Asset.objects.filter(id__in=approve_assets_id).values(
'hostname', 'ip', 'protocols', 'platform__name', 'public_ip' 'hostname', 'ip', 'protocols', 'platform__name', 'public_ip'
@ -43,15 +48,14 @@ class ConstructDisplayFieldMixin:
meta_display = dict(zip(meta_display_fields, meta_display_values)) meta_display = dict(zip(meta_display_fields, meta_display_values))
return meta_display return meta_display
# body
class ConstructBodyMixin: def _construct_meta_body_of_open(self):
def construct_apply_asset_applied_body(self): apply_ip_group = self.ticket.meta.get('apply_ip_group', [])
apply_ip_group = self.meta.get('apply_ip_group', []) apply_hostname_group = self.ticket.meta.get('apply_hostname_group', [])
apply_hostname_group = self.meta.get('apply_hostname_group', []) apply_system_user_group = self.ticket.meta.get('apply_system_user_group', [])
apply_system_user_group = self.meta.get('apply_system_user_group', []) apply_actions_display = self.ticket.meta.get('apply_actions_display', [])
apply_actions_display = self.meta.get('apply_actions_display', []) apply_date_start = self.ticket.meta.get('apply_date_start')
apply_date_start = self.meta.get('apply_date_start') apply_date_expired = self.ticket.meta.get('apply_date_expired')
apply_date_expired = self.meta.get('apply_date_expired')
applied_body = '''{}: {}, applied_body = '''{}: {},
{}: {}, {}: {},
{}: {}, {}: {},
@ -67,18 +71,18 @@ class ConstructBodyMixin:
) )
return applied_body return applied_body
def construct_apply_asset_approved_body(self): def _construct_meta_body_of_approve(self):
approve_assets_snapshot = self.meta.get('approve_assets_snapshot', []) approve_assets_snapshot = self.ticket.meta.get('approve_assets_snapshot', [])
approve_assets_snapshot_display = convert_model_instance_data_field_name_to_verbose_name( approve_assets_snapshot_display = convert_model_data_field_name_to_verbose_name(
Asset, approve_assets_snapshot model=Asset, data=approve_assets_snapshot
) )
approve_system_users_snapshot = self.meta.get('approve_system_users_snapshot', []) approve_system_users_snapshot = self.ticket.meta.get('approve_system_users_snapshot', [])
approve_system_users_snapshot_display = convert_model_instance_data_field_name_to_verbose_name( approve_system_users_snapshot_display = convert_model_data_field_name_to_verbose_name(
SystemUser, approve_system_users_snapshot model=SystemUser, data=approve_system_users_snapshot
) )
approve_actions_display = self.meta.get('approve_actions_display', []) approve_actions_display = self.ticket.meta.get('approve_actions_display', [])
approve_date_start = self.meta.get('approve_date_start') approve_date_start = self.ticket.meta.get('approve_date_start')
approve_date_expired = self.meta.get('approve_date_expired') approve_date_expired = self.ticket.meta.get('approve_date_expired')
approved_body = '''{}: {}, approved_body = '''{}: {},
{}: {}, {}: {},
{}: {}, {}: {},
@ -93,21 +97,20 @@ class ConstructBodyMixin:
) )
return approved_body return approved_body
# permission
class CreatePermissionMixin: def _create_asset_permission(self):
def create_apply_asset_permission(self):
with tmp_to_root_org(): with tmp_to_root_org():
asset_permission = AssetPermission.objects.filter(id=self.id).first() asset_permission = AssetPermission.objects.filter(id=self.ticket.id).first()
if asset_permission: if asset_permission:
return asset_permission return asset_permission
approve_assets_id = self.meta.get('approve_assets', []) approve_assets_id = self.ticket.meta.get('approve_assets', [])
approve_system_users_id = self.meta.get('approve_system_users', []) approve_system_users_id = self.ticket.meta.get('approve_system_users', [])
approve_actions = self.meta.get('approve_actions', Action.NONE) approve_actions = self.ticket.meta.get('approve_actions', Action.NONE)
approve_date_start = self.meta.get('approve_date_start') approve_date_start = self.ticket.meta.get('approve_date_start')
approve_date_expired = self.meta.get('approve_date_expired') approve_date_expired = self.ticket.meta.get('approve_date_expired')
permission_name = '{}({})'.format( permission_name = '{}({})'.format(
__('Created by ticket ({})'.format(self.title)), str(self.id)[:4] __('Created by ticket ({})'.format(self.ticket.title)), str(self.ticket.id)[:4]
) )
permission_comment = __( permission_comment = __(
'Created by the ticket, ' 'Created by the ticket, '
@ -115,20 +118,23 @@ class CreatePermissionMixin:
'ticket applicant: {}, ' 'ticket applicant: {}, '
'ticket processor: {}, ' 'ticket processor: {}, '
'ticket ID: {}' 'ticket ID: {}'
''.format(self.title, self.applicant_display, self.processor_display, str(self.id)) ''.format(
self.ticket.title, self.ticket.applicant_display, self.ticket.processor_display,
str(self.ticket.id)
)
) )
permission_data = { permission_data = {
'id': self.id, 'id': self.ticket.id,
'name': permission_name, 'name': permission_name,
'comment': permission_comment, 'comment': permission_comment,
'created_by': '{}:{}'.format(str(self.__class__.__name__), str(self.id)), 'created_by': '{}:{}'.format(str(self.__class__.__name__), str(self.ticket.id)),
'actions': approve_actions, 'actions': approve_actions,
'date_start': approve_date_start, 'date_start': approve_date_start,
'date_expired': approve_date_expired, 'date_expired': approve_date_expired,
} }
with tmp_to_org(self.org_id): with tmp_to_org(self.ticket.org_id):
asset_permission = AssetPermission.objects.create(**permission_data) asset_permission = AssetPermission.objects.create(**permission_data)
asset_permission.users.add(self.applicant) asset_permission.users.add(self.ticket.applicant)
asset_permission.assets.set(approve_assets_id) asset_permission.assets.set(approve_assets_id)
asset_permission.system_users.set(approve_system_users_id) asset_permission.system_users.set(approve_system_users_id)

View File

@ -0,0 +1,122 @@
from django.utils.translation import ugettext as __
from common.utils import get_logger
from tickets.utils import send_ticket_processed_mail_to_applicant
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()
def _on_approve(self):
meta_display = getattr(self, '_construct_meta_display_of_approve', lambda: {})()
self.ticket.meta.update(meta_display)
self.__on_process()
def _on_reject(self):
self.__on_process()
def _on_close(self):
self.__on_process()
def __on_process(self):
self.ticket.processor_display = str(self.ticket.processor)
self.ticket.set_status_closed()
self._send_processed_mail_to_applicant()
self.ticket.save()
def dispatch(self, action):
self._create_comment_on_action()
method = getattr(self, f'_on_{action}', lambda: None)
return method()
# email
def _send_processed_mail_to_applicant(self):
msg = 'Ticket ({}) has processed, send mail to applicant ({})'.format(
self.ticket.title, self.ticket.applicant_display
)
logger.debug(msg)
send_ticket_processed_mail_to_applicant(self.ticket)
# comments
def _create_comment_on_action(self):
user = self.ticket.applicant if self.ticket.action_open else self.ticket.processor
user_display = str(user)
action_display = self.ticket.get_action_display()
data = {
'body': __('User {} {} the ticket'.format(user_display, action_display)),
'user': user,
'user_display': user_display
}
return self.ticket.comments.create(**data)
# body
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):
body = '''
{}:
{}: {},
{}: {},
{}: {},
{}: {},
{}: {},
{}: {},
{}: {}
'''.format(
__("Ticket basic info"),
__('Ticket title'), self.ticket.title,
__('Ticket type'), self.ticket.get_type_display(),
__('Ticket applicant'), self.ticket.applicant_display,
__('Ticket assignees'), self.ticket.assignees_display,
__('Ticket processor'), self.ticket.processor_display,
__('Ticket action'), self.ticket.get_action_display(),
__('Ticket status'), self.ticket.get_status_display()
)
return body
def _construct_meta_body(self):
body = ''
open_body = self._base_construct_meta_body_of_open()
body += open_body
if self.ticket.action_approve:
approve_body = self._base_construct_meta_body_of_approve()
body += approve_body
return body
def _base_construct_meta_body_of_open(self):
open_body = '''
{}:
{}
'''.format(
__('Ticket applied info'),
getattr(self, '_construct_meta_body_of_open', lambda: 'No')()
)
return open_body
def _base_construct_meta_body_of_approve(self):
approve_body = '''
{}:
{}
'''.format(
__('Ticket approved info'),
getattr(self, '_construct_meta_body_of_approve', lambda: 'No')()
)
return approve_body

View File

@ -0,0 +1,5 @@
from .base import BaseHandler
class Handler(BaseHandler):
pass

View File

@ -1,12 +1,14 @@
from django.utils.translation import ugettext as __ from django.utils.translation import ugettext as __
from .base import BaseHandler
class ConstructBodyMixin: class Handler(BaseHandler):
def construct_login_confirm_applied_body(self): # body
apply_login_ip = self.meta.get('apply_login_ip') def _construct_meta_body_of_open(self):
apply_login_city = self.meta.get('apply_login_city') apply_login_ip = self.ticket.meta.get('apply_login_ip')
apply_login_datetime = self.meta.get('apply_login_datetime') apply_login_city = self.ticket.meta.get('apply_login_city')
apply_login_datetime = self.ticket.meta.get('apply_login_datetime')
applied_body = '''{}: {}, applied_body = '''{}: {},
{}: {}, {}: {},
{}: {} {}: {}

View File

@ -101,7 +101,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='ticket', model_name='ticket',
name='applicant_display', name='applicant_display',
field=models.CharField(default='No', max_length=256, verbose_name='Applicant display'), field=models.CharField(default='', max_length=256, verbose_name='Applicant display'),
), ),
migrations.AddField( migrations.AddField(
model_name='ticket', model_name='ticket',
@ -111,12 +111,12 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='ticket', model_name='ticket',
name='processor_display', name='processor_display',
field=models.CharField(blank=True, default='No', max_length=256, null=True, verbose_name='Processor display'), field=models.CharField(blank=True, default='', max_length=256, null=True, verbose_name='Processor display'),
), ),
migrations.AddField( migrations.AddField(
model_name='ticket', model_name='ticket',
name='assignees_display_new', name='assignees_display_new',
field=models.JSONField(default=list, encoder=tickets.models.ticket.model.ModelJSONFieldEncoder, verbose_name='Assignees display'), field=models.JSONField(default=list, encoder=tickets.models.ticket.ModelJSONFieldEncoder, verbose_name='Assignees display'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='ticket', model_name='ticket',
@ -126,7 +126,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='ticket', model_name='ticket',
name='meta', name='meta',
field=models.JSONField(default=dict, encoder=tickets.models.ticket.model.ModelJSONFieldEncoder, verbose_name='Meta'), field=models.JSONField(default=dict, encoder=tickets.models.ticket.ModelJSONFieldEncoder, verbose_name='Meta'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='ticket', model_name='ticket',

View File

@ -11,8 +11,9 @@ from django.conf import settings
from common.mixins.models import CommonModelMixin from common.mixins.models import CommonModelMixin
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from orgs.utils import tmp_to_root_org, tmp_to_org from orgs.utils import tmp_to_root_org, tmp_to_org
from tickets import const from tickets.const import TicketTypeChoices, TicketActionChoices, TicketStatusChoices
from .mixin import TicketModelMixin from tickets.signals import post_change_ticket_action
from tickets.handler import get_ticket_handler
__all__ = ['Ticket', 'ModelJSONFieldEncoder'] __all__ = ['Ticket', 'ModelJSONFieldEncoder']
@ -30,20 +31,20 @@ class ModelJSONFieldEncoder(json.JSONEncoder):
return super().default(obj) return super().default(obj)
class Ticket(TicketModelMixin, CommonModelMixin, OrgModelMixin): class Ticket(CommonModelMixin, OrgModelMixin):
title = models.CharField(max_length=256, verbose_name=_("Title")) title = models.CharField(max_length=256, verbose_name=_("Title"))
type = models.CharField( type = models.CharField(
max_length=64, choices=const.TicketTypeChoices.choices, max_length=64, choices=TicketTypeChoices.choices,
default=const.TicketTypeChoices.general.value, verbose_name=_("Type") default=TicketTypeChoices.general.value, verbose_name=_("Type")
) )
meta = models.JSONField(encoder=ModelJSONFieldEncoder, default=dict, verbose_name=_("Meta")) meta = models.JSONField(encoder=ModelJSONFieldEncoder, default=dict, verbose_name=_("Meta"))
action = models.CharField( action = models.CharField(
choices=const.TicketActionChoices.choices, max_length=16, choices=TicketActionChoices.choices, max_length=16,
default=const.TicketActionChoices.open.value, verbose_name=_("Action") default=TicketActionChoices.open.value, verbose_name=_("Action")
) )
status = models.CharField( status = models.CharField(
max_length=16, choices=const.TicketStatusChoices.choices, max_length=16, choices=TicketStatusChoices.choices,
default=const.TicketStatusChoices.open.value, verbose_name=_("Status") default=TicketStatusChoices.open.value, verbose_name=_("Status")
) )
# 申请人 # 申请人
applicant = models.ForeignKey( applicant = models.ForeignKey(
@ -51,7 +52,7 @@ class Ticket(TicketModelMixin, CommonModelMixin, OrgModelMixin):
verbose_name=_("Applicant") verbose_name=_("Applicant")
) )
applicant_display = models.CharField( applicant_display = models.CharField(
max_length=256, default='No', verbose_name=_("Applicant display") max_length=256, default='', verbose_name=_("Applicant display")
) )
# 处理人 # 处理人
processor = models.ForeignKey( processor = models.ForeignKey(
@ -59,7 +60,7 @@ class Ticket(TicketModelMixin, CommonModelMixin, OrgModelMixin):
verbose_name=_("Processor") verbose_name=_("Processor")
) )
processor_display = models.CharField( processor_display = models.CharField(
max_length=256, blank=True, null=True, default='No', verbose_name=_("Processor display") max_length=256, blank=True, null=True, default='', verbose_name=_("Processor display")
) )
# 受理人列表 # 受理人列表
assignees = models.ManyToManyField( assignees = models.ManyToManyField(
@ -80,70 +81,71 @@ class Ticket(TicketModelMixin, CommonModelMixin, OrgModelMixin):
# type # type
@property @property
def type_apply_asset(self): def type_apply_asset(self):
return self.type == const.TicketTypeChoices.apply_asset.value return self.type == TicketTypeChoices.apply_asset.value
@property @property
def type_apply_application(self): def type_apply_application(self):
return self.type == const.TicketTypeChoices.apply_application.value return self.type == TicketTypeChoices.apply_application.value
@property @property
def type_login_confirm(self): def type_login_confirm(self):
return self.type == const.TicketTypeChoices.login_confirm.value return self.type == TicketTypeChoices.login_confirm.value
# status # status
@property @property
def status_closed(self): def status_closed(self):
return self.status == const.TicketStatusChoices.closed.value return self.status == TicketStatusChoices.closed.value
@property @property
def status_open(self): def status_open(self):
return self.status == const.TicketStatusChoices.open.value return self.status == TicketStatusChoices.open.value
def set_status_closed(self): def set_status_closed(self):
self.status = const.TicketStatusChoices.closed.value self.status = TicketStatusChoices.closed.value
# action # action
@property @property
def action_open(self): def action_open(self):
return self.action == const.TicketActionChoices.open.value return self.action == TicketActionChoices.open.value
@property @property
def action_approve(self): def action_approve(self):
return self.action == const.TicketActionChoices.approve.value return self.action == TicketActionChoices.approve.value
@property @property
def action_reject(self): def action_reject(self):
return self.action == const.TicketActionChoices.reject.value return self.action == TicketActionChoices.reject.value
@property @property
def action_close(self): def action_close(self):
return self.action == const.TicketActionChoices.close.value return self.action == TicketActionChoices.close.value
@property # action changed
def has_applied(self): def open(self, applicant):
return self.action_open self.applicant = applicant
self._change_action(action=TicketActionChoices.open.value)
@property def approve(self, processor):
def has_processed(self): self.processor = processor
return self.action_approve or self.action_reject or self.action_close self._change_action(action=TicketActionChoices.approve.value)
def set_action_close(self): def reject(self, processor):
self.action = const.TicketActionChoices.close.value self.processor = processor
self._change_action(action=TicketActionChoices.reject.value)
def close(self, processor): def close(self, processor):
self.processor = processor self.processor = processor
self.set_action_close() self._change_action(action=TicketActionChoices.close.value)
def _change_action(self, action):
self.action = action
self.save() self.save()
post_change_ticket_action.send(sender=self.__class__, ticket=self, action=action)
# ticket # ticket
def has_assignee(self, assignee): def has_assignee(self, assignee):
return self.assignees.filter(id=assignee.id).exists() return self.assignees.filter(id=assignee.id).exists()
@classmethod
def all(cls):
with tmp_to_root_org():
return Ticket.objects.all()
@classmethod @classmethod
def get_user_related_tickets(cls, user): def get_user_related_tickets(cls, user):
queries = None queries = None
@ -176,7 +178,22 @@ class Ticket(TicketModelMixin, CommonModelMixin, OrgModelMixin):
tickets = tickets.filter(queries) tickets = tickets.filter(queries)
return tickets.distinct() return tickets.distinct()
@classmethod
def all(cls):
with tmp_to_root_org():
return Ticket.objects.all()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" 确保保存的org_id的是自身的值 """
with tmp_to_org(self.org_id): with tmp_to_org(self.org_id):
# 确保保存的org_id的是自身的值
return super().save(*args, **kwargs) 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

View File

@ -1 +0,0 @@
from .model import *

View File

@ -1 +0,0 @@
from .ticket import TicketModelMixin

View File

@ -1,134 +0,0 @@
import textwrap
from django.utils.translation import ugettext as __
class SetDisplayFieldMixin:
def set_applicant_display(self):
if self.has_applied:
self.applicant_display = str(self.applicant)
def set_assignees_display(self):
if self.has_applied:
self.assignees_display = [str(assignee) for assignee in self.assignees.all()]
def set_processor_display(self):
if self.has_processed:
self.processor_display = str(self.processor)
def set_meta_display(self):
method_name = f'construct_meta_{self.type}_{self.action}_fields_display'
meta_display = getattr(self, method_name, lambda: {})()
self.meta.update(meta_display)
def set_display_fields(self):
self.set_applicant_display()
self.set_processor_display()
self.set_meta_display()
class ConstructBodyMixin:
# applied body
def construct_applied_body(self):
construct_method = getattr(self, f'construct_{self.type}_applied_body', lambda: 'No')
applied_body = construct_method()
body = '''
{}:
{}
'''.format(
__('Ticket applied info'),
applied_body
)
return body
# approved body
def construct_approved_body(self):
construct_method = getattr(self, f'construct_{self.type}_approved_body', lambda: 'No')
approved_body = construct_method()
body = '''
{}:
{}
'''.format(
__('Ticket approved info'),
approved_body
)
return body
# meta body
def construct_meta_body(self):
applied_body = self.construct_applied_body()
if not self.action_approve:
return applied_body
approved_body = self.construct_approved_body()
return applied_body + approved_body
# basic body
def construct_basic_body(self):
basic_body = '''
{}:
{}: {},
{}: {},
{}: {},
{}: {},
{}: {},
{}: {},
{}: {}
'''.format(
__("Ticket basic info"),
__('Ticket title'), self.title,
__('Ticket type'), self.get_type_display(),
__('Ticket applicant'), self.applicant_display,
__('Ticket assignees'), self.assignees_display,
__('Ticket processor'), self.processor_display,
__('Ticket action'), self.get_action_display(),
__('Ticket status'), self.get_status_display()
)
return basic_body
@property
def body(self):
old_body = self.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
class CreatePermissionMixin:
# create permission
def create_permission(self):
create_method = getattr(self, f'create_{self.type}_permission', lambda: None)
permission = create_method()
return permission
class CreateCommentMixin:
def create_comment(self, comment_body):
# 页面展示需要取消缩进
comment_body = textwrap.dedent(comment_body)
comment_data = {
'body': comment_body,
'user': self.processor,
'user_display': self.processor_display
}
return self.comments.create(**comment_data)
def create_applied_comment(self):
comment_body = self.construct_applied_body()
self.create_comment(comment_body)
def create_approved_comment(self):
comment_body = self.construct_approved_body()
self.create_comment(comment_body)
def create_action_comment(self):
if self.has_applied:
user_display = self.applicant_display
if self.has_processed:
user_display = self.processor_display
comment_body = __(
'User {} {} the ticket'.format(user_display, self.get_action_display())
)
self.create_comment(comment_body)

View File

@ -1 +0,0 @@
from .base import *

View File

@ -1,35 +0,0 @@
from . import apply_asset, apply_application, login_confirm
__all__ = ['ConstructDisplayFieldMixin', 'ConstructBodyMixin', 'CreatePermissionMixin']
modules = (apply_asset, apply_application, login_confirm)
construct_display_field_mixin_cls_name = 'ConstructDisplayFieldMixin'
construct_body_mixin_cls_name = 'ConstructBodyMixin'
create_permission_mixin_cls_name = 'CreatePermissionMixin'
def get_mixin_base_cls_list(base_cls_name):
return [
getattr(module, base_cls_name) for module in modules if hasattr(module, base_cls_name)
]
class ConstructDisplayFieldMixin(
*get_mixin_base_cls_list(construct_display_field_mixin_cls_name)
):
pass
class ConstructBodyMixin(
*get_mixin_base_cls_list(construct_body_mixin_cls_name)
):
pass
class CreatePermissionMixin(
*get_mixin_base_cls_list(create_permission_mixin_cls_name)
):
pass

View File

@ -1,30 +0,0 @@
from . import base, meta
__all__ = ['TicketModelMixin']
class TicketSetDisplayFieldMixin(meta.ConstructDisplayFieldMixin, base.SetDisplayFieldMixin):
""" 设置 ticket display 字段(只设置,不保存)"""
pass
class TicketConstructBodyMixin(meta.ConstructBodyMixin, base.ConstructBodyMixin):
""" 构造 ticket body 信息 """
pass
class TicketCreatePermissionMixin(meta.CreatePermissionMixin, base.CreatePermissionMixin):
""" 创建 ticket 相关授权规则"""
pass
class TicketCreateCommentMixin(base.CreateCommentMixin):
""" 创建 ticket 备注"""
pass
class TicketModelMixin(
TicketSetDisplayFieldMixin, TicketConstructBodyMixin, TicketCreatePermissionMixin,
TicketCreateCommentMixin
):
pass

View File

@ -103,7 +103,7 @@ class ApproveSerializer(serializers.Serializer):
return system_users_id return system_users_id
raise serializers.ValidationError(_( raise serializers.ValidationError(_(
'No `Asset` are found under Organization `{}`'.format(self.root.instance.org_name) 'No `SystemUser` are found under Organization `{}`'.format(self.root.instance.org_name)
)) ))

View File

@ -2,19 +2,16 @@
# #
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from common.drf.fields import ReadableHiddenField
from common.drf.serializers import MethodSerializer from common.drf.serializers import MethodSerializer
from orgs.utils import get_org_by_id from orgs.utils import get_org_by_id
from orgs.mixins.serializers import OrgResourceModelSerializerMixin from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from users.models import User from users.models import User
from tickets import const
from tickets.models import Ticket from tickets.models import Ticket
from .meta import type_serializer_classes_mapping from .meta import type_serializer_classes_mapping
__all__ = [ __all__ = [
'TicketSerializer', 'TicketDisplaySerializer', 'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketApproveSerializer',
'TicketApplySerializer', 'TicketApproveSerializer',
'TicketRejectSerializer', 'TicketCloseSerializer',
] ]
@ -22,9 +19,6 @@ class TicketSerializer(OrgResourceModelSerializerMixin):
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type')) type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type'))
action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action')) action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action'))
status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status')) status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status'))
action = ReadableHiddenField(default=const.TicketActionChoices.open.value)
applicant = ReadableHiddenField(default=serializers.CurrentUserDefault())
processor = ReadableHiddenField(default=serializers.CurrentUserDefault())
meta = MethodSerializer() meta = MethodSerializer()
class Meta: class Meta:
@ -72,24 +66,23 @@ class TicketDisplaySerializer(TicketSerializer):
class Meta: class Meta:
model = Ticket model = Ticket
fields = TicketSerializer.Meta.fields fields = TicketSerializer.Meta.fields
read_only_fields = TicketSerializer.Meta.fields read_only_fields = fields
class TicketApplySerializer(TicketSerializer): class TicketApplySerializer(TicketSerializer):
org_id = serializers.CharField( org_id = serializers.CharField(
max_length=36, allow_blank=True, required=True, label=_("Organization") required=True, max_length=36, allow_blank=True, label=_("Organization")
) )
class Meta: class Meta:
model = Ticket model = Ticket
fields = TicketSerializer.Meta.fields fields = TicketSerializer.Meta.fields
required_fields = [ writeable_fields = [
'id', 'title', 'type', 'applicant', 'action', 'meta', 'assignees', 'comment', 'org_id' 'id', 'title', 'type', 'meta', 'assignees', 'comment', 'org_id'
] ]
read_only_fields = list(set(fields) - set(required_fields)) read_only_fields = list(set(fields) - set(writeable_fields))
extra_kwargs = { extra_kwargs = {
'type': {'required': True}, 'type': {'required': True},
'org_id': {'required': True},
} }
def validate_type(self, tp): def validate_type(self, tp):
@ -121,49 +114,16 @@ class TicketApplySerializer(TicketSerializer):
raise serializers.ValidationError(error) raise serializers.ValidationError(error)
return valid_assignees return valid_assignees
def validate_action(self, action):
return const.TicketActionChoices.open.value
class TicketApproveSerializer(TicketSerializer): class TicketApproveSerializer(TicketSerializer):
class Meta: class Meta:
model = Ticket model = Ticket
fields = TicketSerializer.Meta.fields fields = TicketSerializer.Meta.fields
required_fields = ['processor', 'action', 'meta'] writeable_fields = ['meta']
read_only_fields = list(set(fields) - set(required_fields)) read_only_fields = list(set(fields) - set(writeable_fields))
def validate_meta(self, meta): def validate_meta(self, meta):
_meta = self.instance.meta if self.instance else {} _meta = self.instance.meta if self.instance else {}
_meta.update(meta) _meta.update(meta)
return _meta return _meta
@staticmethod
def validate_action(action):
return const.TicketActionChoices.approve.value
class TicketRejectSerializer(TicketSerializer):
meta = MethodSerializer(read_only=True)
class Meta:
model = Ticket
fields = TicketSerializer.Meta.fields
read_only_fields = fields
def validate_action(self, action):
return const.TicketActionChoices.reject.value
class TicketCloseSerializer(TicketSerializer):
meta = MethodSerializer(read_only=True)
class Meta:
model = Ticket
fields = TicketSerializer.Meta.fields
read_only_fields = fields
def validate_action(self, action):
return const.TicketActionChoices.close.value

4
apps/tickets/signals.py Normal file
View File

@ -0,0 +1,4 @@
from django.dispatch import Signal
post_change_ticket_action = Signal()

View File

@ -1,61 +0,0 @@
# -*- coding: utf-8 -*-
#
from django.dispatch import receiver
from django.db.models.signals import m2m_changed, post_save, pre_save
from common.utils import get_logger
from .models import Ticket, Comment
from .utils import (
send_ticket_applied_mail_to_assignees,
send_ticket_processed_mail_to_applicant
)
from . import const
logger = get_logger(__name__)
@receiver(pre_save, sender=Ticket)
def on_ticket_pre_save(sender, instance=None, **kwargs):
instance.set_display_fields()
if instance.has_processed:
instance.set_status_closed()
@receiver(post_save, sender=Ticket)
def on_ticket_post_save(sender, instance=None, created=False, **kwargs):
if created and instance.action_open:
instance.create_action_comment()
instance.create_applied_comment()
if not created and instance.has_processed:
instance.create_action_comment()
msg = 'Ticket () has processed, send mail to applicant: {}'
logger.debug(msg.format(instance.title, instance.applicant_display))
send_ticket_processed_mail_to_applicant(instance)
if instance.action_approve:
instance.create_permission()
instance.create_approved_comment()
@receiver(m2m_changed, sender=Ticket.assignees.through)
def on_ticket_assignees_changed(sender, instance=None, action=None, reverse=False, model=None, pk_set=None, **kwargs):
if reverse:
return
if action != 'post_add':
return
ticket = instance
logger.debug('Receives ticket and assignees changed signal, ticket: {}'.format(ticket.title))
ticket.set_assignees_display()
ticket.save()
assignees = model.objects.filter(pk__in=pk_set)
assignees_display = [str(assignee) for assignee in assignees]
logger.debug('Send applied email to assignees: {}'.format(assignees_display))
send_ticket_applied_mail_to_assignees(ticket, assignees)
@receiver(pre_save, sender=Comment)
def on_comment_create(sender, instance=None, created=False, **kwargs):
instance.set_display_fields()

View File

@ -0,0 +1,2 @@
from .ticket import *
from .comment import *

View File

@ -0,0 +1,9 @@
from django.db.models.signals import pre_save
from django.dispatch import receiver
from ..models import Comment
@receiver(pre_save, sender=Comment)
def on_comment_pre_save(sender, instance, **kwargs):
instance.set_display_fields()

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
#
from django.dispatch import receiver
from django.db.models.signals import m2m_changed
from common.utils import get_logger
from tickets.models import Ticket
from tickets.utils import send_ticket_applied_mail_to_assignees
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)
@receiver(m2m_changed, sender=Ticket.assignees.through)
def on_ticket_assignees_changed(sender, instance, action, reverse, model, pk_set, **kwargs):
if reverse:
return
if action != 'post_add':
return
logger.debug('Receives ticket and assignees changed signal, ticket: {}'.format(instance.title))
instance.assignees_display = [str(assignee) for assignee in instance.assignees.all()]
instance.save()
assignees = model.objects.filter(pk__in=pk_set)
assignees_display = [str(assignee) for assignee in assignees]
logger.debug('Send applied email to assignees: {}'.format(assignees_display))
send_ticket_applied_mail_to_assignees(instance, assignees)

View File

@ -20,7 +20,7 @@ def get_model_field_verbose_name(model, field_name):
return field_verbose_name return field_verbose_name
def convert_model_instance_data_field_name_to_verbose_name(model, data): def convert_model_data_field_name_to_verbose_name(model, data):
if isinstance(data, dict): if isinstance(data, dict):
data = [data] data = [data]
converted_data = [ converted_data = [