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

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

* feat: 工单权限处理

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

* perf: 修改一点

Co-authored-by: feng626 <1304903146@qq.com>
Co-authored-by: ibuler <ibuler@qq.com>
This commit is contained in:
fit2bot
2021-08-25 19:02:50 +08:00
committed by GitHub
parent 1fdc558ef7
commit 0f87f05b3f
30 changed files with 897 additions and 590 deletions

View File

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

View File

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

View File

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

View File

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

View File

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