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, asset=self.serializer.asset,
system_user=self.serializer.system_user, system_user=self.serializer.system_user,
assignees=acl.reviewers.all(), assignees=acl.reviewers.all(),
org_id=self.serializer.org.id org_id=self.serializer.org.id,
) )
confirm_status_url = reverse( confirm_status_url = reverse(
view_name='api-tickets:super-ticket-status', view_name='api-tickets:super-ticket-status',
@ -59,7 +59,7 @@ class LoginAssetCheckAPI(CreateAPIView):
external=True, api_to_ui=True external=True, api_to_ui=True
) )
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type) 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 = { data = {
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url}, 'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', '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 return allow, reject_type
@staticmethod def create_confirm_ticket(self, request):
def construct_confirm_ticket_meta(request=None): 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 = get_request_ip(request) if request else ''
login_ip = login_ip or '0.0.0.0' login_ip = login_ip or '0.0.0.0'
login_city = get_ip_city(login_ip) login_city = get_ip_city(login_ip)
login_datetime = local_now_display() 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 = { data = {
'title': ticket_title, 'title': title,
'type': const.TicketType.login_confirm.value, 'type': const.TicketType.login_confirm,
'meta': ticket_meta, 'applicant': self.user,
'apply_login_city': login_city,
'apply_login_ip': login_ip,
'apply_login_datetime': login_datetime,
'org_id': Organization.ROOT_ID, 'org_id': Organization.ROOT_ID,
} }
ticket = Ticket.objects.create(**data) ticket = ApplyLoginTicket.objects.create(**data)
applicant = self.user
assignees = self.reviewers.all() assignees = self.reviewers.all()
ticket.create_process_map_and_node(assignees, applicant) ticket.open_by_system(assignees)
ticket.open(applicant)
return ticket return ticket

View File

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

View File

@ -69,7 +69,7 @@ class CommandConfirmAPI(CreateAPIView):
external=True, api_to_ui=True external=True, api_to_ui=True
) )
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type) 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 { return {
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url}, 'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
'close_confirm': {'method': 'DELETE', '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): def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
from tickets.const import TicketType from tickets.const import TicketType
from tickets.models import Ticket from tickets.models import ApplyCommandTicket
data = { data = {
'title': _('Command confirm') + ' ({})'.format(session.user), 'title': _('Command confirm') + ' ({})'.format(session.user),
'type': TicketType.command_confirm, 'type': TicketType.command_confirm,
'meta': { 'applicant': session.user_obj,
'apply_run_user': session.user, 'apply_run_user_id': session.user_id,
'apply_run_asset': session.asset, 'apply_run_asset_id': session.asset_id,
'apply_run_system_user': session.system_user, 'apply_run_system_user_id': session.system_user_id,
'apply_run_command': run_command, 'apply_run_command': run_command,
'apply_from_session_id': str(session.id), 'apply_from_session_id': str(session.id),
'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id), 'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id) 'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id),
},
'org_id': org_id, 'org_id': org_id,
} }
ticket = Ticket.objects.create(**data) ticket = ApplyCommandTicket.objects.create(**data)
applicant = session.user_obj
assignees = self.reviewers.all() assignees = self.reviewers.all()
ticket.create_process_map_and_node(assignees, applicant) ticket.open_by_system(assignees)
ticket.open(applicant)
return ticket return ticket
@classmethod @classmethod

View File

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

View File

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

View File

@ -86,7 +86,10 @@ class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer): class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = SystemUser 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): class ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
@ -122,8 +125,7 @@ class ConnectionTokenFilterRuleSerializer(serializers.ModelSerializer):
model = CommandFilterRule model = CommandFilterRule
fields = [ fields = [
'id', 'type', 'content', 'ignore_case', 'pattern', 'id', 'type', 'content', 'ignore_case', 'pattern',
'priority', 'action', 'priority', 'action', 'date_created',
'date_created',
] ]

View File

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

View File

@ -1,20 +1,30 @@
import json import json
from datetime import datetime
import uuid import uuid
import logging
from datetime import datetime
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db import models
from django.conf import settings from django.conf import settings
lazy_type = type(_('ugettext_lazy'))
class ModelJSONFieldEncoder(json.JSONEncoder): class ModelJSONFieldEncoder(json.JSONEncoder):
""" 解决一些类型的字段不能序列化的问题 """ """ 解决一些类型的字段不能序列化的问题 """
def default(self, obj): 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) return obj.strftime(settings.DATETIME_DISPLAY_FORMAT)
if isinstance(obj, uuid.UUID): elif isinstance(obj, (list, tuple)) and len(obj) > 0 \
return str(obj) and isinstance(obj[0], models.Model):
if isinstance(obj, type(_("ugettext_lazy"))): return [str(i) for i in obj]
return str(obj)
else: 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 from functools import reduce, partial
import inspect import inspect
from django.db import transaction
from django.db.models import * from django.db.models import *
from django.db.models import QuerySet from django.db.models import QuerySet
from django.db.models.functions import Concat from django.db.models.functions import Concat
@ -211,3 +212,29 @@ class UnionQuerySet(QuerySet):
qs = cls(assets1, assets2) qs = cls(assets1, assets2)
return qs 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:] end = data[-half:]
data = f'{start}{ellipsis_str}{end}' data = f'{start}{ellipsis_str}{end}'
return data 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 . import const
from .models import ReplayStorage from .models import ReplayStorage
from tickets.models import TicketSession, TicketStep, TicketAssignee from tickets.models import TicketSession, TicketStep, TicketAssignee
from tickets.const import ProcessStatus from tickets.const import StepState
logger = get_logger(__name__) logger = get_logger(__name__)

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .ticket import * from .ticket import *
from .flow import *
from .comment import * from .comment import *
from .super_ticket import * from .super_ticket import *
from .relation 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 import viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
from rest_framework.exceptions import MethodNotAllowed
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.exceptions import MethodNotAllowed
from common.const.http import POST, PUT from common.const.http import POST, PUT
from common.mixins.api import CommonApiMixin from common.mixins.api import CommonApiMixin
from common.drf.api import JMSBulkModelViewSet from orgs.utils import tmp_to_root_org
from rbac.permissions import RBACPermission from rbac.permissions import RBACPermission
from tickets import serializers from tickets import serializers
from tickets.models import Ticket, TicketFlow from tickets import filters
from tickets.filters import TicketFilter
from tickets.permissions.ticket import IsAssignee, IsApplicant 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): class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
serializer_class = serializers.TicketDisplaySerializer serializer_class = serializers.TicketDisplaySerializer
serializer_classes = { serializer_classes = {
'open': serializers.TicketApplySerializer, 'list': serializers.TicketListSerializer,
'approve': serializers.TicketApproveSerializer, 'open': serializers.TicketApplySerializer
} }
filterset_class = TicketFilter model = Ticket
filterset_class = filters.TicketFilter
search_fields = [ search_fields = [
'title', 'type', 'status', 'applicant_display' 'title', 'type', 'status', 'applicant_display'
] ]
ordering_fields = ( ordering_fields = (
'title', 'applicant_display', 'status', 'state', 'action_display', 'title', 'applicant_display', 'status', 'state',
'date_created', 'serial_num', 'action_display', 'date_created', 'serial_num',
) )
ordering = ('-date_created',) ordering = ('-date_created',)
rbac_perms = { rbac_perms = {
@ -48,15 +55,15 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
raise MethodNotAllowed(self.action) raise MethodNotAllowed(self.action)
def get_queryset(self): 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 return queryset
def perform_create(self, serializer): def perform_create(self, serializer):
instance = serializer.save() instance = serializer.save()
applicant = self.request.user instance.applicant = self.request.user
instance.create_related_node(applicant) instance.save(update_fields=['applicant'])
instance.process_map = instance.create_process_map(applicant) instance.open()
instance.open(applicant)
@action(detail=False, methods=[POST], permission_classes=[RBACPermission, ]) @action(detail=False, methods=[POST], permission_classes=[RBACPermission, ])
def open(self, request, *args, **kwargs): def open(self, request, *args, **kwargs):
@ -80,29 +87,41 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
def close(self, request, *args, **kwargs): def close(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
serializer = self.get_serializer(instance) serializer = self.get_serializer(instance)
instance.close(processor=request.user) instance.close()
return Response(serializer.data) return Response(serializer.data)
class TicketFlowViewSet(JMSBulkModelViewSet): class ApplyAssetTicketViewSet(TicketViewSet):
serializer_class = serializers.TicketFlowSerializer 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): class ApplyApplicationTicketViewSet(TicketViewSet):
raise MethodNotAllowed(self.action) 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): class ApplyLoginTicketViewSet(TicketViewSet):
instance = serializer.save() serializer_class = serializers.LoginConfirmSerializer
instance.save() model = ApplyLoginTicket
filterset_class = filters.ApplyLoginTicketFilter
def perform_create(self, serializer):
self.perform_create_or_update(serializer)
def perform_update(self, serializer): class ApplyLoginAssetTicketViewSet(TicketViewSet):
self.perform_create_or_update(serializer) 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): class TicketState(TextChoices):
open = 'open', _('Open') pending = 'pending', _('Open')
approved = 'approved', _('Approved')
rejected = 'rejected', _('Rejected')
closed = 'closed', _('Closed')
class ProcessStatus(TextChoices):
notified = 'notified', _('Notified')
approved = 'approved', _('Approved') approved = 'approved', _('Approved')
rejected = 'rejected', _('Rejected') rejected = 'rejected', _('Rejected')
closed = 'closed', _("Cancel")
reopen = 'reopen', _("Reopen")
class TicketStatus(TextChoices): class TicketStatus(TextChoices):
open = 'open', _("Open") 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") closed = 'closed', _("Closed")
@ -38,7 +47,7 @@ class TicketAction(TextChoices):
reject = 'reject', _('Reject') reject = 'reject', _('Reject')
class TicketApprovalLevel(IntegerChoices): class TicketLevel(IntegerChoices):
one = 1, _("One level") one = 1, _("One level")
two = 2, _("Two level") two = 2, _("Two level")

View File

@ -1,7 +1,10 @@
from django_filters import rest_framework as filters from django_filters import rest_framework as filters
from common.drf.filters import BaseFilterSet from common.drf.filters import BaseFilterSet
from tickets.models import Ticket from tickets.models import (
Ticket, ApplyAssetTicket, ApplyApplicationTicket,
ApplyLoginTicket, ApplyLoginAssetTicket, ApplyCommandTicket
)
class TicketFilter(BaseFilterSet): class TicketFilter(BaseFilterSet):
@ -10,9 +13,38 @@ class TicketFilter(BaseFilterSet):
class Meta: class Meta:
model = Ticket model = Ticket
fields = ( fields = (
'id', 'title', 'type', 'status', 'state', 'applicant', 'assignees__id', 'id', 'title', 'type', 'status', 'state', 'applicant', 'assignees__id'
'applicant_display',
) )
def filter_assignees_id(self, queryset, name, value): def filter_assignees_id(self, queryset, name, value):
return queryset.filter(ticket_steps__ticket_assignees__assignee__id=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): 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) handler_class = import_string(handler_class_path)
return handler_class(ticket=ticket) 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 django.utils.translation import ugettext as _
from tickets.models import ApplyLoginTicket
from .base import BaseHandler from .base import BaseHandler
class Handler(BaseHandler): class Handler(BaseHandler):
ticket: ApplyLoginTicket
# body
def _construct_meta_body_of_open(self): def _construct_meta_body_of_open(self):
apply_login_ip = self.ticket.meta.get('apply_login_ip') apply_login_ip = self.ticket.apply_login_ip
apply_login_city = self.ticket.meta.get('apply_login_city') apply_login_city = self.ticket.apply_login_city
apply_login_datetime = self.ticket.meta.get('apply_login_datetime') apply_login_datetime = self.ticket.apply_login_datetime
applied_body = ''' applied_body = '''
{}: {} {}: {}
{}: {} {}: {}

View File

@ -3,7 +3,7 @@
from django.conf import settings from django.conf import settings
from django.db import migrations, models from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
import tickets.models.ticket import common.db.encoder
TICKET_TYPE_APPLY_ASSET = 'apply_asset' TICKET_TYPE_APPLY_ASSET = 'apply_asset'
@ -116,7 +116,7 @@ class Migration(migrations.Migration):
migrations.AddField( migrations.AddField(
model_name='ticket', model_name='ticket',
name='assignees_display_new', 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( migrations.AlterField(
model_name='ticket', model_name='ticket',
@ -126,7 +126,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='ticket', model_name='ticket',
name='meta', 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( migrations.AlterField(
model_name='ticket', 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 Comment(CommonModelMixin):
class Type(models.TextChoices):
state = 'state', _('State')
common = 'common', _('common')
ticket = models.ForeignKey( ticket = models.ForeignKey(
'tickets.Ticket', on_delete=models.CASCADE, related_name='comments' '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")) user_display = models.CharField(max_length=256, verbose_name=_("User display name"))
body = models.TextField(verbose_name=_("Body")) 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: class Meta:
ordering = ('date_created', ) ordering = ('date_created', )

View File

@ -5,17 +5,18 @@ from django.utils.translation import ugettext_lazy as _
from users.models import User from users.models import User
from common.mixins.models import CommonModelMixin from common.mixins.models import CommonModelMixin
from orgs.mixins.models import OrgModelMixin from orgs.mixins.models import OrgModelMixin
from orgs.models import Organization from orgs.models import Organization
from orgs.utils import tmp_to_org, get_current_org_id 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'] __all__ = ['TicketFlow', 'ApprovalRule']
class ApprovalRule(CommonModelMixin): class ApprovalRule(CommonModelMixin):
level = models.SmallIntegerField( level = models.SmallIntegerField(
default=TicketApprovalLevel.one, choices=TicketApprovalLevel.choices, default=TicketLevel.one, choices=TicketLevel.choices,
verbose_name=_('Approve level') verbose_name=_('Approve level')
) )
strategy = models.CharField( strategy = models.CharField(
@ -56,8 +57,8 @@ class TicketFlow(CommonModelMixin, OrgModelMixin):
default=TicketType.general, verbose_name=_("Type") default=TicketType.general, verbose_name=_("Type")
) )
approval_level = models.SmallIntegerField( approval_level = models.SmallIntegerField(
default=TicketApprovalLevel.one, default=TicketLevel.one,
choices=TicketApprovalLevel.choices, choices=TicketLevel.choices,
verbose_name=_('Approve level') verbose_name=_('Approve level')
) )
rules = models.ManyToManyField(ApprovalRule, related_name='ticket_flows') 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 from urllib.parse import urljoin
import json
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.shortcuts import reverse from django.shortcuts import reverse
from django.db.models.fields import related
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.forms import model_to_dict
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from notifications.notifications import UserMessage from notifications.notifications import UserMessage
from common.utils import get_logger, random_string from common.utils import get_logger, random_string
from common.db.encoder import ModelJSONFieldEncoder
from .models import Ticket from .models import Ticket
from . import const from . import const
@ -41,8 +45,8 @@ class BaseTicketMessage(UserMessage):
def get_html_msg(self) -> dict: def get_html_msg(self) -> dict:
context = dict( context = dict(
title=self.content_title, title=self.content_title,
ticket_detail_url=self.ticket_detail_url, content=self.content,
body=self.ticket.body.replace('\n', '<br/>'), ticket_detail_url=self.ticket_detail_url
) )
message = render_to_string('tickets/_msg_ticket.html', context) message = render_to_string('tickets/_msg_ticket.html', context)
return { return {
@ -54,8 +58,48 @@ class BaseTicketMessage(UserMessage):
def gen_test_msg(cls): def gen_test_msg(cls):
return None 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): def __init__(self, user, ticket):
self.ticket = ticket self.ticket = ticket
super().__init__(user) super().__init__(user)
@ -69,14 +113,14 @@ class TicketAppliedToAssignee(BaseTicketMessage):
@property @property
def content_title(self): def content_title(self):
return _('Your has a new ticket, applicant - {}').format( return _('Your has a new ticket')
str(self.ticket.applicant_display)
)
@property @property
def subject(self): def subject(self):
title = _('New Ticket - {} ({})').format( title = _('{}: New Ticket - {} ({})').format(
self.ticket.title, self.ticket.get_type_display() self.ticket.applicant,
self.ticket.title,
self.ticket.get_type_display()
) )
return title return title
@ -85,19 +129,16 @@ class TicketAppliedToAssignee(BaseTicketMessage):
return urljoin(settings.SITE_URL, url) return urljoin(settings.SITE_URL, url)
def get_html_msg(self) -> dict: def get_html_msg(self) -> dict:
body = self.ticket.body.replace('\n', '<br/>')
context = dict( context = dict(
title=self.content_title, title=self.content_title,
ticket_detail_url=self.ticket_detail_url, content=self.content,
body=body, ticket_detail_url=self.ticket_detail_url
) )
ticket_approval_url = self.get_ticket_approval_url() ticket_approval_url = self.get_ticket_approval_url()
context.update({'ticket_approval_url': ticket_approval_url}) context.update({'ticket_approval_url': ticket_approval_url})
message = render_to_string('tickets/_msg_ticket.html', context) message = render_to_string('tickets/_msg_ticket.html', context)
cache.set(self.token, { cache.set(self.token, {'ticket_id': self.ticket.id, 'content': self.content}, 3600)
'body': body, 'ticket_id': self.ticket.id
}, 3600)
return { return {
'subject': self.subject, 'subject': self.subject,
'message': message 'message': message
@ -112,7 +153,7 @@ class TicketAppliedToAssignee(BaseTicketMessage):
return cls(user, ticket) return cls(user, ticket)
class TicketProcessedToApplicant(BaseTicketMessage): class TicketProcessedToApplicantMessage(BaseTicketMessage):
def __init__(self, user, ticket, processor): def __init__(self, user, ticket, processor):
self.ticket = ticket self.ticket = ticket
self.processor = processor self.processor = processor

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from .ticket import * from .ticket import *
from .flow import *
from .comment import * from .comment import *
from .relation 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'] fields = ['id', 'status', 'state', 'processor']
@staticmethod @staticmethod
def get_processor(ticket): def get_processor(instance):
if not ticket.processor: if not instance.processor:
return '' return ''
return str(ticket.processor) return str(instance.processor)

View File

@ -1,2 +1,7 @@
from .ticket import * 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 -*- # -*- coding: utf-8 -*-
# #
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.db.transaction import atomic
from rest_framework import serializers from rest_framework import serializers
from common.drf.serializers import MethodSerializer
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from perms.models import AssetPermission
from orgs.models import Organization from orgs.models import Organization
from orgs.utils import tmp_to_org from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from tickets.models import Ticket, TicketFlow, ApprovalRule from tickets.models import Ticket, TicketFlow
from tickets.const import TicketApprovalStrategy
from .meta import type_serializer_classes_mapping
__all__ = [ __all__ = [
'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketApproveSerializer', 'TicketFlowSerializer' 'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketListSerializer'
] ]
class TicketSerializer(OrgResourceModelSerializerMixin): class TicketSerializer(OrgResourceModelSerializerMixin):
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display')) type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status display')) status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status display'))
meta = MethodSerializer()
class Meta: class Meta:
model = Ticket model = Ticket
fields_mini = ['id', 'title'] fields_mini = ['id', 'title']
fields_small = fields_mini + [ fields_small = fields_mini + [
'type', 'type_display', 'meta', 'state', 'approval_step', 'type', 'type_display', 'status', 'status_display',
'status', 'status_display', 'applicant_display', 'process_map', 'state', 'approval_step', 'rel_snapshot', 'comment',
'date_created', 'date_updated', 'comment', 'org_id', 'org_name', 'body', 'date_created', 'date_updated', 'org_id', 'rel_snapshot',
'serial_num', 'process_map', 'org_name', 'serial_num'
] ]
fields_fk = ['applicant', ] fields_fk = ['applicant', ]
fields = fields_small + fields_fk 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: class TicketListSerializer(TicketSerializer):
action_serializer_classes_mapping = type_serializer_classes_mapping.get(_type) class Meta:
if action_serializer_classes_mapping: model = Ticket
query_action = self.context['request'].query_params.get('action') fields = [
action = query_action if query_action else self.context['view'].action 'id', 'title', 'serial_num', 'type', 'type_display', 'status',
serializer_class = action_serializer_classes_mapping.get(action) 'state', 'rel_snapshot', 'date_created', 'rel_snapshot'
if not serializer_class: ]
serializer_class = action_serializer_classes_mapping.get('default') read_only_fields = fields
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 TicketDisplaySerializer(TicketSerializer): class TicketDisplaySerializer(TicketSerializer):
@ -80,24 +54,10 @@ class TicketApplySerializer(TicketSerializer):
class Meta: class Meta:
model = Ticket model = Ticket
fields = TicketSerializer.Meta.fields fields = TicketSerializer.Meta.fields
writeable_fields = [
'id', 'title', 'type', 'meta', 'comment', 'org_id'
]
read_only_fields = list(set(fields) - set(writeable_fields))
extra_kwargs = { 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 @staticmethod
def validate_org_id(org_id): def validate_org_id(org_id):
org = Organization.get_instance(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)) error = _('The ticket flow `{}` does not exist'.format(ticket_type))
raise serializers.ValidationError(error) raise serializers.ValidationError(error)
return attrs 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 -*- # -*- coding: utf-8 -*-
# #
from django.dispatch import receiver 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 common.utils import get_logger
from tickets.models import Ticket from tickets.models import Ticket
from ..signals import post_change_ticket_action
logger = get_logger(__name__) logger = get_logger(__name__)
@receiver(post_change_ticket_action, sender=Ticket) @on_transaction_commit
def on_post_change_ticket_action(sender, ticket, action, **kwargs): def after_save_set_rel_snapshot(sender, instance, update_fields=None, **kwargs):
ticket.handler.dispatch(action) if update_fields and list(update_fields)[0] == 'rel_snapshot':
return
instance.set_rel_snapshot()
@receiver(post_save, sender=Ticket) @on_transaction_commit
def on_pre_save_ensure_serial_num(sender, instance: Ticket, **kwargs): def on_m2m_change(sender, action, instance, reverse=False, **kwargs):
instance.update_serial_num_if_need() 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 from django.dispatch import Signal
post_change_ticket_action = Signal() active_step = Signal()
post_or_update_change_ticket_flow_approval = 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 }} {{ title | safe }}
</p> </p>
<div> <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> </div>
<br> <br>
<div> <div>

View File

@ -10,7 +10,17 @@
<div class="ibox-content"> <div class="ibox-content">
<h2 class="font-bold" style="display: inline">{% trans 'Ticket information' %}</h2> <h2 class="font-bold" style="display: inline">{% trans 'Ticket information' %}</h2>
<h1></h1> <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> </div>
<div class="col-lg-6"> <div class="col-lg-6">

View File

@ -10,6 +10,11 @@ app_name = 'tickets'
router = BulkRouter() router = BulkRouter()
router.register('tickets', api.TicketViewSet, 'ticket') 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('flows', api.TicketFlowViewSet, 'flows')
router.register('comments', api.CommentViewSet, 'comment') router.register('comments', api.CommentViewSet, 'comment')
router.register('ticket-session-relation', api.TicketSessionRelationViewSet, 'ticket-session-relation') router.register('ticket-session-relation', api.TicketSessionRelationViewSet, 'ticket-session-relation')

View File

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

View File

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