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:
feng
2026-08-03 18:12:12 +08:00
committed by feng626
parent a8317b1b7b
commit 4e8504f150
20 changed files with 429 additions and 33 deletions

View File

@@ -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 ""

View File

@@ -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 "チケットセッションの関係"

View File

@@ -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 "工单会话"

View File

@@ -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 "工單會話"

View File

@@ -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

View File

@@ -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', '*', '*'),

View File

@@ -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'
}

View File

@@ -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()

View File

@@ -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):

View File

@@ -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:

View File

@@ -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'
),
),
]

View File

@@ -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',
),
),
]

View File

@@ -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):

View File

@@ -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

View File

@@ -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)

View File

@@ -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()
)

View File

@@ -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']

View File

@@ -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},

View File

@@ -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)

View File

@@ -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()