mirror of https://github.com/jumpserver/jumpserver
feat: 工单多级审批 + 模版创建 (#6640)
* feat: 工单多级审批 + 模版创建 * feat: 工单权限处理 * fix: 工单关闭后 再审批bug * perf: 修改一点 Co-authored-by: feng626 <1304903146@qq.com> Co-authored-by: ibuler <ibuler@qq.com>pull/6730/head
parent
1fdc558ef7
commit
0f87f05b3f
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -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())
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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")
|
|
@ -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)
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -2,3 +2,5 @@
|
||||||
#
|
#
|
||||||
from .ticket import *
|
from .ticket import *
|
||||||
from .comment import *
|
from .comment import *
|
||||||
|
from .flow import *
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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():
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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', ])
|
||||||
|
|
|
@ -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')
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue