mirror of
https://github.com/jumpserver/jumpserver.git
synced 2026-08-09 16:11:22 +00:00
perf: Supports CC recipients and multi-level approval (up to 5 levels); work order flows can also be created manually
This commit is contained in:
@@ -10288,6 +10288,18 @@ msgstr ""
|
||||
msgid "Two level"
|
||||
msgstr ""
|
||||
|
||||
#: tickets/const.py:52
|
||||
msgid "Three level"
|
||||
msgstr ""
|
||||
|
||||
#: tickets/const.py:53
|
||||
msgid "Four level"
|
||||
msgstr ""
|
||||
|
||||
#: tickets/const.py:54
|
||||
msgid "Five level"
|
||||
msgstr ""
|
||||
|
||||
#: tickets/const.py:55
|
||||
msgid "All assets"
|
||||
msgstr ""
|
||||
@@ -10353,6 +10365,22 @@ msgstr ""
|
||||
msgid "Ticket flow"
|
||||
msgstr ""
|
||||
|
||||
#: tickets/serializers/flow.py:50
|
||||
msgid "A ticket flow with the same name and type already exists"
|
||||
msgstr ""
|
||||
|
||||
#: tickets/serializers/flow.py:57
|
||||
msgid "The number of approval rules must match the approval level"
|
||||
msgstr ""
|
||||
|
||||
#: tickets/serializers/ticket/ticket.py:109
|
||||
msgid "The selected ticket flow is not available"
|
||||
msgstr ""
|
||||
|
||||
#: tickets/serializers/ticket/ticket.py:113
|
||||
msgid "Please select a ticket flow"
|
||||
msgstr ""
|
||||
|
||||
#: tickets/models/relation.py:12
|
||||
msgid "Ticket session relation"
|
||||
msgstr ""
|
||||
|
||||
@@ -10444,6 +10444,18 @@ msgstr "1つのレベル"
|
||||
msgid "Two level"
|
||||
msgstr "2つのレベル"
|
||||
|
||||
#: tickets/const.py:52
|
||||
msgid "Three level"
|
||||
msgstr "3つのレベル"
|
||||
|
||||
#: tickets/const.py:53
|
||||
msgid "Four level"
|
||||
msgstr "4つのレベル"
|
||||
|
||||
#: tickets/const.py:54
|
||||
msgid "Five level"
|
||||
msgstr "5つのレベル"
|
||||
|
||||
#: tickets/const.py:55
|
||||
msgid "All assets"
|
||||
msgstr "すべての資産"
|
||||
@@ -10509,6 +10521,22 @@ msgstr "チケットフロー承認ルール"
|
||||
msgid "Ticket flow"
|
||||
msgstr "チケットの流れ"
|
||||
|
||||
#: tickets/serializers/flow.py:50
|
||||
msgid "A ticket flow with the same name and type already exists"
|
||||
msgstr "同じ名前と種類のチケットフローが既に存在します"
|
||||
|
||||
#: tickets/serializers/flow.py:57
|
||||
msgid "The number of approval rules must match the approval level"
|
||||
msgstr "承認ルール数は承認レベル数と一致する必要があります"
|
||||
|
||||
#: tickets/serializers/ticket/ticket.py:109
|
||||
msgid "The selected ticket flow is not available"
|
||||
msgstr "選択したチケットフローは利用できません"
|
||||
|
||||
#: tickets/serializers/ticket/ticket.py:113
|
||||
msgid "Please select a ticket flow"
|
||||
msgstr "チケットフローを選択してください"
|
||||
|
||||
#: tickets/models/relation.py:12
|
||||
msgid "Ticket session relation"
|
||||
msgstr "チケットセッションの関係"
|
||||
|
||||
@@ -10628,6 +10628,18 @@ msgstr "1 级"
|
||||
msgid "Two level"
|
||||
msgstr "2 级"
|
||||
|
||||
#: tickets/const.py:52
|
||||
msgid "Three level"
|
||||
msgstr "3 级"
|
||||
|
||||
#: tickets/const.py:53
|
||||
msgid "Four level"
|
||||
msgstr "4 级"
|
||||
|
||||
#: tickets/const.py:54
|
||||
msgid "Five level"
|
||||
msgstr "5 级"
|
||||
|
||||
#: tickets/const.py:55
|
||||
msgid "All assets"
|
||||
msgstr "所有资产"
|
||||
@@ -10694,6 +10706,22 @@ msgstr "工单批准信息"
|
||||
msgid "Ticket flow"
|
||||
msgstr "工单流程"
|
||||
|
||||
#: tickets/serializers/flow.py:50
|
||||
msgid "A ticket flow with the same name and type already exists"
|
||||
msgstr "已存在同名且同类型的工单流"
|
||||
|
||||
#: tickets/serializers/flow.py:57
|
||||
msgid "The number of approval rules must match the approval level"
|
||||
msgstr "审批规则数量必须与审批级数一致"
|
||||
|
||||
#: tickets/serializers/ticket/ticket.py:109
|
||||
msgid "The selected ticket flow is not available"
|
||||
msgstr "所选工单流不可用"
|
||||
|
||||
#: tickets/serializers/ticket/ticket.py:113
|
||||
msgid "Please select a ticket flow"
|
||||
msgstr "请选择工单流"
|
||||
|
||||
#: tickets/models/relation.py:12
|
||||
msgid "Ticket session relation"
|
||||
msgstr "工单会话"
|
||||
|
||||
@@ -10371,6 +10371,18 @@ msgstr "1 級"
|
||||
msgid "Two level"
|
||||
msgstr "2 級"
|
||||
|
||||
#: tickets/const.py:52
|
||||
msgid "Three level"
|
||||
msgstr "3 級"
|
||||
|
||||
#: tickets/const.py:53
|
||||
msgid "Four level"
|
||||
msgstr "4 級"
|
||||
|
||||
#: tickets/const.py:54
|
||||
msgid "Five level"
|
||||
msgstr "5 級"
|
||||
|
||||
#: tickets/const.py:55
|
||||
msgid "All assets"
|
||||
msgstr "所有資產"
|
||||
@@ -10436,6 +10448,22 @@ msgstr "工單批准資訊"
|
||||
msgid "Ticket flow"
|
||||
msgstr "工單流程"
|
||||
|
||||
#: tickets/serializers/flow.py:50
|
||||
msgid "A ticket flow with the same name and type already exists"
|
||||
msgstr "已存在同名且同類型的工單流"
|
||||
|
||||
#: tickets/serializers/flow.py:57
|
||||
msgid "The number of approval rules must match the approval level"
|
||||
msgstr "審批規則數量必須與審批級數一致"
|
||||
|
||||
#: tickets/serializers/ticket/ticket.py:109
|
||||
msgid "The selected ticket flow is not available"
|
||||
msgstr "所選工單流不可用"
|
||||
|
||||
#: tickets/serializers/ticket/ticket.py:113
|
||||
msgid "Please select a ticket flow"
|
||||
msgstr "請選擇工單流"
|
||||
|
||||
#: tickets/models/relation.py:12
|
||||
msgid "Ticket session relation"
|
||||
msgstr "工單會話"
|
||||
|
||||
@@ -7,11 +7,12 @@ from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.generics import RetrieveAPIView
|
||||
|
||||
from assets.models import (
|
||||
Asset, Zone, Label, Node,
|
||||
Asset, Zone, Node,
|
||||
)
|
||||
from common.api import JMSBulkModelViewSet
|
||||
from common.permissions import IsValidUser, IsValidLicenseForWriteAction
|
||||
from common.utils import get_logger
|
||||
from labels.models import Label
|
||||
from orgs.utils import current_org, tmp_to_root_org
|
||||
from perms.models import AssetPermission
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
@@ -109,7 +109,7 @@ exclude_permissions = (
|
||||
('audits', 'usersession', 'add,delete,change', 'usersession'),
|
||||
('audits', 'ftplog', 'delete', 'ftplog'),
|
||||
('tickets', 'ticketassignee', '*', 'ticketassignee'),
|
||||
('tickets', 'ticketflow', 'add,delete', 'ticketflow'),
|
||||
('tickets', 'ticketflow', 'delete', 'ticketflow'),
|
||||
('tickets', 'comment', '*', '*'),
|
||||
('tickets', 'ticket', 'add,delete,change', 'ticket'),
|
||||
('tickets', 'ticketstep', '*', '*'),
|
||||
|
||||
@@ -7,14 +7,14 @@ from common.utils import lazyproperty
|
||||
from rbac.permissions import RBACPermission
|
||||
from tickets import serializers
|
||||
from tickets.models import Ticket, Comment
|
||||
from tickets.permissions.comment import IsAssignee, IsApplicant, IsSwagger
|
||||
from tickets.permissions.comment import IsAssignee, IsApplicant, IsCcUser, IsSwagger
|
||||
|
||||
__all__ = ['CommentViewSet']
|
||||
|
||||
|
||||
class CommentViewSet(mixins.CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = serializers.CommentSerializer
|
||||
permission_classes = (RBACPermission, IsSwagger | IsAssignee | IsApplicant)
|
||||
permission_classes = (RBACPermission, IsSwagger | IsAssignee | IsApplicant | IsCcUser)
|
||||
rbac_perms = {
|
||||
'*': 'tickets.view_ticket'
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import MethodNotAllowed
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
|
||||
from tickets import serializers
|
||||
from tickets.models import TicketFlow
|
||||
from common.api import JMSBulkModelViewSet
|
||||
from common.const.http import GET
|
||||
|
||||
__all__ = ['TicketFlowViewSet']
|
||||
|
||||
|
||||
class TicketFlowViewSet(JMSBulkModelViewSet):
|
||||
serializer_class = serializers.TicketFlowSerializer
|
||||
filterset_fields = ['id', 'type']
|
||||
search_fields = ['id', 'type']
|
||||
filterset_fields = ['id', 'name', 'type']
|
||||
search_fields = ['id', 'name', 'type']
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
raise MethodNotAllowed(self.action)
|
||||
@@ -19,6 +23,46 @@ class TicketFlowViewSet(JMSBulkModelViewSet):
|
||||
queryset = TicketFlow.get_org_related_flows()
|
||||
return queryset
|
||||
|
||||
@action(
|
||||
detail=False, methods=[GET], permission_classes=[IsAuthenticated],
|
||||
url_path='options'
|
||||
)
|
||||
def flow_options(self, request, *args, **kwargs):
|
||||
ticket_type = request.query_params.get('type')
|
||||
org_id = request.query_params.get('org_id')
|
||||
if not ticket_type or not org_id:
|
||||
return Response([])
|
||||
|
||||
flows = TicketFlow.get_org_related_flows(org_id=org_id).filter(
|
||||
type=ticket_type
|
||||
).prefetch_related('cc_users').order_by('name', 'date_created')
|
||||
serializer = serializers.TicketFlowOptionSerializer(flows, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(
|
||||
detail=False, methods=[GET], permission_classes=[IsAuthenticated],
|
||||
url_path='cc-users'
|
||||
)
|
||||
def cc_users(self, request, *args, **kwargs):
|
||||
ticket_type = request.query_params.get('type')
|
||||
org_id = request.query_params.get('org_id')
|
||||
if not ticket_type or not org_id:
|
||||
return Response([])
|
||||
|
||||
flow_id = request.query_params.get('flow_id')
|
||||
flows = TicketFlow.get_org_related_flows(org_id=org_id).filter(type=ticket_type)
|
||||
if flow_id:
|
||||
flows = flows.filter(id=flow_id)
|
||||
elif flows.count() != 1:
|
||||
return Response([])
|
||||
|
||||
flow = flows.first()
|
||||
if not flow:
|
||||
return Response([])
|
||||
|
||||
serializer = self.get_serializer(flow)
|
||||
return Response(serializer.data['cc_users'])
|
||||
|
||||
def perform_create_or_update(self, serializer):
|
||||
instance = serializer.save()
|
||||
instance.save()
|
||||
|
||||
@@ -49,6 +49,9 @@ class TicketAction(TextChoices):
|
||||
class TicketLevel(IntegerChoices):
|
||||
one = 1, _("One level")
|
||||
two = 2, _("Two level")
|
||||
three = 3, _("Three level")
|
||||
four = 4, _("Four level")
|
||||
five = 5, _("Five level")
|
||||
|
||||
|
||||
class TicketApplyAssetScope(TextChoices):
|
||||
|
||||
@@ -5,7 +5,8 @@ from common.utils import get_logger, convert_html_to_markdown
|
||||
from tickets.const import TicketState, TicketType
|
||||
from tickets.utils import (
|
||||
send_ticket_processed_mail_to_applicant,
|
||||
send_ticket_applied_mail_to_assignees
|
||||
send_ticket_applied_mail_to_assignees,
|
||||
send_ticket_updated_mail_to_cc_users
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -23,11 +24,14 @@ class BaseHandler:
|
||||
|
||||
def _on_pending(self):
|
||||
self._send_applied_mail_to_assignees()
|
||||
self._send_ticket_updated_mail_to_cc_users()
|
||||
|
||||
def on_step_state_change(self, step, state):
|
||||
self._create_state_change_comment(state)
|
||||
handler = getattr(self, f'_on_step_{state}', lambda: None)
|
||||
return handler(step)
|
||||
result = handler(step)
|
||||
self._send_ticket_updated_mail_to_cc_users()
|
||||
return result
|
||||
|
||||
def _on_step_approved(self, step):
|
||||
next_step = step.next()
|
||||
@@ -60,6 +64,12 @@ class BaseHandler:
|
||||
logger.debug('Send processed mail to applicant: {}'.format(applicant))
|
||||
send_ticket_processed_mail_to_applicant(self.ticket, processor)
|
||||
|
||||
def _send_ticket_updated_mail_to_cc_users(self):
|
||||
cc_users = self.ticket.cc_users.exclude(id=self.ticket.applicant_id)
|
||||
cc_users_display = ', '.join([str(user) for user in cc_users])
|
||||
logger.debug('Send updated email to CC users: {}'.format(cc_users_display))
|
||||
send_ticket_updated_mail_to_cc_users(self.ticket)
|
||||
|
||||
def _diff_prev_approve_context(self, state):
|
||||
diff_context = {}
|
||||
if state != TicketState.approved:
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Generated by Django 4.1.13 on 2026-08-03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tickets', '0005_alter_ticket_state_alter_ticket_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='cc_users',
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name='cc_tickets', to='users.user', verbose_name='CC users'
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticketflow',
|
||||
name='cc_users',
|
||||
field=models.ManyToManyField(
|
||||
blank=True, related_name='cc_ticket_flows', to='users.user', verbose_name='CC users'
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,66 @@
|
||||
# Generated by Django 4.1.13 on 2026-08-03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tickets', '0006_ticket_cc_users_ticketflow_cc_users'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ticketflow',
|
||||
name='name',
|
||||
field=models.CharField(blank=True, default='', max_length=128, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='approvalrule',
|
||||
name='level',
|
||||
field=models.SmallIntegerField(
|
||||
choices=[
|
||||
(1, 'One level'), (2, 'Two level'), (3, 'Three level'),
|
||||
(4, 'Four level'), (5, 'Five level')
|
||||
],
|
||||
default=1,
|
||||
verbose_name='Approve level',
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticketflow',
|
||||
name='approval_level',
|
||||
field=models.SmallIntegerField(
|
||||
choices=[
|
||||
(1, 'One level'), (2, 'Two level'), (3, 'Three level'),
|
||||
(4, 'Four level'), (5, 'Five level')
|
||||
],
|
||||
default=1,
|
||||
verbose_name='Approve level',
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticketstep',
|
||||
name='level',
|
||||
field=models.SmallIntegerField(
|
||||
choices=[
|
||||
(1, 'One level'), (2, 'Two level'), (3, 'Three level'),
|
||||
(4, 'Four level'), (5, 'Five level')
|
||||
],
|
||||
default=1,
|
||||
verbose_name='Approve level',
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticket',
|
||||
name='approval_step',
|
||||
field=models.SmallIntegerField(
|
||||
choices=[
|
||||
(1, 'One level'), (2, 'Two level'), (3, 'Three level'),
|
||||
(4, 'Four level'), (5, 'Five level')
|
||||
],
|
||||
default=1,
|
||||
verbose_name='Approval step',
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -38,6 +38,9 @@ class ApprovalRule(JMSBaseModel):
|
||||
|
||||
|
||||
class TicketFlow(JMSBaseModel, OrgModelMixin):
|
||||
name = models.CharField(
|
||||
max_length=128, blank=True, default='', verbose_name=_('Name')
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=64, choices=TicketType.choices,
|
||||
default=TicketType.general, verbose_name=_("Type")
|
||||
@@ -48,12 +51,16 @@ class TicketFlow(JMSBaseModel, OrgModelMixin):
|
||||
verbose_name=_('Approve level')
|
||||
)
|
||||
rules = models.ManyToManyField(ApprovalRule, related_name='ticket_flows')
|
||||
cc_users = models.ManyToManyField(
|
||||
'users.User', related_name='cc_ticket_flows', blank=True,
|
||||
verbose_name=_('CC users')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Ticket flow')
|
||||
|
||||
def __str__(self):
|
||||
return '{}'.format(self.type)
|
||||
return self.name or self.get_type_display()
|
||||
|
||||
@classmethod
|
||||
def get_org_related_flows(cls, org_id=None):
|
||||
|
||||
@@ -133,6 +133,7 @@ class StatusMixin:
|
||||
self._change_state_by_applicant(TicketState.pending)
|
||||
|
||||
def open(self):
|
||||
self.cc_users.set(self.flow.cc_users.all())
|
||||
self.create_process_steps_by_flow()
|
||||
self._open()
|
||||
|
||||
@@ -288,6 +289,10 @@ class Ticket(StatusMixin, JMSBaseModel):
|
||||
'users.User', related_name='applied_tickets', null=True,
|
||||
on_delete=models.SET_NULL, verbose_name=_("Applicant")
|
||||
)
|
||||
cc_users = models.ManyToManyField(
|
||||
'users.User', related_name='cc_tickets', blank=True,
|
||||
verbose_name=_('CC users')
|
||||
)
|
||||
flow = models.ForeignKey(
|
||||
'TicketFlow', related_name='tickets', null=True,
|
||||
on_delete=models.SET_NULL, verbose_name=_('TicketFlow')
|
||||
@@ -343,12 +348,16 @@ class Ticket(StatusMixin, JMSBaseModel):
|
||||
|
||||
@classmethod
|
||||
def get_user_related_tickets(cls, user):
|
||||
queries = Q(applicant=user) | Q(ticket_steps__ticket_assignees__assignee=user)
|
||||
queries = (
|
||||
Q(applicant=user) |
|
||||
Q(ticket_steps__ticket_assignees__assignee=user) |
|
||||
Q(cc_users=user)
|
||||
)
|
||||
# TODO: 与 StatusMixin.process_map 内连表查询有部分重叠 有优化空间 待验证排除是否不影响其它调用
|
||||
prefetch_ticket_assignee = Prefetch('ticket_steps__ticket_assignees',
|
||||
queryset=TicketAssignee.objects.select_related('assignee'), )
|
||||
tickets = cls.objects.prefetch_related(prefetch_ticket_assignee) \
|
||||
.select_related('applicant') \
|
||||
tickets = cls.objects.prefetch_related(prefetch_ticket_assignee, 'cc_users') \
|
||||
.select_related('applicant', 'flow') \
|
||||
.filter(queries) \
|
||||
.distinct()
|
||||
return tickets
|
||||
|
||||
@@ -94,7 +94,10 @@ class BaseTicketMessage(UserMessage):
|
||||
|
||||
@property
|
||||
def basic_items(self):
|
||||
item_names = ['serial_num', 'title', 'type', 'state', 'org_id', 'applicant', 'comment']
|
||||
item_names = [
|
||||
'serial_num', 'title', 'type', 'state', 'org_id',
|
||||
'applicant', 'cc_users', 'comment'
|
||||
]
|
||||
return self._get_fields_items(item_names)
|
||||
|
||||
@property
|
||||
@@ -182,3 +185,27 @@ class TicketProcessedToApplicantMessage(BaseTicketMessage):
|
||||
user = User.objects.first()
|
||||
processor = User.objects.last()
|
||||
return cls(user, ticket, processor)
|
||||
|
||||
|
||||
class TicketUpdatedToCcUserMessage(BaseTicketMessage):
|
||||
def __init__(self, user, ticket):
|
||||
self.ticket = ticket
|
||||
super().__init__(user)
|
||||
|
||||
@property
|
||||
def content_title(self):
|
||||
return _('A ticket you are copied on has been updated')
|
||||
|
||||
@property
|
||||
def subject(self):
|
||||
return _('Ticket updated - {} ({})').format(
|
||||
self.ticket.title, self.ticket.get_type_display()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def gen_test_msg(cls):
|
||||
from .models import Ticket
|
||||
from users.models import User
|
||||
ticket = Ticket.objects.first()
|
||||
user = User.objects.first()
|
||||
return cls(user, ticket)
|
||||
|
||||
@@ -16,3 +16,11 @@ class IsApplicant(permissions.BasePermission):
|
||||
class IsAssignee(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
return view.ticket.has_all_assignee(request.user)
|
||||
|
||||
|
||||
class IsCcUser(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
return (
|
||||
request.method in permissions.SAFE_METHODS and
|
||||
view.ticket.cc_users.filter(id=request.user.id).exists()
|
||||
)
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.serializers.fields import LabeledChoiceField, JSONManyToManyField
|
||||
from common.serializers.fields import LabeledChoiceField, JSONManyToManyField, ObjectRelatedField
|
||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||
from orgs.models import Organization
|
||||
from orgs.utils import get_current_org_id
|
||||
from tickets.const import TicketType
|
||||
from tickets.const import TicketLevel, TicketType
|
||||
from tickets.models import TicketFlow, ApprovalRule
|
||||
from users.models import User
|
||||
|
||||
__all__ = ['TicketFlowSerializer']
|
||||
__all__ = ['TicketFlowSerializer', 'TicketFlowOptionSerializer']
|
||||
|
||||
|
||||
class TicketFlowApproveSerializer(serializers.ModelSerializer):
|
||||
@@ -21,27 +22,50 @@ class TicketFlowApproveSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class TicketFlowSerializer(OrgResourceModelSerializerMixin):
|
||||
name = serializers.CharField(
|
||||
required=True, allow_blank=False, max_length=128, label=_('Name')
|
||||
)
|
||||
type = LabeledChoiceField(
|
||||
choices=TicketType.choices, required=True, label=_('Type')
|
||||
choices=TicketType.choices, read_only=True, label=_('Type')
|
||||
)
|
||||
rules = TicketFlowApproveSerializer(many=True, required=True)
|
||||
cc_users = ObjectRelatedField(
|
||||
queryset=User.objects, many=True, required=False,
|
||||
attrs=('id', 'name', 'username'), label=_('CC users')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = TicketFlow
|
||||
fields_mini = ['id', 'type']
|
||||
fields_mini = ['id', 'name', 'type']
|
||||
fields_small = fields_mini + [
|
||||
'approval_level', 'created_by', 'date_created',
|
||||
'date_updated', 'org_id', 'org_name'
|
||||
]
|
||||
fields = fields_small + ['rules']
|
||||
fields = fields_small + ['rules', 'cc_users']
|
||||
read_only_fields = ['created_by', 'date_created', 'date_updated']
|
||||
|
||||
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 validate(self, attrs):
|
||||
attrs = super().validate(attrs)
|
||||
name = attrs.get('name', getattr(self.instance, 'name', ''))
|
||||
ticket_type = attrs.get('type', getattr(self.instance, 'type', None))
|
||||
current_org_id = str(get_current_org_id())
|
||||
flows = TicketFlow.objects.filter(
|
||||
org_id=current_org_id, name__iexact=name, type=ticket_type
|
||||
)
|
||||
if self.instance and self.instance.org_id == current_org_id:
|
||||
flows = flows.exclude(id=self.instance.id)
|
||||
if flows.exists():
|
||||
error = _('A ticket flow with the same name and type already exists')
|
||||
raise serializers.ValidationError({'name': error})
|
||||
|
||||
approval_level = attrs.get(
|
||||
'approval_level', getattr(self.instance, 'approval_level', TicketLevel.one)
|
||||
)
|
||||
rules = attrs.get('rules')
|
||||
if rules is not None and len(rules) != approval_level:
|
||||
error = _('The number of approval rules must match the approval level')
|
||||
raise serializers.ValidationError({'rules': error})
|
||||
return attrs
|
||||
|
||||
def create_or_update(self, action, validated_data, instance=None):
|
||||
children = validated_data.pop('rules', [])
|
||||
@@ -59,13 +83,28 @@ class TicketFlowSerializer(OrgResourceModelSerializerMixin):
|
||||
return instance
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['type'] = TicketType.apply_asset
|
||||
return self.create_or_update('create', validated_data)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
current_org_id = get_current_org_id()
|
||||
current_org_id = str(get_current_org_id())
|
||||
root_org_id = Organization.ROOT_ID
|
||||
if instance.org_id == root_org_id and current_org_id != root_org_id:
|
||||
instance = self.create(validated_data)
|
||||
else:
|
||||
instance = self.create_or_update('update', validated_data, instance)
|
||||
return instance
|
||||
|
||||
|
||||
class TicketFlowOptionSerializer(serializers.ModelSerializer):
|
||||
type = LabeledChoiceField(choices=TicketType.choices, read_only=True, label=_('Type'))
|
||||
approval_level = LabeledChoiceField(
|
||||
choices=TicketLevel.choices, read_only=True, label=_('Approve level')
|
||||
)
|
||||
cc_users = ObjectRelatedField(
|
||||
many=True, read_only=True, attrs=('id', 'name', 'username'), label=_('CC users')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = TicketFlow
|
||||
fields = ['id', 'name', 'type', 'approval_level', 'cc_users']
|
||||
|
||||
@@ -31,12 +31,13 @@ class ApplyAssetSerializer(BaseApplyAssetSerializer, TicketApplySerializer):
|
||||
class Meta(TicketApplySerializer.Meta):
|
||||
model = ApplyAssetTicket
|
||||
writeable_fields = [
|
||||
'id', 'title', 'type', 'apply_nodes', 'apply_assets',
|
||||
'id', 'title', 'type', 'flow_id', 'apply_nodes', 'apply_assets',
|
||||
'apply_accounts', 'apply_actions', 'apply_date_start',
|
||||
'apply_date_expired', 'comment', 'org_id'
|
||||
]
|
||||
read_only_fields = TicketApplySerializer.Meta.read_only_fields + ['apply_permission_name', ]
|
||||
fields = TicketApplySerializer.Meta.fields_small + writeable_fields + read_only_fields
|
||||
fields = TicketApplySerializer.Meta.fields_small + \
|
||||
TicketApplySerializer.Meta.fields_m2m + writeable_fields + read_only_fields
|
||||
ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs
|
||||
extra_kwargs = {
|
||||
'apply_accounts': {'required': False},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.serializers.fields import LabeledChoiceField
|
||||
from common.serializers.fields import LabeledChoiceField, ObjectRelatedField
|
||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||
from orgs.models import Organization
|
||||
from tickets.const import TicketType, TicketStatus, TicketState
|
||||
@@ -20,17 +20,24 @@ class TicketSerializer(OrgResourceModelSerializerMixin):
|
||||
status = LabeledChoiceField(choices=TicketStatus.choices, read_only=True, label=_('Status'))
|
||||
state = LabeledChoiceField(choices=TicketState.choices, read_only=True, label=_("State"))
|
||||
process_map = serializers.JSONField(read_only=True, default=list, label=_('Process map'))
|
||||
cc_users = ObjectRelatedField(
|
||||
many=True, read_only=True, attrs=('id', 'name', 'username'), label=_('CC users')
|
||||
)
|
||||
flow = ObjectRelatedField(
|
||||
read_only=True, attrs=('id', 'name'), label=_('Ticket flow')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields_mini = ['id', 'title']
|
||||
fields_small = fields_mini + ['org_id', 'comment']
|
||||
fields_small = fields_mini + ['org_id', 'flow', 'comment']
|
||||
fields_m2m = ['cc_users']
|
||||
read_only_fields = [
|
||||
'serial_num', 'process_map', 'approval_step', 'type',
|
||||
'state', 'applicant', 'status', 'date_created',
|
||||
'date_updated', 'org_name', 'rel_snapshot'
|
||||
]
|
||||
fields = fields_small + read_only_fields
|
||||
fields = fields_small + fields_m2m + read_only_fields
|
||||
extra_kwargs = {}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
@@ -58,11 +65,17 @@ class TicketApproveSerializer(TicketSerializer):
|
||||
|
||||
|
||||
class TicketApplySerializer(TicketSerializer):
|
||||
flow_id = serializers.UUIDField(
|
||||
required=False, write_only=True, label=_('Ticket flow')
|
||||
)
|
||||
org_id = serializers.CharField(
|
||||
required=True, max_length=36, allow_blank=True, label=_("Organization")
|
||||
)
|
||||
applicant = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
class Meta(TicketSerializer.Meta):
|
||||
fields = TicketSerializer.Meta.fields + ['flow_id']
|
||||
|
||||
def get_applicant(self, applicant_id):
|
||||
current_user = self.context['request'].user
|
||||
want_applicant = User.objects.filter(id=applicant_id).first()
|
||||
@@ -82,11 +95,25 @@ class TicketApplySerializer(TicketSerializer):
|
||||
|
||||
def validate(self, attrs):
|
||||
if self.instance:
|
||||
attrs.pop('flow_id', None)
|
||||
return attrs
|
||||
|
||||
ticket_type = attrs.get('type')
|
||||
org_id = attrs.get('org_id')
|
||||
flow = TicketFlow.get_org_related_flows(org_id=org_id).filter(type=ticket_type).first()
|
||||
flow_id = attrs.pop('flow_id', None)
|
||||
flows = TicketFlow.get_org_related_flows(org_id=org_id).filter(
|
||||
type=ticket_type
|
||||
).order_by('date_created')
|
||||
if flow_id:
|
||||
flow = flows.filter(id=flow_id).first()
|
||||
if not flow:
|
||||
error = _('The selected ticket flow is not available')
|
||||
raise serializers.ValidationError({'flow_id': error})
|
||||
else:
|
||||
if flows.count() > 1:
|
||||
error = _('Please select a ticket flow')
|
||||
raise serializers.ValidationError({'flow_id': error})
|
||||
flow = flows.first()
|
||||
if not flow:
|
||||
error = _('The ticket flow `{}` does not exist'.format(ticket_type))
|
||||
raise serializers.ValidationError(error)
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
from .notifications import TicketAppliedToAssigneeMessage, TicketProcessedToApplicantMessage
|
||||
from .notifications import (
|
||||
TicketAppliedToAssigneeMessage, TicketProcessedToApplicantMessage,
|
||||
TicketUpdatedToCcUserMessage
|
||||
)
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
@@ -33,3 +36,15 @@ def send_ticket_processed_mail_to_applicant(ticket, processor):
|
||||
if settings.DEBUG:
|
||||
logger.debug(instance)
|
||||
instance.publish_async()
|
||||
|
||||
|
||||
def send_ticket_updated_mail_to_cc_users(ticket):
|
||||
cc_users = ticket.cc_users.exclude(id=ticket.applicant_id)
|
||||
if not cc_users:
|
||||
return
|
||||
|
||||
for user in cc_users:
|
||||
instance = TicketUpdatedToCcUserMessage(user, ticket)
|
||||
if settings.DEBUG:
|
||||
logger.debug(instance)
|
||||
instance.publish_async()
|
||||
|
||||
Reference in New Issue
Block a user