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

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

* feat: 工单权限处理

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

* perf: 修改一点

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

View File

@ -83,11 +83,11 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
@classmethod
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
data = {
'title': _('Login asset confirm') + ' ({})'.format(user),
'type': TicketTypeChoices.login_asset_confirm,
'type': TicketType.login_asset_confirm,
'meta': {
'apply_login_user': str(user),
'apply_login_asset': str(asset),
@ -96,7 +96,7 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
'org_id': org_id,
}
ticket = Ticket.objects.create(**data)
ticket.assignees.set(assignees)
ticket.create_process_map_and_node(assignees)
ticket.open(applicant=user)
return ticket

View File

@ -105,11 +105,11 @@ class CommandFilterRule(OrgModelMixin):
return '{} % {}'.format(self.type, self.content)
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
data = {
'title': _('Command confirm') + ' ({})'.format(session.user),
'type': TicketTypeChoices.command_confirm,
'type': TicketType.command_confirm,
'meta': {
'apply_run_user': session.user,
'apply_run_asset': session.asset,
@ -122,6 +122,6 @@ class CommandFilterRule(OrgModelMixin):
'org_id': org_id,
}
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)
return ticket

View File

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

View File

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

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

@ -0,0 +1,20 @@
import json
from datetime import datetime
import uuid
from django.utils.translation import ugettext_lazy as _
from django.conf import settings
class ModelJSONFieldEncoder(json.JSONEncoder):
""" 解决一些类型的字段不能序列化的问题 """
def default(self, obj):
if isinstance(obj, datetime):
return obj.strftime(settings.DATETIME_DISPLAY_FORMAT)
if isinstance(obj, uuid.UUID):
return str(obj)
if isinstance(obj, type(_("ugettext_lazy"))):
return str(obj)
else:
return super().default(obj)

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ from common.utils import get_logger
from tickets.utils import (
send_ticket_processed_mail_to_applicant, send_ticket_applied_mail_to_assignees
)
from tickets.const import TicketAction
logger = get_logger(__name__)
@ -16,48 +16,72 @@ class BaseHandler(object):
# on action
def _on_open(self):
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: {})()
self.ticket.meta.update(meta_display)
self.ticket.save()
self._send_applied_mail_to_assignees()
def _on_approve(self):
meta_display = getattr(self, '_construct_meta_display_of_approve', lambda: {})()
self.ticket.meta.update(meta_display)
self.__on_process()
if self.ticket.approval_step != len(self.ticket.process_map):
self.ticket.approval_step += 1
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):
self.__on_process()
self.ticket.set_state_reject()
self.ticket.set_status_closed()
self.__on_process(self.ticket.processor)
def _on_close(self):
self.__on_process()
def __on_process(self):
self.ticket.processor_display = str(self.ticket.processor)
self.ticket.set_state_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()
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)
return method()
# email
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)
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))
send_ticket_processed_mail_to_applicant(self.ticket)
send_ticket_processed_mail_to_applicant(self.ticket, processor)
# comments
def _create_comment_on_action(self):
user = self.ticket.applicant if self.ticket.action_open else self.ticket.processor
def _create_comment_on_action(self, action):
user = self.ticket.processor
# 打开或关闭工单,备注显示是自己,其他是受理人
if self.ticket.state_open or self.ticket.state_close:
user = self.ticket.applicant
user_display = str(user)
action_display = self.ticket.get_action_display()
action_display = getattr(TicketAction, action).label
data = {
'body': _('{} {} the ticket').format(user_display, action_display),
'user': user,
@ -85,18 +109,12 @@ class BaseHandler(object):
{}: {},
{}: {},
{}: {},
{}: {},
{}: {}
'''.format(
_('Ticket title'), self.ticket.title,
_('Ticket type'), self.ticket.get_type_display(),
_('Ticket status'), self.ticket.get_status_display(),
_('Ticket action'), self.ticket.get_action_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)
return body
@ -104,9 +122,6 @@ class BaseHandler(object):
body = ''
open_body = self._base_construct_meta_body_of_open()
body += open_body
if self.ticket.action_approve:
approve_body = self._base_construct_meta_body_of_approve()
body += approve_body
return body
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)
return body
def _base_construct_meta_body_of_approve(self):
meta_body_of_approve = getattr(
self, '_construct_meta_body_of_approve', lambda: _('No content')
)()
body = self.body_html_format.format(_('Ticket approved info'), meta_body_of_approve)
return body

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,9 +26,10 @@ EMAIL_TEMPLATE = '''
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(
ticket, str(ticket.id), ticket.assignees)
ticket, str(ticket.id), assignees)
)
return
@ -42,24 +43,24 @@ def send_ticket_applied_mail_to_assignees(ticket):
)
if settings.DEBUG:
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)
def send_ticket_processed_mail_to_applicant(ticket):
def send_ticket_processed_mail_to_applicant(ticket, processor):
if not ticket.applicant:
logger.error("Not found applicant: {}({})".format(ticket.title, ticket.id))
return
processor_display = str(processor)
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(
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_description=_('click here to review'),
body=ticket.body.replace('\n', '<br/>'),
)
if settings.DEBUG:
logger.debug(message)
recipient_list = [ticket.applicant.email]
recipient_list = [ticket.applicant.email, ]
send_mail_async.delay(subject, message, recipient_list, html_message=message)

View File

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