perf: 重构 ticket (#8281)

* perf: 重构 ticket

* perf: 优化 tickets

* perf: 暂存

* perf: 建立 ticket model

* perf: 暂存一下

* perf: 修改 tickets

* perf: 修改 import

* perf: 修改model

* perf: 暂存一波

* perf: 修改...

* del process_map field

* 工单重构

* 资产 应用对接前端

* perf: 修改 ticket

* fix: bug

* 修改迁移文件

* 添加其他api

* 去掉process_map

* perf: 优化去掉 signal

* perf: 修改这里

* 修改一点

* perf: 修改工单

* perf: 修改状态

* perf: 修改工单流转

* step 状态切换

* perf: 修改 ticket open

* perf: 修改流程

* perf: stash it

* 改又改

* stash it

* perf: stash

* stash

* migrate

* perf migrate

* 调整一下

* 修复bug

* 修改一点

* 修改一点

* 优化一波

* perf: ticket migrations

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: feng626 <1304903146@qq.com>
pull/8465/head
fit2bot 2022-06-23 13:52:28 +08:00 committed by GitHub
parent 2471787277
commit 7e2f81a418
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
73 changed files with 2004 additions and 1417 deletions

View File

@ -47,7 +47,7 @@ class LoginAssetCheckAPI(CreateAPIView):
asset=self.serializer.asset,
system_user=self.serializer.system_user,
assignees=acl.reviewers.all(),
org_id=self.serializer.org.id
org_id=self.serializer.org.id,
)
confirm_status_url = reverse(
view_name='api-tickets:super-ticket-status',
@ -59,7 +59,7 @@ class LoginAssetCheckAPI(CreateAPIView):
external=True, api_to_ui=True
)
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
ticket_assignees = ticket.current_node.first().ticket_assignees.all()
ticket_assignees = ticket.current_step.ticket_assignees.all()
data = {
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},

View File

@ -97,34 +97,25 @@ class LoginACL(BaseACL):
return allow, reject_type
@staticmethod
def construct_confirm_ticket_meta(request=None):
def create_confirm_ticket(self, request):
from tickets import const
from tickets.models import ApplyLoginTicket
from orgs.models import Organization
title = _('Login confirm') + ' {}'.format(self.user)
login_ip = get_request_ip(request) if request else ''
login_ip = login_ip or '0.0.0.0'
login_city = get_ip_city(login_ip)
login_datetime = local_now_display()
ticket_meta = {
'apply_login_ip': login_ip,
'apply_login_city': login_city,
'apply_login_datetime': login_datetime,
}
return ticket_meta
def create_confirm_ticket(self, request=None):
from tickets import const
from tickets.models import Ticket
from orgs.models import Organization
ticket_title = _('Login confirm') + ' {}'.format(self.user)
ticket_meta = self.construct_confirm_ticket_meta(request)
data = {
'title': ticket_title,
'type': const.TicketType.login_confirm.value,
'meta': ticket_meta,
'title': title,
'type': const.TicketType.login_confirm,
'applicant': self.user,
'apply_login_city': login_city,
'apply_login_ip': login_ip,
'apply_login_datetime': login_datetime,
'org_id': Organization.ROOT_ID,
}
ticket = Ticket.objects.create(**data)
applicant = self.user
ticket = ApplyLoginTicket.objects.create(**data)
assignees = self.reviewers.all()
ticket.create_process_map_and_node(assignees, applicant)
ticket.open(applicant)
ticket.open_by_system(assignees)
return ticket

View File

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

View File

@ -69,7 +69,7 @@ class CommandConfirmAPI(CreateAPIView):
external=True, api_to_ui=True
)
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
ticket_assignees = ticket.current_node.first().ticket_assignees.all()
ticket_assignees = ticket.current_step.ticket_assignees.all()
return {
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},

View File

@ -165,26 +165,23 @@ class CommandFilterRule(OrgModelMixin):
def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
from tickets.const import TicketType
from tickets.models import Ticket
from tickets.models import ApplyCommandTicket
data = {
'title': _('Command confirm') + ' ({})'.format(session.user),
'type': TicketType.command_confirm,
'meta': {
'apply_run_user': session.user,
'apply_run_asset': session.asset,
'apply_run_system_user': session.system_user,
'apply_run_command': run_command,
'apply_from_session_id': str(session.id),
'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id)
},
'applicant': session.user_obj,
'apply_run_user_id': session.user_id,
'apply_run_asset_id': session.asset_id,
'apply_run_system_user_id': session.system_user_id,
'apply_run_command': run_command,
'apply_from_session_id': str(session.id),
'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id),
'org_id': org_id,
}
ticket = Ticket.objects.create(**data)
applicant = session.user_obj
ticket = ApplyCommandTicket.objects.create(**data)
assignees = self.reviewers.all()
ticket.create_process_map_and_node(assignees, applicant)
ticket.open(applicant)
ticket.open_by_system(assignees)
return ticket
@classmethod

View File

@ -25,5 +25,5 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
ticket = self.get_ticket()
if ticket:
request.session.pop('auth_ticket_id', '')
ticket.close(processor=self.get_user_from_session())
ticket.close()
return Response('', status=200)

View File

@ -337,18 +337,18 @@ class AuthACLMixin:
raise errors.TimePeriodNotAllowed(username=user.username, request=self.request)
def get_ticket(self):
from tickets.models import Ticket
from tickets.models import ApplyLoginTicket
ticket_id = self.request.session.get("auth_ticket_id")
logger.debug('Login confirm ticket id: {}'.format(ticket_id))
if not ticket_id:
ticket = None
else:
ticket = Ticket.all().filter(id=ticket_id).first()
ticket = ApplyLoginTicket.all().filter(id=ticket_id).first()
return ticket
def get_ticket_or_create(self, confirm_setting):
ticket = self.get_ticket()
if not ticket or ticket.status_closed:
if not ticket or ticket.is_status(ticket.Status.closed):
ticket = confirm_setting.create_confirm_ticket(self.request)
self.request.session['auth_ticket_id'] = str(ticket.id)
return ticket
@ -357,16 +357,17 @@ class AuthACLMixin:
ticket = self.get_ticket()
if not ticket:
raise errors.LoginConfirmOtherError('', "Not found")
if ticket.status_open:
if ticket.is_status(ticket.Status.open):
raise errors.LoginConfirmWaitError(ticket.id)
elif ticket.state_approve:
elif ticket.is_state(ticket.State.approved):
self.request.session["auth_confirm"] = "1"
return
elif ticket.state_reject:
elif ticket.is_state(ticket.State.rejected):
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_state_display()
)
elif ticket.state_close:
elif ticket.is_state(ticket.State.closed):
raise errors.LoginConfirmOtherError(
ticket.id, ticket.get_state_display()
)

View File

@ -86,7 +86,10 @@ class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
class Meta:
model = SystemUser
fields = ['id', 'name', 'username', 'password', 'private_key', 'protocol', 'ad_domain', 'org_id']
fields = [
'id', 'name', 'username', 'password', 'private_key',
'protocol', 'ad_domain', 'org_id'
]
class ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
@ -122,8 +125,7 @@ class ConnectionTokenFilterRuleSerializer(serializers.ModelSerializer):
model = CommandFilterRule
fields = [
'id', 'type', 'content', 'ignore_case', 'pattern',
'priority', 'action',
'date_created',
'priority', 'action', 'date_created',
]

View File

@ -282,8 +282,7 @@ class UserLoginWaitConfirmView(TemplateView):
if ticket:
timestamp_created = datetime.datetime.timestamp(ticket.date_created)
ticket_detail_url = TICKET_DETAIL_URL.format(id=ticket_id, type=ticket.type)
assignees = ticket.current_node.first().ticket_assignees.all()
assignees_display = ', '.join([str(i.assignee) for i in assignees])
assignees_display = ', '.join([str(assignee) for assignee in ticket.current_assignees])
msg = _("""Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>
Don't close this page""").format(assignees_display)
else:

View File

@ -1,20 +1,30 @@
import json
from datetime import datetime
import uuid
import logging
from datetime import datetime
from django.utils.translation import ugettext_lazy as _
from django.db import models
from django.conf import settings
lazy_type = type(_('ugettext_lazy'))
class ModelJSONFieldEncoder(json.JSONEncoder):
""" 解决一些类型的字段不能序列化的问题 """
def default(self, obj):
if isinstance(obj, datetime):
str_cls = (models.Model, lazy_type, models.ImageField, uuid.UUID)
if isinstance(obj, str_cls):
return str(obj)
elif 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)
elif isinstance(obj, (list, tuple)) and len(obj) > 0 \
and isinstance(obj[0], models.Model):
return [str(i) for i in obj]
else:
return super().default(obj)
try:
return super().default(obj)
except TypeError:
logging.error('Type error: ', type(obj))
return str(obj)

View File

@ -13,6 +13,7 @@ import uuid
from functools import reduce, partial
import inspect
from django.db import transaction
from django.db.models import *
from django.db.models import QuerySet
from django.db.models.functions import Concat
@ -211,3 +212,29 @@ class UnionQuerySet(QuerySet):
qs = cls(assets1, assets2)
return qs
class MultiTableChildQueryset(QuerySet):
def bulk_create(self, objs, batch_size=None):
assert batch_size is None or batch_size > 0
if not objs:
return objs
self._for_write = True
objs = list(objs)
parent_model = self.model._meta.pk.related_model
parent_objs = []
for obj in objs:
parent_values = {}
for field in [f for f in parent_model._meta.fields if hasattr(obj, f.name)]:
parent_values[field.name] = getattr(obj, field.name)
parent_objs.append(parent_model(**parent_values))
setattr(obj, self.model._meta.pk.attname, obj.id)
parent_model.objects.bulk_create(parent_objs, batch_size=batch_size)
with transaction.atomic(using=self.db, savepoint=False):
self._batched_insert(objs, self.model._meta.local_fields, batch_size)
return objs

View File

@ -361,3 +361,7 @@ def pretty_string(data: str, max_length=128, ellipsis_str='...'):
end = data[-half:]
data = f'{start}{ellipsis_str}{end}'
return data
def group_by_count(it, count):
return [it[i:i+count] for i in range(0, len(it), count)]

View File

@ -12,7 +12,7 @@ from common.utils import get_logger
from . import const
from .models import ReplayStorage
from tickets.models import TicketSession, TicketStep, TicketAssignee
from tickets.const import ProcessStatus
from tickets.const import StepState
logger = get_logger(__name__)

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
#
from .ticket import *
from .flow import *
from .comment import *
from .super_ticket import *
from .relation import *

31
apps/tickets/api/flow.py Normal file
View File

@ -0,0 +1,31 @@
from rest_framework.exceptions import MethodNotAllowed
from tickets import serializers
from tickets.models import TicketFlow
from common.drf.api import JMSBulkModelViewSet
__all__ = ['TicketFlowViewSet']
class TicketFlowViewSet(JMSBulkModelViewSet):
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()
def perform_create(self, serializer):
self.perform_create_or_update(serializer)
def perform_update(self, serializer):
self.perform_create_or_update(serializer)

View File

@ -2,36 +2,43 @@
#
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.exceptions import MethodNotAllowed
from rest_framework.response import Response
from rest_framework.exceptions import MethodNotAllowed
from common.const.http import POST, PUT
from common.mixins.api import CommonApiMixin
from common.drf.api import JMSBulkModelViewSet
from orgs.utils import tmp_to_root_org
from rbac.permissions import RBACPermission
from tickets import serializers
from tickets.models import Ticket, TicketFlow
from tickets.filters import TicketFilter
from tickets import filters
from tickets.permissions.ticket import IsAssignee, IsApplicant
from tickets.models import (
Ticket, ApplyAssetTicket, ApplyApplicationTicket,
ApplyLoginTicket, ApplyLoginAssetTicket, ApplyCommandTicket
)
__all__ = ['TicketViewSet', 'TicketFlowViewSet']
__all__ = [
'TicketViewSet', 'ApplyAssetTicketViewSet', 'ApplyApplicationTicketViewSet',
'ApplyLoginTicketViewSet', 'ApplyLoginAssetTicketViewSet', 'ApplyCommandTicketViewSet'
]
class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
serializer_class = serializers.TicketDisplaySerializer
serializer_classes = {
'open': serializers.TicketApplySerializer,
'approve': serializers.TicketApproveSerializer,
'list': serializers.TicketListSerializer,
'open': serializers.TicketApplySerializer
}
filterset_class = TicketFilter
model = Ticket
filterset_class = filters.TicketFilter
search_fields = [
'title', 'type', 'status', 'applicant_display'
]
ordering_fields = (
'title', 'applicant_display', 'status', 'state', 'action_display',
'date_created', 'serial_num',
'title', 'applicant_display', 'status', 'state',
'action_display', 'date_created', 'serial_num',
)
ordering = ('-date_created',)
rbac_perms = {
@ -48,15 +55,15 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
raise MethodNotAllowed(self.action)
def get_queryset(self):
queryset = Ticket.get_user_related_tickets(self.request.user)
with tmp_to_root_org():
queryset = self.model.get_user_related_tickets(self.request.user)
return queryset
def perform_create(self, serializer):
instance = serializer.save()
applicant = self.request.user
instance.create_related_node(applicant)
instance.process_map = instance.create_process_map(applicant)
instance.open(applicant)
instance.applicant = self.request.user
instance.save(update_fields=['applicant'])
instance.open()
@action(detail=False, methods=[POST], permission_classes=[RBACPermission, ])
def open(self, request, *args, **kwargs):
@ -80,29 +87,41 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
def close(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
instance.close(processor=request.user)
instance.close()
return Response(serializer.data)
class TicketFlowViewSet(JMSBulkModelViewSet):
serializer_class = serializers.TicketFlowSerializer
class ApplyAssetTicketViewSet(TicketViewSet):
serializer_class = serializers.ApplyAssetDisplaySerializer
serializer_classes = {
'open': serializers.ApplyAssetSerializer
}
model = ApplyAssetTicket
filterset_class = filters.ApplyAssetTicketFilter
filterset_fields = ['id', 'type']
search_fields = ['id', 'type']
def destroy(self, request, *args, **kwargs):
raise MethodNotAllowed(self.action)
class ApplyApplicationTicketViewSet(TicketViewSet):
serializer_class = serializers.ApplyApplicationDisplaySerializer
serializer_classes = {
'open': serializers.ApplyApplicationSerializer
}
model = ApplyApplicationTicket
filterset_class = filters.ApplyApplicationTicketFilter
def get_queryset(self):
queryset = TicketFlow.get_org_related_flows()
return queryset
def perform_create_or_update(self, serializer):
instance = serializer.save()
instance.save()
class ApplyLoginTicketViewSet(TicketViewSet):
serializer_class = serializers.LoginConfirmSerializer
model = ApplyLoginTicket
filterset_class = filters.ApplyLoginTicketFilter
def perform_create(self, serializer):
self.perform_create_or_update(serializer)
def perform_update(self, serializer):
self.perform_create_or_update(serializer)
class ApplyLoginAssetTicketViewSet(TicketViewSet):
serializer_class = serializers.LoginAssetConfirmSerializer
model = ApplyLoginAssetTicket
filterset_class = filters.ApplyLoginAssetTicketFilter
class ApplyCommandTicketViewSet(TicketViewSet):
serializer_class = serializers.ApplyCommandConfirmSerializer
model = ApplyCommandTicket
filterset_class = filters.ApplyCommandTicketFilter

View File

@ -14,20 +14,29 @@ class TicketType(TextChoices):
class TicketState(TextChoices):
open = 'open', _('Open')
approved = 'approved', _('Approved')
rejected = 'rejected', _('Rejected')
closed = 'closed', _('Closed')
class ProcessStatus(TextChoices):
notified = 'notified', _('Notified')
pending = 'pending', _('Open')
approved = 'approved', _('Approved')
rejected = 'rejected', _('Rejected')
closed = 'closed', _("Cancel")
reopen = 'reopen', _("Reopen")
class TicketStatus(TextChoices):
open = 'open', _("Open")
closed = 'closed', _("Finished")
class StepState(TextChoices):
pending = 'pending', _('Pending')
approved = 'approved', _('Approved')
rejected = 'rejected', _('Rejected')
closed = 'closed', _("Closed")
reopen = 'reopen', _("Reopen")
class StepStatus(TextChoices):
pending = 'pending', _('Pending')
active = 'active', _('Active')
closed = 'closed', _("Closed")
@ -38,7 +47,7 @@ class TicketAction(TextChoices):
reject = 'reject', _('Reject')
class TicketApprovalLevel(IntegerChoices):
class TicketLevel(IntegerChoices):
one = 1, _("One level")
two = 2, _("Two level")

View File

@ -1,7 +1,10 @@
from django_filters import rest_framework as filters
from common.drf.filters import BaseFilterSet
from tickets.models import Ticket
from tickets.models import (
Ticket, ApplyAssetTicket, ApplyApplicationTicket,
ApplyLoginTicket, ApplyLoginAssetTicket, ApplyCommandTicket
)
class TicketFilter(BaseFilterSet):
@ -10,9 +13,38 @@ class TicketFilter(BaseFilterSet):
class Meta:
model = Ticket
fields = (
'id', 'title', 'type', 'status', 'state', 'applicant', 'assignees__id',
'applicant_display',
'id', 'title', 'type', 'status', 'state', 'applicant', 'assignees__id'
)
def filter_assignees_id(self, queryset, name, value):
return queryset.filter(ticket_steps__ticket_assignees__assignee__id=value)
class ApplyAssetTicketFilter(BaseFilterSet):
class Meta:
model = ApplyAssetTicket
fields = ('id',)
class ApplyApplicationTicketFilter(BaseFilterSet):
class Meta:
model = ApplyApplicationTicket
fields = ('id',)
class ApplyLoginTicketFilter(BaseFilterSet):
class Meta:
model = ApplyLoginTicket
fields = ('id',)
class ApplyLoginAssetTicketFilter(BaseFilterSet):
class Meta:
model = ApplyLoginAssetTicket
fields = ('id',)
class ApplyCommandTicketFilter(BaseFilterSet):
class Meta:
model = ApplyCommandTicket
fields = ('id',)

View File

@ -1,108 +0,0 @@
from django.utils.translation import ugettext as _
from orgs.utils import tmp_to_org, tmp_to_root_org
from applications.const import AppCategory, AppType
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):
is_finished = super()._on_approve()
if is_finished:
self._create_application_permission()
# display
def _construct_meta_display_of_open(self):
meta_display_fields = ['apply_category_display', 'apply_type_display']
apply_category = self.ticket.meta.get('apply_category')
apply_category_display = AppCategory.get_label(apply_category)
apply_type = self.ticket.meta.get('apply_type')
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))
apply_system_users = self.ticket.meta.get('apply_system_users')
apply_applications = self.ticket.meta.get('apply_applications')
with tmp_to_org(self.ticket.org_id):
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)]
})
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_applications = self.ticket.meta.get('apply_applications_display', [])
apply_system_users = self.ticket.meta.get('apply_system_users_display', [])
apply_date_start = self.ticket.meta.get('apply_date_start')
apply_date_expired = self.ticket.meta.get('apply_date_expired')
applied_body = '''{}: {},
{}: {}
{}: {}
{}: {}
{}: {}
{}: {}
'''.format(
_('Applied category'), apply_category_display,
_('Applied type'), apply_type_display,
_('Applied application group'), ','.join(apply_applications),
_('Applied system user group'), ','.join(apply_system_users),
_('Applied date start'), apply_date_start,
_('Applied date expired'), apply_date_expired,
)
return applied_body
# permission
def _create_application_permission(self):
with tmp_to_root_org():
application_permission = ApplicationPermission.objects.filter(id=self.ticket.id).first()
if application_permission:
return application_permission
apply_category = self.ticket.meta.get('apply_category')
apply_type = self.ticket.meta.get('apply_type')
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)
)
permission_comment = _(
'Created by the ticket, '
'ticket title: {}, '
'ticket applicant: {}, '
'ticket processor: {}, '
'ticket ID: {}'
).format(
self.ticket.title,
self.ticket.applicant_display,
','.join([i['processor_display'] for i in self.ticket.process_map]),
str(self.ticket.id)
)
permissions_data = {
'id': self.ticket.id,
'name': apply_permission_name,
'from_ticket': True,
'category': apply_category,
'type': apply_type,
'comment': str(permission_comment),
'created_by': permission_created_by,
'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(apply_applications)
application_permission.system_users.set(apply_system_users)
return application_permission

View File

@ -1,106 +0,0 @@
from django.utils.translation import ugettext as _
from assets.models import Node, Asset, SystemUser
from perms.models import AssetPermission, Action
from orgs.utils import tmp_to_org, tmp_to_root_org
from .base import BaseHandler
class Handler(BaseHandler):
def _on_approve(self):
is_finished = super()._on_approve()
if is_finished:
self._create_asset_permission()
# display
def _construct_meta_display_of_open(self):
meta_display_fields = ['apply_actions_display']
apply_actions = self.ticket.meta.get('apply_actions', Action.NONE)
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))
apply_nodes = self.ticket.meta.get('apply_nodes', [])
apply_assets = self.ticket.meta.get('apply_assets', [])
apply_system_users = self.ticket.meta.get('apply_system_users')
with tmp_to_org(self.ticket.org_id):
meta_display.update({
'apply_nodes_display': [str(i) for i in Node.objects.filter(id__in=apply_nodes)],
'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_nodes = self.ticket.meta.get('apply_nodes_display', [])
apply_assets = self.ticket.meta.get('apply_assets_display', [])
apply_system_users = self.ticket.meta.get('apply_system_users_display', [])
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')
applied_body = '''{}: {},
{}: {},
{}: {},
{}: {},
{}: {}
'''.format(
_("Applied node group"), ','.join(apply_nodes),
_("Applied hostname group"), ','.join(apply_assets),
_("Applied system user group"), ','.join(apply_system_users),
_("Applied actions"), ','.join(apply_actions_display),
_('Applied date start'), apply_date_start,
_('Applied date expired'), apply_date_expired,
)
return applied_body
# permission
def _create_asset_permission(self):
with tmp_to_root_org():
asset_permission = AssetPermission.objects.filter(id=self.ticket.id).first()
if asset_permission:
return asset_permission
apply_permission_name = self.ticket.meta.get('apply_permission_name', )
apply_nodes = self.ticket.meta.get('apply_nodes', [])
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)
)
permission_comment = _(
'Created by the ticket '
'ticket title: {} '
'ticket applicant: {} '
'ticket processor: {} '
'ticket ID: {}'
).format(
self.ticket.title,
self.ticket.applicant_display,
','.join([i['processor_display'] for i in self.ticket.process_map]),
str(self.ticket.id)
)
permission_data = {
'id': self.ticket.id,
'name': apply_permission_name,
'from_ticket': True,
'comment': str(permission_comment),
'created_by': permission_created_by,
'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.nodes.set(apply_nodes)
asset_permission.assets.set(apply_assets)
asset_permission.system_users.set(apply_system_users)
return asset_permission

View File

@ -1,135 +0,0 @@
from django.utils.translation import ugettext as _
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__)
class BaseHandler(object):
def __init__(self, ticket):
self.ticket = ticket
# on action
def _on_open(self):
self.ticket.applicant_display = str(self.ticket.applicant)
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):
if self.ticket.approval_step != len(self.ticket.process_map):
self._send_processed_mail_to_applicant(self.ticket.processor)
self.ticket.approval_step += 1
self.ticket.create_related_node()
self._send_applied_mail_to_assignees()
is_finished = False
else:
self.ticket.set_state_approve()
self.ticket.set_status_closed()
self._send_processed_mail_to_applicant(self.ticket.processor)
is_finished = True
self.ticket.save()
return is_finished
def _on_reject(self):
self.ticket.set_state_reject()
self.ticket.set_status_closed()
self.__on_process(self.ticket.processor)
def _on_close(self):
self.ticket.set_state_closed()
self.ticket.set_status_closed()
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):
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):
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, processor):
logger.debug('Send processed mail to applicant: {}'.format(self.ticket.applicant_display))
send_ticket_processed_mail_to_applicant(self.ticket, processor)
# comments
def _create_comment_on_action(self, action):
user = self.ticket.processor
# 打开或关闭工单,备注显示是自己,其他是受理人
if action == TicketAction.open or action == TicketAction.close:
user = self.ticket.applicant
user_display = str(user)
action_display = getattr(TicketAction, action).label
data = {
'body': _('{} {} the ticket').format(user_display, action_display),
'user': user,
'user_display': user_display
}
return self.ticket.comments.create(**data)
# body
body_html_format = '''
{}:
<div style="margin-left: 20px;">{}</div>
'''
def get_body(self):
old_body = self.ticket.meta.get('body')
if old_body:
# 之前版本的body
return old_body
basic_body = self._construct_basic_body()
meta_body = self._construct_meta_body()
return basic_body + meta_body
def _construct_basic_body(self):
basic_body = '''
{}: {}
{}: {}
{}: {}
{}: {}
'''.format(
_('Ticket title'), self.ticket.title,
_('Ticket type'), self.ticket.get_type_display(),
_('Ticket status'), self.ticket.get_status_display(),
_('Ticket applicant'), self.ticket.applicant_display,
).strip()
body = self.body_html_format.format(_("Ticket basic info"), basic_body)
return body
def _construct_meta_body(self):
body = ''
open_body = self._base_construct_meta_body_of_open().strip()
body += open_body
return body
def _base_construct_meta_body_of_open(self):
meta_body_of_open = getattr(
self, '_construct_meta_body_of_open', lambda: _('No content')
)().strip()
body = self.body_html_format.format(_('Ticket applied info'), meta_body_of_open)
return body

View File

@ -1,33 +0,0 @@
from django.utils.translation import ugettext as _
from .base import BaseHandler
class Handler(BaseHandler):
# body
def _construct_meta_body_of_open(self):
apply_run_user = self.ticket.meta.get('apply_run_user')
apply_run_asset = self.ticket.meta.get('apply_run_asset')
apply_run_system_user = self.ticket.meta.get('apply_run_system_user')
apply_run_command = self.ticket.meta.get('apply_run_command')
apply_from_session_id = self.ticket.meta.get('apply_from_session_id')
apply_from_cmd_filter_rule_id = self.ticket.meta.get('apply_from_cmd_filter_rule_id')
apply_from_cmd_filter_id = self.ticket.meta.get('apply_from_cmd_filter_id')
applied_body = '''
{}: {}
{}: {}
{}: {}
{}: {}
{}: {}
{}: {}
'''.format(
_("Applied run user"), apply_run_user,
_("Applied run asset"), apply_run_asset,
_("Applied run system user"), apply_run_system_user,
_("Applied run command"), apply_run_command,
_("Applied from session"), apply_from_session_id,
_("Applied from command filter rules"), apply_from_cmd_filter_rule_id,
_("Applied from command filter"), apply_from_cmd_filter_id,
)
return applied_body

View File

@ -1,21 +0,0 @@
from django.utils.translation import ugettext as _
from .base import BaseHandler
class Handler(BaseHandler):
# body
def _construct_meta_body_of_open(self):
apply_login_user = self.ticket.meta.get('apply_login_user')
apply_login_asset = self.ticket.meta.get('apply_login_asset')
apply_login_system_user = self.ticket.meta.get('apply_login_system_user')
applied_body = '''
{}: {}
{}: {}
{}: {}
'''.format(
_("Applied login user"), apply_login_user,
_("Applied login asset"), apply_login_asset,
_("Applied login system user"), apply_login_system_user,
)
return applied_body

View File

@ -2,6 +2,6 @@ from django.utils.module_loading import import_string
def get_ticket_handler(ticket):
handler_class_path = 'tickets.handler.{}.Handler'.format(ticket.type)
handler_class_path = 'tickets.handlers.{}.Handler'.format(ticket.type)
handler_class = import_string(handler_class_path)
return handler_class(ticket=ticket)

View File

@ -0,0 +1,63 @@
from django.utils.translation import ugettext as _
from orgs.utils import tmp_to_org, tmp_to_root_org
from perms.models import ApplicationPermission
from tickets.models import ApplyApplicationTicket
from .base import BaseHandler
class Handler(BaseHandler):
ticket: ApplyApplicationTicket
def _on_step_approved(self, step):
is_finished = super()._on_step_approved(step)
if is_finished:
self._create_application_permission()
# permission
def _create_application_permission(self):
with tmp_to_root_org():
application_permission = ApplicationPermission.objects.filter(id=self.ticket.id).first()
if application_permission:
return application_permission
apply_permission_name = self.ticket.apply_permission_name
apply_category = self.ticket.apply_category
apply_type = self.ticket.apply_type
apply_applications = self.ticket.apply_applications.all()
apply_system_users = self.ticket.apply_system_users.all()
apply_date_start = self.ticket.apply_date_start
apply_date_expired = self.ticket.apply_date_expired
permission_created_by = '{}:{}'.format(
str(self.ticket.__class__.__name__), str(self.ticket.id)
)
permission_comment = _(
'Created by the ticket, '
'ticket title: {}, '
'ticket applicant: {}, '
'ticket processor: {}, '
'ticket ID: {}'
).format(
self.ticket.title,
self.ticket.applicant,
','.join([i['processor_display'] for i in self.ticket.process_map]),
str(self.ticket.id)
)
permissions_data = {
'id': self.ticket.id,
'name': apply_permission_name,
'from_ticket': True,
'category': apply_category,
'type': apply_type,
'comment': str(permission_comment),
'created_by': permission_created_by,
'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(apply_applications)
application_permission.system_users.set(apply_system_users)
return application_permission

View File

@ -0,0 +1,64 @@
from django.utils.translation import ugettext as _
from perms.models import AssetPermission
from orgs.utils import tmp_to_org, tmp_to_root_org
from tickets.models import ApplyAssetTicket
from .base import BaseHandler
class Handler(BaseHandler):
ticket: ApplyAssetTicket
def _on_step_approved(self, step):
is_finished = super()._on_step_approved(step)
if is_finished:
self._create_asset_permission()
# permission
def _create_asset_permission(self):
with tmp_to_root_org():
asset_permission = AssetPermission.objects.filter(id=self.ticket.id).first()
if asset_permission:
return asset_permission
apply_permission_name = self.ticket.apply_permission_name
apply_nodes = self.ticket.apply_nodes.all()
apply_assets = self.ticket.apply_assets.all()
apply_system_users = self.ticket.apply_system_users.all()
apply_actions = self.ticket.apply_actions
apply_date_start = self.ticket.apply_date_start
apply_date_expired = self.ticket.apply_date_expired
permission_created_by = '{}:{}'.format(
str(self.ticket.__class__.__name__), str(self.ticket.id)
)
permission_comment = _(
'Created by the ticket '
'ticket title: {} '
'ticket applicant: {} '
'ticket processor: {} '
'ticket ID: {}'
).format(
self.ticket.title,
self.ticket.applicant,
','.join([i['processor_display'] for i in self.ticket.process_map]),
str(self.ticket.id)
)
permission_data = {
'id': self.ticket.id,
'name': apply_permission_name,
'from_ticket': True,
'comment': str(permission_comment),
'created_by': permission_created_by,
'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.nodes.set(apply_nodes)
asset_permission.assets.set(apply_assets)
asset_permission.system_users.set(apply_system_users)
return asset_permission

View File

@ -0,0 +1,81 @@
from django.utils.translation import ugettext as _
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 TicketState, TicketStatus
logger = get_logger(__name__)
class BaseHandler:
def __init__(self, ticket):
self.ticket = ticket
def on_change_state(self, state):
self._create_state_change_comment(state)
handler = getattr(self, f'_on_{state}', lambda: None)
return handler()
def _on_pending(self):
self._send_applied_mail_to_assignees()
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)
def _on_step_approved(self, step):
next_step = step.next()
is_finished = not next_step
if is_finished:
self._send_processed_mail_to_applicant(step)
else:
self._send_processed_mail_to_applicant(step)
self._send_applied_mail_to_assignees(next_step)
return is_finished
def _on_step_rejected(self, step):
self._send_processed_mail_to_applicant(step)
def _on_step_closed(self, step):
self._send_processed_mail_to_applicant()
def _send_applied_mail_to_assignees(self, step=None):
if step:
assignees = [step.assignee for step in step.ticket_assignees.all()]
else:
assignees = self.ticket.current_assignees
assignees_display = ', '.join([str(assignee) for assignee in assignees])
logger.debug('Send applied email to assignees: {}'.format(assignees_display))
send_ticket_applied_mail_to_assignees(self.ticket, assignees)
def _send_processed_mail_to_applicant(self, step=None):
applicant = self.ticket.applicant
if self.ticket.status == TicketStatus.closed:
processor = applicant
else:
processor = step.processor if step else self.ticket.processor
logger.debug('Send processed mail to applicant: {}'.format(applicant))
send_ticket_processed_mail_to_applicant(self.ticket, processor)
def _create_state_change_comment(self, state):
# 打开或关闭工单,备注显示是自己,其他是受理人
if state in [TicketState.reopen, TicketState.pending, TicketState.closed]:
user = self.ticket.applicant
else:
user = self.ticket.processor
user_display = str(user)
state_display = getattr(TicketState, state).label
data = {
'body': _('{} {} the ticket').format(user_display, state_display),
'user': user,
'user_display': str(user),
'type': 'state',
'state': state
}
return self.ticket.comments.create(**data)

View File

@ -0,0 +1,6 @@
from tickets.models import ApplyCommandTicket
from .base import BaseHandler
class Handler(BaseHandler):
ticket: ApplyCommandTicket

View File

@ -0,0 +1,6 @@
from tickets.models import ApplyLoginAssetTicket
from .base import BaseHandler
class Handler(BaseHandler):
ticket: ApplyLoginAssetTicket

View File

@ -1,14 +1,15 @@
from django.utils.translation import ugettext as _
from tickets.models import ApplyLoginTicket
from .base import BaseHandler
class Handler(BaseHandler):
ticket: ApplyLoginTicket
# body
def _construct_meta_body_of_open(self):
apply_login_ip = self.ticket.meta.get('apply_login_ip')
apply_login_city = self.ticket.meta.get('apply_login_city')
apply_login_datetime = self.ticket.meta.get('apply_login_datetime')
apply_login_ip = self.ticket.apply_login_ip
apply_login_city = self.ticket.apply_login_city
apply_login_datetime = self.ticket.apply_login_datetime
applied_body = '''
{}: {}
{}: {}

View File

@ -3,7 +3,7 @@
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import tickets.models.ticket
import common.db.encoder
TICKET_TYPE_APPLY_ASSET = 'apply_asset'
@ -116,7 +116,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='ticket',
name='assignees_display_new',
field=models.JSONField(default=list, encoder=tickets.models.ticket.ModelJSONFieldEncoder, verbose_name='Assignees display'),
field=models.JSONField(default=list, encoder=common.db.encoder.ModelJSONFieldEncoder, verbose_name='Assignees display'),
),
migrations.AlterField(
model_name='ticket',
@ -126,7 +126,7 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='ticket',
name='meta',
field=models.JSONField(default=dict, encoder=tickets.models.ticket.ModelJSONFieldEncoder, verbose_name='Meta'),
field=models.JSONField(default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, verbose_name='Meta'),
),
migrations.AlterField(
model_name='ticket',

View File

@ -0,0 +1,189 @@
# Generated by Django 3.1.14 on 2022-06-09 09:58
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('terminal', '0049_endpoint_redis_port'),
('assets', '0090_auto_20220412_1145'),
('applications', '0020_auto_20220316_2028'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('tickets', '0015_superticket'),
]
operations = [
migrations.CreateModel(
name='ApplyLoginTicket',
fields=[
('ticket_ptr',
models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
primary_key=True, serialize=False, to='tickets.ticket')),
('apply_login_ip', models.GenericIPAddressField(null=True, verbose_name='Login ip')),
('apply_login_city', models.CharField(max_length=64, null=True, verbose_name='Login city')),
('apply_login_datetime', models.DateTimeField(null=True, verbose_name='Login datetime')),
],
options={
'abstract': False,
},
bases=('tickets.ticket',),
),
migrations.RemoveField(
model_name='ticket',
name='process_map',
),
migrations.AddField(
model_name='comment',
name='state',
field=models.CharField(max_length=16, null=True),
),
migrations.AddField(
model_name='comment',
name='type',
field=models.CharField(choices=[('state', 'State'), ('common', 'common')], default='common', max_length=16,
verbose_name='Type'),
),
migrations.AddField(
model_name='ticket',
name='rel_snapshot',
field=models.JSONField(default=dict, verbose_name='Relation snapshot'),
),
migrations.AddField(
model_name='ticketstep',
name='status',
field=models.CharField(choices=[('pending', 'Pending'), ('active', 'Active'), ('closed', 'Closed')],
default='pending', max_length=16),
),
migrations.AlterField(
model_name='ticket',
name='state',
field=models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected'),
('closed', 'Cancel'), ('reopen', 'Reopen')], default='pending',
max_length=16, verbose_name='State'),
),
migrations.AlterField(
model_name='ticket',
name='status',
field=models.CharField(choices=[('open', 'Open'), ('closed', 'Finished')], default='open', max_length=16,
verbose_name='Status'),
),
migrations.AlterField(
model_name='ticketassignee',
name='state',
field=models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected'),
('closed', 'Cancel'), ('reopen', 'Reopen')], default='pending',
max_length=64),
),
migrations.AlterField(
model_name='ticketstep',
name='state',
field=models.CharField(choices=[('pending', 'Pending'), ('approved', 'Approved'), ('rejected', 'Rejected'),
('closed', 'Closed')], default='pending', max_length=64,
verbose_name='State'),
),
migrations.CreateModel(
name='ApplyLoginAssetTicket',
fields=[
('ticket_ptr',
models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
primary_key=True, serialize=False, to='tickets.ticket')),
('apply_login_asset',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.asset',
verbose_name='Login asset')),
('apply_login_system_user',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.systemuser',
verbose_name='Login system user')),
('apply_login_user',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL,
verbose_name='Login user')),
],
options={
'abstract': False,
},
bases=('tickets.ticket',),
),
migrations.CreateModel(
name='ApplyCommandTicket',
fields=[
('ticket_ptr',
models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
primary_key=True, serialize=False, to='tickets.ticket')),
('apply_run_command', models.CharField(max_length=4096, verbose_name='Run command')),
('apply_from_cmd_filter',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.commandfilter',
verbose_name='From cmd filter')),
('apply_from_cmd_filter_rule',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL,
to='assets.commandfilterrule', verbose_name='From cmd filter rule')),
('apply_from_session',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='terminal.session',
verbose_name='Session')),
('apply_run_asset',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.asset',
verbose_name='Run asset')),
('apply_run_system_user',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='assets.systemuser',
verbose_name='Run system user')),
('apply_run_user',
models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL,
verbose_name='Run user')),
],
options={
'abstract': False,
},
bases=('tickets.ticket',),
),
migrations.CreateModel(
name='ApplyAssetTicket',
fields=[
('ticket_ptr',
models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
primary_key=True, serialize=False, to='tickets.ticket')),
('apply_permission_name', models.CharField(max_length=128, verbose_name='Apply name')),
('apply_actions', models.IntegerField(
choices=[(255, 'All'), (1, 'Connect'), (2, 'Upload file'), (4, 'Download file'),
(6, 'Upload download'), (8, 'Clipboard copy'), (16, 'Clipboard paste'),
(24, 'Clipboard copy paste')], default=255, verbose_name='Actions')),
('apply_date_start', models.DateTimeField(null=True, verbose_name='Date start')),
('apply_date_expired', models.DateTimeField(null=True, verbose_name='Date expired')),
('apply_assets', models.ManyToManyField(to='assets.Asset', verbose_name='Apply assets')),
('apply_nodes', models.ManyToManyField(to='assets.Node', verbose_name='Apply nodes')),
('apply_system_users',
models.ManyToManyField(to='assets.SystemUser', verbose_name='Apply system users')),
],
options={
'abstract': False,
},
bases=('tickets.ticket',),
),
migrations.CreateModel(
name='ApplyApplicationTicket',
fields=[
('ticket_ptr',
models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True,
primary_key=True, serialize=False, to='tickets.ticket')),
('apply_permission_name', models.CharField(max_length=128, verbose_name='Apply name')),
('apply_category',
models.CharField(choices=[('db', 'Database'), ('remote_app', 'Remote app'), ('cloud', 'Cloud')],
max_length=16, verbose_name='Category')),
('apply_type', models.CharField(
choices=[('mysql', 'MySQL'), ('mariadb', 'MariaDB'), ('oracle', 'Oracle'),
('postgresql', 'PostgreSQL'), ('sqlserver', 'SQLServer'), ('redis', 'Redis'),
('mongodb', 'MongoDB'), ('chrome', 'Chrome'), ('mysql_workbench', 'MySQL Workbench'),
('vmware_client', 'vSphere Client'), ('custom', 'Custom'), ('k8s', 'Kubernetes')],
max_length=16, verbose_name='Type')),
('apply_date_start', models.DateTimeField(null=True, verbose_name='Date start')),
('apply_date_expired', models.DateTimeField(null=True, verbose_name='Date expired')),
('apply_applications',
models.ManyToManyField(to='applications.Application', verbose_name='Apply applications')),
('apply_system_users',
models.ManyToManyField(to='assets.SystemUser', verbose_name='Apply system users')),
],
options={
'abstract': False,
},
bases=('tickets.ticket',),
),
]

View File

@ -0,0 +1,339 @@
# Generated by Django 3.1.14 on 2022-06-23 02:27
import re
from datetime import datetime
from collections import defaultdict
from django.utils import timezone as dj_timezone
from django.db import migrations
from perms.models import Action
from tickets.const import TicketType
pt = re.compile(r'(\w+)\((\w+)\)')
def time_conversion(t):
if not t:
return
try:
return datetime.strptime(t, '%Y-%m-%d %H:%M:%S'). \
astimezone(dj_timezone.get_current_timezone())
except Exception:
return
nodes_dict = defaultdict(set)
assets_dict = defaultdict(set)
system_users_dict = defaultdict(set)
apps_dict = defaultdict(set)
global_inited = {}
def init_global_dict(apps):
if global_inited:
return
node_model = apps.get_model('assets', 'Node')
asset_model = apps.get_model('assets', 'Asset')
system_user_model = apps.get_model('assets', 'SystemUser')
application_model = apps.get_model('applications', 'Application')
node_qs = node_model.objects.values('id', 'org_id')
asset_qs = asset_model.objects.values('id', 'org_id')
system_user_qs = system_user_model.objects.values('id', 'org_id')
app_qs = application_model.objects.values('id', 'org_id')
for d, qs in [
(nodes_dict, node_qs),
(assets_dict, asset_qs),
(system_users_dict, system_user_qs),
(apps_dict, app_qs)
]:
for i in qs:
_id = str(i['id'])
org_id = str(i['org_id'])
d[org_id].add(_id)
global_inited['inited'] = True
def apply_asset_migrate(apps, *args):
init_global_dict(apps)
ticket_model = apps.get_model('tickets', 'Ticket')
tickets = ticket_model.objects.filter(type=TicketType.apply_asset)
ticket_apply_asset_model = apps.get_model('tickets', 'ApplyAssetTicket')
for instance in tickets:
meta = instance.meta
org_id = instance.org_id
apply_actions = meta.get('apply_actions')
# 数据库中数据结构有问题,可能是 list可能是 int
if isinstance(apply_actions, list):
apply_actions = Action.choices_to_value(value=apply_actions)
elif isinstance(apply_actions, int):
apply_actions = apply_actions
else:
apply_actions = 0
data = {
'ticket_ptr_id': instance.pk,
'apply_permission_name': meta.get('apply_permission_name', ''),
'apply_date_start': time_conversion(meta.get('apply_date_start')),
'apply_date_expired': time_conversion(meta.get('apply_date_expired')),
'apply_actions': apply_actions,
}
child = ticket_apply_asset_model(**data)
child.__dict__.update(instance.__dict__)
child.save()
apply_nodes = list(set(meta.get('apply_nodes', [])) & nodes_dict[org_id])
apply_assets = list(set(meta.get('apply_assets', [])) & assets_dict[org_id])
apply_system_users = list(set(meta.get('apply_system_users', [])) & system_users_dict[org_id])
child.apply_nodes.set(apply_nodes)
child.apply_assets.set(apply_assets)
child.apply_system_users.set(apply_system_users)
if (not apply_nodes and not apply_assets) or not apply_system_users:
continue
rel_snapshot = {
'applicant': instance.applicant_display,
'apply_nodes': meta.get('apply_nodes_display', []),
'apply_assets': meta.get('apply_assets_display', []),
'apply_system_users': meta.get('apply_system_users', []),
}
instance.rel_snapshot = rel_snapshot
instance.save(update_fields=['rel_snapshot'])
def apply_application_migrate(apps, *args):
init_global_dict(apps)
ticket_model = apps.get_model('tickets', 'Ticket')
tickets = ticket_model.objects.filter(type=TicketType.apply_application)
ticket_apply_app_model = apps.get_model('tickets', 'ApplyApplicationTicket')
for instance in tickets:
meta = instance.meta
org_id = instance.org_id
data = {
'ticket_ptr_id': instance.pk,
'apply_permission_name': meta.get('apply_permission_name', ''),
'apply_category': meta.get('apply_category'),
'apply_type': meta.get('apply_type'),
'apply_date_start': time_conversion(meta.get('apply_date_start')),
'apply_date_expired': time_conversion(meta.get('apply_date_expired')),
}
child = ticket_apply_app_model(**data)
child.__dict__.update(instance.__dict__)
child.save()
apply_applications = list(set(meta.get('apply_applications', [])) & apps_dict[org_id])
apply_system_users = list(set(meta.get('apply_system_users', [])) & system_users_dict[org_id])
if not apply_applications or not apply_system_users:
continue
child.apply_applications.set(apply_applications)
child.apply_system_users.set(apply_system_users)
rel_snapshot = {
'applicant': instance.applicant_display,
'apply_applications': meta.get('apply_applications_display', []),
'apply_system_users': meta.get('apply_system_users', []),
}
instance.rel_snapshot = rel_snapshot
instance.save(update_fields=['rel_snapshot'])
def login_confirm_migrate(apps, *args):
ticket_model = apps.get_model('tickets', 'Ticket')
tickets = ticket_model.objects.filter(type=TicketType.login_confirm)
ticket_apply_login_model = apps.get_model('tickets', 'ApplyLoginTicket')
for instance in tickets:
meta = instance.meta
data = {
'ticket_ptr_id': instance.pk,
'apply_login_ip': meta.get('apply_login_ip'),
'apply_login_city': meta.get('apply_login_city'),
'apply_login_datetime': time_conversion(meta.get('apply_login_datetime')),
}
rel_snapshot = {
'applicant': instance.applicant_display
}
instance.rel_snapshot = rel_snapshot
instance.save(update_fields=['rel_snapshot'])
child = ticket_apply_login_model(**data)
child.__dict__.update(instance.__dict__)
child.save()
def analysis_instance_name(name: str):
if not name:
return None
matched = pt.match(name)
if not matched:
return None
return matched.groups()
def login_asset_confirm_migrate(apps, *args):
user_model = apps.get_model('users', 'User')
asset_model = apps.get_model('assets', 'Asset')
system_user_model = apps.get_model('assets', 'SystemUser')
ticket_model = apps.get_model('tickets', 'Ticket')
tickets = ticket_model.objects.filter(type=TicketType.login_asset_confirm)
ticket_apply_login_asset_model = apps.get_model('tickets', 'ApplyLoginAssetTicket')
for instance in tickets:
meta = instance.meta
name_username = analysis_instance_name(meta.get('apply_login_user'))
apply_login_user = user_model.objects.filter(
name=name_username[0],
username=name_username[1]
).first() if name_username else None
hostname_ip = analysis_instance_name(meta.get('apply_login_asset'))
apply_login_asset = asset_model.objects.filter(
org_id=instance.org_id,
hostname=hostname_ip[0],
ip=hostname_ip[1]
).first() if hostname_ip else None
name_username = analysis_instance_name(meta.get('apply_login_system_user'))
apply_login_system_user = system_user_model.objects.filter(
org_id=instance.org_id,
name=name_username[0],
username=name_username[1]
).first() if name_username else None
data = {
'ticket_ptr_id': instance.pk,
'apply_login_user': apply_login_user,
'apply_login_asset': apply_login_asset,
'apply_login_system_user': apply_login_system_user,
}
child = ticket_apply_login_asset_model(**data)
child.__dict__.update(instance.__dict__)
child.save()
rel_snapshot = {
'applicant': instance.applicant_display,
'apply_login_user': meta.get('apply_login_user', ''),
'apply_login_asset': meta.get('apply_login_asset', ''),
'apply_login_system_user': meta.get('apply_login_system_user', ''),
}
instance.rel_snapshot = rel_snapshot
instance.save(update_fields=['rel_snapshot'])
def command_confirm_migrate(apps, *args):
user_model = apps.get_model('users', 'User')
asset_model = apps.get_model('assets', 'Asset')
system_user_model = apps.get_model('assets', 'SystemUser')
ticket_model = apps.get_model('tickets', 'Ticket')
session_model = apps.get_model('terminal', 'Session')
command_filter_model = apps.get_model('assets', 'CommandFilter')
command_filter_rule_model = apps.get_model('assets', 'CommandFilterRule')
tickets = ticket_model.objects.filter(type=TicketType.command_confirm)
session_ids = tickets.values_list('meta__apply_from_session_id', flat=True)
session_ids = session_model.objects.filter(id__in=session_ids).values_list('id', flat=True)
command_filter_ids = tickets.values_list('meta__apply_from_cmd_filter_id', flat=True)
command_filter_ids = command_filter_model.objects\
.filter(id__in=command_filter_ids)\
.values_list('id', flat=True)
command_filter_rule_ids = tickets.values_list('meta__apply_from_cmd_filter_rule_id', flat=True)
command_filter_rule_ids = command_filter_rule_model.objects\
.filter(id__in=command_filter_rule_ids)\
.values_list('id', flat=True)
ticket_apply_command_model = apps.get_model('tickets', 'ApplyCommandTicket')
for instance in tickets:
meta = instance.meta
name_username = analysis_instance_name(meta.get('apply_run_user'))
apply_run_user = user_model.objects.filter(
name=name_username[0], username=name_username[1]
).first() if name_username else None
hostname_ip = analysis_instance_name(meta.get('apply_run_asset'))
apply_run_asset = asset_model.objects.filter(
org_id=instance.org_id, hostname=hostname_ip[0], ip=hostname_ip[1]
).first() if hostname_ip else None
name_username = analysis_instance_name(meta.get('apply_run_system_user'))
apply_run_system_user = system_user_model.objects.filter(
org_id=instance.org_id, name=name_username[0], username=name_username[1]
).first() if name_username else None
apply_from_session_id = meta.get('apply_from_session_id')
apply_from_cmd_filter_id = meta.get('apply_from_cmd_filter_id')
apply_from_cmd_filter_rule_id = meta.get('apply_from_cmd_filter_rule_id')
if apply_from_session_id not in session_ids:
apply_from_session_id = None
if apply_from_cmd_filter_id not in command_filter_ids:
apply_from_cmd_filter_id = None
if apply_from_cmd_filter_rule_id not in command_filter_rule_ids:
apply_from_cmd_filter_rule_id = None
data = {
'ticket_ptr_id': instance.pk,
'apply_run_user': apply_run_user,
'apply_run_asset': apply_run_asset,
'apply_run_system_user': apply_run_system_user,
'apply_run_command': meta.get('apply_run_command', ''),
'apply_from_session_id': apply_from_session_id,
'apply_from_cmd_filter_id': apply_from_cmd_filter_id,
'apply_from_cmd_filter_rule_id': apply_from_cmd_filter_rule_id,
}
rel_snapshot = {
'applicant': instance.applicant_display,
'apply_run_user': meta.get('apply_run_user', ''),
'apply_run_asset': meta.get('apply_run_asset', ''),
'apply_run_system_user': meta.get('apply_run_system_user', ''),
'apply_from_session': meta.get('apply_from_session_id', ''),
'apply_from_cmd_filter': meta.get('apply_from_cmd_filter_id', ''),
'apply_from_cmd_filter_rule': meta.get('apply_from_cmd_filter_rule_id', ''),
}
child = ticket_apply_command_model(**data)
child.__dict__.update(instance.__dict__)
child.save()
instance.rel_snapshot = rel_snapshot
instance.save(update_fields=['rel_snapshot'])
def migrate_ticket_state(apps, *args):
ticket_model = apps.get_model('tickets', 'Ticket')
ticket_step_model = apps.get_model('tickets', 'TicketStep')
ticket_assignee_model = apps.get_model('tickets', 'TicketAssignee')
ticket_model.objects.filter(state='open').update(state='pending')
ticket_step_model.objects.filter(state='notified').update(state='pending')
ticket_assignee_model.objects.filter(state='notified').update(state='pending')
class Migration(migrations.Migration):
dependencies = [
('tickets', '0016_auto_20220609_1758'),
]
operations = [
migrations.RunPython(migrate_ticket_state),
migrations.RunPython(apply_asset_migrate),
migrations.RunPython(apply_application_migrate),
migrations.RunPython(login_confirm_migrate),
migrations.RunPython(login_asset_confirm_migrate),
migrations.RunPython(command_confirm_migrate),
migrations.RemoveField(
model_name='ticket',
name='applicant_display',
),
]

View File

@ -9,6 +9,10 @@ __all__ = ['Comment']
class Comment(CommonModelMixin):
class Type(models.TextChoices):
state = 'state', _('State')
common = 'common', _('common')
ticket = models.ForeignKey(
'tickets.Ticket', on_delete=models.CASCADE, related_name='comments'
)
@ -18,6 +22,10 @@ class Comment(CommonModelMixin):
)
user_display = models.CharField(max_length=256, verbose_name=_("User display name"))
body = models.TextField(verbose_name=_("Body"))
type = models.CharField(
max_length=16, choices=Type.choices, default=Type.common, verbose_name=_("Type")
)
state = models.CharField(max_length=16, null=True)
class Meta:
ordering = ('date_created', )

View File

@ -5,17 +5,18 @@ from django.utils.translation import ugettext_lazy as _
from users.models import User
from common.mixins.models import CommonModelMixin
from orgs.mixins.models import OrgModelMixin
from orgs.models import Organization
from orgs.utils import tmp_to_org, get_current_org_id
from ..const import TicketType, TicketApprovalLevel, TicketApprovalStrategy
from ..const import TicketType, TicketLevel, TicketApprovalStrategy
__all__ = ['TicketFlow', 'ApprovalRule']
class ApprovalRule(CommonModelMixin):
level = models.SmallIntegerField(
default=TicketApprovalLevel.one, choices=TicketApprovalLevel.choices,
default=TicketLevel.one, choices=TicketLevel.choices,
verbose_name=_('Approve level')
)
strategy = models.CharField(
@ -56,8 +57,8 @@ class TicketFlow(CommonModelMixin, OrgModelMixin):
default=TicketType.general, verbose_name=_("Type")
)
approval_level = models.SmallIntegerField(
default=TicketApprovalLevel.one,
choices=TicketApprovalLevel.choices,
default=TicketLevel.one,
choices=TicketLevel.choices,
verbose_name=_('Approve level')
)
rules = models.ManyToManyField(ApprovalRule, related_name='ticket_flows')

View File

@ -1,328 +0,0 @@
# -*- coding: utf-8 -*-
#
from typing import Callable
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.db.utils import IntegrityError
from common.exceptions import JMSException
from common.utils.timezone import as_current_tz
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 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', 'TicketStep', 'TicketAssignee', 'SuperTicket']
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 Meta:
verbose_name = _("Ticket step")
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 StatusMixin:
state: str
status: str
applicant: models.ForeignKey
current_node: models.Manager
save: Callable
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 = TicketStatus.closed
# status
@property
def status_open(self):
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
# action changed
def open(self, applicant):
self.applicant = applicant
self._change_action(TicketAction.open)
def approve(self, processor):
self.update_current_step_state_and_assignee(processor, TicketState.approved)
self._change_action(TicketAction.approve)
def reject(self, processor):
self.update_current_step_state_and_assignee(processor, TicketState.rejected)
self._change_action(TicketAction.reject)
def close(self, processor):
self.update_current_step_state_and_assignee(processor, TicketState.closed)
self._change_action(TicketAction.close)
def update_current_step_state_and_assignee(self, processor, state):
if self.status_closed:
raise AlreadyClosed
if state != TicketState.approved:
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 _change_action(self, action):
self.save()
post_change_ticket_action.send(sender=self.__class__, ticket=self, action=action)
class Ticket(CommonModelMixin, StatusMixin, OrgModelMixin):
title = models.CharField(max_length=256, verbose_name=_("Title"))
type = models.CharField(
max_length=64, choices=TicketType.choices,
default=TicketType.general, verbose_name=_("Type")
)
meta = models.JSONField(encoder=ModelJSONFieldEncoder, default=dict, verbose_name=_("Meta"))
state = models.CharField(
max_length=16, choices=TicketState.choices,
default=TicketState.open, verbose_name=_("State")
)
status = models.CharField(
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"))
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")
)
serial_num = models.CharField(max_length=128, unique=True, null=True, verbose_name=_('Serial number'))
class Meta:
ordering = ('-date_created',)
verbose_name = _('Ticket')
def __str__(self):
return '{}({})'.format(self.title, self.applicant_display)
# type
@property
def type_apply_asset(self):
return self.type == TicketType.apply_asset.value
@property
def type_apply_application(self):
return self.type == TicketType.apply_application.value
@property
def type_login_confirm(self):
return self.type == TicketType.login_confirm.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 ignore_applicant(self, assignees, applicant=None):
applicant = applicant if applicant else self.applicant
if len(assignees) != 1:
assignees = set(assignees) - {applicant, }
return list(assignees)
def create_related_node(self, applicant=None):
org_id = self.flow.org_id
approval_rule = self.get_current_ticket_flow_approve()
ticket_step = TicketStep.objects.create(ticket=self, level=self.approval_step)
ticket_assignees = []
assignees = approval_rule.get_assignees(org_id=org_id)
assignees = self.ignore_applicant(assignees, applicant)
for assignee in assignees:
ticket_assignees.append(TicketAssignee(step=ticket_step, assignee=assignee))
TicketAssignee.objects.bulk_create(ticket_assignees)
def create_process_map(self, applicant=None):
org_id = self.flow.org_id
approval_rules = self.flow.rules.order_by('level')
nodes = list()
for node in approval_rules:
assignees = node.get_assignees(org_id=org_id)
assignees = self.ignore_applicant(assignees, applicant)
assignee_ids = [assignee.id for assignee in assignees]
assignees_display = [str(assignee) for assignee in assignees]
nodes.append(
{
'approval_level': node.level,
'state': ProcessStatus.notified,
'assignees': assignee_ids,
'assignees_display': assignees_display
}
)
return nodes
# TODO 兼容不存在流的工单
def create_process_map_and_node(self, assignees, applicant):
assignees = self.ignore_applicant(assignees, applicant)
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)
def has_current_assignee(self, assignee):
return self.ticket_steps.filter(ticket_assignees__assignee=assignee, level=self.approval_step).exists()
def has_all_assignee(self, assignee):
return self.ticket_steps.filter(ticket_assignees__assignee=assignee).exists()
@classmethod
def get_user_related_tickets(cls, 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():
return Ticket.objects.all()
def save(self, *args, **kwargs):
""" 确保保存的org_id的是自身的值 """
with tmp_to_org(self.org_id):
return super().save(*args, **kwargs)
@property
def handler(self):
return get_ticket_handler(ticket=self)
# body
@property
def body(self):
_body = self.handler.get_body()
return _body
def get_serial_num_date(self):
date_created = as_current_tz(self.date_created)
date = date_created.strftime('%Y%m%d')
return date
def get_last_serail_num(self):
date_created = as_current_tz(self.date_created)
date_prefix = date_created.strftime('%Y%m%d')
ticket = Ticket.all().select_for_update().filter(
serial_num__startswith=date_prefix
).order_by('-date_created').first()
if ticket:
# 202212010001
num_str = ticket.serial_num[8:]
num = int(num_str)
return num
return None
def get_next_serail_num(self):
num = self.get_last_serail_num()
if num is None:
num = 0
return '%04d' % (num + 1)
def construct_serial_num(self):
date_prefix = self.get_serial_num_date()
num_suffix = self.get_next_serail_num()
return date_prefix + num_suffix
def update_serial_num_if_need(self):
if self.serial_num:
return
try:
self.serial_num = self.construct_serial_num()
self.save(update_fields=('serial_num',))
except IntegrityError as e:
if e.args[0] == 1062:
# 虽然做了 `select_for_update` 但是每天的第一条工单仍可能造成冲突
# 但概率小,这里只报错,用户重新提交即可
raise JMSException(detail=_('Please try again'), code='please_try_again')
raise e
class SuperTicket(Ticket):
class Meta:
proxy = True
verbose_name = _("Super ticket")

View File

@ -0,0 +1,6 @@
from .general import *
from .apply_asset import *
from .apply_application import *
from .command_confirm import *
from .login_asset_confirm import *
from .login_confirm import *

View File

@ -0,0 +1,34 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from .general import Ticket
from applications.const import AppCategory, AppType
__all__ = ['ApplyApplicationTicket']
class ApplyApplicationTicket(Ticket):
apply_permission_name = models.CharField(max_length=128, verbose_name=_('Apply name'))
# 申请信息
apply_category = models.CharField(
max_length=16, choices=AppCategory.choices, verbose_name=_('Category')
)
apply_type = models.CharField(
max_length=16, choices=AppType.choices, verbose_name=_('Type')
)
apply_applications = models.ManyToManyField(
'applications.Application', verbose_name=_('Apply applications'),
)
apply_system_users = models.ManyToManyField(
'assets.SystemUser', verbose_name=_('Apply system users'),
)
apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True)
apply_date_expired = models.DateTimeField(verbose_name=_('Date expired'), null=True)
@property
def apply_category_display(self):
return AppCategory.get_label(self.apply_category)
@property
def apply_type_display(self):
return AppType.get_label(self.apply_type)

View File

@ -0,0 +1,28 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from perms.models import Action
from .general import Ticket
__all__ = ['ApplyAssetTicket']
asset_or_node_help_text = _("Select at least one asset or node")
class ApplyAssetTicket(Ticket):
apply_permission_name = models.CharField(max_length=128, verbose_name=_('Apply name'))
apply_nodes = models.ManyToManyField('assets.Node', verbose_name=_('Apply nodes'))
# 申请信息
apply_assets = models.ManyToManyField('assets.Asset', verbose_name=_('Apply assets'))
apply_system_users = models.ManyToManyField(
'assets.SystemUser', verbose_name=_('Apply system users')
)
apply_actions = models.IntegerField(
choices=Action.DB_CHOICES, default=Action.ALL, verbose_name=_('Actions')
)
apply_date_start = models.DateTimeField(verbose_name=_('Date start'), null=True)
apply_date_expired = models.DateTimeField(verbose_name=_('Date expired'), null=True)
@property
def apply_actions_display(self):
return Action.value_to_choices_display(self.apply_actions)

View File

@ -0,0 +1,33 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from .general import Ticket
class ApplyCommandTicket(Ticket):
apply_run_user = models.ForeignKey(
'users.User', on_delete=models.SET_NULL,
null=True, verbose_name=_('Run user')
)
apply_run_asset = models.ForeignKey(
'assets.Asset', on_delete=models.SET_NULL,
null=True, verbose_name=_('Run asset')
)
apply_run_system_user = models.ForeignKey(
'assets.SystemUser', on_delete=models.SET_NULL,
null=True, verbose_name=_('Run system user')
)
apply_run_command = models.CharField(max_length=4096, verbose_name=_('Run command'))
apply_from_session = models.ForeignKey(
'terminal.Session', on_delete=models.SET_NULL,
null=True, verbose_name=_("Session")
)
apply_from_cmd_filter = models.ForeignKey(
'assets.CommandFilter', on_delete=models.SET_NULL,
null=True, verbose_name=_('From cmd filter')
)
apply_from_cmd_filter_rule = models.ForeignKey(
'assets.CommandFilterRule', on_delete=models.SET_NULL,
null=True, verbose_name=_('From cmd filter rule')
)

View File

@ -0,0 +1,389 @@
# -*- coding: utf-8 -*-
#
from typing import Callable
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.db.utils import IntegrityError
from django.db.models.fields import related
from common.exceptions import JMSException
from common.utils.timezone import as_current_tz
from common.mixins.models import CommonModelMixin
from common.db.encoder import ModelJSONFieldEncoder
from orgs.models import Organization
from tickets.const import (
TicketType, TicketStatus, TicketState,
TicketLevel, StepState, StepStatus
)
from tickets.handlers import get_ticket_handler
from tickets.errors import AlreadyClosed
from ..flow import TicketFlow
__all__ = ['Ticket', 'TicketStep', 'TicketAssignee', 'SuperTicket', 'SubTicketManager']
class TicketStep(CommonModelMixin):
ticket = models.ForeignKey(
'Ticket', related_name='ticket_steps',
on_delete=models.CASCADE, verbose_name='Ticket'
)
level = models.SmallIntegerField(
default=TicketLevel.one, choices=TicketLevel.choices,
verbose_name=_('Approve level')
)
state = models.CharField(
max_length=64, choices=StepState.choices,
default=StepState.pending, verbose_name=_("State")
)
status = models.CharField(
max_length=16, choices=StepStatus.choices,
default=StepStatus.pending
)
def change_state(self, state, processor):
if state != StepState.closed:
assignees = self.ticket_assignees.filter(assignee=processor)
if not assignees:
raise PermissionError('Only assignees can do this')
assignees.update(state=state)
self.status = StepStatus.closed
self.state = state
self.save(update_fields=['state', 'status'])
def set_active(self):
self.status = StepStatus.active
self.save(update_fields=['status'])
def next(self):
kwargs = dict(ticket=self.ticket, level=self.level + 1, status=StepStatus.pending)
return self.__class__.objects.filter(**kwargs).first()
@property
def processor(self):
processor = self.ticket_assignees.exclude(state=StepState.pending).first()
return processor.assignee if processor else None
class Meta:
verbose_name = _("Ticket step")
class TicketAssignee(CommonModelMixin):
assignee = models.ForeignKey(
'users.User', related_name='ticket_assignees',
on_delete=models.CASCADE, verbose_name='Assignee'
)
state = models.CharField(
choices=TicketState.choices, max_length=64,
default=TicketState.pending
)
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 StatusMixin:
State = TicketState
Status = TicketStatus
state: str
status: str
applicant: models.ForeignKey
current_step: TicketStep
save: Callable
create_process_steps_by_flow: Callable
create_process_steps_by_assignees: Callable
assignees: Callable
set_serial_num: Callable
set_rel_snapshot: Callable
approval_step: int
handler: None
flow: TicketFlow
ticket_steps: models.Manager
def is_state(self, state: TicketState):
return self.state == state
def is_status(self, status: TicketStatus):
return self.status == status
def _open(self):
self.set_serial_num()
self.set_rel_snapshot()
self._change_state_by_applicant(TicketState.pending)
def open(self):
self.create_process_steps_by_flow()
self._open()
def open_by_system(self, assignees):
self.create_process_steps_by_assignees(assignees)
self._open()
def approve(self, processor):
self._change_state(StepState.approved, processor)
def reject(self, processor):
self._change_state(StepState.rejected, processor)
def reopen(self):
self._change_state_by_applicant(TicketState.reopen)
def close(self):
self._change_state(TicketState.closed, self.applicant)
def _change_state_by_applicant(self, state):
if state == TicketState.closed:
self.status = TicketStatus.closed
elif state in [TicketState.reopen, TicketState.pending]:
self.status = TicketStatus.open
else:
raise ValueError("Not supported state: {}".format(state))
self.state = state
self.save(update_fields=['state', 'status'])
self.handler.on_change_state(state)
def _change_state(self, state, processor):
if self.is_status(self.Status.closed):
raise AlreadyClosed
current_step = self.current_step
current_step.change_state(state, processor)
self._finish_or_next(current_step, state)
def _finish_or_next(self, current_step, state):
next_step = current_step.next()
# 提前结束,或者最后一步
if state in [TicketState.rejected, TicketState.closed] or not next_step:
self.state = state
self.status = Ticket.Status.closed
self.save(update_fields=['state', 'status'])
self.handler.on_step_state_change(current_step, state)
else:
self.handler.on_step_state_change(current_step, state)
next_step.set_active()
self.approval_step += 1
self.save(update_fields=['approval_step'])
@property
def process_map(self):
process_map = []
steps = self.ticket_steps.all()
for step in steps:
assignee_ids = []
assignees_display = []
ticket_assignees = step.ticket_assignees.all()
processor = None
state = step.state
for i in ticket_assignees:
assignee_ids.append(i.assignee_id)
assignees_display.append(str(i.assignee))
if state != StepState.pending and state == i.state:
processor = i.assignee
if state != StepState.closed:
processor = self.applicant
step_info = {
'state': state,
'approval_level': step.level,
'assignees': assignee_ids,
'assignees_display': assignees_display,
'approval_date': str(step.date_updated),
'processor': processor.id if processor else '',
'processor_display': str(processor) if processor else ''
}
process_map.append(step_info)
return process_map
def exclude_applicant(self, assignees, applicant=None):
applicant = applicant if applicant else self.applicant
if len(assignees) != 1:
assignees = set(assignees) - {applicant, }
return list(assignees)
def create_process_steps_by_flow(self):
org_id = self.flow.org_id
flow_rules = self.flow.rules.order_by('level')
for rule in flow_rules:
step = TicketStep.objects.create(ticket=self, level=rule.level)
assignees = rule.get_assignees(org_id=org_id)
assignees = self.exclude_applicant(assignees, self.applicant)
step_assignees = [TicketAssignee(step=step, assignee=user) for user in assignees]
TicketAssignee.objects.bulk_create(step_assignees)
def create_process_steps_by_assignees(self, assignees):
assignees = self.exclude_applicant(assignees, self.applicant)
step = TicketStep.objects.create(ticket=self, level=1)
ticket_assignees = [TicketAssignee(step=step, assignee=user) for user in assignees]
TicketAssignee.objects.bulk_create(ticket_assignees)
@property
def current_step(self):
return self.ticket_steps.filter(level=self.approval_step).first()
@property
def current_assignees(self):
ticket_assignees = self.current_step.ticket_assignees.all()
return [i.assignee for i in ticket_assignees]
@property
def processor(self):
processor = self.current_step.ticket_assignees \
.exclude(state=StepState.pending) \
.first()
return processor.assignee if processor else None
def has_current_assignee(self, assignee):
return self.ticket_steps.filter(
ticket_assignees__assignee=assignee,
level=self.approval_step
).exists()
def has_all_assignee(self, assignee):
return self.ticket_steps.filter(ticket_assignees__assignee=assignee).exists()
@property
def handler(self):
return get_ticket_handler(ticket=self)
class Ticket(StatusMixin, CommonModelMixin):
title = models.CharField(max_length=256, verbose_name=_('Title'))
type = models.CharField(
max_length=64, choices=TicketType.choices,
default=TicketType.general, verbose_name=_('Type')
)
state = models.CharField(
max_length=16, choices=TicketState.choices,
default=TicketState.pending, verbose_name=_('State')
)
status = models.CharField(
max_length=16, choices=TicketStatus.choices,
default=TicketStatus.open, verbose_name=_('Status')
)
# 申请人
applicant = models.ForeignKey(
'users.User', related_name='applied_tickets', on_delete=models.SET_NULL,
null=True, verbose_name=_("Applicant")
)
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')
)
approval_step = models.SmallIntegerField(
default=TicketLevel.one, choices=TicketLevel.choices, verbose_name=_('Approval step')
)
serial_num = models.CharField(_('Serial number'), max_length=128, unique=True, null=True)
rel_snapshot = models.JSONField(verbose_name=_('Relation snapshot'), default=dict)
meta = models.JSONField(encoder=ModelJSONFieldEncoder, default=dict, verbose_name=_("Meta"))
org_id = models.CharField(
max_length=36, blank=True, default='', verbose_name=_('Organization'), db_index=True
)
class Meta:
ordering = ('-date_created',)
verbose_name = _('Ticket')
def __str__(self):
return '{}({})'.format(self.title, self.applicant)
@property
def spec_ticket(self):
attr = self.type.replace('_', '') + 'ticket'
return getattr(self, attr)
# TODO 先单独处理一下
@property
def org_name(self):
org = Organization.get_instance(self.org_id)
return org.name
def is_type(self, tp: TicketType):
return self.type == tp
@classmethod
def get_user_related_tickets(cls, user):
queries = Q(applicant=user) | Q(ticket_steps__ticket_assignees__assignee=user)
tickets = cls.objects.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):
return cls.objects.all()
def set_rel_snapshot(self, save=True):
rel_fields = set()
m2m_fields = set()
excludes = ['ticket_ptr_id', 'ticket_ptr', 'flow_id', 'flow', 'applicant_id']
for name, field in self._meta._forward_fields_map.items():
if name in excludes:
continue
if isinstance(field, related.RelatedField):
rel_fields.add(name)
if isinstance(field, related.ManyToManyField):
m2m_fields.add(name)
snapshot = {}
for field in rel_fields:
value = getattr(self, field)
if field in m2m_fields:
value = [str(v) for v in value.all()]
else:
value = str(value) if value else ''
snapshot[field] = value
self.rel_snapshot.update(snapshot)
if save:
self.save(update_fields=('rel_snapshot',))
def get_next_serial_num(self):
date_created = as_current_tz(self.date_created)
date_prefix = date_created.strftime('%Y%m%d')
ticket = Ticket.objects.all().select_for_update().filter(
serial_num__startswith=date_prefix
).order_by('-date_created').first()
last_num = 0
if ticket:
last_num = ticket.serial_num[8:]
last_num = int(last_num)
num = '%04d' % (last_num + 1)
return '{}{}'.format(date_prefix, num)
def set_serial_num(self):
if self.serial_num:
return
try:
self.serial_num = self.get_next_serial_num()
self.save(update_fields=('serial_num',))
except IntegrityError as e:
if e.args[0] == 1062:
# 虽然做了 `select_for_update` 但是每天的第一条工单仍可能造成冲突
# 但概率小,这里只报错,用户重新提交即可
raise JMSException(detail=_('Please try again'), code='please_try_again')
raise e
class SuperTicket(Ticket):
class Meta:
proxy = True
verbose_name = _("Super ticket")
class SubTicketManager(models.Manager):
pass

View File

@ -0,0 +1,21 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from .general import Ticket
__all__ = ['ApplyLoginAssetTicket']
class ApplyLoginAssetTicket(Ticket):
apply_login_user = models.ForeignKey(
'users.User', on_delete=models.SET_NULL, null=True,
verbose_name=_('Login user'),
)
apply_login_asset = models.ForeignKey(
'assets.Asset', on_delete=models.SET_NULL, null=True,
verbose_name=_('Login asset'),
)
apply_login_system_user = models.ForeignKey(
'assets.SystemUser', on_delete=models.SET_NULL, null=True,
verbose_name=_('Login system user'),
)

View File

@ -0,0 +1,12 @@
from django.db import models
from django.utils.translation import gettext_lazy as _
from .general import Ticket, SubTicketManager
__all__ = ['ApplyLoginTicket']
class ApplyLoginTicket(Ticket):
apply_login_ip = models.GenericIPAddressField(verbose_name=_('Login ip'), null=True)
apply_login_city = models.CharField(max_length=64, verbose_name=_('Login city'), null=True)
apply_login_datetime = models.DateTimeField(verbose_name=_('Login datetime'), null=True)

View File

@ -1,13 +1,17 @@
from urllib.parse import urljoin
import json
from django.conf import settings
from django.core.cache import cache
from django.shortcuts import reverse
from django.db.models.fields import related
from django.template.loader import render_to_string
from django.forms import model_to_dict
from django.utils.translation import ugettext_lazy as _
from notifications.notifications import UserMessage
from common.utils import get_logger, random_string
from common.db.encoder import ModelJSONFieldEncoder
from .models import Ticket
from . import const
@ -41,8 +45,8 @@ class BaseTicketMessage(UserMessage):
def get_html_msg(self) -> dict:
context = dict(
title=self.content_title,
ticket_detail_url=self.ticket_detail_url,
body=self.ticket.body.replace('\n', '<br/>'),
content=self.content,
ticket_detail_url=self.ticket_detail_url
)
message = render_to_string('tickets/_msg_ticket.html', context)
return {
@ -54,8 +58,48 @@ class BaseTicketMessage(UserMessage):
def gen_test_msg(cls):
return None
@property
def content(self):
content = [
{'title': _('Ticket basic info'), 'content': self.basic_items},
{'title': _('Ticket applied info'), 'content': self.spec_items},
]
return content
class TicketAppliedToAssignee(BaseTicketMessage):
def _get_fields_items(self, item_names):
fields = self.ticket._meta._forward_fields_map
json_data = json.dumps(model_to_dict(self.ticket), cls=ModelJSONFieldEncoder)
data = json.loads(json_data)
items = []
for name in item_names:
field = fields[name]
item = {'name': name, 'title': field.verbose_name}
value = data.get(name)
if hasattr(self.ticket, f'get_{name}_display'):
value = getattr(self.ticket, f'get_{name}_display')()
elif isinstance(field, related.ForeignKey):
value = self.ticket.rel_snapshot[name]
elif isinstance(field, related.ManyToManyField):
value = ', '.join(self.ticket.rel_snapshot[name])
item['value'] = value
items.append(item)
return items
@property
def basic_items(self):
item_names = ['serial_num', 'title', 'type', 'state', 'applicant', 'comment']
return self._get_fields_items(item_names)
@property
def spec_items(self):
fields = self.ticket._meta.local_fields + self.ticket._meta.local_many_to_many
excludes = ['ticket_ptr']
item_names = [field.name for field in fields if field.name not in excludes]
return self._get_fields_items(item_names)
class TicketAppliedToAssigneeMessage(BaseTicketMessage):
def __init__(self, user, ticket):
self.ticket = ticket
super().__init__(user)
@ -69,14 +113,14 @@ class TicketAppliedToAssignee(BaseTicketMessage):
@property
def content_title(self):
return _('Your has a new ticket, applicant - {}').format(
str(self.ticket.applicant_display)
)
return _('Your has a new ticket')
@property
def subject(self):
title = _('New Ticket - {} ({})').format(
self.ticket.title, self.ticket.get_type_display()
title = _('{}: New Ticket - {} ({})').format(
self.ticket.applicant,
self.ticket.title,
self.ticket.get_type_display()
)
return title
@ -85,19 +129,16 @@ class TicketAppliedToAssignee(BaseTicketMessage):
return urljoin(settings.SITE_URL, url)
def get_html_msg(self) -> dict:
body = self.ticket.body.replace('\n', '<br/>')
context = dict(
title=self.content_title,
ticket_detail_url=self.ticket_detail_url,
body=body,
content=self.content,
ticket_detail_url=self.ticket_detail_url
)
ticket_approval_url = self.get_ticket_approval_url()
context.update({'ticket_approval_url': ticket_approval_url})
message = render_to_string('tickets/_msg_ticket.html', context)
cache.set(self.token, {
'body': body, 'ticket_id': self.ticket.id
}, 3600)
cache.set(self.token, {'ticket_id': self.ticket.id, 'content': self.content}, 3600)
return {
'subject': self.subject,
'message': message
@ -112,7 +153,7 @@ class TicketAppliedToAssignee(BaseTicketMessage):
return cls(user, ticket)
class TicketProcessedToApplicant(BaseTicketMessage):
class TicketProcessedToApplicantMessage(BaseTicketMessage):
def __init__(self, user, ticket, processor):
self.ticket = ticket
self.processor = processor

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
#
from .ticket import *
from .flow import *
from .comment import *
from .relation import *
from .super_ticket import *
from .super_ticket import *

View File

@ -0,0 +1,103 @@
from django.db.transaction import atomic
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from orgs.models import Organization
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from tickets.models import TicketFlow, ApprovalRule
from tickets.const import TicketApprovalStrategy
__all__ = ['TicketFlowSerializer']
class TicketFlowApproveSerializer(serializers.ModelSerializer):
strategy_display = serializers.ReadOnlyField(source='get_strategy_display', label=_('Approve strategy'))
assignees_read_only = serializers.SerializerMethodField(label=_('Assignees'))
assignees_display = serializers.SerializerMethodField(label=_('Assignees display'))
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, 'required': False}
}
@staticmethod
def get_assignees_display(instance):
return [str(assignee) for assignee in instance.get_assignees()]
@staticmethod
def get_assignees_read_only(instance):
if instance.strategy == TicketApprovalStrategy.custom_user:
return instance.assignees.values_list('id', flat=True)
return []
def validate(self, attrs):
if attrs['strategy'] == TicketApprovalStrategy.custom_user and not attrs.get('assignees'):
error = _('Please select the Assignees')
raise serializers.ValidationError({'assignees': error})
return super().validate(attrs)
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, instance=None):
related = 'rules'
assignees = 'assignees'
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
# Todo: 这个权限的判断
for level, data in enumerate(childs, 1):
data_m2m = data.pop(assignees, None)
child_instance = related_model.objects.create(**data, level=level)
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)
@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, instance)
return instance

View File

@ -15,7 +15,7 @@ class SuperTicketSerializer(serializers.ModelSerializer):
fields = ['id', 'status', 'state', 'processor']
@staticmethod
def get_processor(ticket):
if not ticket.processor:
def get_processor(instance):
if not instance.processor:
return ''
return str(ticket.processor)
return str(instance.processor)

View File

@ -1,2 +1,7 @@
from .ticket import *
from .meta import *
from .apply_asset import *
from .apply_application import *
from .login_confirm import *
from .login_asset_confirm import *
from .command_confirm import *
from .common import *

View File

@ -0,0 +1,57 @@
from rest_framework import serializers
from perms.models import ApplicationPermission
from orgs.utils import tmp_to_org
from applications.models import Application
from tickets.models import ApplyApplicationTicket
from .ticket import TicketApplySerializer
from .common import BaseApplyAssetApplicationSerializer
__all__ = ['ApplyApplicationSerializer', 'ApplyApplicationDisplaySerializer']
class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer):
permission_model = ApplicationPermission
class Meta:
model = ApplyApplicationTicket
writeable_fields = [
'id', 'title', 'type', 'apply_category',
'apply_type', 'apply_applications', 'apply_system_users',
'apply_date_start', 'apply_date_expired', 'org_id'
]
fields = TicketApplySerializer.Meta.fields + writeable_fields + ['apply_permission_name']
read_only_fields = list(set(fields) - set(writeable_fields))
ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs
extra_kwargs = {}
extra_kwargs.update(ticket_extra_kwargs)
def validate_apply_applications(self, apply_applications):
type = self.initial_data.get('apply_type')
org_id = self.initial_data.get('org_id')
application_ids = [app.id for app in apply_applications]
with tmp_to_org(org_id):
applications = Application.objects.filter(
id__in=application_ids, type=type
).values_list('id', flat=True)
return list(applications)
class ApplyApplicationDisplaySerializer(ApplyApplicationSerializer):
apply_applications = serializers.SerializerMethodField()
apply_system_users = serializers.SerializerMethodField()
class Meta:
model = ApplyApplicationSerializer.Meta.model
fields = ApplyApplicationSerializer.Meta.fields
read_only_fields = fields
@staticmethod
def get_apply_applications(instance):
with tmp_to_org(instance.org_id):
return instance.apply_applications.values_list('id', flat=True)
@staticmethod
def get_apply_system_users(instance):
with tmp_to_org(instance.org_id):
return instance.apply_system_users.values_list('id', flat=True)

View File

@ -0,0 +1,70 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from perms.serializers.base import ActionsField
from perms.models import AssetPermission
from orgs.utils import tmp_to_org
from tickets.models import ApplyAssetTicket
from .ticket import TicketApplySerializer
from .common import BaseApplyAssetApplicationSerializer
__all__ = ['ApplyAssetSerializer', 'ApplyAssetDisplaySerializer']
asset_or_node_help_text = _("Select at least one asset or node")
class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySerializer):
apply_actions = ActionsField(required=True, allow_null=True)
permission_model = AssetPermission
class Meta:
model = ApplyAssetTicket
writeable_fields = [
'id', 'title', 'type', 'apply_nodes', 'apply_assets',
'apply_system_users', 'apply_actions', 'apply_actions_display',
'apply_date_start', 'apply_date_expired', 'org_id'
]
fields = TicketApplySerializer.Meta.fields + writeable_fields + ['apply_permission_name']
read_only_fields = list(set(fields) - set(writeable_fields))
ticket_extra_kwargs = TicketApplySerializer.Meta.extra_kwargs
extra_kwargs = {
'apply_nodes': {'required': False, 'help_text': asset_or_node_help_text},
'apply_assets': {'required': False, 'help_text': asset_or_node_help_text}
}
extra_kwargs.update(ticket_extra_kwargs)
def validate(self, attrs):
attrs = super().validate(attrs)
if not attrs.get('apply_nodes') and not attrs.get('apply_assets'):
raise serializers.ValidationError({
'apply_nodes': asset_or_node_help_text,
'apply_assets': asset_or_node_help_text,
})
return attrs
class ApplyAssetDisplaySerializer(ApplyAssetSerializer):
apply_nodes = serializers.SerializerMethodField()
apply_assets = serializers.SerializerMethodField()
apply_system_users = serializers.SerializerMethodField()
class Meta:
model = ApplyAssetSerializer.Meta.model
fields = ApplyAssetSerializer.Meta.fields
read_only_fields = fields
@staticmethod
def get_apply_nodes(instance):
with tmp_to_org(instance.org_id):
return instance.apply_nodes.values_list('id', flat=True)
@staticmethod
def get_apply_assets(instance):
with tmp_to_org(instance.org_id):
return instance.apply_assets.values_list('id', flat=True)
@staticmethod
def get_apply_system_users(instance):
with tmp_to_org(instance.org_id):
return instance.apply_system_users.values_list('id', flat=True)

View File

@ -0,0 +1,16 @@
from tickets.models import ApplyCommandTicket
from .ticket import TicketApplySerializer
__all__ = [
'ApplyCommandConfirmSerializer',
]
class ApplyCommandConfirmSerializer(TicketApplySerializer):
class Meta:
model = ApplyCommandTicket
fields = TicketApplySerializer.Meta.fields + [
'apply_run_user', 'apply_run_asset', 'apply_run_system_user',
'apply_run_command', 'apply_from_session', 'apply_from_cmd_filter',
'apply_from_cmd_filter_rule'
]

View File

@ -0,0 +1,63 @@
from django.db.transaction import atomic
from django.db.models import Model
from django.utils.translation import ugettext as _
from rest_framework import serializers
from orgs.utils import tmp_to_org
from tickets.models import Ticket
__all__ = ['DefaultPermissionName', 'get_default_permission_name', 'BaseApplyAssetApplicationSerializer']
def get_default_permission_name(ticket):
name = ''
if isinstance(ticket, Ticket):
name = _('Created by ticket ({}-{})').format(ticket.title, str(ticket.id)[:4])
return name
class DefaultPermissionName(object):
default = None
@staticmethod
def _construct_default_permission_name(serializer_field):
permission_name = ''
ticket = serializer_field.root.instance
if isinstance(ticket, Ticket):
permission_name = get_default_permission_name(ticket)
return permission_name
def set_context(self, serializer_field):
self.default = self._construct_default_permission_name(serializer_field)
def __call__(self):
return self.default
class BaseApplyAssetApplicationSerializer(serializers.Serializer):
permission_model: Model
def validate(self, attrs):
attrs = super().validate(attrs)
apply_date_start = attrs['apply_date_start'].strftime('%Y-%m-%d %H:%M:%S')
apply_date_expired = attrs['apply_date_expired'].strftime('%Y-%m-%d %H:%M:%S')
if apply_date_expired <= apply_date_start:
error = _('The expiration date should be greater than the start date')
raise serializers.ValidationError({'apply_date_expired': error})
attrs['apply_date_start'] = apply_date_start
attrs['apply_date_expired'] = apply_date_expired
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 self.permission_model.objects.filter(name=name).exists():
instance.apply_permission_name = name
instance.save()
return instance
raise serializers.ValidationError(_('Permission named `{}` already exists'.format(name)))

View File

@ -0,0 +1,14 @@
from tickets.models import ApplyLoginAssetTicket
from .ticket import TicketApplySerializer
__all__ = [
'LoginAssetConfirmSerializer'
]
class LoginAssetConfirmSerializer(TicketApplySerializer):
class Meta:
model = ApplyLoginAssetTicket
fields = TicketApplySerializer.Meta.fields + [
'apply_login_user', 'apply_login_asset', 'apply_login_system_user'
]

View File

@ -0,0 +1,14 @@
from tickets.models import ApplyLoginTicket
from .ticket import TicketApplySerializer
__all__ = [
'LoginConfirmSerializer'
]
class LoginConfirmSerializer(TicketApplySerializer):
class Meta:
model = ApplyLoginTicket
fields = TicketApplySerializer.Meta.fields + [
'apply_login_ip', 'apply_login_city', 'apply_login_datetime'
]

View File

@ -1 +0,0 @@
from .meta import *

View File

@ -1,43 +0,0 @@
from tickets import const
from .ticket_type import (
apply_asset, apply_application, login_confirm,
login_asset_confirm, command_confirm
)
__all__ = [
'type_serializer_classes_mapping',
]
# ticket action
# -------------
action_open = const.TicketAction.open.value
action_approve = const.TicketAction.approve.value
# defines `meta` field dynamic mapping serializers
# ------------------------------------------------
type_serializer_classes_mapping = {
const.TicketType.apply_asset.value: {
'default': apply_asset.ApplySerializer
},
const.TicketType.apply_application.value: {
'default': apply_application.ApplySerializer
},
const.TicketType.login_confirm.value: {
'default': login_confirm.LoginConfirmSerializer,
action_open: login_confirm.ApplySerializer,
action_approve: login_confirm.LoginConfirmSerializer(read_only=True),
},
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.TicketType.command_confirm.value: {
'default': command_confirm.CommandConfirmSerializer,
action_open: command_confirm.ApplySerializer,
action_approve: command_confirm.CommandConfirmSerializer(read_only=True)
}
}

View File

@ -1,93 +0,0 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from perms.models import ApplicationPermission
from applications.const import AppCategory, AppType
from orgs.utils import tmp_to_org
from tickets.models import Ticket
from applications.models import Application
from .common import DefaultPermissionName
__all__ = [
'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'),
allow_null=True,
)
apply_category_display = serializers.CharField(
read_only=True, label=_('Category display'), allow_null=True,
)
apply_type = serializers.ChoiceField(
required=True, choices=AppType.choices, label=_('Type'),
allow_null=True
)
apply_type_display = serializers.CharField(
required=False, read_only=True, label=_('Type display'),
allow_null=True
)
apply_applications = serializers.ListField(
required=True, child=serializers.UUIDField(), label=_('Apply applications'),
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
)
apply_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
with tmp_to_org(self.root.instance.org_id):
already_exists = ApplicationPermission.objects.filter(name=permission_name).exists()
if not already_exists:
return permission_name
raise serializers.ValidationError(_(
'Permission named `{}` already exists'.format(permission_name)
))
def validate_apply_applications(self, apply_applications):
type = self.root.initial_data['meta'].get('apply_type')
org_id = self.root.initial_data.get('org_id')
with tmp_to_org(org_id):
applications = Application.objects.filter(
id__in=apply_applications, type=type
).values_list('id', flat=True)
return list(applications)
def validate(self, attrs):
apply_date_start = attrs['apply_date_start'].strftime('%Y-%m-%d %H:%M:%S')
apply_date_expired = attrs['apply_date_expired'].strftime('%Y-%m-%d %H:%M:%S')
if apply_date_expired <= apply_date_start:
error = _('The expiration date should be greater than the start date')
raise serializers.ValidationError({'apply_date_expired': error})
attrs['apply_date_start'] = apply_date_start
attrs['apply_date_expired'] = apply_date_expired
return attrs

View File

@ -1,92 +0,0 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from perms.serializers.base import ActionsField
from perms.models import AssetPermission
from orgs.utils import tmp_to_org
from tickets.models import Ticket
from .common import DefaultPermissionName
__all__ = [
'ApplySerializer',
]
asset_or_node_help_text = _("Select at least one asset or node")
class ApplySerializer(serializers.Serializer):
apply_permission_name = serializers.CharField(
max_length=128, default=DefaultPermissionName(), label=_('Apply name')
)
apply_nodes = serializers.ListField(
required=False, allow_null=True, child=serializers.UUIDField(),
label=_('Apply nodes'), help_text=asset_or_node_help_text,
default=list
)
apply_nodes_display = serializers.ListField(
child=serializers.CharField(), label=_('Apply nodes display'), required=False
)
# 申请信息
apply_assets = serializers.ListField(
required=False, allow_null=True, child=serializers.UUIDField(),
label=_('Apply assets'), help_text=asset_or_node_help_text
)
apply_assets_display = serializers.ListField(
required=False, read_only=True, child=serializers.CharField(),
label=_('Apply assets display'), allow_null=True,
default=list
)
apply_system_users = serializers.ListField(
required=True, allow_null=True, child=serializers.UUIDField(),
label=_('Apply 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=_('Apply assets display'), allow_null=True,
default=list,
)
apply_date_start = serializers.DateTimeField(
required=True, label=_('Date start'), allow_null=True,
)
apply_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
with tmp_to_org(self.root.instance.org_id):
already_exists = AssetPermission.objects.filter(name=permission_name).exists()
if not already_exists:
return permission_name
raise serializers.ValidationError(_(
'Permission named `{}` already exists'.format(permission_name)
))
def validate(self, attrs):
if not attrs.get('apply_nodes') and not attrs.get('apply_assets'):
raise serializers.ValidationError({
'apply_nodes': asset_or_node_help_text,
'apply_assets': asset_or_node_help_text,
})
apply_date_start = attrs['apply_date_start'].strftime('%Y-%m-%d %H:%M:%S')
apply_date_expired = attrs['apply_date_expired'].strftime('%Y-%m-%d %H:%M:%S')
if apply_date_expired <= apply_date_start:
error = _('The expiration date should be greater than the start date')
raise serializers.ValidationError({'apply_date_expired': error})
attrs['apply_date_start'] = apply_date_start
attrs['apply_date_expired'] = apply_date_expired
return attrs

View File

@ -1,26 +0,0 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
__all__ = [
'ApplySerializer', 'CommandConfirmSerializer',
]
class ApplySerializer(serializers.Serializer):
# 申请信息
apply_run_user = serializers.CharField(required=True, label=_('Run user'))
apply_run_asset = serializers.CharField(required=True, label=_('Run asset'))
apply_run_system_user = serializers.CharField(
required=True, max_length=64, label=_('Run system user')
)
apply_run_command = serializers.CharField(required=True, label=_('Run command'))
apply_from_session_id = serializers.UUIDField(required=False, label=_('From session'))
apply_from_cmd_filter_rule_id = serializers.UUIDField(
required=False, label=_('From cmd filter rule')
)
apply_from_cmd_filter_id = serializers.UUIDField(required=False, label=_('From cmd filter'))
class CommandConfirmSerializer(ApplySerializer):
pass

View File

@ -1,30 +0,0 @@
from django.utils.translation import ugettext as _
from tickets.models import Ticket
__all__ = ['DefaultPermissionName', 'get_default_permission_name']
def get_default_permission_name(ticket):
name = ''
if isinstance(ticket, Ticket):
name = _('Created by ticket ({}-{})').format(ticket.title, str(ticket.id)[:4])
return name
class DefaultPermissionName(object):
default = None
@staticmethod
def _construct_default_permission_name(serializer_field):
permission_name = ''
ticket = serializer_field.root.instance
if isinstance(ticket, Ticket):
permission_name = get_default_permission_name(ticket)
return permission_name
def set_context(self, serializer_field):
self.default = self._construct_default_permission_name(serializer_field)
def __call__(self):
return self.default

View File

@ -1,21 +0,0 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
__all__ = [
'ApplySerializer', 'LoginAssetConfirmSerializer',
]
class ApplySerializer(serializers.Serializer):
# 申请信息
apply_login_user = serializers.CharField(required=True, label=_('Login user'))
apply_login_asset = serializers.CharField(required=True, label=_('Login asset'))
apply_login_system_user = serializers.CharField(
required=True, max_length=64, label=_('Login system user')
)
class LoginAssetConfirmSerializer(ApplySerializer):
pass

View File

@ -1,25 +0,0 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
__all__ = [
'ApplySerializer', 'LoginConfirmSerializer',
]
class ApplySerializer(serializers.Serializer):
# 申请信息
apply_login_ip = serializers.IPAddressField(
required=True, label=_('Login ip'), allow_null=True
)
apply_login_city = serializers.CharField(
required=True, max_length=64, label=_('Login city'), allow_null=True
)
apply_login_datetime = serializers.DateTimeField(
required=True, label=_('Login datetime'), allow_null=True
)
class LoginConfirmSerializer(ApplySerializer):
pass

View File

@ -1,68 +1,42 @@
# -*- 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 tickets.models import Ticket, TicketFlow, ApprovalRule
from tickets.const import TicketApprovalStrategy
from .meta import type_serializer_classes_mapping
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from tickets.models import Ticket, TicketFlow
__all__ = [
'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketApproveSerializer', 'TicketFlowSerializer'
'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketListSerializer'
]
class TicketSerializer(OrgResourceModelSerializerMixin):
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type 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', 'state', 'approval_step',
'status', 'status_display', 'applicant_display', 'process_map',
'date_created', 'date_updated', 'comment', 'org_id', 'org_name', 'body',
'serial_num',
'type', 'type_display', 'status', 'status_display',
'state', 'approval_step', 'rel_snapshot', 'comment',
'date_created', 'date_updated', 'org_id', 'rel_snapshot',
'process_map', 'org_name', 'serial_num'
]
fields_fk = ['applicant', ]
fields = fields_small + fields_fk
def get_meta_serializer(self):
default_serializer = serializers.Serializer(read_only=True)
if isinstance(self.instance, Ticket):
_type = self.instance.type
else:
_type = self.context['request'].query_params.get('type')
if _type:
action_serializer_classes_mapping = type_serializer_classes_mapping.get(_type)
if action_serializer_classes_mapping:
query_action = self.context['request'].query_params.get('action')
action = query_action if query_action else self.context['view'].action
serializer_class = action_serializer_classes_mapping.get(action)
if not serializer_class:
serializer_class = action_serializer_classes_mapping.get('default')
else:
serializer_class = default_serializer
else:
serializer_class = default_serializer
if not serializer_class:
serializer_class = default_serializer
if isinstance(serializer_class, type):
serializer = serializer_class()
else:
serializer = serializer_class
return serializer
class TicketListSerializer(TicketSerializer):
class Meta:
model = Ticket
fields = [
'id', 'title', 'serial_num', 'type', 'type_display', 'status',
'state', 'rel_snapshot', 'date_created', 'rel_snapshot'
]
read_only_fields = fields
class TicketDisplaySerializer(TicketSerializer):
@ -80,24 +54,10 @@ class TicketApplySerializer(TicketSerializer):
class Meta:
model = Ticket
fields = TicketSerializer.Meta.fields
writeable_fields = [
'id', 'title', 'type', 'meta', 'comment', 'org_id'
]
read_only_fields = list(set(fields) - set(writeable_fields))
extra_kwargs = {
'type': {'required': True},
'type': {'required': True}
}
def validate_type(self, tp):
request_type = self.context['request'].query_params.get('type')
if tp != request_type:
error = _(
'The `type` in the submission data (`{}`) is different from the type '
'in the request url (`{}`)'.format(tp, request_type)
)
raise serializers.ValidationError(error)
return tp
@staticmethod
def validate_org_id(org_id):
org = Organization.get_instance(org_id)
@ -116,113 +76,3 @@ class TicketApplySerializer(TicketSerializer):
error = _('The ticket flow `{}` does not exist'.format(ticket_type))
raise serializers.ValidationError(error)
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
read_only_fields = fields
class TicketFlowApproveSerializer(serializers.ModelSerializer):
strategy_display = serializers.ReadOnlyField(source='get_strategy_display', label=_('Approve strategy'))
assignees_read_only = serializers.SerializerMethodField(label=_('Assignees'))
assignees_display = serializers.SerializerMethodField(label=_('Assignees display'))
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, 'required': False}
}
def get_assignees_display(self, obj):
return [str(assignee) for assignee in obj.get_assignees()]
def get_assignees_read_only(self, obj):
if obj.strategy == TicketApprovalStrategy.custom_user:
return obj.assignees.values_list('id', flat=True)
return []
def validate(self, attrs):
if attrs['strategy'] == TicketApprovalStrategy.custom_user and not attrs.get('assignees'):
error = _('Please select the Assignees')
raise serializers.ValidationError({'assignees': error})
return super().validate(attrs)
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, instance=None):
related = 'rules'
assignees = 'assignees'
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
# Todo: 这个权限的判断
for level, data in enumerate(childs, 1):
data_m2m = data.pop(assignees, None)
child_instance = related_model.objects.create(**data, level=level)
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)
@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, instance)
return instance

View File

@ -1,19 +1,28 @@
# -*- coding: utf-8 -*-
#
from django.dispatch import receiver
from django.db.models.signals import post_save
from django.db.models.signals import post_save, m2m_changed
from common.decorator import on_transaction_commit
from common.utils import get_logger
from tickets.models import Ticket
from ..signals import post_change_ticket_action
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)
@on_transaction_commit
def after_save_set_rel_snapshot(sender, instance, update_fields=None, **kwargs):
if update_fields and list(update_fields)[0] == 'rel_snapshot':
return
instance.set_rel_snapshot()
@receiver(post_save, sender=Ticket)
def on_pre_save_ensure_serial_num(sender, instance: Ticket, **kwargs):
instance.update_serial_num_if_need()
@on_transaction_commit
def on_m2m_change(sender, action, instance, reverse=False, **kwargs):
if action.startswith('post'):
instance.set_rel_snapshot()
for ticket_cls in Ticket.__subclasses__():
post_save.connect(after_save_set_rel_snapshot, sender=ticket_cls)
m2m_changed.connect(on_m2m_change, sender=ticket_cls)

View File

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

View File

@ -1,20 +0,0 @@
{% load i18n %}
<div>
<p class="ticket-info">
{{ ticket_info }}
</p>
<div>
<p>
</p>
<p>
</p>
{{ body | safe }}
</div>
<div>
<a href={{ ticket_detail_url }}>
{% trans 'Click here to review' %}
</a>
</div>
</div>

View File

@ -4,7 +4,15 @@
{{ title | safe }}
</p>
<div>
{{ body | safe }}
{% for child in content %}
<h2>{{ child.title }}</h2>
<hr>
{% for item in child.content %}
<li style="list-style-type:none">
<b>{{ item.title }}: </b> {{ item.value }}
</li>
{% endfor %}
{% endfor %}
</div>
<br>
<div>

View File

@ -10,7 +10,17 @@
<div class="ibox-content">
<h2 class="font-bold" style="display: inline">{% trans 'Ticket information' %}</h2>
<h1></h1>
<div style="word-break: break-all">{{ ticket_info | safe }}</div>
<div>
{% for child in content %}
<h2>{{ child.title }}</h2>
<hr>
{% for item in child.content %}
<li style="list-style-type:none">
<b>{{ item.title }}: </b> {{ item.value }}
</li>
{% endfor %}
{% endfor %}
</div>
</div>
</div>
<div class="col-lg-6">

View File

@ -10,6 +10,11 @@ app_name = 'tickets'
router = BulkRouter()
router.register('tickets', api.TicketViewSet, 'ticket')
router.register('apply-asset-tickets', api.ApplyAssetTicketViewSet, 'apply-asset-ticket')
router.register('apply-app-tickets', api.ApplyApplicationTicketViewSet, 'apply-app-ticket')
router.register('apply-login-tickets', api.ApplyLoginTicketViewSet, 'apply-login-ticket')
router.register('apply-login-asset-tickets', api.ApplyLoginAssetTicketViewSet, 'apply-login-asset-ticket')
router.register('apply-command-tickets', api.ApplyCommandTicketViewSet, 'apply-command-ticket')
router.register('flows', api.TicketFlowViewSet, 'flows')
router.register('comments', api.CommentViewSet, 'comment')
router.register('ticket-session-relation', api.TicketSessionRelationViewSet, 'ticket-session-relation')

View File

@ -3,21 +3,22 @@
from django.conf import settings
from common.utils import get_logger
from .notifications import TicketAppliedToAssignee, TicketProcessedToApplicant
from .notifications import TicketAppliedToAssigneeMessage, TicketProcessedToApplicantMessage
logger = get_logger(__file__)
def send_ticket_applied_mail_to_assignees(ticket):
ticket_assignees = ticket.current_node.first().ticket_assignees.all()
if not ticket_assignees:
def send_ticket_applied_mail_to_assignees(ticket, assignees):
if not assignees:
logger.debug(
"Not found assignees, ticket: {}({}), assignees: {}".format(ticket, str(ticket.id), ticket_assignees)
"Not found assignees, ticket: {}({}), assignees: {}".format(
ticket, str(ticket.id), assignees
)
)
return
for ticket_assignee in ticket_assignees:
instance = TicketAppliedToAssignee(ticket_assignee.assignee, ticket)
for user in assignees:
instance = TicketAppliedToAssigneeMessage(user, ticket)
if settings.DEBUG:
logger.debug(instance)
instance.publish_async()
@ -28,7 +29,7 @@ def send_ticket_processed_mail_to_applicant(ticket, processor):
logger.error("Not found applicant: {}({})".format(ticket.title, ticket.id))
return
instance = TicketProcessedToApplicant(ticket.applicant, ticket, processor)
instance = TicketProcessedToApplicantMessage(ticket.applicant, ticket, processor)
if settings.DEBUG:
logger.debug(instance)
instance.publish_async()

View File

@ -50,13 +50,13 @@ class TicketDirectApproveView(TemplateView):
def get_context_data(self, **kwargs):
# 放入工单信息
token = kwargs.get('token')
ticket_info = cache.get(token, {}).get('body', '')
content = cache.get(token, {}).get('content', [])
if self.request.user.is_authenticated:
prompt_msg = _('Click the button below to approve or reject')
else:
prompt_msg = _('After successful authentication, this ticket can be approved directly')
kwargs.update({
'ticket_info': ticket_info, 'prompt_msg': prompt_msg,
'content': content, 'prompt_msg': prompt_msg,
'login_url': '%s&next=%s' % (
self.login_url,
reverse('tickets:direct-approve', kwargs={'token': token})