reactor&feat: 重构工单模块 & 支持申请应用工单 (#5352)

* reactor: 修改工单Model,添加工单迁移文件

* reactor: 修改工单Model,添加工单迁移文件

* reactor: 重构工单模块

* reactor: 重构工单模块2

* reactor: 重构工单模块3

* reactor: 重构工单模块4

* reactor: 重构工单模块5

* reactor: 重构工单模块6

* reactor: 重构工单模块7

* reactor: 重构工单模块8

* reactor: 重构工单模块9

* reactor: 重构工单模块10

* reactor: 重构工单模块11

* reactor: 重构工单模块12

* reactor: 重构工单模块13

* reactor: 重构工单模块14

* reactor: 重构工单模块15

* reactor: 重构工单模块16

* reactor: 重构工单模块17

* reactor: 重构工单模块18

* reactor: 重构工单模块19

* reactor: 重构工单模块20

* reactor: 重构工单模块21

* reactor: 重构工单模块22

* reactor: 重构工单模块23

* reactor: 重构工单模块24

* reactor: 重构工单模块25

* reactor: 重构工单模块26

* reactor: 重构工单模块27

* reactor: 重构工单模块28

* reactor: 重构工单模块29

* reactor: 重构工单模块30

* reactor: 重构工单模块31

* reactor: 重构工单模块32

* reactor: 重构工单模块33

* reactor: 重构工单模块34

* reactor: 重构工单模块35

* reactor: 重构工单模块36

* reactor: 重构工单模块37

* reactor: 重构工单模块38

* reactor: 重构工单模块39
This commit is contained in:
Jiangjie.Bai
2020-12-30 00:19:59 +08:00
committed by GitHub
parent 9d4f1a01fd
commit 3b056ff953
54 changed files with 1585 additions and 948 deletions

View File

@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
#
from .ticket import *
from .comment import *

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
#
from django.db import models
from django.utils.translation import ugettext_lazy as _
from common.mixins.models import CommonModelMixin
__all__ = ['Comment']
class Comment(CommonModelMixin):
ticket = models.ForeignKey(
'tickets.Ticket', on_delete=models.CASCADE, related_name='comments'
)
user = models.ForeignKey(
'users.User', on_delete=models.SET_NULL, null=True, related_name='comments',
verbose_name=_("User")
)
user_display = models.CharField(max_length=256, verbose_name=_("User display name"))
body = models.TextField(verbose_name=_("Body"))
class Meta:
ordering = ('date_created', )

View File

@@ -1,141 +0,0 @@
# -*- coding: utf-8 -*-
#
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from common.db.models import ChoiceSet
from common.mixins.models import CommonModelMixin
from common.fields.model import JsonDictTextField
from orgs.mixins.models import OrgModelMixin
__all__ = ['Ticket', 'Comment']
class Ticket(OrgModelMixin, CommonModelMixin):
class STATUS(ChoiceSet):
OPEN = 'open', _("Open")
CLOSED = 'closed', _("Closed")
class TYPE(ChoiceSet):
GENERAL = 'general', _("General")
LOGIN_CONFIRM = 'login_confirm', _("Login confirm")
REQUEST_ASSET_PERM = 'request_asset', _('Request asset permission')
class ACTION(ChoiceSet):
APPROVE = 'approve', _('Approve')
REJECT = 'reject', _('Reject')
user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_requested', verbose_name=_("User"))
user_display = models.CharField(max_length=128, verbose_name=_("User display name"))
title = models.CharField(max_length=256, verbose_name=_("Title"))
body = models.TextField(verbose_name=_("Body"))
meta = JsonDictTextField(verbose_name=_("Meta"), default='{}')
assignee = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='%(class)s_handled', verbose_name=_("Assignee"))
assignee_display = models.CharField(max_length=128, blank=True, null=True, verbose_name=_("Assignee display name"), default='')
assignees = models.ManyToManyField('users.User', related_name='%(class)s_assigned', verbose_name=_("Assignees"))
assignees_display = models.CharField(max_length=128, verbose_name=_("Assignees display name"), blank=True)
type = models.CharField(max_length=16, choices=TYPE.choices, default=TYPE.GENERAL, verbose_name=_("Type"))
status = models.CharField(choices=STATUS.choices, max_length=16, default='open')
action = models.CharField(choices=ACTION.choices, max_length=16, default='', blank=True)
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
origin_objects = models.Manager()
def __str__(self):
return '{}: {}'.format(self.user_display, self.title)
@property
def body_as_html(self):
return self.body.replace('\n', '<br/>')
@property
def status_display(self):
return self.get_status_display()
@property
def type_display(self):
return self.get_type_display()
@property
def action_display(self):
return self.get_action_display()
def create_status_comment(self, status, user):
if status == self.STATUS.CLOSED:
action = _("Close")
else:
action = _("Open")
body = _('{} {} this ticket').format(self.user, action)
self.comments.create(user=user, body=body)
def perform_status(self, status, user, extra_comment=None):
self.create_comment(
self.STATUS.get(status),
user,
extra_comment
)
self.status = status
self.assignee = user
self.save()
def create_comment(self, action_display, user, extra_comment=None):
body = '{} {} {}'.format(user, action_display, _("this ticket"))
if extra_comment is not None:
body += extra_comment
self.comments.create(body=body, user=user, user_display=str(user))
def perform_action(self, action, user, extra_comment=None):
self.create_comment(
self.ACTION.get(action),
user,
extra_comment
)
self.action = action
self.status = self.STATUS.CLOSED
self.assignee = user
self.save()
def is_assignee(self, user):
return self.assignees.filter(id=user.id).exists()
def is_user(self, user):
return self.user == user
@classmethod
def get_related_tickets(cls, user, queryset=None):
if queryset is None:
queryset = cls.objects.all()
queryset = queryset.filter(
Q(assignees=user) | Q(user=user)
).distinct()
return queryset
@classmethod
def get_assigned_tickets(cls, user, queryset=None):
if queryset is None:
queryset = cls.objects.all()
queryset = queryset.filter(assignees=user)
return queryset
@classmethod
def get_my_tickets(cls, user, queryset=None):
if queryset is None:
queryset = cls.objects.all()
queryset = queryset.filter(user=user)
return queryset
class Meta:
ordering = ('-date_created',)
class Comment(CommonModelMixin):
ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE, related_name='comments')
user = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, verbose_name=_("User"), related_name='comments')
user_display = models.CharField(max_length=128, verbose_name=_("User display name"))
body = models.TextField(verbose_name=_("Body"))
class Meta:
ordering = ('date_created', )

View File

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

View File

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

View File

@@ -0,0 +1,100 @@
from django.utils.translation import ugettext as __
from orgs.utils import tmp_to_org, tmp_to_root_org
from applications.models import Application, Category
from assets.models import SystemUser
from perms.models import ApplicationPermission
class ConstructBodyMixin:
def construct_apply_application_applied_body(self):
apply_category = self.meta['apply_category']
apply_category_display = dict(Category.choices)[apply_category]
apply_type = self.meta['apply_type']
apply_type_display = dict(Category.get_type_choices(apply_category))[apply_type]
apply_application_group = self.meta['apply_application_group']
apply_system_user_group = self.meta['apply_system_user_group']
apply_date_start = self.meta['apply_date_start']
apply_date_expired = self.meta['apply_date_expired']
applied_body = '''{}: {},
{}: {},
{}: {},
{}: {},
{}: {},
{}: {},
'''.format(
__('Applied category'), apply_category_display,
__('Applied type'), apply_type_display,
__('Applied application group'), apply_application_group,
__('Applied system user group'), apply_system_user_group,
__('Applied date start'), apply_date_start,
__('Applied date expired'), apply_date_expired,
)
return applied_body
def construct_apply_application_approved_body(self):
# 审批信息
approve_applications_id = self.meta['approve_applications']
approve_system_users_id = self.meta['approve_system_users']
with tmp_to_org(self.org_id):
approve_applications = Application.objects.filter(id__in=approve_applications_id)
approve_system_users = SystemUser.objects.filter(id__in=approve_system_users_id)
approve_applications_display = [str(application) for application in approve_applications]
approve_system_users_display = [str(system_user) for system_user in approve_system_users]
approve_date_start = self.meta['approve_date_start']
approve_date_expired = self.meta['approve_date_expired']
approved_body = '''{}: {},
{}: {},
{}: {},
{}: {},
'''.format(
__('Approved applications'), ', '.join(approve_applications_display),
__('Approved system users'), ', '.join(approve_system_users_display),
__('Approved date start'), approve_date_start,
__('Approved date expired'), approve_date_expired
)
return approved_body
class CreatePermissionMixin:
def create_apply_application_permission(self):
with tmp_to_root_org():
application_permission = ApplicationPermission.objects.filter(id=self.id).first()
if application_permission:
return application_permission
apply_category = self.meta['apply_category']
apply_type = self.meta['apply_type']
approved_applications_id = self.meta['approve_applications']
approve_system_users_id = self.meta['approve_system_users']
approve_date_start = self.meta['approve_date_start']
approve_date_expired = self.meta['approve_date_expired']
permission_name = '{}({})'.format(
__('Created by ticket ({})'.format(self.title)), str(self.id)[:4]
)
permission_comment = __(
'Created by the ticket, '
'ticket title: {}, '
'ticket applicant: {}, '
'ticket processor: {}, '
'ticket ID: {}'
''.format(self.title, self.applicant_display, self.processor_display, str(self.id))
)
permissions_data = {
'id': self.id,
'name': permission_name,
'category': apply_category,
'type': apply_type,
'comment': permission_comment,
'created_by': self.processor_display,
'date_start': approve_date_start,
'date_expired': approve_date_expired,
}
with tmp_to_org(self.org_id):
application_permission = ApplicationPermission.objects.create(**permissions_data)
application_permission.users.add(self.applicant)
application_permission.applications.set(approved_applications_id)
application_permission.system_users.set(approve_system_users_id)
return application_permission

View File

@@ -0,0 +1,99 @@
from django.utils.translation import ugettext as __
from perms.models import AssetPermission, Action
from assets.models import Asset, SystemUser
from orgs.utils import tmp_to_org, tmp_to_root_org
class ConstructBodyMixin:
def construct_apply_asset_applied_body(self):
apply_ip_group = self.meta['apply_ip_group']
apply_hostname_group = self.meta['apply_hostname_group']
apply_system_user_group = self.meta['apply_system_user_group']
apply_actions = self.meta['apply_actions']
apply_actions_display = Action.value_to_choices_display(apply_actions)
apply_actions_display = [str(action_display) for action_display in apply_actions_display]
apply_date_start = self.meta['apply_date_start']
apply_date_expired = self.meta['apply_date_expired']
applied_body = '''{}: {},
{}: {},
{}: {},
{}: {},
{}: {}
'''.format(
__('Applied IP group'), apply_ip_group,
__("Applied hostname group"), apply_hostname_group,
__("Applied system user group"), apply_system_user_group,
__("Applied actions"), apply_actions_display,
__('Applied date start'), apply_date_start,
__('Applied date expired'), apply_date_expired,
)
return applied_body
def construct_apply_asset_approved_body(self):
approve_assets_id = self.meta['approve_assets']
approve_system_users_id = self.meta['approve_system_users']
with tmp_to_org(self.org_id):
approve_assets = Asset.objects.filter(id__in=approve_assets_id)
approve_system_users = SystemUser.objects.filter(id__in=approve_system_users_id)
approve_assets_display = [str(asset) for asset in approve_assets]
approve_system_users_display = [str(system_user) for system_user in approve_system_users]
approve_actions = self.meta['approve_actions']
approve_actions_display = Action.value_to_choices_display(approve_actions)
approve_actions_display = [str(action_display) for action_display in approve_actions_display]
approve_date_start = self.meta['approve_date_start']
approve_date_expired = self.meta['approve_date_expired']
approved_body = '''{}: {},
{}: {},
{}: {},
{}: {},
{}: {}
'''.format(
__('Approved assets'), ', '.join(approve_assets_display),
__('Approved system users'), ', '.join(approve_system_users_display),
__('Approved actions'), ', '.join(approve_actions_display),
__('Approved date start'), approve_date_start,
__('Approved date expired'), approve_date_expired,
)
return approved_body
class CreatePermissionMixin:
def create_apply_asset_permission(self):
with tmp_to_root_org():
asset_permission = AssetPermission.objects.filter(id=self.id).first()
if asset_permission:
return asset_permission
approve_assets_id = self.meta['approve_assets']
approve_system_users_id = self.meta['approve_system_users']
approve_actions = self.meta['approve_actions']
approve_date_start = self.meta['approve_date_start']
approve_date_expired = self.meta['approve_date_expired']
permission_name = '{}({})'.format(
__('Created by ticket ({})'.format(self.title)), str(self.id)[:4]
)
permission_comment = __(
'Created by the ticket, '
'ticket title: {}, '
'ticket applicant: {}, '
'ticket processor: {}, '
'ticket ID: {}'
''.format(self.title, self.applicant_display, self.processor_display, str(self.id))
)
permission_data = {
'id': self.id,
'name': permission_name,
'comment': permission_comment,
'created_by': self.processor_display,
'actions': approve_actions,
'date_start': approve_date_start,
'date_expired': approve_date_expired,
}
with tmp_to_org(self.org_id):
asset_permission = AssetPermission.objects.create(**permission_data)
asset_permission.users.add(self.applicant)
asset_permission.assets.set(approve_assets_id)
asset_permission.system_users.set(approve_system_users_id)
return asset_permission

View File

@@ -0,0 +1,100 @@
import textwrap
from django.utils.translation import ugettext as __
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.is_approved:
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)
create_method()
class CreateCommentMixin:
def create_comment(self, comment_body):
comment_data = {
'body': comment_body,
'user': self.processor,
'user_display': self.processor_display
}
return self.comments.create(**comment_data)
def create_approved_comment(self):
comment_body = self.construct_approved_body()
# 页面展示需要取消缩进
comment_body = textwrap.dedent(comment_body)
self.create_comment(comment_body)
def create_action_comment(self):
comment_body = __(
'User {} {} the ticket'.format(self.processor_display, self.get_action_display())
)
self.create_comment(comment_body)

View File

@@ -0,0 +1,18 @@
from django.utils.translation import ugettext as __
class ConstructBodyMixin:
def construct_login_confirm_applied_body(self):
apply_login_ip = self.meta['apply_login_ip']
apply_login_city = self.meta['apply_login_city']
apply_login_datetime = self.meta['apply_login_datetime']
applied_body = '''{}: {},
{}: {},
{}: {}
'''.format(
__("Applied login IP"), apply_login_ip,
__("Applied login city"), apply_login_city,
__("Applied login datetime"), apply_login_datetime,
)
return applied_body

View File

@@ -0,0 +1,32 @@
from . import base, apply_asset, apply_application, login_confirm
__all__ = ['TicketModelMixin']
class TicketConstructBodyMixin(
base.ConstructBodyMixin,
apply_asset.ConstructBodyMixin,
apply_application.ConstructBodyMixin,
login_confirm.ConstructBodyMixin
):
pass
class TicketCreatePermissionMixin(
base.CreatePermissionMixin,
apply_asset.CreatePermissionMixin,
apply_application.CreatePermissionMixin
):
pass
class TicketCreateCommentMixin(
base.CreateCommentMixin
):
pass
class TicketModelMixin(
TicketConstructBodyMixin, TicketCreatePermissionMixin, TicketCreateCommentMixin
):
pass

View File

@@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
#
import json
import uuid
from datetime import datetime
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from common.mixins.models import CommonModelMixin
from orgs.mixins.models import OrgModelMixin
from orgs.utils import tmp_to_root_org, tmp_to_org
from tickets import const
from .mixin import TicketModelMixin
__all__ = ['Ticket']
class ModelJSONFieldEncoder(json.JSONEncoder):
""" 解决一些类型的字段不能序列化的问题 """
def default(self, obj):
if isinstance(obj, datetime):
return obj.strftime(settings.DATETIME_DISPLAY_FORMAT)
if isinstance(obj, uuid.UUID):
return str(obj)
else:
return super().default(obj)
class Ticket(TicketModelMixin, CommonModelMixin, OrgModelMixin):
title = models.CharField(max_length=256, verbose_name=_("Title"))
type = models.CharField(
max_length=64, choices=const.TicketTypeChoices.choices,
default=const.TicketTypeChoices.general.value, verbose_name=_("Type")
)
meta = models.JSONField(encoder=ModelJSONFieldEncoder, verbose_name=_("Meta"))
action = models.CharField(
choices=const.TicketActionChoices.choices, max_length=16,
default=const.TicketActionChoices.apply.value, verbose_name=_("Action")
)
status = models.CharField(
max_length=16, choices=const.TicketStatusChoices.choices,
default=const.TicketStatusChoices.open.value, verbose_name=_("Status")
)
# 申请人
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='No', verbose_name=_("Applicant display")
)
# 处理人
processor = models.ForeignKey(
'users.User', related_name='processed_tickets', on_delete=models.SET_NULL, null=True,
verbose_name=_("Processor")
)
processor_display = models.CharField(
max_length=256, blank=True, null=True, default='No', verbose_name=_("Processor display")
)
# 受理人列表
assignees = models.ManyToManyField(
'users.User', related_name='assigned_tickets', verbose_name=_("Assignees")
)
assignees_display = models.TextField(
blank=True, default='No', verbose_name=_("Assignees display")
)
# 评论
comment = models.TextField(default='', blank=True, verbose_name=_('Comment'))
class Meta:
ordering = ('-date_created',)
def __str__(self):
return '{}({})'.format(self.title, self.applicant_display)
def has_assignee(self, assignee):
return self.assignees.filter(id=assignee.id).exists()
# status
@property
def status_closed(self):
return self.status == const.TicketStatusChoices.closed.value
@property
def status_open(self):
return self.status == const.TicketStatusChoices.open.value
# action
@property
def is_applied(self):
return self.action == const.TicketActionChoices.apply.value
@property
def is_approved(self):
return self.action == const.TicketActionChoices.approve.value
@property
def is_rejected(self):
return self.action == const.TicketActionChoices.reject.value
@property
def is_closed(self):
return self.action == const.TicketActionChoices.close.value
@property
def is_processed(self):
return self.is_approved or self.is_rejected or self.is_closed
# perform action
def close(self, processor):
self.processor = processor
self.action = const.TicketActionChoices.close.value
self.save()
# tickets
@classmethod
def all(cls):
with tmp_to_root_org():
return Ticket.objects.all()
@classmethod
def get_user_related_tickets(cls, user):
queries = None
tickets = cls.all()
if user.is_superuser:
pass
elif user.is_super_auditor:
pass
elif user.is_org_admin:
admin_orgs_id = [
str(org_id) for org_id in user.admin_orgs.values_list('id', flat=True)
]
assigned_tickets_id = [
str(ticket_id) for ticket_id in user.assigned_tickets.values_list('id', flat=True)
]
queries = Q(applicant=user)
queries |= Q(processor=user)
queries |= Q(org_id__in=admin_orgs_id)
queries |= Q(id__in=assigned_tickets_id)
elif user.is_org_auditor:
audit_orgs_id = [
str(org_id) for org_id in user.audit_orgs.values_list('id', flat=True)
]
queries = Q(org_id__in=audit_orgs_id)
elif user.is_common_user:
queries = Q(applicant=user)
else:
tickets = cls.objects.none()
if queries:
tickets = tickets.filter(queries)
return tickets.distinct()
def save(self, *args, **kwargs):
with tmp_to_org(self.org_id):
# 确保保存的org_id的是自身的值
return super().save(*args, **kwargs)