feat: 工单多级审批 + 模版创建 (#6640)

* feat: 工单多级审批 + 模版创建

* feat: 工单权限处理

* fix: 工单关闭后 再审批bug

* perf: 修改一点

Co-authored-by: feng626 <1304903146@qq.com>
Co-authored-by: ibuler <ibuler@qq.com>
pull/6730/head
fit2bot 2021-08-25 19:02:50 +08:00 committed by GitHub
parent 1fdc558ef7
commit 0f87f05b3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 897 additions and 590 deletions

View File

@ -83,11 +83,11 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
@classmethod @classmethod
def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id): def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id):
from tickets.const import TicketTypeChoices from tickets.const import TicketType
from tickets.models import Ticket from tickets.models import Ticket
data = { data = {
'title': _('Login asset confirm') + ' ({})'.format(user), 'title': _('Login asset confirm') + ' ({})'.format(user),
'type': TicketTypeChoices.login_asset_confirm, 'type': TicketType.login_asset_confirm,
'meta': { 'meta': {
'apply_login_user': str(user), 'apply_login_user': str(user),
'apply_login_asset': str(asset), 'apply_login_asset': str(asset),
@ -96,7 +96,7 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
'org_id': org_id, 'org_id': org_id,
} }
ticket = Ticket.objects.create(**data) ticket = Ticket.objects.create(**data)
ticket.assignees.set(assignees) ticket.create_process_map_and_node(assignees)
ticket.open(applicant=user) ticket.open(applicant=user)
return ticket return ticket

View File

@ -105,11 +105,11 @@ class CommandFilterRule(OrgModelMixin):
return '{} % {}'.format(self.type, self.content) return '{} % {}'.format(self.type, self.content)
def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id): def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
from tickets.const import TicketTypeChoices from tickets.const import TicketType
from tickets.models import Ticket from tickets.models import Ticket
data = { data = {
'title': _('Command confirm') + ' ({})'.format(session.user), 'title': _('Command confirm') + ' ({})'.format(session.user),
'type': TicketTypeChoices.command_confirm, 'type': TicketType.command_confirm,
'meta': { 'meta': {
'apply_run_user': session.user, 'apply_run_user': session.user,
'apply_run_asset': session.asset, 'apply_run_asset': session.asset,
@ -122,6 +122,6 @@ class CommandFilterRule(OrgModelMixin):
'org_id': org_id, 'org_id': org_id,
} }
ticket = Ticket.objects.create(**data) ticket = Ticket.objects.create(**data)
ticket.assignees.set(self.reviewers.all()) ticket.create_process_map_and_node(self.reviewers.all())
ticket.open(applicant=session.user_obj) ticket.open(applicant=session.user_obj)
return ticket return ticket

View File

@ -363,14 +363,14 @@ class AuthMixin:
raise errors.LoginConfirmOtherError('', "Not found") raise errors.LoginConfirmOtherError('', "Not found")
if ticket.status_open: if ticket.status_open:
raise errors.LoginConfirmWaitError(ticket.id) raise errors.LoginConfirmWaitError(ticket.id)
elif ticket.action_approve: elif ticket.state_approve:
self.request.session["auth_confirm"] = "1" self.request.session["auth_confirm"] = "1"
return return
elif ticket.action_reject: elif ticket.state_reject:
raise errors.LoginConfirmOtherError( raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_action_display() ticket.id, ticket.get_action_display()
) )
elif ticket.action_close: elif ticket.state_close:
raise errors.LoginConfirmOtherError( raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_action_display() ticket.id, ticket.get_action_display()
) )

View File

@ -71,15 +71,14 @@ class LoginConfirmSetting(CommonModelMixin):
from orgs.models import Organization from orgs.models import Organization
ticket_title = _('Login confirm') + ' {}'.format(self.user) ticket_title = _('Login confirm') + ' {}'.format(self.user)
ticket_meta = self.construct_confirm_ticket_meta(request) ticket_meta = self.construct_confirm_ticket_meta(request)
ticket_assignees = self.reviewers.all()
data = { data = {
'title': ticket_title, 'title': ticket_title,
'type': const.TicketTypeChoices.login_confirm.value, 'type': const.TicketType.login_confirm.value,
'meta': ticket_meta, 'meta': ticket_meta,
'org_id': Organization.ROOT_ID, 'org_id': Organization.ROOT_ID,
} }
ticket = Ticket.objects.create(**data) ticket = Ticket.objects.create(**data)
ticket.assignees.set(ticket_assignees) ticket.create_process_map_and_node(self.reviewers.all())
ticket.open(self.user) ticket.open(self.user)
return ticket return ticket

20
apps/common/db/encoder.py Normal file
View File

@ -0,0 +1,20 @@
import json
from datetime import datetime
import uuid
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
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)
if isinstance(obj, type(_("ugettext_lazy"))):
return str(obj)
else:
return super().default(obj)

View File

@ -19,6 +19,7 @@ __all__ = [
class OrgManager(models.Manager): class OrgManager(models.Manager):
def all_group_by_org(self): def all_group_by_org(self):
from ..models import Organization from ..models import Organization
orgs = list(Organization.objects.all()) orgs = list(Organization.objects.all())

View File

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from orgs.models import Organization
from rest_framework import viewsets from rest_framework import viewsets
from common.permissions import IsValidUser from common.permissions import IsValidUser
from common.exceptions import JMSException from common.exceptions import JMSException
from users.models import User from users.models import User
from orgs.models import Organization
from .. import serializers from .. import serializers
@ -15,8 +15,7 @@ class AssigneeViewSet(viewsets.ReadOnlyModelViewSet):
filterset_fields = ('id', 'name', 'username', 'email', 'source') filterset_fields = ('id', 'name', 'username', 'email', 'source')
search_fields = filterset_fields search_fields = filterset_fields
def get_org(self): def get_org(self, org_id):
org_id = self.request.query_params.get('org_id')
org = Organization.get_instance(org_id) org = Organization.get_instance(org_id)
if not org: if not org:
error = ('The organization `{}` does not exist'.format(org_id)) error = ('The organization `{}` does not exist'.format(org_id))
@ -24,6 +23,13 @@ class AssigneeViewSet(viewsets.ReadOnlyModelViewSet):
return org return org
def get_queryset(self): def get_queryset(self):
org = self.get_org() org_id = self.request.query_params.get('org_id')
queryset = User.get_super_and_org_admins(org=org) type = self.request.query_params.get('type')
if type == 'super':
queryset = User.get_super_admins()
elif type == 'super_admin':
org = self.get_org(org_id)
queryset = User.get_super_and_org_admins(org=org)
else:
queryset = User.objects.all()
return queryset return queryset

View File

@ -15,16 +15,16 @@ class GenericTicketStatusRetrieveCloseAPI(RetrieveDestroyAPIView):
permission_classes = (IsAppUser, ) permission_classes = (IsAppUser, )
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
if self.ticket.action_open: if self.ticket.state_open:
status = 'await' status = 'await'
elif self.ticket.action_approve: elif self.ticket.state_approve:
status = 'approve' status = 'approved'
else: else:
status = 'reject' status = 'rejected'
data = { data = {
'status': status, 'status': status,
'action': self.ticket.action, 'action': self.ticket.state,
'processor': self.ticket.processor_display 'processor': str(self.ticket.processor)
} }
return Response(data=data, status=200) return Response(data=data, status=200)
@ -32,9 +32,9 @@ class GenericTicketStatusRetrieveCloseAPI(RetrieveDestroyAPIView):
if self.ticket.status_open: if self.ticket.status_open:
self.ticket.close(processor=self.ticket.applicant) self.ticket.close(processor=self.ticket.applicant)
data = { data = {
'action': self.ticket.action, 'action': self.ticket.state,
'status': self.ticket.status, 'status': self.ticket.status,
'processor': self.ticket.processor_display 'processor': str(self.ticket.processor)
} }
return Response(data=data, status=200) return Response(data=data, status=200)

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext_lazy as _
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
@ -8,14 +7,15 @@ 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
from common.permissions import IsValidUser, IsOrgAdmin from common.permissions import IsValidUser, IsOrgAdmin, IsSuperUser
from common.drf.api import JMSBulkModelViewSet
from tickets import serializers from tickets import serializers
from tickets.models import Ticket from tickets.models import Ticket, TicketFlow
from tickets.permissions.ticket import IsAssignee, IsAssigneeOrApplicant, NotClosed from tickets.filters import TicketFilter
from tickets.permissions.ticket import IsAssignee, IsApplicant
__all__ = ['TicketViewSet', 'TicketFlowViewSet']
__all__ = ['TicketViewSet']
class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet): class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
@ -25,12 +25,9 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
'open': serializers.TicketApplySerializer, 'open': serializers.TicketApplySerializer,
'approve': serializers.TicketApproveSerializer, 'approve': serializers.TicketApproveSerializer,
} }
filterset_fields = [ filterset_class = TicketFilter
'id', 'title', 'type', 'action', 'status', 'applicant', 'applicant_display', 'processor',
'processor_display', 'assignees__id'
]
search_fields = [ search_fields = [
'title', 'action', 'type', 'status', 'applicant_display', 'processor_display' 'title', 'action', 'type', 'status', 'applicant_display'
] ]
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
@ -48,6 +45,8 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
def perform_create(self, serializer): def perform_create(self, serializer):
instance = serializer.save() instance = serializer.save()
instance.create_related_node()
instance.process_map = instance.create_process_map()
instance.open(applicant=self.request.user) instance.open(applicant=self.request.user)
@action(detail=False, methods=[POST], permission_classes=[IsValidUser, ]) @action(detail=False, methods=[POST], permission_classes=[IsValidUser, ])
@ -57,24 +56,46 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
@action(detail=True, methods=[PUT], permission_classes=[IsAssignee, ]) @action(detail=True, methods=[PUT], permission_classes=[IsAssignee, ])
def approve(self, request, *args, **kwargs): def approve(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
if instance.status_closed: serializer = self.get_serializer(instance)
return Response(data={"error": _("Ticket already closed")}, status=400) instance.approve(processor=request.user)
response = super().update(request, *args, **kwargs) return Response(serializer.data)
self.get_object().approve(processor=self.request.user)
return response
@action(detail=True, methods=[PUT], permission_classes=[IsAssignee, ]) @action(detail=True, methods=[PUT], permission_classes=[IsAssignee, ])
def reject(self, request, *args, **kwargs): def reject(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
if instance.status_closed:
return Response(data={"error": _("Ticket already closed")}, status=400)
serializer = self.get_serializer(instance) serializer = self.get_serializer(instance)
instance.reject(processor=request.user) instance.reject(processor=request.user)
return Response(serializer.data) return Response(serializer.data)
@action(detail=True, methods=[PUT], permission_classes=[IsAssigneeOrApplicant, NotClosed]) @action(detail=True, methods=[PUT], permission_classes=[IsApplicant, ])
def close(self, request, *args, **kwargs): def close(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
serializer = self.get_serializer(instance) serializer = self.get_serializer(instance)
instance.close(processor=request.user) instance.close(processor=request.user)
return Response(serializer.data) return Response(serializer.data)
class TicketFlowViewSet(JMSBulkModelViewSet):
permission_classes = (IsOrgAdmin, IsSuperUser)
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()
instance.rules.model.change_assignees_display(instance.rules.all())
def perform_create(self, serializer):
self.perform_create_or_update(serializer)
def perform_update(self, serializer):
self.perform_create_or_update(serializer)

View File

@ -1,10 +1,10 @@
from django.db.models import TextChoices from django.db.models import TextChoices, IntegerChoices
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
TICKET_DETAIL_URL = '/ui/#/tickets/tickets/{id}' TICKET_DETAIL_URL = '/ui/#/tickets/tickets/{id}'
class TicketTypeChoices(TextChoices): class TicketType(TextChoices):
general = 'general', _("General") general = 'general', _("General")
login_confirm = 'login_confirm', _("Login confirm") login_confirm = 'login_confirm', _("Login confirm")
apply_asset = 'apply_asset', _('Apply for asset') apply_asset = 'apply_asset', _('Apply for asset')
@ -13,13 +13,38 @@ class TicketTypeChoices(TextChoices):
command_confirm = 'command_confirm', _('Command confirm') command_confirm = 'command_confirm', _('Command confirm')
class TicketActionChoices(TextChoices): class TicketState(TextChoices):
open = 'open', _('Open') open = 'open', _('Open')
approve = 'approve', _('Approve') approved = 'approved', _('Approved')
reject = 'reject', _('Reject') rejected = 'rejected', _('Rejected')
close = 'close', _('Close') closed = 'closed', _('Closed')
class TicketStatusChoices(TextChoices): class ProcessStatus(TextChoices):
notified = 'notified', _('Notified')
approved = 'approved', _('Approved')
rejected = 'rejected', _('Rejected')
class TicketStatus(TextChoices):
open = 'open', _("Open") open = 'open', _("Open")
closed = 'closed', _("Closed") closed = 'closed', _("Closed")
class TicketAction(TextChoices):
open = 'open', _("Open")
close = 'close', _("Close")
approve = 'approve', _('Approve')
reject = 'reject', _('Reject')
class TicketApprovalLevel(IntegerChoices):
one = 1, _("One level")
two = 2, _("Two level")
class TicketApprovalStrategy(TextChoices):
super = 'super', _("Super user")
admin = 'admin', _("Admin user")
super_admin = 'super_admin', _("Super admin user")
custom = 'custom', _("Custom user")

9
apps/tickets/errors.py Normal file
View File

@ -0,0 +1,9 @@
# -*- coding: utf-8 -*-
#
from django.utils.translation import ugettext_lazy as _
from common.exceptions import JMSException
class AlreadyClosed(JMSException):
default_detail = _("Ticket already closed")

18
apps/tickets/filters.py Normal file
View File

@ -0,0 +1,18 @@
from django_filters import rest_framework as filters
from common.drf.filters import BaseFilterSet
from tickets.models import Ticket
class TicketFilter(BaseFilterSet):
assignees__id = filters.UUIDFilter(method='filter_assignees_id')
class Meta:
model = Ticket
fields = (
'id', 'title', 'type', 'status', 'applicant', 'assignees__id',
'applicant_display',
)
def filter_assignees_id(self, queryset, name, value):
return queryset.filter(ticket_steps__ticket_assignees__assignee__id=value)

View File

@ -1,17 +1,19 @@
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from orgs.utils import tmp_to_org, tmp_to_root_org from orgs.utils import tmp_to_org, tmp_to_root_org
from applications.models import Application
from applications.const import AppCategory, AppType from applications.const import AppCategory, AppType
from assets.models import SystemUser from applications.models import Application
from perms.models import ApplicationPermission from perms.models import ApplicationPermission
from assets.models import SystemUser
from .base import BaseHandler from .base import BaseHandler
class Handler(BaseHandler): class Handler(BaseHandler):
def _on_approve(self): def _on_approve(self):
super()._on_approve() is_finished = super()._on_approve()
self._create_application_permission() if is_finished:
self._create_application_permission()
# display # display
def _construct_meta_display_of_open(self): def _construct_meta_display_of_open(self):
@ -22,27 +24,21 @@ class Handler(BaseHandler):
apply_type_display = AppType.get_label(apply_type) apply_type_display = AppType.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 apply_system_users = self.ticket.meta.get('apply_system_users')
apply_applications = self.ticket.meta.get('apply_applications')
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)]
})
def _construct_meta_display_of_approve(self):
meta_display_fields = ['approve_applications_display', 'approve_system_users_display']
approve_application_ids = self.ticket.meta.get('approve_applications', [])
approve_system_user_ids = self.ticket.meta.get('approve_system_users', [])
with tmp_to_org(self.ticket.org_id):
approve_applications = Application.objects.filter(id__in=approve_application_ids)
system_users = SystemUser.objects.filter(id__in=approve_system_user_ids)
approve_applications_display = [str(application) for application in approve_applications]
approve_system_users_display = [str(system_user) for system_user in system_users]
meta_display_values = [approve_applications_display, approve_system_users_display]
meta_display = dict(zip(meta_display_fields, meta_display_values))
return meta_display return meta_display
# body # body
def _construct_meta_body_of_open(self): def _construct_meta_body_of_open(self):
apply_category_display = self.ticket.meta.get('apply_category_display') apply_category_display = self.ticket.meta.get('apply_category_display')
apply_type_display = self.ticket.meta.get('apply_type_display') apply_type_display = self.ticket.meta.get('apply_type_display')
apply_application_group = self.ticket.meta.get('apply_application_group', []) apply_applications = self.ticket.meta.get('apply_applications', [])
apply_system_user_group = self.ticket.meta.get('apply_system_user_group', []) apply_system_users = self.ticket.meta.get('apply_system_users', [])
apply_date_start = self.ticket.meta.get('apply_date_start') apply_date_start = self.ticket.meta.get('apply_date_start')
apply_date_expired = self.ticket.meta.get('apply_date_expired') apply_date_expired = self.ticket.meta.get('apply_date_expired')
applied_body = '''{}: {}, applied_body = '''{}: {},
@ -54,31 +50,13 @@ class Handler(BaseHandler):
'''.format( '''.format(
_('Applied category'), apply_category_display, _('Applied category'), apply_category_display,
_('Applied type'), apply_type_display, _('Applied type'), apply_type_display,
_('Applied application group'), apply_application_group, _('Applied application group'), apply_applications,
_('Applied system user group'), apply_system_user_group, _('Applied system user group'), apply_system_users,
_('Applied date start'), apply_date_start, _('Applied date start'), apply_date_start,
_('Applied date expired'), apply_date_expired, _('Applied date expired'), apply_date_expired,
) )
return applied_body return applied_body
def _construct_meta_body_of_approve(self):
# 审批信息
approve_applications_display = self.ticket.meta.get('approve_applications_display', [])
approve_system_users_display = self.ticket.meta.get('approve_system_users_display', [])
approve_date_start = self.ticket.meta.get('approve_date_start')
approve_date_expired = self.ticket.meta.get('approve_date_expired')
approved_body = '''{}: {},
{}: {},
{}: {},
{}: {},
'''.format(
_('Approved applications'), approve_applications_display,
_('Approved system users'), approve_system_users_display,
_('Approved date start'), approve_date_start,
_('Approved date expired'), approve_date_expired
)
return approved_body
# permission # permission
def _create_application_permission(self): def _create_application_permission(self):
with tmp_to_root_org(): with tmp_to_root_org():
@ -88,11 +66,11 @@ class Handler(BaseHandler):
apply_category = self.ticket.meta.get('apply_category') apply_category = self.ticket.meta.get('apply_category')
apply_type = self.ticket.meta.get('apply_type') apply_type = self.ticket.meta.get('apply_type')
approve_permission_name = self.ticket.meta.get('approve_permission_name', '') apply_permission_name = self.ticket.meta.get('apply_permission_name', '')
approved_application_ids = self.ticket.meta.get('approve_applications', []) apply_applications = self.ticket.meta.get('apply_applications', [])
approve_system_user_ids = self.ticket.meta.get('approve_system_users', []) apply_system_users = self.ticket.meta.get('apply_system_users', [])
approve_date_start = self.ticket.meta.get('approve_date_start') apply_date_start = self.ticket.meta.get('apply_date_start')
approve_date_expired = self.ticket.meta.get('approve_date_expired') apply_date_expired = self.ticket.meta.get('apply_date_expired')
permission_created_by = '{}:{}'.format( permission_created_by = '{}:{}'.format(
str(self.ticket.__class__.__name__), str(self.ticket.id) str(self.ticket.__class__.__name__), str(self.ticket.id)
) )
@ -105,23 +83,23 @@ class Handler(BaseHandler):
).format( ).format(
self.ticket.title, self.ticket.title,
self.ticket.applicant_display, self.ticket.applicant_display,
self.ticket.processor_display, str(self.ticket.processor),
str(self.ticket.id) str(self.ticket.id)
) )
permissions_data = { permissions_data = {
'id': self.ticket.id, 'id': self.ticket.id,
'name': approve_permission_name, 'name': apply_permission_name,
'category': apply_category, 'category': apply_category,
'type': apply_type, 'type': apply_type,
'comment': str(permission_comment), 'comment': str(permission_comment),
'created_by': permission_created_by, 'created_by': permission_created_by,
'date_start': approve_date_start, 'date_start': apply_date_start,
'date_expired': approve_date_expired, 'date_expired': apply_date_expired,
} }
with tmp_to_org(self.ticket.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.ticket.applicant) application_permission.users.add(self.ticket.applicant)
application_permission.applications.set(approved_application_ids) application_permission.applications.set(apply_applications)
application_permission.system_users.set(approve_system_user_ids) application_permission.system_users.set(apply_system_users)
return application_permission return application_permission

View File

@ -1,16 +1,19 @@
from assets.models import Asset
from assets.models import SystemUser
from .base import BaseHandler 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 orgs.utils import tmp_to_org, tmp_to_root_org from orgs.utils import tmp_to_org, tmp_to_root_org
class Handler(BaseHandler): class Handler(BaseHandler):
def _on_approve(self): def _on_approve(self):
super()._on_approve() is_finished = super()._on_approve()
self._create_asset_permission() if is_finished:
self._create_asset_permission()
# display # display
def _construct_meta_display_of_open(self): def _construct_meta_display_of_open(self):
@ -19,32 +22,18 @@ class Handler(BaseHandler):
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 apply_assets = self.ticket.meta.get('apply_assets')
apply_system_users = self.ticket.meta.get('apply_system_users')
def _construct_meta_display_of_approve(self): meta_display.update({
meta_display_fields = [ 'apply_assets_display': [str(i) for i in Asset.objects.filter(id__in=apply_assets)],
'approve_actions_display', 'approve_assets_display', 'approve_system_users_display' 'apply_system_users_display': [str(i)for i in SystemUser.objects.filter(id__in=apply_system_users)]
] })
approve_actions = self.ticket.meta.get('approve_actions', Action.NONE)
approve_actions_display = Action.value_to_choices_display(approve_actions)
approve_asset_ids = self.ticket.meta.get('approve_assets', [])
approve_system_user_ids = self.ticket.meta.get('approve_system_users', [])
with tmp_to_org(self.ticket.org_id):
assets = Asset.objects.filter(id__in=approve_asset_ids)
system_users = SystemUser.objects.filter(id__in=approve_system_user_ids)
approve_assets_display = [str(asset) for asset in assets]
approve_system_users_display = [str(system_user) for system_user in system_users]
meta_display_values = [
approve_actions_display, approve_assets_display, approve_system_users_display
]
meta_display = dict(zip(meta_display_fields, meta_display_values))
return meta_display return meta_display
# body # body
def _construct_meta_body_of_open(self): def _construct_meta_body_of_open(self):
apply_ip_group = self.ticket.meta.get('apply_ip_group', []) apply_assets = self.ticket.meta.get('apply_assets', [])
apply_hostname_group = self.ticket.meta.get('apply_hostname_group', []) apply_system_users = self.ticket.meta.get('apply_system_users', [])
apply_system_user_group = self.ticket.meta.get('apply_system_user_group', [])
apply_actions_display = self.ticket.meta.get('apply_actions_display', []) apply_actions_display = self.ticket.meta.get('apply_actions_display', [])
apply_date_start = self.ticket.meta.get('apply_date_start') apply_date_start = self.ticket.meta.get('apply_date_start')
apply_date_expired = self.ticket.meta.get('apply_date_expired') apply_date_expired = self.ticket.meta.get('apply_date_expired')
@ -54,35 +43,14 @@ class Handler(BaseHandler):
{}: {}, {}: {},
{}: {} {}: {}
'''.format( '''.format(
_('Applied IP group'), apply_ip_group, _("Applied hostname group"), apply_assets,
_("Applied hostname group"), apply_hostname_group, _("Applied system user group"), apply_system_users,
_("Applied system user group"), apply_system_user_group,
_("Applied actions"), apply_actions_display, _("Applied actions"), apply_actions_display,
_('Applied date start'), apply_date_start, _('Applied date start'), apply_date_start,
_('Applied date expired'), apply_date_expired, _('Applied date expired'), apply_date_expired,
) )
return applied_body return applied_body
def _construct_meta_body_of_approve(self):
approve_assets_display = self.ticket.meta.get('approve_assets_display', [])
approve_system_users_display = self.ticket.meta.get('approve_system_users_display', [])
approve_actions_display = self.ticket.meta.get('approve_actions_display', [])
approve_date_start = self.ticket.meta.get('approve_date_start')
approve_date_expired = self.ticket.meta.get('approve_date_expired')
approved_body = '''{}: {},
{}: {},
{}: {},
{}: {},
{}: {}
'''.format(
_('Approved assets'), approve_assets_display,
_('Approved system users'), 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
# permission # permission
def _create_asset_permission(self): def _create_asset_permission(self):
with tmp_to_root_org(): with tmp_to_root_org():
@ -90,12 +58,12 @@ class Handler(BaseHandler):
if asset_permission: if asset_permission:
return asset_permission return asset_permission
approve_permission_name = self.ticket.meta.get('approve_permission_name', ) apply_permission_name = self.ticket.meta.get('apply_permission_name', )
approve_asset_ids = self.ticket.meta.get('approve_assets', []) apply_assets = self.ticket.meta.get('apply_assets', [])
approve_system_user_ids = self.ticket.meta.get('approve_system_users', []) apply_system_users = self.ticket.meta.get('apply_system_users', [])
approve_actions = self.ticket.meta.get('approve_actions', Action.NONE) apply_actions = self.ticket.meta.get('apply_actions', Action.NONE)
approve_date_start = self.ticket.meta.get('approve_date_start') apply_date_start = self.ticket.meta.get('apply_date_start')
approve_date_expired = self.ticket.meta.get('approve_date_expired') apply_date_expired = self.ticket.meta.get('apply_date_expired')
permission_created_by = '{}:{}'.format( permission_created_by = '{}:{}'.format(
str(self.ticket.__class__.__name__), str(self.ticket.id) str(self.ticket.__class__.__name__), str(self.ticket.id)
) )
@ -108,23 +76,23 @@ class Handler(BaseHandler):
).format( ).format(
self.ticket.title, self.ticket.title,
self.ticket.applicant_display, self.ticket.applicant_display,
self.ticket.processor_display, str(self.ticket.processor),
str(self.ticket.id) str(self.ticket.id)
) )
permission_data = { permission_data = {
'id': self.ticket.id, 'id': self.ticket.id,
'name': approve_permission_name, 'name': apply_permission_name,
'comment': str(permission_comment), 'comment': str(permission_comment),
'created_by': permission_created_by, 'created_by': permission_created_by,
'actions': approve_actions, 'actions': apply_actions,
'date_start': approve_date_start, 'date_start': apply_date_start,
'date_expired': approve_date_expired, 'date_expired': apply_date_expired,
} }
with tmp_to_org(self.ticket.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.ticket.applicant) asset_permission.users.add(self.ticket.applicant)
asset_permission.assets.set(approve_asset_ids) asset_permission.assets.set(apply_assets)
asset_permission.system_users.set(approve_system_user_ids) asset_permission.system_users.set(apply_system_users)
return asset_permission return asset_permission

View File

@ -3,7 +3,7 @@ from common.utils import get_logger
from tickets.utils import ( from tickets.utils import (
send_ticket_processed_mail_to_applicant, send_ticket_applied_mail_to_assignees send_ticket_processed_mail_to_applicant, send_ticket_applied_mail_to_assignees
) )
from tickets.const import TicketAction
logger = get_logger(__name__) logger = get_logger(__name__)
@ -16,48 +16,72 @@ class BaseHandler(object):
# on action # on action
def _on_open(self): def _on_open(self):
self.ticket.applicant_display = str(self.ticket.applicant) self.ticket.applicant_display = str(self.ticket.applicant)
self.ticket.assignees_display = [str(assignee) for assignee in self.ticket.assignees.all()]
meta_display = getattr(self, '_construct_meta_display_of_open', lambda: {})() meta_display = getattr(self, '_construct_meta_display_of_open', lambda: {})()
self.ticket.meta.update(meta_display) self.ticket.meta.update(meta_display)
self.ticket.save() self.ticket.save()
self._send_applied_mail_to_assignees() self._send_applied_mail_to_assignees()
def _on_approve(self): def _on_approve(self):
meta_display = getattr(self, '_construct_meta_display_of_approve', lambda: {})() if self.ticket.approval_step != len(self.ticket.process_map):
self.ticket.meta.update(meta_display) self.ticket.approval_step += 1
self.__on_process() self.ticket.create_related_node()
is_finished = False
else:
self.ticket.set_state_approve()
self.ticket.set_status_closed()
is_finished = True
self._send_applied_mail_to_assignees()
self.__on_process(self.ticket.processor)
return is_finished
def _on_reject(self): def _on_reject(self):
self.__on_process() self.ticket.set_state_reject()
self.ticket.set_status_closed()
self.__on_process(self.ticket.processor)
def _on_close(self): def _on_close(self):
self.__on_process() self.ticket.set_state_closed()
def __on_process(self):
self.ticket.processor_display = str(self.ticket.processor)
self.ticket.set_status_closed() self.ticket.set_status_closed()
self._send_processed_mail_to_applicant() self.__on_process(self.ticket.processor)
def __on_process(self, processor):
self._send_processed_mail_to_applicant(processor)
self.ticket.save() self.ticket.save()
def dispatch(self, action): def dispatch(self, action):
self._create_comment_on_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) method = getattr(self, f'_on_{action}', lambda: None)
return method() return method()
# email # email
def _send_applied_mail_to_assignees(self): def _send_applied_mail_to_assignees(self):
logger.debug('Send applied email to assignees: {}'.format(self.ticket.assignees_display)) 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) send_ticket_applied_mail_to_assignees(self.ticket)
def _send_processed_mail_to_applicant(self): def _send_processed_mail_to_applicant(self, processor):
logger.debug('Send processed mail to applicant: {}'.format(self.ticket.applicant_display)) logger.debug('Send processed mail to applicant: {}'.format(self.ticket.applicant_display))
send_ticket_processed_mail_to_applicant(self.ticket) send_ticket_processed_mail_to_applicant(self.ticket, processor)
# comments # comments
def _create_comment_on_action(self): def _create_comment_on_action(self, action):
user = self.ticket.applicant if self.ticket.action_open else self.ticket.processor user = self.ticket.processor
# 打开或关闭工单,备注显示是自己,其他是受理人
if self.ticket.state_open or self.ticket.state_close:
user = self.ticket.applicant
user_display = str(user) user_display = str(user)
action_display = self.ticket.get_action_display() action_display = getattr(TicketAction, action).label
data = { data = {
'body': _('{} {} the ticket').format(user_display, action_display), 'body': _('{} {} the ticket').format(user_display, action_display),
'user': user, 'user': user,
@ -85,18 +109,12 @@ class BaseHandler(object):
{}: {}, {}: {},
{}: {}, {}: {},
{}: {}, {}: {},
{}: {},
{}: {}
'''.format( '''.format(
_('Ticket title'), self.ticket.title, _('Ticket title'), self.ticket.title,
_('Ticket type'), self.ticket.get_type_display(), _('Ticket type'), self.ticket.get_type_display(),
_('Ticket status'), self.ticket.get_status_display(), _('Ticket status'), self.ticket.get_status_display(),
_('Ticket action'), self.ticket.get_action_display(),
_('Ticket applicant'), self.ticket.applicant_display, _('Ticket applicant'), self.ticket.applicant_display,
_('Ticket assignees'), ', '.join(self.ticket.assignees_display),
) )
if self.ticket.status_closed:
basic_body += '''{}: {}'''.format(_('Ticket processor'), self.ticket.processor_display)
body = self.body_html_format.format(_("Ticket basic info"), basic_body) body = self.body_html_format.format(_("Ticket basic info"), basic_body)
return body return body
@ -104,9 +122,6 @@ class BaseHandler(object):
body = '' body = ''
open_body = self._base_construct_meta_body_of_open() open_body = self._base_construct_meta_body_of_open()
body += open_body body += open_body
if self.ticket.action_approve:
approve_body = self._base_construct_meta_body_of_approve()
body += approve_body
return body return body
def _base_construct_meta_body_of_open(self): def _base_construct_meta_body_of_open(self):
@ -115,10 +130,3 @@ class BaseHandler(object):
)() )()
body = self.body_html_format.format(_('Ticket applied info'), meta_body_of_open) body = self.body_html_format.format(_('Ticket applied info'), meta_body_of_open)
return body return body
def _base_construct_meta_body_of_approve(self):
meta_body_of_approve = getattr(
self, '_construct_meta_body_of_approve', lambda: _('No content')
)()
body = self.body_html_format.format(_('Ticket approved info'), meta_body_of_approve)
return body

View File

@ -0,0 +1,235 @@
# Generated by Django 3.1.6 on 2021-08-12 08:18
import common.db.encoder
from django.conf import settings
from django.db import migrations, models, transaction
import django.db.models.deletion
import uuid
from tickets.const import TicketType
ticket_assignee_m2m = list()
def get_ticket_assignee_m2m_info(apps, schema_editor):
ticket_model = apps.get_model("tickets", "Ticket")
for i in ticket_model.objects.only('id', 'assignees', 'action', 'created_by'):
ticket_assignee_m2m.append((i.id, list(i.assignees.values_list('id', flat=True)), i.action, i.created_by))
def update_ticket_process_meta_state_status(apps, schema_editor):
ticket_model = apps.get_model("tickets", "Ticket")
updates = list()
with transaction.atomic():
for instance in ticket_model.objects.all():
if instance.action == 'open':
state = 'notified'
elif instance.action == 'approve':
state = 'approved'
elif instance.action == 'reject':
state = 'rejected'
else:
state = 'closed'
instance.process_map = [{
'state': state,
'approval_level': 1,
'approval_date': str(instance.date_updated),
'processor': instance.processor.id if instance.processor else '',
'processor_display': instance.processor_display if instance.processor_display else '',
'assignees': list(instance.assignees.values_list('id', flat=True)) if instance.assignees else [],
'assignees_display': instance.assignees_display if instance.assignees_display else []
}, ]
instance.state = state
instance.meta['apply_assets'] = instance.meta.pop('approve_assets', [])
instance.meta['apply_assets_display'] = instance.meta.pop('approve_assets_display', [])
instance.meta['apply_actions'] = instance.meta.pop('approve_actions', 0)
instance.meta['apply_actions_display'] = instance.meta.pop('approve_actions_display', [])
instance.meta['apply_applications'] = instance.meta.pop('approve_applications', [])
instance.meta['apply_applications_display'] = instance.meta.pop('approve_applications_display', [])
instance.meta['apply_system_users'] = instance.meta.pop('approve_system_users', [])
instance.meta['apply_system_users_display'] = instance.meta.pop('approve_system_users_display', [])
updates.append(instance)
ticket_model.objects.bulk_update(updates, ['process_map', 'state', 'meta', 'status'])
def create_step_and_assignee(apps, schema_editor):
ticket_step_model = apps.get_model("tickets", "TicketStep")
ticket_assignee_model = apps.get_model("tickets", "TicketAssignee")
creates = list()
with transaction.atomic():
for ticket_id, assignees, action, created_by in ticket_assignee_m2m:
if action == 'open':
state = 'notified'
elif action == 'approve':
state = 'approved'
else:
state = 'rejected'
step_instance = ticket_step_model.objects.create(ticket_id=ticket_id, state=state, created_by=created_by)
for assignee_id in assignees:
creates.append(
ticket_assignee_model(
step=step_instance, assignee_id=assignee_id, state=state, created_by=created_by
)
)
ticket_assignee_model.objects.bulk_create(creates)
def create_ticket_flow_and_approval_rule(apps, schema_editor):
user_model = apps.get_model("users", "User")
org_id = '00000000-0000-0000-0000-000000000000'
ticket_flow_model = apps.get_model("tickets", "TicketFlow")
approval_rule_model = apps.get_model("tickets", "ApprovalRule")
super_user = user_model.objects.filter(role='Admin')
assignees_display = ['{0.name}({0.username})'.format(i) for i in super_user]
with transaction.atomic():
for ticket_type in TicketType.values:
ticket_flow_instance = ticket_flow_model.objects.create(created_by='System',
type=ticket_type, org_id=org_id)
approval_rule_instance = approval_rule_model.objects.create(strategy='super',
assignees_display=assignees_display)
approval_rule_instance.assignees.set(list(super_user))
ticket_flow_instance.rules.set([approval_rule_instance, ])
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('tickets', '0009_auto_20210426_1720'),
]
operations = [
migrations.CreateModel(
name='ApprovalRule',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('level', models.SmallIntegerField(choices=[(1, 'One level'), (2, 'Two level')], default=1,
verbose_name='Approve level')),
('strategy', models.CharField(
choices=[('super', 'Super user'), ('admin', 'Admin user'), ('super_admin', 'Super admin user'),
('custom', 'Custom user')],
default='super', max_length=64, verbose_name='Approve strategy')),
('assignees_display', models.JSONField(default=list, encoder=common.db.encoder.ModelJSONFieldEncoder,
verbose_name='Assignees display')),
('assignees',
models.ManyToManyField(related_name='assigned_ticket_flow_approval_rule', to=settings.AUTH_USER_MODEL,
verbose_name='Assignees')),
],
options={
'verbose_name': 'Ticket flow approval rule',
},
),
migrations.RunPython(get_ticket_assignee_m2m_info),
migrations.AddField(
model_name='ticket',
name='process_map',
field=models.JSONField(default=list, encoder=common.db.encoder.ModelJSONFieldEncoder,
verbose_name='Process'),
),
migrations.AddField(
model_name='ticket',
name='state',
field=models.CharField(
choices=[('open', 'Open'), ('approved', 'Approved'), ('rejected', 'Rejected'), ('closed', 'Closed')],
default='open', max_length=16, verbose_name='State'),
),
migrations.RunPython(update_ticket_process_meta_state_status),
migrations.RemoveField(
model_name='ticket',
name='action',
),
migrations.RemoveField(
model_name='ticket',
name='assignees',
),
migrations.RemoveField(
model_name='ticket',
name='assignees_display',
),
migrations.RemoveField(
model_name='ticket',
name='processor',
),
migrations.RemoveField(
model_name='ticket',
name='processor_display',
),
migrations.AddField(
model_name='ticket',
name='approval_step',
field=models.SmallIntegerField(choices=[(1, 'One level'), (2, 'Two level')], default=1,
verbose_name='Approval step'),
),
migrations.CreateModel(
name='TicketStep',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('level', models.SmallIntegerField(choices=[(1, 'One level'), (2, 'Two level')], default=1,
verbose_name='Approve level')),
('state', models.CharField(
choices=[('notified', 'Notified'), ('approved', 'Approved'), ('rejected', 'Rejected')],
default='notified', max_length=64)),
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_steps',
to='tickets.ticket', verbose_name='Ticket')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='TicketFlow',
fields=[
('org_id',
models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('type', models.CharField(choices=[('general', 'General'), ('login_confirm', 'Login confirm'),
('apply_asset', 'Apply for asset'),
('apply_application', 'Apply for application'),
('login_asset_confirm', 'Login asset confirm'),
('command_confirm', 'Command confirm')], default='general',
max_length=64, verbose_name='Type')),
('approval_level', models.SmallIntegerField(choices=[(1, 'One level'), (2, 'Two level')], default=1,
verbose_name='Approval level')),
('rules', models.ManyToManyField(related_name='ticket_flows', to='tickets.ApprovalRule')),
],
options={
'verbose_name': 'Ticket flow',
},
),
migrations.CreateModel(
name='TicketAssignee',
fields=[
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
('state', models.CharField(
choices=[('notified', 'Notified'), ('approved', 'Approved'), ('rejected', 'Rejected')],
default='notified', max_length=64)),
('assignee',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_assignees',
to=settings.AUTH_USER_MODEL, verbose_name='Assignee')),
('step', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_assignees',
to='tickets.ticketstep')),
],
options={
'verbose_name': 'Ticket assignee',
},
),
migrations.RunPython(create_step_and_assignee),
migrations.RunPython(create_ticket_flow_and_approval_rule),
migrations.AddField(
model_name='ticket',
name='flow',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tickets',
to='tickets.ticketflow', verbose_name='TicketFlow'),
),
]

View File

@ -2,3 +2,5 @@
# #
from .ticket import * from .ticket import *
from .comment import * from .comment import *
from .flow import *

View File

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
#
from django.db import models
from django.utils.translation import ugettext_lazy as _
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
from ..const import TicketType, TicketApprovalLevel, TicketApprovalStrategy
from ..signals import post_or_update_change_ticket_flow_approval
__all__ = ['TicketFlow', 'ApprovalRule']
class ApprovalRule(CommonModelMixin):
level = models.SmallIntegerField(
default=TicketApprovalLevel.one, choices=TicketApprovalLevel.choices,
verbose_name=_('Approve level')
)
strategy = models.CharField(
max_length=64, default=TicketApprovalStrategy.super,
choices=TicketApprovalStrategy.choices,
verbose_name=_('Approve strategy')
)
# 受理人列表
assignees = models.ManyToManyField(
'users.User', related_name='assigned_ticket_flow_approval_rule',
verbose_name=_("Assignees")
)
assignees_display = models.JSONField(
encoder=ModelJSONFieldEncoder, default=list,
verbose_name=_('Assignees display')
)
class Meta:
verbose_name = _('Ticket flow approval rule')
def __str__(self):
return '{}({})'.format(self.id, self.level)
@classmethod
def change_assignees_display(cls, qs):
post_or_update_change_ticket_flow_approval.send(sender=cls, qs=qs)
class TicketFlow(CommonModelMixin, OrgModelMixin):
type = models.CharField(
max_length=64, choices=TicketType.choices,
default=TicketType.general, verbose_name=_("Type")
)
approval_level = models.SmallIntegerField(
default=TicketApprovalLevel.one,
choices=TicketApprovalLevel.choices,
verbose_name=_('Approval level')
)
rules = models.ManyToManyField(ApprovalRule, related_name='ticket_flows')
class Meta:
verbose_name = _('Ticket flow')
def __str__(self):
return '{}'.format(self.type)
@classmethod
def get_org_related_flows(cls):
flows = cls.objects.all()
cur_flow_types = flows.values_list('type', flat=True)
with tmp_to_root_org():
diff_global_flows = cls.objects.exclude(type__in=cur_flow_types)
return flows | diff_global_flows

View File

@ -1,76 +1,78 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
import json
import uuid
from datetime import datetime
from django.db import models from django.db import models
from django.db.models import Q from django.db.models import Q
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.conf import settings
from common.mixins.models import CommonModelMixin from common.mixins.models import CommonModelMixin
from common.db.encoder import ModelJSONFieldEncoder
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.const import TicketTypeChoices, TicketActionChoices, TicketStatusChoices from tickets.const import TicketType, TicketStatus, TicketState, TicketApprovalLevel, ProcessStatus, TicketAction
from tickets.signals import post_change_ticket_action from tickets.signals import post_change_ticket_action
from tickets.handler import get_ticket_handler from tickets.handler import get_ticket_handler
from tickets.errors import AlreadyClosed
__all__ = ['Ticket', 'ModelJSONFieldEncoder'] __all__ = ['Ticket']
class ModelJSONFieldEncoder(json.JSONEncoder): class TicketStep(CommonModelMixin):
""" 解决一些类型的字段不能序列化的问题 """ ticket = models.ForeignKey(
def default(self, obj): 'Ticket', related_name='ticket_steps', on_delete=models.CASCADE, verbose_name='Ticket'
if isinstance(obj, datetime): )
return obj.strftime(settings.DATETIME_DISPLAY_FORMAT) level = models.SmallIntegerField(
if isinstance(obj, uuid.UUID): default=TicketApprovalLevel.one, choices=TicketApprovalLevel.choices,
return str(obj) verbose_name=_('Approve level')
if isinstance(obj, type(_("ugettext_lazy"))): )
return str(obj) state = models.CharField(choices=ProcessStatus.choices, max_length=64, default=ProcessStatus.notified)
else:
return super().default(obj)
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 Ticket(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=TicketTypeChoices.choices, max_length=64, choices=TicketType.choices,
default=TicketTypeChoices.general.value, verbose_name=_("Type") default=TicketType.general, 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( state = models.CharField(
choices=TicketActionChoices.choices, max_length=16, max_length=16, choices=TicketState.choices,
default=TicketActionChoices.open.value, verbose_name=_("Action") default=TicketState.open, verbose_name=_("State")
) )
status = models.CharField( status = models.CharField(
max_length=16, choices=TicketStatusChoices.choices, max_length=16, choices=TicketStatus.choices,
default=TicketStatusChoices.open.value, verbose_name=_("Status") default=TicketStatus.open, verbose_name=_("Status")
)
approval_step = models.SmallIntegerField(
default=TicketApprovalLevel.one, choices=TicketApprovalLevel.choices,
verbose_name=_('Approval step')
) )
# 申请人 # 申请人
applicant = models.ForeignKey( applicant = models.ForeignKey(
'users.User', related_name='applied_tickets', on_delete=models.SET_NULL, null=True, 'users.User', related_name='applied_tickets', on_delete=models.SET_NULL, null=True,
verbose_name=_("Applicant") verbose_name=_("Applicant")
) )
applicant_display = models.CharField( applicant_display = models.CharField(max_length=256, default='', verbose_name=_("Applicant display"))
max_length=256, default='', verbose_name=_("Applicant display") process_map = models.JSONField(encoder=ModelJSONFieldEncoder, default=list, verbose_name=_("Process"))
)
# 处理人
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='', verbose_name=_("Processor display")
)
# 受理人列表
assignees = models.ManyToManyField(
'users.User', related_name='assigned_tickets', verbose_name=_("Assignees")
)
assignees_display = models.JSONField(
encoder=ModelJSONFieldEncoder, default=list, verbose_name=_('Assignees display')
)
# 评论 # 评论
comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) 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")
)
class Meta: class Meta:
ordering = ('-date_created',) ordering = ('-date_created',)
@ -81,77 +83,142 @@ class Ticket(CommonModelMixin, OrgModelMixin):
# type # type
@property @property
def type_apply_asset(self): def type_apply_asset(self):
return self.type == TicketTypeChoices.apply_asset.value return self.type == TicketType.apply_asset.value
@property @property
def type_apply_application(self): def type_apply_application(self):
return self.type == TicketTypeChoices.apply_application.value return self.type == TicketType.apply_application.value
@property @property
def type_login_confirm(self): def type_login_confirm(self):
return self.type == TicketTypeChoices.login_confirm.value return self.type == TicketType.login_confirm.value
# status # status
@property
def status_closed(self):
return self.status == TicketStatusChoices.closed.value
@property @property
def status_open(self): def status_open(self):
return self.status == TicketStatusChoices.open.value 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
@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 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): def set_status_closed(self):
self.status = TicketStatusChoices.closed.value self.status = TicketStatus.closed
# action def create_related_node(self):
@property approval_rule = self.get_current_ticket_flow_approve()
def action_open(self): ticket_step = TicketStep.objects.create(ticket=self, level=self.approval_step)
return self.action == TicketActionChoices.open.value ticket_assignees = []
assignees = approval_rule.assignees.all()
for assignee in assignees:
ticket_assignees.append(TicketAssignee(step=ticket_step, assignee=assignee))
TicketAssignee.objects.bulk_create(ticket_assignees)
@property def create_process_map(self):
def action_approve(self): approval_rules = self.flow.rules.order_by('level')
return self.action == TicketActionChoices.approve.value nodes = list()
for node in approval_rules:
nodes.append(
{
'approval_level': node.level,
'state': ProcessStatus.notified,
'assignees': [i for i in node.assignees.values_list('id', flat=True)],
'assignees_display': node.assignees_display
}
)
return nodes
@property # TODO 兼容不存在流的工单
def action_reject(self): def create_process_map_and_node(self, assignees):
return self.action == TicketActionChoices.reject.value self.process_map = [{
'approval_level': 1,
@property 'state': 'notified',
def action_close(self): 'assignees': [assignee.id for assignee in assignees],
return self.action == TicketActionChoices.close.value '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)
# action changed # action changed
def open(self, applicant): def open(self, applicant):
self.applicant = applicant self.applicant = applicant
self._change_action(action=TicketActionChoices.open.value) self._change_action(TicketAction.open)
def update_current_step_state_and_assignee(self, processor, state):
if self.status_closed:
raise AlreadyClosed
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 approve(self, processor): def approve(self, processor):
self.processor = processor self.update_current_step_state_and_assignee(processor, TicketState.approved)
self._change_action(action=TicketActionChoices.approve.value) self._change_action(TicketAction.approve)
def reject(self, processor): def reject(self, processor):
self.processor = processor self.update_current_step_state_and_assignee(processor, TicketState.rejected)
self._change_action(action=TicketActionChoices.reject.value) self._change_action(TicketAction.reject)
def close(self, processor): def close(self, processor):
self.processor = processor self.update_current_step_state_and_assignee(processor, TicketState.closed)
self._change_action(action=TicketActionChoices.close.value) self._change_action(TicketAction.close)
def _change_action(self, action): def _change_action(self, action):
self.action = action
self.save() self.save()
post_change_ticket_action.send(sender=self.__class__, ticket=self, action=action) 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.ticket_steps.filter(ticket_assignees__assignee=assignee, level=self.approval_step).exists()
@classmethod @classmethod
def get_user_related_tickets(cls, user): def get_user_related_tickets(cls, user):
queries = Q(applicant=user) | Q(assignees=user) queries = Q(applicant=user) | Q(ticket_steps__ticket_assignees__assignee=user)
tickets = cls.all().filter(queries).distinct() tickets = cls.all().filter(queries).distinct()
return tickets return tickets
def get_current_ticket_flow_approve(self):
return self.flow.rules.filter(level=self.approval_step).first()
@classmethod @classmethod
def all(cls): def all(cls):
with tmp_to_root_org(): with tmp_to_root_org():

View File

@ -1,4 +1,3 @@
from rest_framework import permissions from rest_framework import permissions
@ -7,12 +6,7 @@ class IsAssignee(permissions.BasePermission):
return obj.has_assignee(request.user) return obj.has_assignee(request.user)
class IsAssigneeOrApplicant(IsAssignee): class IsApplicant(permissions.BasePermission):
def has_object_permission(self, request, view, obj): def has_object_permission(self, request, view, obj):
return super().has_object_permission(request, view, obj) or obj.applicant == request.user return obj.applicant == request.user
class NotClosed(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return not obj.status_closed

View File

@ -26,7 +26,7 @@ class CommentSerializer(serializers.ModelSerializer):
'body', 'user_display', 'body', 'user_display',
'date_created', 'date_updated' 'date_created', 'date_updated'
] ]
fields_fk = ['ticket', 'user',] fields_fk = ['ticket', 'user', ]
fields = fields_small + fields_fk fields = fields_small + fields_fk
read_only_fields = [ read_only_fields = [
'user_display', 'date_created', 'date_updated' 'user_display', 'date_created', 'date_updated'

View File

@ -1,6 +1,7 @@
from tickets import const from tickets import const
from .ticket_type import ( from .ticket_type import (
apply_asset, apply_application, login_confirm, login_asset_confirm, command_confirm apply_asset, apply_application, login_confirm,
login_asset_confirm, command_confirm
) )
__all__ = [ __all__ = [
@ -10,35 +11,31 @@ __all__ = [
# ticket action # ticket action
# ------------- # -------------
action_open = const.TicketActionChoices.open.value action_open = const.TicketAction.open.value
action_approve = const.TicketActionChoices.approve.value action_approve = const.TicketAction.approve.value
# defines `meta` field dynamic mapping serializers # defines `meta` field dynamic mapping serializers
# ------------------------------------------------ # ------------------------------------------------
type_serializer_classes_mapping = { type_serializer_classes_mapping = {
const.TicketTypeChoices.apply_asset.value: { const.TicketType.apply_asset.value: {
'default': apply_asset.ApplyAssetSerializer, 'default': apply_asset.ApplySerializer
action_open: apply_asset.ApplySerializer,
action_approve: apply_asset.ApproveSerializer,
}, },
const.TicketTypeChoices.apply_application.value: { const.TicketType.apply_application.value: {
'default': apply_application.ApplyApplicationSerializer, 'default': apply_application.ApplySerializer
action_open: apply_application.ApplySerializer,
action_approve: apply_application.ApproveSerializer,
}, },
const.TicketTypeChoices.login_confirm.value: { const.TicketType.login_confirm.value: {
'default': login_confirm.LoginConfirmSerializer, 'default': login_confirm.LoginConfirmSerializer,
action_open: login_confirm.ApplySerializer, action_open: login_confirm.ApplySerializer,
action_approve: login_confirm.LoginConfirmSerializer(read_only=True), action_approve: login_confirm.LoginConfirmSerializer(read_only=True),
}, },
const.TicketTypeChoices.login_asset_confirm.value: { const.TicketType.login_asset_confirm.value: {
'default': login_asset_confirm.LoginAssetConfirmSerializer, 'default': login_asset_confirm.LoginAssetConfirmSerializer,
action_open: login_asset_confirm.ApplySerializer, action_open: login_asset_confirm.ApplySerializer,
action_approve: login_asset_confirm.LoginAssetConfirmSerializer(read_only=True), action_approve: login_asset_confirm.LoginAssetConfirmSerializer(read_only=True),
}, },
const.TicketTypeChoices.command_confirm.value: { const.TicketType.command_confirm.value: {
'default': command_confirm.CommandConfirmSerializer, 'default': command_confirm.CommandConfirmSerializer,
action_open: command_confirm.ApplySerializer, action_open: command_confirm.ApplySerializer,
action_approve: command_confirm.CommandConfirmSerializer(read_only=True) action_approve: command_confirm.CommandConfirmSerializer(read_only=True)

View File

@ -1,20 +1,20 @@
from rest_framework import serializers from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.models import Q
from perms.models import ApplicationPermission from perms.models import ApplicationPermission
from applications.models import Application
from applications.const import AppCategory, AppType from applications.const import AppCategory, AppType
from assets.models import SystemUser
from orgs.utils import tmp_to_org from orgs.utils import tmp_to_org
from tickets.models import Ticket from tickets.models import Ticket
from .common import DefaultPermissionName from .common import DefaultPermissionName
__all__ = [ __all__ = [
'ApplyApplicationSerializer', 'ApplySerializer', 'ApproveSerializer', 'ApplySerializer',
] ]
class ApplySerializer(serializers.Serializer): class ApplySerializer(serializers.Serializer):
apply_permission_name = serializers.CharField(
max_length=128, default=DefaultPermissionName(), label=_('Apply name')
)
# 申请信息 # 申请信息
apply_category = serializers.ChoiceField( apply_category = serializers.ChoiceField(
required=True, choices=AppCategory.choices, label=_('Category'), required=True, choices=AppCategory.choices, label=_('Category'),
@ -31,13 +31,23 @@ class ApplySerializer(serializers.Serializer):
required=False, read_only=True, label=_('Type display'), required=False, read_only=True, label=_('Type display'),
allow_null=True allow_null=True
) )
apply_application_group = serializers.ListField( apply_applications = serializers.ListField(
required=False, child=serializers.CharField(), label=_('Application group'), required=True, child=serializers.UUIDField(), label=_('Apply applications'),
default=list, allow_null=True allow_null=True
) )
apply_system_user_group = serializers.ListField( apply_applications_display = serializers.ListField(
required=False, child=serializers.CharField(), label=_('System user group'), required=False, read_only=True, child=serializers.CharField(),
default=list, allow_null=True 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( apply_date_start = serializers.DateTimeField(
required=True, label=_('Date start'), allow_null=True required=True, label=_('Date start'), allow_null=True
@ -46,37 +56,6 @@ class ApplySerializer(serializers.Serializer):
required=True, label=_('Date expired'), allow_null=True required=True, label=_('Date expired'), allow_null=True
) )
class ApproveSerializer(serializers.Serializer):
# 审批信息
approve_permission_name = serializers.CharField(
max_length=128, default=DefaultPermissionName(), label=_('Permission name')
)
approve_applications = serializers.ListField(
required=True, child=serializers.UUIDField(), label=_('Approve applications'),
allow_null=True
)
approve_applications_display = serializers.ListField(
required=False, read_only=True, child=serializers.CharField(),
label=_('Approve applications display'), allow_null=True,
default=list
)
approve_system_users = serializers.ListField(
required=True, child=serializers.UUIDField(), label=_('Approve system users'),
allow_null=True
)
approve_system_users_display = serializers.ListField(
required=False, read_only=True, child=serializers.CharField(),
label=_('Approve system user display'), allow_null=True,
default=list
)
approve_date_start = serializers.DateTimeField(
required=True, label=_('Date start'), allow_null=True
)
approve_date_expired = serializers.DateTimeField(
required=True, label=_('Date expired'), allow_null=True
)
def validate_approve_permission_name(self, permission_name): def validate_approve_permission_name(self, permission_name):
if not isinstance(self.root.instance, Ticket): if not isinstance(self.root.instance, Ticket):
return permission_name return permission_name
@ -90,83 +69,5 @@ class ApproveSerializer(serializers.Serializer):
'Permission named `{}` already exists'.format(permission_name) 'Permission named `{}` already exists'.format(permission_name)
)) ))
def validate_approve_applications(self, approve_applications):
if not isinstance(self.root.instance, Ticket):
return []
with tmp_to_org(self.root.instance.org_id):
apply_type = self.root.instance.meta.get('apply_type')
queries = Q(type=apply_type)
queries &= Q(id__in=approve_applications)
application_ids = Application.objects.filter(queries).values_list('id', flat=True)
application_ids = [str(application_id) for application_id in application_ids]
if application_ids:
return application_ids
raise serializers.ValidationError(_(
'No `Application` are found under Organization `{}`'.format(self.root.instance.org_name)
))
def validate_approve_system_users(self, approve_system_users):
if not isinstance(self.root.instance, Ticket):
return []
with tmp_to_org(self.root.instance.org_id):
apply_type = self.root.instance.meta.get('apply_type')
protocol = SystemUser.get_protocol_by_application_type(apply_type)
queries = Q(protocol=protocol)
queries &= Q(id__in=approve_system_users)
system_user_ids = SystemUser.objects.filter(queries).values_list('id', flat=True)
system_user_ids = [str(system_user_id) for system_user_id in system_user_ids]
if system_user_ids:
return system_user_ids
raise serializers.ValidationError(_(
'No `SystemUser` are found under Organization `{}`'.format(self.root.instance.org_name)
))
class ApplyApplicationSerializer(ApplySerializer, ApproveSerializer):
# 推荐信息
recommend_applications = serializers.SerializerMethodField()
recommend_system_users = serializers.SerializerMethodField()
def get_recommend_applications(self, value):
if not isinstance(self.root.instance, Ticket):
return []
apply_application_group = value.get('apply_application_group', [])
if not apply_application_group:
return []
apply_type = value.get('apply_type')
queries = Q()
for application in apply_application_group:
queries |= Q(name__icontains=application)
queries &= Q(type=apply_type)
with tmp_to_org(self.root.instance.org_id):
application_ids = Application.objects.filter(queries).values_list('id', flat=True)[:15]
application_ids = [str(application_id) for application_id in application_ids]
return application_ids
def get_recommend_system_users(self, value):
if not isinstance(self.root.instance, Ticket):
return []
apply_system_user_group = value.get('apply_system_user_group', [])
if not apply_system_user_group:
return []
apply_type = value.get('apply_type')
protocol = SystemUser.get_protocol_by_application_type(apply_type)
queries = Q()
for system_user in apply_system_user_group:
queries |= Q(username__icontains=system_user)
queries |= Q(name__icontains=system_user)
queries &= Q(protocol=protocol)
with tmp_to_org(self.root.instance.org_id):
system_user_ids = SystemUser.objects.filter(queries).values_list('id', flat=True)[:5]
system_user_ids = [str(system_user_id) for system_user_id in system_user_ids]
return system_user_ids

View File

@ -1,39 +1,44 @@
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.models import Q
from rest_framework import serializers from rest_framework import serializers
from perms.serializers import ActionsField from perms.serializers import ActionsField
from perms.models import AssetPermission from perms.models import AssetPermission
from assets.models import Asset, SystemUser
from orgs.utils import tmp_to_org from orgs.utils import tmp_to_org
from tickets.models import Ticket from tickets.models import Ticket
from .common import DefaultPermissionName from .common import DefaultPermissionName
__all__ = [ __all__ = [
'ApplyAssetSerializer', 'ApplySerializer', 'ApproveSerializer', 'ApplySerializer',
] ]
class ApplySerializer(serializers.Serializer): class ApplySerializer(serializers.Serializer):
apply_permission_name = serializers.CharField(
max_length=128, default=DefaultPermissionName(), label=_('Apply name')
)
# 申请信息 # 申请信息
apply_ip_group = serializers.ListField( apply_assets = serializers.ListField(
required=False, child=serializers.IPAddressField(), label=_('IP group'), required=True, allow_null=True, child=serializers.UUIDField(), label=_('Apply assets')
default=list, allow_null=True,
) )
apply_hostname_group = serializers.ListField( apply_assets_display = serializers.ListField(
required=False, child=serializers.CharField(), label=_('Hostname group'), required=False, read_only=True, child=serializers.CharField(),
default=list, allow_null=True, label=_('Approve assets display'), allow_null=True,
default=list,
) )
apply_system_user_group = serializers.ListField( apply_system_users = serializers.ListField(
required=False, child=serializers.CharField(), label=_('System user group'), required=True, allow_null=True, child=serializers.UUIDField(),
default=list, allow_null=True label=_('Approve 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( apply_actions = ActionsField(
required=True, allow_null=True required=True, allow_null=True
) )
apply_actions_display = serializers.ListField( apply_actions_display = serializers.ListField(
required=False, read_only=True, child=serializers.CharField(), required=False, read_only=True, child=serializers.CharField(),
label=_('Approve assets display'), allow_null=True, label=_('Apply assets display'), allow_null=True,
default=list, default=list,
) )
apply_date_start = serializers.DateTimeField( apply_date_start = serializers.DateTimeField(
@ -43,44 +48,6 @@ class ApplySerializer(serializers.Serializer):
required=True, label=_('Date expired'), allow_null=True, required=True, label=_('Date expired'), allow_null=True,
) )
class ApproveSerializer(serializers.Serializer):
# 审批信息
approve_permission_name = serializers.CharField(
max_length=128, default=DefaultPermissionName(), label=_('Permission name')
)
approve_assets = serializers.ListField(
required=True, allow_null=True, child=serializers.UUIDField(), label=_('Approve assets')
)
approve_assets_display = serializers.ListField(
required=False, read_only=True, child=serializers.CharField(),
label=_('Approve assets display'), allow_null=True,
default=list,
)
approve_system_users = serializers.ListField(
required=True, allow_null=True, child=serializers.UUIDField(),
label=_('Approve system users')
)
approve_system_users_display = serializers.ListField(
required=False, read_only=True, child=serializers.CharField(),
label=_('Approve assets display'), allow_null=True,
default=list,
)
approve_actions = ActionsField(
required=True, allow_null=True,
)
approve_actions_display = serializers.ListField(
required=False, read_only=True, child=serializers.CharField(),
label=_('Approve assets display'), allow_null=True,
default=list,
)
approve_date_start = serializers.DateTimeField(
required=True, label=_('Date start'), allow_null=True,
)
approve_date_expired = serializers.DateTimeField(
required=True, label=_('Date expired'), allow_null=True
)
def validate_approve_permission_name(self, permission_name): def validate_approve_permission_name(self, permission_name):
if not isinstance(self.root.instance, Ticket): if not isinstance(self.root.instance, Ticket):
return permission_name return permission_name
@ -93,76 +60,3 @@ class ApproveSerializer(serializers.Serializer):
raise serializers.ValidationError(_( raise serializers.ValidationError(_(
'Permission named `{}` already exists'.format(permission_name) 'Permission named `{}` already exists'.format(permission_name)
)) ))
def validate_approve_assets(self, approve_assets):
if not isinstance(self.root.instance, Ticket):
return []
with tmp_to_org(self.root.instance.org_id):
asset_ids = Asset.objects.filter(id__in=approve_assets).values_list('id', flat=True)
asset_ids = [str(asset_id) for asset_id in asset_ids]
if asset_ids:
return asset_ids
raise serializers.ValidationError(_(
'No `Asset` are found under Organization `{}`'.format(self.root.instance.org_name)
))
def validate_approve_system_users(self, approve_system_users):
if not isinstance(self.root.instance, Ticket):
return []
with tmp_to_org(self.root.instance.org_id):
queries = Q(protocol__in=SystemUser.ASSET_CATEGORY_PROTOCOLS)
queries &= Q(id__in=approve_system_users)
system_user_ids = SystemUser.objects.filter(queries).values_list('id', flat=True)
system_user_ids = [str(system_user_id) for system_user_id in system_user_ids]
if system_user_ids:
return system_user_ids
raise serializers.ValidationError(_(
'No `SystemUser` are found under Organization `{}`'.format(self.root.instance.org_name)
))
class ApplyAssetSerializer(ApplySerializer, ApproveSerializer):
# 推荐信息
recommend_assets = serializers.SerializerMethodField()
recommend_system_users = serializers.SerializerMethodField()
def get_recommend_assets(self, value):
if not isinstance(self.root.instance, Ticket):
return []
apply_ip_group = value.get('apply_ip_group', [])
apply_hostname_group = value.get('apply_hostname_group', [])
queries = Q()
if apply_ip_group:
queries |= Q(ip__in=apply_ip_group)
for hostname in apply_hostname_group:
queries |= Q(hostname__icontains=hostname)
if not queries:
return []
with tmp_to_org(self.root.instance.org_id):
asset_ids = Asset.objects.filter(queries).values_list('id', flat=True)[:100]
asset_ids = [str(asset_id) for asset_id in asset_ids]
return asset_ids
def get_recommend_system_users(self, value):
if not isinstance(self.root.instance, Ticket):
return []
apply_system_user_group = value.get('apply_system_user_group', [])
if not apply_system_user_group:
return []
queries = Q()
for system_user in apply_system_user_group:
queries |= Q(username__icontains=system_user)
queries |= Q(name__icontains=system_user)
queries &= Q(protocol__in=SystemUser.ASSET_CATEGORY_PROTOCOLS)
with tmp_to_org(self.root.instance.org_id):
system_user_ids = SystemUser.objects.filter(queries).values_list('id', flat=True)[:5]
system_user_ids = [str(system_user_id) for system_user_id in system_user_ids]
return system_user_ids

View File

@ -1,43 +1,38 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.transaction import atomic
from rest_framework import serializers from rest_framework import serializers
from common.drf.serializers import MethodSerializer from common.drf.serializers import MethodSerializer
from orgs.mixins.serializers import OrgResourceModelSerializerMixin from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from perms.models import AssetPermission
from orgs.models import Organization from orgs.models import Organization
from orgs.utils import tmp_to_org
from users.models import User from users.models import User
from tickets.models import Ticket from tickets.models import Ticket, TicketFlow, ApprovalRule
from tickets.const import TicketApprovalStrategy
from .meta import type_serializer_classes_mapping from .meta import type_serializer_classes_mapping
__all__ = [ __all__ = [
'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketApproveSerializer', 'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketApproveSerializer', 'TicketFlowSerializer'
] ]
class TicketSerializer(OrgResourceModelSerializerMixin): class TicketSerializer(OrgResourceModelSerializerMixin):
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
action_display = serializers.ReadOnlyField( status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status display'))
source='get_action_display', label=_('Action display')
)
status_display = serializers.ReadOnlyField(
source='get_status_display', label=_('Status display')
)
meta = MethodSerializer() meta = MethodSerializer()
class Meta: class Meta:
model = Ticket model = Ticket
fields_mini = ['id', 'title'] fields_mini = ['id', 'title']
fields_small = fields_mini + [ fields_small = fields_mini + [
'type', 'type_display', 'meta', 'body', 'type', 'type_display', 'meta', 'state', 'approval_step',
'action', 'action_display', 'status', 'status_display', 'status', 'status_display', 'applicant_display', 'process_map',
'applicant_display', 'processor_display', 'assignees_display', 'date_created', 'date_updated', 'comment', 'org_id', 'org_name', 'body'
'date_created', 'date_updated',
'comment', 'org_id', 'org_name',
] ]
fields_fk = ['applicant', 'processor',] fields_fk = ['applicant', ]
fields_m2m = ['assignees'] fields = fields_small + fields_fk
fields = fields_small + fields_fk + fields_m2m
def get_meta_serializer(self): def get_meta_serializer(self):
default_serializer = serializers.Serializer(read_only=True) default_serializer = serializers.Serializer(read_only=True)
@ -71,7 +66,6 @@ class TicketSerializer(OrgResourceModelSerializerMixin):
class TicketDisplaySerializer(TicketSerializer): class TicketDisplaySerializer(TicketSerializer):
class Meta: class Meta:
model = Ticket model = Ticket
fields = TicketSerializer.Meta.fields fields = TicketSerializer.Meta.fields
@ -87,7 +81,7 @@ class TicketApplySerializer(TicketSerializer):
model = Ticket model = Ticket
fields = TicketSerializer.Meta.fields fields = TicketSerializer.Meta.fields
writeable_fields = [ writeable_fields = [
'id', 'title', 'type', 'meta', 'assignees', 'comment', 'org_id' 'id', 'title', 'type', 'meta', 'comment', 'org_id'
] ]
read_only_fields = list(set(fields) - set(writeable_fields)) read_only_fields = list(set(fields) - set(writeable_fields))
extra_kwargs = { extra_kwargs = {
@ -112,27 +106,115 @@ class TicketApplySerializer(TicketSerializer):
raise serializers.ValidationError(error) raise serializers.ValidationError(error)
return org_id return org_id
def validate_assignees(self, assignees): def validate(self, attrs):
org_id = self.initial_data.get('org_id') ticket_type = attrs.get('type')
self.validate_org_id(org_id) flow = TicketFlow.get_org_related_flows().filter(type=ticket_type).first()
org = Organization.get_instance(org_id) if flow:
admins = User.get_super_and_org_admins(org) attrs['flow'] = flow
valid_assignees = list(set(assignees) & set(admins)) else:
if not valid_assignees: error = _('The ticket flow `{}` does not exist'.format(ticket_type))
error = _('None of the assignees belong to Organization `{}` admins'.format(org.name))
raise serializers.ValidationError(error) raise serializers.ValidationError(error)
return valid_assignees 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): class TicketApproveSerializer(TicketSerializer):
meta = serializers.ReadOnlyField()
class Meta: class Meta:
model = Ticket model = Ticket
fields = TicketSerializer.Meta.fields fields = TicketSerializer.Meta.fields
writeable_fields = ['meta'] read_only_fields = fields
read_only_fields = list(set(fields) - set(writeable_fields))
def validate_meta(self, meta):
_meta = self.instance.meta if self.instance else {} class TicketFlowApproveSerializer(serializers.ModelSerializer):
_meta.update(meta) strategy_display = serializers.ReadOnlyField(source='get_strategy_display', label=_('Approve strategy'))
return _meta assignees_read_only = serializers.SerializerMethodField(label=_("Assignees"))
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}
}
def get_assignees_read_only(self, obj):
if obj.strategy == TicketApprovalStrategy.custom:
return obj.assignees.values_list('id', flat=True)
return []
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, related, assignees, instance=None):
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
for level, data in enumerate(childs, 1):
data_m2m = data.pop(assignees, None)
child_instance = related_model.objects.create(**data, level=level)
if child_instance.strategy == 'super':
data_m2m = list(User.get_super_admins())
elif child_instance.strategy == 'admin':
data_m2m = list(User.get_org_admins())
elif child_instance.strategy == 'super_admin':
data_m2m = list(User.get_super_and_org_admins())
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, 'rules', 'assignees')
@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, 'rules', 'assignees', instance)
return instance

View File

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

View File

@ -3,9 +3,8 @@
from django.dispatch import receiver from django.dispatch import receiver
from common.utils import get_logger from common.utils import get_logger
from tickets.models import Ticket from tickets.models import Ticket, ApprovalRule
from ..signals import post_change_ticket_action from ..signals import post_change_ticket_action, post_or_update_change_ticket_flow_approval
logger = get_logger(__name__) logger = get_logger(__name__)
@ -13,3 +12,12 @@ logger = get_logger(__name__)
@receiver(post_change_ticket_action, sender=Ticket) @receiver(post_change_ticket_action, sender=Ticket)
def on_post_change_ticket_action(sender, ticket, action, **kwargs): def on_post_change_ticket_action(sender, ticket, action, **kwargs):
ticket.handler.dispatch(action) ticket.handler.dispatch(action)
@receiver(post_or_update_change_ticket_flow_approval, sender=ApprovalRule)
def post_or_update_change_ticket_flow_approval(sender, qs, **kwargs):
updates = []
for instance in qs:
instance.assignees_display = [str(assignee) for assignee in instance.assignees.all()]
updates.append(instance)
sender.objects.bulk_update(updates, ['assignees_display', ])

View File

@ -8,6 +8,7 @@ app_name = 'tickets'
router = BulkRouter() router = BulkRouter()
router.register('tickets', api.TicketViewSet, 'ticket') router.register('tickets', api.TicketViewSet, 'ticket')
router.register('flows', api.TicketFlowViewSet, 'flows')
router.register('assignees', api.AssigneeViewSet, 'assignee') router.register('assignees', api.AssigneeViewSet, 'assignee')
router.register('comments', api.CommentViewSet, 'comment') router.register('comments', api.CommentViewSet, 'comment')

View File

@ -26,9 +26,10 @@ EMAIL_TEMPLATE = '''
def send_ticket_applied_mail_to_assignees(ticket): def send_ticket_applied_mail_to_assignees(ticket):
if not ticket.assignees: assignees = ticket.current_node.first().ticket_assignees.all()
if not assignees:
logger.debug("Not found assignees, ticket: {}({}), assignees: {}".format( logger.debug("Not found assignees, ticket: {}({}), assignees: {}".format(
ticket, str(ticket.id), ticket.assignees) ticket, str(ticket.id), assignees)
) )
return return
@ -42,24 +43,24 @@ def send_ticket_applied_mail_to_assignees(ticket):
) )
if settings.DEBUG: if settings.DEBUG:
logger.debug(message) logger.debug(message)
recipient_list = [assignee.email for assignee in ticket.assignees.all()] recipient_list = [i.assignee.email for i in assignees]
send_mail_async.delay(subject, message, recipient_list, html_message=message) send_mail_async.delay(subject, message, recipient_list, html_message=message)
def send_ticket_processed_mail_to_applicant(ticket): def send_ticket_processed_mail_to_applicant(ticket, processor):
if not ticket.applicant: if not ticket.applicant:
logger.error("Not found applicant: {}({})".format(ticket.title, ticket.id)) logger.error("Not found applicant: {}({})".format(ticket.title, ticket.id))
return return
processor_display = str(processor)
ticket_detail_url = urljoin(settings.SITE_URL, const.TICKET_DETAIL_URL.format(id=str(ticket.id))) ticket_detail_url = urljoin(settings.SITE_URL, const.TICKET_DETAIL_URL.format(id=str(ticket.id)))
subject = _('Ticket has processed - {} ({})').format(ticket.title, ticket.processor_display) subject = _('Ticket has processed - {} ({})').format(ticket.title, processor_display)
message = EMAIL_TEMPLATE.format( message = EMAIL_TEMPLATE.format(
title=_('Your ticket has been processed, processor - {}').format(ticket.processor_display), title=_('Your ticket has been processed, processor - {}').format(processor_display),
ticket_detail_url=ticket_detail_url, ticket_detail_url=ticket_detail_url,
ticket_detail_url_description=_('click here to review'), ticket_detail_url_description=_('click here to review'),
body=ticket.body.replace('\n', '<br/>'), body=ticket.body.replace('\n', '<br/>'),
) )
if settings.DEBUG: if settings.DEBUG:
logger.debug(message) logger.debug(message)
recipient_list = [ticket.applicant.email] recipient_list = [ticket.applicant.email, ]
send_mail_async.delay(subject, message, recipient_list, html_message=message) send_mail_async.delay(subject, message, recipient_list, html_message=message)

View File

@ -27,7 +27,6 @@ from common.db.models import TextChoices
from users.exceptions import MFANotEnabled from users.exceptions import MFANotEnabled
from ..signals import post_user_change_password from ..signals import post_user_change_password
__all__ = ['User', 'UserPasswordHistory'] __all__ = ['User', 'UserPasswordHistory']
logger = get_logger(__file__) logger = get_logger(__file__)
@ -358,6 +357,10 @@ class RoleMixin:
def get_super_admins(cls): def get_super_admins(cls):
return cls.objects.filter(role=cls.ROLE.ADMIN) return cls.objects.filter(role=cls.ROLE.ADMIN)
@classmethod
def get_auditor_and_users(cls):
return cls.objects.filter(role__in=[cls.ROLE.USER, cls.ROLE.AUDITOR])
@classmethod @classmethod
def get_org_admins(cls, org=None): def get_org_admins(cls, org=None):
from orgs.models import Organization from orgs.models import Organization
@ -369,12 +372,9 @@ class RoleMixin:
@classmethod @classmethod
def get_super_and_org_admins(cls, org=None): def get_super_and_org_admins(cls, org=None):
super_admins = cls.get_super_admins() super_admins = cls.get_super_admins()
super_admin_ids = list(super_admins.values_list('id', flat=True)) org_admins = cls.get_org_admins(org=org)
org_admins = cls.get_org_admins(org) admins = org_admins | super_admins
org_admin_ids = list(org_admins.values_list('id', flat=True)) return admins.distinct()
admin_ids = set(org_admin_ids + super_admin_ids)
admins = User.objects.filter(id__in=admin_ids)
return admins
class TokenMixin: class TokenMixin: