mirror of https://github.com/jumpserver/jumpserver
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
parent
2471787277
commit
7e2f81a418
|
@ -47,7 +47,7 @@ class LoginAssetCheckAPI(CreateAPIView):
|
|||
asset=self.serializer.asset,
|
||||
system_user=self.serializer.system_user,
|
||||
assignees=acl.reviewers.all(),
|
||||
org_id=self.serializer.org.id
|
||||
org_id=self.serializer.org.id,
|
||||
)
|
||||
confirm_status_url = reverse(
|
||||
view_name='api-tickets:super-ticket-status',
|
||||
|
@ -59,7 +59,7 @@ class LoginAssetCheckAPI(CreateAPIView):
|
|||
external=True, api_to_ui=True
|
||||
)
|
||||
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
|
||||
ticket_assignees = ticket.current_node.first().ticket_assignees.all()
|
||||
ticket_assignees = ticket.current_step.ticket_assignees.all()
|
||||
data = {
|
||||
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
|
||||
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
|
||||
|
|
|
@ -97,34 +97,25 @@ class LoginACL(BaseACL):
|
|||
|
||||
return allow, reject_type
|
||||
|
||||
@staticmethod
|
||||
def construct_confirm_ticket_meta(request=None):
|
||||
def create_confirm_ticket(self, request):
|
||||
from tickets import const
|
||||
from tickets.models import ApplyLoginTicket
|
||||
from orgs.models import Organization
|
||||
title = _('Login confirm') + ' {}'.format(self.user)
|
||||
login_ip = get_request_ip(request) if request else ''
|
||||
login_ip = login_ip or '0.0.0.0'
|
||||
login_city = get_ip_city(login_ip)
|
||||
login_datetime = local_now_display()
|
||||
ticket_meta = {
|
||||
'apply_login_ip': login_ip,
|
||||
'apply_login_city': login_city,
|
||||
'apply_login_datetime': login_datetime,
|
||||
}
|
||||
return ticket_meta
|
||||
|
||||
def create_confirm_ticket(self, request=None):
|
||||
from tickets import const
|
||||
from tickets.models import Ticket
|
||||
from orgs.models import Organization
|
||||
ticket_title = _('Login confirm') + ' {}'.format(self.user)
|
||||
ticket_meta = self.construct_confirm_ticket_meta(request)
|
||||
data = {
|
||||
'title': ticket_title,
|
||||
'type': const.TicketType.login_confirm.value,
|
||||
'meta': ticket_meta,
|
||||
'title': title,
|
||||
'type': const.TicketType.login_confirm,
|
||||
'applicant': self.user,
|
||||
'apply_login_city': login_city,
|
||||
'apply_login_ip': login_ip,
|
||||
'apply_login_datetime': login_datetime,
|
||||
'org_id': Organization.ROOT_ID,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
applicant = self.user
|
||||
ticket = ApplyLoginTicket.objects.create(**data)
|
||||
assignees = self.reviewers.all()
|
||||
ticket.create_process_map_and_node(assignees, applicant)
|
||||
ticket.open(applicant)
|
||||
ticket.open_by_system(assignees)
|
||||
return ticket
|
||||
|
|
|
@ -85,19 +85,18 @@ class LoginAssetACL(BaseACL, OrgModelMixin):
|
|||
@classmethod
|
||||
def create_login_asset_confirm_ticket(cls, user, asset, system_user, assignees, org_id):
|
||||
from tickets.const import TicketType
|
||||
from tickets.models import Ticket
|
||||
from tickets.models import ApplyLoginAssetTicket
|
||||
title = _('Login asset confirm') + ' ({})'.format(user)
|
||||
data = {
|
||||
'title': _('Login asset confirm') + ' ({})'.format(user),
|
||||
'title': title,
|
||||
'type': TicketType.login_asset_confirm,
|
||||
'meta': {
|
||||
'apply_login_user': str(user),
|
||||
'apply_login_asset': str(asset),
|
||||
'apply_login_system_user': str(system_user),
|
||||
},
|
||||
'applicant': user,
|
||||
'apply_login_user': user,
|
||||
'apply_login_asset': asset,
|
||||
'apply_login_system_user': system_user,
|
||||
'org_id': org_id,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
ticket.create_process_map_and_node(assignees, user)
|
||||
ticket.open(applicant=user)
|
||||
ticket = ApplyLoginAssetTicket.objects.create(**data)
|
||||
ticket.open_by_system(assignees)
|
||||
return ticket
|
||||
|
||||
|
|
|
@ -69,7 +69,7 @@ class CommandConfirmAPI(CreateAPIView):
|
|||
external=True, api_to_ui=True
|
||||
)
|
||||
ticket_detail_url = '{url}?type={type}'.format(url=ticket_detail_url, type=ticket.type)
|
||||
ticket_assignees = ticket.current_node.first().ticket_assignees.all()
|
||||
ticket_assignees = ticket.current_step.ticket_assignees.all()
|
||||
return {
|
||||
'check_confirm_status': {'method': 'GET', 'url': confirm_status_url},
|
||||
'close_confirm': {'method': 'DELETE', 'url': confirm_status_url},
|
||||
|
|
|
@ -165,26 +165,23 @@ class CommandFilterRule(OrgModelMixin):
|
|||
|
||||
def create_command_confirm_ticket(self, run_command, session, cmd_filter_rule, org_id):
|
||||
from tickets.const import TicketType
|
||||
from tickets.models import Ticket
|
||||
from tickets.models import ApplyCommandTicket
|
||||
data = {
|
||||
'title': _('Command confirm') + ' ({})'.format(session.user),
|
||||
'type': TicketType.command_confirm,
|
||||
'meta': {
|
||||
'apply_run_user': session.user,
|
||||
'apply_run_asset': session.asset,
|
||||
'apply_run_system_user': session.system_user,
|
||||
'apply_run_command': run_command,
|
||||
'apply_from_session_id': str(session.id),
|
||||
'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
|
||||
'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id)
|
||||
},
|
||||
'applicant': session.user_obj,
|
||||
'apply_run_user_id': session.user_id,
|
||||
'apply_run_asset_id': session.asset_id,
|
||||
'apply_run_system_user_id': session.system_user_id,
|
||||
'apply_run_command': run_command,
|
||||
'apply_from_session_id': str(session.id),
|
||||
'apply_from_cmd_filter_rule_id': str(cmd_filter_rule.id),
|
||||
'apply_from_cmd_filter_id': str(cmd_filter_rule.filter.id),
|
||||
'org_id': org_id,
|
||||
}
|
||||
ticket = Ticket.objects.create(**data)
|
||||
applicant = session.user_obj
|
||||
ticket = ApplyCommandTicket.objects.create(**data)
|
||||
assignees = self.reviewers.all()
|
||||
ticket.create_process_map_and_node(assignees, applicant)
|
||||
ticket.open(applicant)
|
||||
ticket.open_by_system(assignees)
|
||||
return ticket
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -25,5 +25,5 @@ class TicketStatusApi(mixins.AuthMixin, APIView):
|
|||
ticket = self.get_ticket()
|
||||
if ticket:
|
||||
request.session.pop('auth_ticket_id', '')
|
||||
ticket.close(processor=self.get_user_from_session())
|
||||
ticket.close()
|
||||
return Response('', status=200)
|
||||
|
|
|
@ -337,18 +337,18 @@ class AuthACLMixin:
|
|||
raise errors.TimePeriodNotAllowed(username=user.username, request=self.request)
|
||||
|
||||
def get_ticket(self):
|
||||
from tickets.models import Ticket
|
||||
from tickets.models import ApplyLoginTicket
|
||||
ticket_id = self.request.session.get("auth_ticket_id")
|
||||
logger.debug('Login confirm ticket id: {}'.format(ticket_id))
|
||||
if not ticket_id:
|
||||
ticket = None
|
||||
else:
|
||||
ticket = Ticket.all().filter(id=ticket_id).first()
|
||||
ticket = ApplyLoginTicket.all().filter(id=ticket_id).first()
|
||||
return ticket
|
||||
|
||||
def get_ticket_or_create(self, confirm_setting):
|
||||
ticket = self.get_ticket()
|
||||
if not ticket or ticket.status_closed:
|
||||
if not ticket or ticket.is_status(ticket.Status.closed):
|
||||
ticket = confirm_setting.create_confirm_ticket(self.request)
|
||||
self.request.session['auth_ticket_id'] = str(ticket.id)
|
||||
return ticket
|
||||
|
@ -357,16 +357,17 @@ class AuthACLMixin:
|
|||
ticket = self.get_ticket()
|
||||
if not ticket:
|
||||
raise errors.LoginConfirmOtherError('', "Not found")
|
||||
if ticket.status_open:
|
||||
|
||||
if ticket.is_status(ticket.Status.open):
|
||||
raise errors.LoginConfirmWaitError(ticket.id)
|
||||
elif ticket.state_approve:
|
||||
elif ticket.is_state(ticket.State.approved):
|
||||
self.request.session["auth_confirm"] = "1"
|
||||
return
|
||||
elif ticket.state_reject:
|
||||
elif ticket.is_state(ticket.State.rejected):
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket.id, ticket.get_state_display()
|
||||
)
|
||||
elif ticket.state_close:
|
||||
elif ticket.is_state(ticket.State.closed):
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket.id, ticket.get_state_display()
|
||||
)
|
||||
|
|
|
@ -86,7 +86,10 @@ class ConnectionTokenAssetSerializer(serializers.ModelSerializer):
|
|||
class ConnectionTokenSystemUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
fields = ['id', 'name', 'username', 'password', 'private_key', 'protocol', 'ad_domain', 'org_id']
|
||||
fields = [
|
||||
'id', 'name', 'username', 'password', 'private_key',
|
||||
'protocol', 'ad_domain', 'org_id'
|
||||
]
|
||||
|
||||
|
||||
class ConnectionTokenGatewaySerializer(serializers.ModelSerializer):
|
||||
|
@ -122,8 +125,7 @@ class ConnectionTokenFilterRuleSerializer(serializers.ModelSerializer):
|
|||
model = CommandFilterRule
|
||||
fields = [
|
||||
'id', 'type', 'content', 'ignore_case', 'pattern',
|
||||
'priority', 'action',
|
||||
'date_created',
|
||||
'priority', 'action', 'date_created',
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -282,8 +282,7 @@ class UserLoginWaitConfirmView(TemplateView):
|
|||
if ticket:
|
||||
timestamp_created = datetime.datetime.timestamp(ticket.date_created)
|
||||
ticket_detail_url = TICKET_DETAIL_URL.format(id=ticket_id, type=ticket.type)
|
||||
assignees = ticket.current_node.first().ticket_assignees.all()
|
||||
assignees_display = ', '.join([str(i.assignee) for i in assignees])
|
||||
assignees_display = ', '.join([str(assignee) for assignee in ticket.current_assignees])
|
||||
msg = _("""Wait for <b>{}</b> confirm, You also can copy link to her/him <br/>
|
||||
Don't close this page""").format(assignees_display)
|
||||
else:
|
||||
|
|
|
@ -1,20 +1,30 @@
|
|||
import json
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
|
||||
lazy_type = type(_('ugettext_lazy'))
|
||||
|
||||
|
||||
class ModelJSONFieldEncoder(json.JSONEncoder):
|
||||
""" 解决一些类型的字段不能序列化的问题 """
|
||||
|
||||
def default(self, obj):
|
||||
if isinstance(obj, datetime):
|
||||
str_cls = (models.Model, lazy_type, models.ImageField, uuid.UUID)
|
||||
if isinstance(obj, str_cls):
|
||||
return str(obj)
|
||||
elif isinstance(obj, datetime):
|
||||
return obj.strftime(settings.DATETIME_DISPLAY_FORMAT)
|
||||
if isinstance(obj, uuid.UUID):
|
||||
return str(obj)
|
||||
if isinstance(obj, type(_("ugettext_lazy"))):
|
||||
return str(obj)
|
||||
elif isinstance(obj, (list, tuple)) and len(obj) > 0 \
|
||||
and isinstance(obj[0], models.Model):
|
||||
return [str(i) for i in obj]
|
||||
else:
|
||||
return super().default(obj)
|
||||
try:
|
||||
return super().default(obj)
|
||||
except TypeError:
|
||||
logging.error('Type error: ', type(obj))
|
||||
return str(obj)
|
||||
|
|
|
@ -13,6 +13,7 @@ import uuid
|
|||
from functools import reduce, partial
|
||||
import inspect
|
||||
|
||||
from django.db import transaction
|
||||
from django.db.models import *
|
||||
from django.db.models import QuerySet
|
||||
from django.db.models.functions import Concat
|
||||
|
@ -211,3 +212,29 @@ class UnionQuerySet(QuerySet):
|
|||
|
||||
qs = cls(assets1, assets2)
|
||||
return qs
|
||||
|
||||
|
||||
class MultiTableChildQueryset(QuerySet):
|
||||
|
||||
def bulk_create(self, objs, batch_size=None):
|
||||
assert batch_size is None or batch_size > 0
|
||||
if not objs:
|
||||
return objs
|
||||
|
||||
self._for_write = True
|
||||
objs = list(objs)
|
||||
parent_model = self.model._meta.pk.related_model
|
||||
|
||||
parent_objs = []
|
||||
for obj in objs:
|
||||
parent_values = {}
|
||||
for field in [f for f in parent_model._meta.fields if hasattr(obj, f.name)]:
|
||||
parent_values[field.name] = getattr(obj, field.name)
|
||||
parent_objs.append(parent_model(**parent_values))
|
||||
setattr(obj, self.model._meta.pk.attname, obj.id)
|
||||
parent_model.objects.bulk_create(parent_objs, batch_size=batch_size)
|
||||
|
||||
with transaction.atomic(using=self.db, savepoint=False):
|
||||
self._batched_insert(objs, self.model._meta.local_fields, batch_size)
|
||||
|
||||
return objs
|
||||
|
|
|
@ -361,3 +361,7 @@ def pretty_string(data: str, max_length=128, ellipsis_str='...'):
|
|||
end = data[-half:]
|
||||
data = f'{start}{ellipsis_str}{end}'
|
||||
return data
|
||||
|
||||
|
||||
def group_by_count(it, count):
|
||||
return [it[i:i+count] for i in range(0, len(it), count)]
|
||||
|
|
|
@ -12,7 +12,7 @@ from common.utils import get_logger
|
|||
from . import const
|
||||
from .models import ReplayStorage
|
||||
from tickets.models import TicketSession, TicketStep, TicketAssignee
|
||||
from tickets.const import ProcessStatus
|
||||
from tickets.const import StepState
|
||||
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .ticket import *
|
||||
from .flow import *
|
||||
from .comment import *
|
||||
from .super_ticket import *
|
||||
from .relation import *
|
||||
|
|
|
@ -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)
|
|
@ -2,36 +2,43 @@
|
|||
#
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import MethodNotAllowed
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.exceptions import MethodNotAllowed
|
||||
|
||||
from common.const.http import POST, PUT
|
||||
from common.mixins.api import CommonApiMixin
|
||||
from common.drf.api import JMSBulkModelViewSet
|
||||
from orgs.utils import tmp_to_root_org
|
||||
|
||||
from rbac.permissions import RBACPermission
|
||||
|
||||
from tickets import serializers
|
||||
from tickets.models import Ticket, TicketFlow
|
||||
from tickets.filters import TicketFilter
|
||||
from tickets import filters
|
||||
from tickets.permissions.ticket import IsAssignee, IsApplicant
|
||||
from tickets.models import (
|
||||
Ticket, ApplyAssetTicket, ApplyApplicationTicket,
|
||||
ApplyLoginTicket, ApplyLoginAssetTicket, ApplyCommandTicket
|
||||
)
|
||||
|
||||
__all__ = ['TicketViewSet', 'TicketFlowViewSet']
|
||||
__all__ = [
|
||||
'TicketViewSet', 'ApplyAssetTicketViewSet', 'ApplyApplicationTicketViewSet',
|
||||
'ApplyLoginTicketViewSet', 'ApplyLoginAssetTicketViewSet', 'ApplyCommandTicketViewSet'
|
||||
]
|
||||
|
||||
|
||||
class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
||||
serializer_class = serializers.TicketDisplaySerializer
|
||||
serializer_classes = {
|
||||
'open': serializers.TicketApplySerializer,
|
||||
'approve': serializers.TicketApproveSerializer,
|
||||
'list': serializers.TicketListSerializer,
|
||||
'open': serializers.TicketApplySerializer
|
||||
}
|
||||
filterset_class = TicketFilter
|
||||
model = Ticket
|
||||
filterset_class = filters.TicketFilter
|
||||
search_fields = [
|
||||
'title', 'type', 'status', 'applicant_display'
|
||||
]
|
||||
ordering_fields = (
|
||||
'title', 'applicant_display', 'status', 'state', 'action_display',
|
||||
'date_created', 'serial_num',
|
||||
'title', 'applicant_display', 'status', 'state',
|
||||
'action_display', 'date_created', 'serial_num',
|
||||
)
|
||||
ordering = ('-date_created',)
|
||||
rbac_perms = {
|
||||
|
@ -48,15 +55,15 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
|||
raise MethodNotAllowed(self.action)
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = Ticket.get_user_related_tickets(self.request.user)
|
||||
with tmp_to_root_org():
|
||||
queryset = self.model.get_user_related_tickets(self.request.user)
|
||||
return queryset
|
||||
|
||||
def perform_create(self, serializer):
|
||||
instance = serializer.save()
|
||||
applicant = self.request.user
|
||||
instance.create_related_node(applicant)
|
||||
instance.process_map = instance.create_process_map(applicant)
|
||||
instance.open(applicant)
|
||||
instance.applicant = self.request.user
|
||||
instance.save(update_fields=['applicant'])
|
||||
instance.open()
|
||||
|
||||
@action(detail=False, methods=[POST], permission_classes=[RBACPermission, ])
|
||||
def open(self, request, *args, **kwargs):
|
||||
|
@ -80,29 +87,41 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
|
|||
def close(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
serializer = self.get_serializer(instance)
|
||||
instance.close(processor=request.user)
|
||||
instance.close()
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class TicketFlowViewSet(JMSBulkModelViewSet):
|
||||
serializer_class = serializers.TicketFlowSerializer
|
||||
class ApplyAssetTicketViewSet(TicketViewSet):
|
||||
serializer_class = serializers.ApplyAssetDisplaySerializer
|
||||
serializer_classes = {
|
||||
'open': serializers.ApplyAssetSerializer
|
||||
}
|
||||
model = ApplyAssetTicket
|
||||
filterset_class = filters.ApplyAssetTicketFilter
|
||||
|
||||
filterset_fields = ['id', 'type']
|
||||
search_fields = ['id', 'type']
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
raise MethodNotAllowed(self.action)
|
||||
class ApplyApplicationTicketViewSet(TicketViewSet):
|
||||
serializer_class = serializers.ApplyApplicationDisplaySerializer
|
||||
serializer_classes = {
|
||||
'open': serializers.ApplyApplicationSerializer
|
||||
}
|
||||
model = ApplyApplicationTicket
|
||||
filterset_class = filters.ApplyApplicationTicketFilter
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = TicketFlow.get_org_related_flows()
|
||||
return queryset
|
||||
|
||||
def perform_create_or_update(self, serializer):
|
||||
instance = serializer.save()
|
||||
instance.save()
|
||||
class ApplyLoginTicketViewSet(TicketViewSet):
|
||||
serializer_class = serializers.LoginConfirmSerializer
|
||||
model = ApplyLoginTicket
|
||||
filterset_class = filters.ApplyLoginTicketFilter
|
||||
|
||||
def perform_create(self, serializer):
|
||||
self.perform_create_or_update(serializer)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
self.perform_create_or_update(serializer)
|
||||
class ApplyLoginAssetTicketViewSet(TicketViewSet):
|
||||
serializer_class = serializers.LoginAssetConfirmSerializer
|
||||
model = ApplyLoginAssetTicket
|
||||
filterset_class = filters.ApplyLoginAssetTicketFilter
|
||||
|
||||
|
||||
class ApplyCommandTicketViewSet(TicketViewSet):
|
||||
serializer_class = serializers.ApplyCommandConfirmSerializer
|
||||
model = ApplyCommandTicket
|
||||
filterset_class = filters.ApplyCommandTicketFilter
|
||||
|
|
|
@ -14,20 +14,29 @@ class TicketType(TextChoices):
|
|||
|
||||
|
||||
class TicketState(TextChoices):
|
||||
open = 'open', _('Open')
|
||||
approved = 'approved', _('Approved')
|
||||
rejected = 'rejected', _('Rejected')
|
||||
closed = 'closed', _('Closed')
|
||||
|
||||
|
||||
class ProcessStatus(TextChoices):
|
||||
notified = 'notified', _('Notified')
|
||||
pending = 'pending', _('Open')
|
||||
approved = 'approved', _('Approved')
|
||||
rejected = 'rejected', _('Rejected')
|
||||
closed = 'closed', _("Cancel")
|
||||
reopen = 'reopen', _("Reopen")
|
||||
|
||||
|
||||
class TicketStatus(TextChoices):
|
||||
open = 'open', _("Open")
|
||||
closed = 'closed', _("Finished")
|
||||
|
||||
|
||||
class StepState(TextChoices):
|
||||
pending = 'pending', _('Pending')
|
||||
approved = 'approved', _('Approved')
|
||||
rejected = 'rejected', _('Rejected')
|
||||
closed = 'closed', _("Closed")
|
||||
reopen = 'reopen', _("Reopen")
|
||||
|
||||
|
||||
class StepStatus(TextChoices):
|
||||
pending = 'pending', _('Pending')
|
||||
active = 'active', _('Active')
|
||||
closed = 'closed', _("Closed")
|
||||
|
||||
|
||||
|
@ -38,7 +47,7 @@ class TicketAction(TextChoices):
|
|||
reject = 'reject', _('Reject')
|
||||
|
||||
|
||||
class TicketApprovalLevel(IntegerChoices):
|
||||
class TicketLevel(IntegerChoices):
|
||||
one = 1, _("One level")
|
||||
two = 2, _("Two level")
|
||||
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
from django_filters import rest_framework as filters
|
||||
from common.drf.filters import BaseFilterSet
|
||||
|
||||
from tickets.models import Ticket
|
||||
from tickets.models import (
|
||||
Ticket, ApplyAssetTicket, ApplyApplicationTicket,
|
||||
ApplyLoginTicket, ApplyLoginAssetTicket, ApplyCommandTicket
|
||||
)
|
||||
|
||||
|
||||
class TicketFilter(BaseFilterSet):
|
||||
|
@ -10,9 +13,38 @@ class TicketFilter(BaseFilterSet):
|
|||
class Meta:
|
||||
model = Ticket
|
||||
fields = (
|
||||
'id', 'title', 'type', 'status', 'state', 'applicant', 'assignees__id',
|
||||
'applicant_display',
|
||||
'id', 'title', 'type', 'status', 'state', 'applicant', 'assignees__id'
|
||||
)
|
||||
|
||||
def filter_assignees_id(self, queryset, name, value):
|
||||
return queryset.filter(ticket_steps__ticket_assignees__assignee__id=value)
|
||||
|
||||
|
||||
class ApplyAssetTicketFilter(BaseFilterSet):
|
||||
class Meta:
|
||||
model = ApplyAssetTicket
|
||||
fields = ('id',)
|
||||
|
||||
|
||||
class ApplyApplicationTicketFilter(BaseFilterSet):
|
||||
class Meta:
|
||||
model = ApplyApplicationTicket
|
||||
fields = ('id',)
|
||||
|
||||
|
||||
class ApplyLoginTicketFilter(BaseFilterSet):
|
||||
class Meta:
|
||||
model = ApplyLoginTicket
|
||||
fields = ('id',)
|
||||
|
||||
|
||||
class ApplyLoginAssetTicketFilter(BaseFilterSet):
|
||||
class Meta:
|
||||
model = ApplyLoginAssetTicket
|
||||
fields = ('id',)
|
||||
|
||||
|
||||
class ApplyCommandTicketFilter(BaseFilterSet):
|
||||
class Meta:
|
||||
model = ApplyCommandTicket
|
||||
fields = ('id',)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -2,6 +2,6 @@ from django.utils.module_loading import import_string
|
|||
|
||||
|
||||
def get_ticket_handler(ticket):
|
||||
handler_class_path = 'tickets.handler.{}.Handler'.format(ticket.type)
|
||||
handler_class_path = 'tickets.handlers.{}.Handler'.format(ticket.type)
|
||||
handler_class = import_string(handler_class_path)
|
||||
return handler_class(ticket=ticket)
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -0,0 +1,6 @@
|
|||
from tickets.models import ApplyCommandTicket
|
||||
from .base import BaseHandler
|
||||
|
||||
|
||||
class Handler(BaseHandler):
|
||||
ticket: ApplyCommandTicket
|
|
@ -0,0 +1,6 @@
|
|||
from tickets.models import ApplyLoginAssetTicket
|
||||
from .base import BaseHandler
|
||||
|
||||
|
||||
class Handler(BaseHandler):
|
||||
ticket: ApplyLoginAssetTicket
|
|
@ -1,14 +1,15 @@
|
|||
from django.utils.translation import ugettext as _
|
||||
from tickets.models import ApplyLoginTicket
|
||||
from .base import BaseHandler
|
||||
|
||||
|
||||
class Handler(BaseHandler):
|
||||
ticket: ApplyLoginTicket
|
||||
|
||||
# body
|
||||
def _construct_meta_body_of_open(self):
|
||||
apply_login_ip = self.ticket.meta.get('apply_login_ip')
|
||||
apply_login_city = self.ticket.meta.get('apply_login_city')
|
||||
apply_login_datetime = self.ticket.meta.get('apply_login_datetime')
|
||||
apply_login_ip = self.ticket.apply_login_ip
|
||||
apply_login_city = self.ticket.apply_login_city
|
||||
apply_login_datetime = self.ticket.apply_login_datetime
|
||||
applied_body = '''
|
||||
{}: {}
|
||||
{}: {}
|
|
@ -3,7 +3,7 @@
|
|||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import tickets.models.ticket
|
||||
import common.db.encoder
|
||||
|
||||
TICKET_TYPE_APPLY_ASSET = 'apply_asset'
|
||||
|
||||
|
@ -116,7 +116,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='assignees_display_new',
|
||||
field=models.JSONField(default=list, encoder=tickets.models.ticket.ModelJSONFieldEncoder, verbose_name='Assignees display'),
|
||||
field=models.JSONField(default=list, encoder=common.db.encoder.ModelJSONFieldEncoder, verbose_name='Assignees display'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticket',
|
||||
|
@ -126,7 +126,7 @@ class Migration(migrations.Migration):
|
|||
migrations.AlterField(
|
||||
model_name='ticket',
|
||||
name='meta',
|
||||
field=models.JSONField(default=dict, encoder=tickets.models.ticket.ModelJSONFieldEncoder, verbose_name='Meta'),
|
||||
field=models.JSONField(default=dict, encoder=common.db.encoder.ModelJSONFieldEncoder, verbose_name='Meta'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticket',
|
||||
|
|
|
@ -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',),
|
||||
),
|
||||
]
|
|
@ -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',
|
||||
),
|
||||
]
|
|
@ -9,6 +9,10 @@ __all__ = ['Comment']
|
|||
|
||||
|
||||
class Comment(CommonModelMixin):
|
||||
class Type(models.TextChoices):
|
||||
state = 'state', _('State')
|
||||
common = 'common', _('common')
|
||||
|
||||
ticket = models.ForeignKey(
|
||||
'tickets.Ticket', on_delete=models.CASCADE, related_name='comments'
|
||||
)
|
||||
|
@ -18,6 +22,10 @@ class Comment(CommonModelMixin):
|
|||
)
|
||||
user_display = models.CharField(max_length=256, verbose_name=_("User display name"))
|
||||
body = models.TextField(verbose_name=_("Body"))
|
||||
type = models.CharField(
|
||||
max_length=16, choices=Type.choices, default=Type.common, verbose_name=_("Type")
|
||||
)
|
||||
state = models.CharField(max_length=16, null=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ('date_created', )
|
||||
|
|
|
@ -5,17 +5,18 @@ from django.utils.translation import ugettext_lazy as _
|
|||
|
||||
from users.models import User
|
||||
from common.mixins.models import CommonModelMixin
|
||||
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
from orgs.models import Organization
|
||||
from orgs.utils import tmp_to_org, get_current_org_id
|
||||
from ..const import TicketType, TicketApprovalLevel, TicketApprovalStrategy
|
||||
from ..const import TicketType, TicketLevel, TicketApprovalStrategy
|
||||
|
||||
__all__ = ['TicketFlow', 'ApprovalRule']
|
||||
|
||||
|
||||
class ApprovalRule(CommonModelMixin):
|
||||
level = models.SmallIntegerField(
|
||||
default=TicketApprovalLevel.one, choices=TicketApprovalLevel.choices,
|
||||
default=TicketLevel.one, choices=TicketLevel.choices,
|
||||
verbose_name=_('Approve level')
|
||||
)
|
||||
strategy = models.CharField(
|
||||
|
@ -56,8 +57,8 @@ class TicketFlow(CommonModelMixin, OrgModelMixin):
|
|||
default=TicketType.general, verbose_name=_("Type")
|
||||
)
|
||||
approval_level = models.SmallIntegerField(
|
||||
default=TicketApprovalLevel.one,
|
||||
choices=TicketApprovalLevel.choices,
|
||||
default=TicketLevel.one,
|
||||
choices=TicketLevel.choices,
|
||||
verbose_name=_('Approve level')
|
||||
)
|
||||
rules = models.ManyToManyField(ApprovalRule, related_name='ticket_flows')
|
||||
|
|
|
@ -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")
|
|
@ -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 *
|
|
@ -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)
|
|
@ -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)
|
|
@ -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')
|
||||
)
|
||||
|
|
@ -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
|
|
@ -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'),
|
||||
)
|
|
@ -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)
|
|
@ -1,13 +1,17 @@
|
|||
from urllib.parse import urljoin
|
||||
import json
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.shortcuts import reverse
|
||||
from django.db.models.fields import related
|
||||
from django.template.loader import render_to_string
|
||||
from django.forms import model_to_dict
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from notifications.notifications import UserMessage
|
||||
from common.utils import get_logger, random_string
|
||||
from common.db.encoder import ModelJSONFieldEncoder
|
||||
from .models import Ticket
|
||||
from . import const
|
||||
|
||||
|
@ -41,8 +45,8 @@ class BaseTicketMessage(UserMessage):
|
|||
def get_html_msg(self) -> dict:
|
||||
context = dict(
|
||||
title=self.content_title,
|
||||
ticket_detail_url=self.ticket_detail_url,
|
||||
body=self.ticket.body.replace('\n', '<br/>'),
|
||||
content=self.content,
|
||||
ticket_detail_url=self.ticket_detail_url
|
||||
)
|
||||
message = render_to_string('tickets/_msg_ticket.html', context)
|
||||
return {
|
||||
|
@ -54,8 +58,48 @@ class BaseTicketMessage(UserMessage):
|
|||
def gen_test_msg(cls):
|
||||
return None
|
||||
|
||||
@property
|
||||
def content(self):
|
||||
content = [
|
||||
{'title': _('Ticket basic info'), 'content': self.basic_items},
|
||||
{'title': _('Ticket applied info'), 'content': self.spec_items},
|
||||
]
|
||||
return content
|
||||
|
||||
class TicketAppliedToAssignee(BaseTicketMessage):
|
||||
def _get_fields_items(self, item_names):
|
||||
fields = self.ticket._meta._forward_fields_map
|
||||
json_data = json.dumps(model_to_dict(self.ticket), cls=ModelJSONFieldEncoder)
|
||||
data = json.loads(json_data)
|
||||
items = []
|
||||
|
||||
for name in item_names:
|
||||
field = fields[name]
|
||||
item = {'name': name, 'title': field.verbose_name}
|
||||
value = data.get(name)
|
||||
if hasattr(self.ticket, f'get_{name}_display'):
|
||||
value = getattr(self.ticket, f'get_{name}_display')()
|
||||
elif isinstance(field, related.ForeignKey):
|
||||
value = self.ticket.rel_snapshot[name]
|
||||
elif isinstance(field, related.ManyToManyField):
|
||||
value = ', '.join(self.ticket.rel_snapshot[name])
|
||||
item['value'] = value
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
@property
|
||||
def basic_items(self):
|
||||
item_names = ['serial_num', 'title', 'type', 'state', 'applicant', 'comment']
|
||||
return self._get_fields_items(item_names)
|
||||
|
||||
@property
|
||||
def spec_items(self):
|
||||
fields = self.ticket._meta.local_fields + self.ticket._meta.local_many_to_many
|
||||
excludes = ['ticket_ptr']
|
||||
item_names = [field.name for field in fields if field.name not in excludes]
|
||||
return self._get_fields_items(item_names)
|
||||
|
||||
|
||||
class TicketAppliedToAssigneeMessage(BaseTicketMessage):
|
||||
def __init__(self, user, ticket):
|
||||
self.ticket = ticket
|
||||
super().__init__(user)
|
||||
|
@ -69,14 +113,14 @@ class TicketAppliedToAssignee(BaseTicketMessage):
|
|||
|
||||
@property
|
||||
def content_title(self):
|
||||
return _('Your has a new ticket, applicant - {}').format(
|
||||
str(self.ticket.applicant_display)
|
||||
)
|
||||
return _('Your has a new ticket')
|
||||
|
||||
@property
|
||||
def subject(self):
|
||||
title = _('New Ticket - {} ({})').format(
|
||||
self.ticket.title, self.ticket.get_type_display()
|
||||
title = _('{}: New Ticket - {} ({})').format(
|
||||
self.ticket.applicant,
|
||||
self.ticket.title,
|
||||
self.ticket.get_type_display()
|
||||
)
|
||||
return title
|
||||
|
||||
|
@ -85,19 +129,16 @@ class TicketAppliedToAssignee(BaseTicketMessage):
|
|||
return urljoin(settings.SITE_URL, url)
|
||||
|
||||
def get_html_msg(self) -> dict:
|
||||
body = self.ticket.body.replace('\n', '<br/>')
|
||||
context = dict(
|
||||
title=self.content_title,
|
||||
ticket_detail_url=self.ticket_detail_url,
|
||||
body=body,
|
||||
content=self.content,
|
||||
ticket_detail_url=self.ticket_detail_url
|
||||
)
|
||||
|
||||
ticket_approval_url = self.get_ticket_approval_url()
|
||||
context.update({'ticket_approval_url': ticket_approval_url})
|
||||
message = render_to_string('tickets/_msg_ticket.html', context)
|
||||
cache.set(self.token, {
|
||||
'body': body, 'ticket_id': self.ticket.id
|
||||
}, 3600)
|
||||
cache.set(self.token, {'ticket_id': self.ticket.id, 'content': self.content}, 3600)
|
||||
return {
|
||||
'subject': self.subject,
|
||||
'message': message
|
||||
|
@ -112,7 +153,7 @@ class TicketAppliedToAssignee(BaseTicketMessage):
|
|||
return cls(user, ticket)
|
||||
|
||||
|
||||
class TicketProcessedToApplicant(BaseTicketMessage):
|
||||
class TicketProcessedToApplicantMessage(BaseTicketMessage):
|
||||
def __init__(self, user, ticket, processor):
|
||||
self.ticket = ticket
|
||||
self.processor = processor
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .ticket import *
|
||||
from .flow import *
|
||||
from .comment import *
|
||||
from .relation import *
|
||||
from .super_ticket import *
|
||||
from .super_ticket import *
|
||||
|
|
|
@ -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
|
|
@ -15,7 +15,7 @@ class SuperTicketSerializer(serializers.ModelSerializer):
|
|||
fields = ['id', 'status', 'state', 'processor']
|
||||
|
||||
@staticmethod
|
||||
def get_processor(ticket):
|
||||
if not ticket.processor:
|
||||
def get_processor(instance):
|
||||
if not instance.processor:
|
||||
return ''
|
||||
return str(ticket.processor)
|
||||
return str(instance.processor)
|
||||
|
|
|
@ -1,2 +1,7 @@
|
|||
from .ticket import *
|
||||
from .meta import *
|
||||
from .apply_asset import *
|
||||
from .apply_application import *
|
||||
from .login_confirm import *
|
||||
from .login_asset_confirm import *
|
||||
from .command_confirm import *
|
||||
from .common import *
|
||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -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'
|
||||
]
|
|
@ -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)))
|
|
@ -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'
|
||||
]
|
|
@ -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'
|
||||
]
|
|
@ -1 +0,0 @@
|
|||
from .meta import *
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,68 +1,42 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.db.transaction import atomic
|
||||
from rest_framework import serializers
|
||||
from common.drf.serializers import MethodSerializer
|
||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||
from perms.models import AssetPermission
|
||||
|
||||
from orgs.models import Organization
|
||||
from orgs.utils import tmp_to_org
|
||||
from tickets.models import Ticket, TicketFlow, ApprovalRule
|
||||
from tickets.const import TicketApprovalStrategy
|
||||
from .meta import type_serializer_classes_mapping
|
||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
|
||||
from tickets.models import Ticket, TicketFlow
|
||||
|
||||
__all__ = [
|
||||
'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketApproveSerializer', 'TicketFlowSerializer'
|
||||
'TicketDisplaySerializer', 'TicketApplySerializer', 'TicketListSerializer'
|
||||
]
|
||||
|
||||
|
||||
class TicketSerializer(OrgResourceModelSerializerMixin):
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
|
||||
status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status display'))
|
||||
meta = MethodSerializer()
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields_mini = ['id', 'title']
|
||||
fields_small = fields_mini + [
|
||||
'type', 'type_display', 'meta', 'state', 'approval_step',
|
||||
'status', 'status_display', 'applicant_display', 'process_map',
|
||||
'date_created', 'date_updated', 'comment', 'org_id', 'org_name', 'body',
|
||||
'serial_num',
|
||||
'type', 'type_display', 'status', 'status_display',
|
||||
'state', 'approval_step', 'rel_snapshot', 'comment',
|
||||
'date_created', 'date_updated', 'org_id', 'rel_snapshot',
|
||||
'process_map', 'org_name', 'serial_num'
|
||||
]
|
||||
fields_fk = ['applicant', ]
|
||||
fields = fields_small + fields_fk
|
||||
|
||||
def get_meta_serializer(self):
|
||||
default_serializer = serializers.Serializer(read_only=True)
|
||||
if isinstance(self.instance, Ticket):
|
||||
_type = self.instance.type
|
||||
else:
|
||||
_type = self.context['request'].query_params.get('type')
|
||||
|
||||
if _type:
|
||||
action_serializer_classes_mapping = type_serializer_classes_mapping.get(_type)
|
||||
if action_serializer_classes_mapping:
|
||||
query_action = self.context['request'].query_params.get('action')
|
||||
action = query_action if query_action else self.context['view'].action
|
||||
serializer_class = action_serializer_classes_mapping.get(action)
|
||||
if not serializer_class:
|
||||
serializer_class = action_serializer_classes_mapping.get('default')
|
||||
else:
|
||||
serializer_class = default_serializer
|
||||
else:
|
||||
serializer_class = default_serializer
|
||||
|
||||
if not serializer_class:
|
||||
serializer_class = default_serializer
|
||||
|
||||
if isinstance(serializer_class, type):
|
||||
serializer = serializer_class()
|
||||
else:
|
||||
serializer = serializer_class
|
||||
|
||||
return serializer
|
||||
class TicketListSerializer(TicketSerializer):
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = [
|
||||
'id', 'title', 'serial_num', 'type', 'type_display', 'status',
|
||||
'state', 'rel_snapshot', 'date_created', 'rel_snapshot'
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class TicketDisplaySerializer(TicketSerializer):
|
||||
|
@ -80,24 +54,10 @@ class TicketApplySerializer(TicketSerializer):
|
|||
class Meta:
|
||||
model = Ticket
|
||||
fields = TicketSerializer.Meta.fields
|
||||
writeable_fields = [
|
||||
'id', 'title', 'type', 'meta', 'comment', 'org_id'
|
||||
]
|
||||
read_only_fields = list(set(fields) - set(writeable_fields))
|
||||
extra_kwargs = {
|
||||
'type': {'required': True},
|
||||
'type': {'required': True}
|
||||
}
|
||||
|
||||
def validate_type(self, tp):
|
||||
request_type = self.context['request'].query_params.get('type')
|
||||
if tp != request_type:
|
||||
error = _(
|
||||
'The `type` in the submission data (`{}`) is different from the type '
|
||||
'in the request url (`{}`)'.format(tp, request_type)
|
||||
)
|
||||
raise serializers.ValidationError(error)
|
||||
return tp
|
||||
|
||||
@staticmethod
|
||||
def validate_org_id(org_id):
|
||||
org = Organization.get_instance(org_id)
|
||||
|
@ -116,113 +76,3 @@ class TicketApplySerializer(TicketSerializer):
|
|||
error = _('The ticket flow `{}` does not exist'.format(ticket_type))
|
||||
raise serializers.ValidationError(error)
|
||||
return attrs
|
||||
|
||||
@atomic
|
||||
def create(self, validated_data):
|
||||
instance = super().create(validated_data)
|
||||
name = _('Created by ticket ({}-{})').format(instance.title, str(instance.id)[:4])
|
||||
with tmp_to_org(instance.org_id):
|
||||
if not AssetPermission.objects.filter(name=name).exists():
|
||||
instance.meta.update({'apply_permission_name': name})
|
||||
return instance
|
||||
raise serializers.ValidationError(_('Permission named `{}` already exists'.format(name)))
|
||||
|
||||
|
||||
class TicketApproveSerializer(TicketSerializer):
|
||||
meta = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = Ticket
|
||||
fields = TicketSerializer.Meta.fields
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class TicketFlowApproveSerializer(serializers.ModelSerializer):
|
||||
strategy_display = serializers.ReadOnlyField(source='get_strategy_display', label=_('Approve strategy'))
|
||||
assignees_read_only = serializers.SerializerMethodField(label=_('Assignees'))
|
||||
assignees_display = serializers.SerializerMethodField(label=_('Assignees display'))
|
||||
|
||||
class Meta:
|
||||
model = ApprovalRule
|
||||
fields_small = [
|
||||
'level', 'strategy', 'assignees_read_only', 'assignees_display', 'strategy_display'
|
||||
]
|
||||
fields_m2m = ['assignees', ]
|
||||
fields = fields_small + fields_m2m
|
||||
read_only_fields = ['level', 'assignees_display']
|
||||
extra_kwargs = {
|
||||
'assignees': {'write_only': True, 'allow_empty': True, 'required': False}
|
||||
}
|
||||
|
||||
def get_assignees_display(self, obj):
|
||||
return [str(assignee) for assignee in obj.get_assignees()]
|
||||
|
||||
def get_assignees_read_only(self, obj):
|
||||
if obj.strategy == TicketApprovalStrategy.custom_user:
|
||||
return obj.assignees.values_list('id', flat=True)
|
||||
return []
|
||||
|
||||
def validate(self, attrs):
|
||||
if attrs['strategy'] == TicketApprovalStrategy.custom_user and not attrs.get('assignees'):
|
||||
error = _('Please select the Assignees')
|
||||
raise serializers.ValidationError({'assignees': error})
|
||||
return super().validate(attrs)
|
||||
|
||||
|
||||
class TicketFlowSerializer(OrgResourceModelSerializerMixin):
|
||||
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type display'))
|
||||
rules = TicketFlowApproveSerializer(many=True, required=True)
|
||||
|
||||
class Meta:
|
||||
model = TicketFlow
|
||||
fields_mini = ['id', ]
|
||||
fields_small = fields_mini + [
|
||||
'type', 'type_display', 'approval_level', 'created_by', 'date_created', 'date_updated',
|
||||
'org_id', 'org_name'
|
||||
]
|
||||
fields = fields_small + ['rules', ]
|
||||
read_only_fields = ['created_by', 'org_id', 'date_created', 'date_updated']
|
||||
extra_kwargs = {
|
||||
'type': {'required': True},
|
||||
'approval_level': {'required': True}
|
||||
}
|
||||
|
||||
def validate_type(self, value):
|
||||
if not self.instance or (self.instance and self.instance.type != value):
|
||||
if self.Meta.model.objects.filter(type=value).exists():
|
||||
error = _('The current organization type already exists')
|
||||
raise serializers.ValidationError(error)
|
||||
return value
|
||||
|
||||
def create_or_update(self, action, validated_data, instance=None):
|
||||
related = 'rules'
|
||||
assignees = 'assignees'
|
||||
childs = validated_data.pop(related, [])
|
||||
if not instance:
|
||||
instance = getattr(super(), action)(validated_data)
|
||||
else:
|
||||
instance = getattr(super(), action)(instance, validated_data)
|
||||
getattr(instance, related).all().delete()
|
||||
instance_related = getattr(instance, related)
|
||||
child_instances = []
|
||||
related_model = instance_related.model
|
||||
# Todo: 这个权限的判断
|
||||
for level, data in enumerate(childs, 1):
|
||||
data_m2m = data.pop(assignees, None)
|
||||
child_instance = related_model.objects.create(**data, level=level)
|
||||
getattr(child_instance, assignees).set(data_m2m)
|
||||
child_instances.append(child_instance)
|
||||
instance_related.set(child_instances)
|
||||
return instance
|
||||
|
||||
@atomic
|
||||
def create(self, validated_data):
|
||||
return self.create_or_update('create', validated_data)
|
||||
|
||||
@atomic
|
||||
def update(self, instance, validated_data):
|
||||
if instance.org_id == Organization.ROOT_ID:
|
||||
instance = self.create(validated_data)
|
||||
else:
|
||||
instance = self.create_or_update('update', validated_data, instance)
|
||||
return instance
|
||||
|
|
|
@ -1,19 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save
|
||||
from django.db.models.signals import post_save, m2m_changed
|
||||
|
||||
from common.decorator import on_transaction_commit
|
||||
from common.utils import get_logger
|
||||
from tickets.models import Ticket
|
||||
from ..signals import post_change_ticket_action
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@receiver(post_change_ticket_action, sender=Ticket)
|
||||
def on_post_change_ticket_action(sender, ticket, action, **kwargs):
|
||||
ticket.handler.dispatch(action)
|
||||
@on_transaction_commit
|
||||
def after_save_set_rel_snapshot(sender, instance, update_fields=None, **kwargs):
|
||||
if update_fields and list(update_fields)[0] == 'rel_snapshot':
|
||||
return
|
||||
instance.set_rel_snapshot()
|
||||
|
||||
|
||||
@receiver(post_save, sender=Ticket)
|
||||
def on_pre_save_ensure_serial_num(sender, instance: Ticket, **kwargs):
|
||||
instance.update_serial_num_if_need()
|
||||
@on_transaction_commit
|
||||
def on_m2m_change(sender, action, instance, reverse=False, **kwargs):
|
||||
if action.startswith('post'):
|
||||
instance.set_rel_snapshot()
|
||||
|
||||
|
||||
for ticket_cls in Ticket.__subclasses__():
|
||||
post_save.connect(after_save_set_rel_snapshot, sender=ticket_cls)
|
||||
m2m_changed.connect(on_m2m_change, sender=ticket_cls)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from django.dispatch import Signal
|
||||
|
||||
post_change_ticket_action = Signal()
|
||||
|
||||
active_step = Signal()
|
||||
post_or_update_change_ticket_flow_approval = Signal()
|
||||
|
|
|
@ -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>
|
|
@ -4,7 +4,15 @@
|
|||
{{ title | safe }}
|
||||
</p>
|
||||
<div>
|
||||
{{ body | safe }}
|
||||
{% for child in content %}
|
||||
<h2>{{ child.title }}</h2>
|
||||
<hr>
|
||||
{% for item in child.content %}
|
||||
<li style="list-style-type:none">
|
||||
<b>{{ item.title }}: </b> {{ item.value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<br>
|
||||
<div>
|
||||
|
|
|
@ -10,7 +10,17 @@
|
|||
<div class="ibox-content">
|
||||
<h2 class="font-bold" style="display: inline">{% trans 'Ticket information' %}</h2>
|
||||
<h1></h1>
|
||||
<div style="word-break: break-all">{{ ticket_info | safe }}</div>
|
||||
<div>
|
||||
{% for child in content %}
|
||||
<h2>{{ child.title }}</h2>
|
||||
<hr>
|
||||
{% for item in child.content %}
|
||||
<li style="list-style-type:none">
|
||||
<b>{{ item.title }}: </b> {{ item.value }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
|
|
|
@ -10,6 +10,11 @@ app_name = 'tickets'
|
|||
router = BulkRouter()
|
||||
|
||||
router.register('tickets', api.TicketViewSet, 'ticket')
|
||||
router.register('apply-asset-tickets', api.ApplyAssetTicketViewSet, 'apply-asset-ticket')
|
||||
router.register('apply-app-tickets', api.ApplyApplicationTicketViewSet, 'apply-app-ticket')
|
||||
router.register('apply-login-tickets', api.ApplyLoginTicketViewSet, 'apply-login-ticket')
|
||||
router.register('apply-login-asset-tickets', api.ApplyLoginAssetTicketViewSet, 'apply-login-asset-ticket')
|
||||
router.register('apply-command-tickets', api.ApplyCommandTicketViewSet, 'apply-command-ticket')
|
||||
router.register('flows', api.TicketFlowViewSet, 'flows')
|
||||
router.register('comments', api.CommentViewSet, 'comment')
|
||||
router.register('ticket-session-relation', api.TicketSessionRelationViewSet, 'ticket-session-relation')
|
||||
|
|
|
@ -3,21 +3,22 @@
|
|||
from django.conf import settings
|
||||
|
||||
from common.utils import get_logger
|
||||
from .notifications import TicketAppliedToAssignee, TicketProcessedToApplicant
|
||||
from .notifications import TicketAppliedToAssigneeMessage, TicketProcessedToApplicantMessage
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
def send_ticket_applied_mail_to_assignees(ticket):
|
||||
ticket_assignees = ticket.current_node.first().ticket_assignees.all()
|
||||
if not ticket_assignees:
|
||||
def send_ticket_applied_mail_to_assignees(ticket, assignees):
|
||||
if not assignees:
|
||||
logger.debug(
|
||||
"Not found assignees, ticket: {}({}), assignees: {}".format(ticket, str(ticket.id), ticket_assignees)
|
||||
"Not found assignees, ticket: {}({}), assignees: {}".format(
|
||||
ticket, str(ticket.id), assignees
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
for ticket_assignee in ticket_assignees:
|
||||
instance = TicketAppliedToAssignee(ticket_assignee.assignee, ticket)
|
||||
for user in assignees:
|
||||
instance = TicketAppliedToAssigneeMessage(user, ticket)
|
||||
if settings.DEBUG:
|
||||
logger.debug(instance)
|
||||
instance.publish_async()
|
||||
|
@ -28,7 +29,7 @@ def send_ticket_processed_mail_to_applicant(ticket, processor):
|
|||
logger.error("Not found applicant: {}({})".format(ticket.title, ticket.id))
|
||||
return
|
||||
|
||||
instance = TicketProcessedToApplicant(ticket.applicant, ticket, processor)
|
||||
instance = TicketProcessedToApplicantMessage(ticket.applicant, ticket, processor)
|
||||
if settings.DEBUG:
|
||||
logger.debug(instance)
|
||||
instance.publish_async()
|
||||
|
|
|
@ -50,13 +50,13 @@ class TicketDirectApproveView(TemplateView):
|
|||
def get_context_data(self, **kwargs):
|
||||
# 放入工单信息
|
||||
token = kwargs.get('token')
|
||||
ticket_info = cache.get(token, {}).get('body', '')
|
||||
content = cache.get(token, {}).get('content', [])
|
||||
if self.request.user.is_authenticated:
|
||||
prompt_msg = _('Click the button below to approve or reject')
|
||||
else:
|
||||
prompt_msg = _('After successful authentication, this ticket can be approved directly')
|
||||
kwargs.update({
|
||||
'ticket_info': ticket_info, 'prompt_msg': prompt_msg,
|
||||
'content': content, 'prompt_msg': prompt_msg,
|
||||
'login_url': '%s&next=%s' % (
|
||||
self.login_url,
|
||||
reverse('tickets:direct-approve', kwargs={'token': token})
|
||||
|
|
Loading…
Reference in New Issue