mirror of https://github.com/jumpserver/jumpserver
				
				
				
			perf: Approval process role selection supports multiple strategies
							parent
							
								
									920cfdac5c
								
							
						
					
					
						commit
						41b2ce06a8
					
				| 
						 | 
				
			
			@ -50,13 +50,6 @@ class TicketLevel(IntegerChoices):
 | 
			
		|||
    two = 2, _("Two level")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TicketApprovalStrategy(TextChoices):
 | 
			
		||||
    org_admin = 'org_admin', _("Org admin")
 | 
			
		||||
    custom_user = 'custom_user', _("Custom user")
 | 
			
		||||
    super_admin = 'super_admin', _("Super admin")
 | 
			
		||||
    super_org_admin = 'super_org_admin', _("Super admin and org admin")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TicketApplyAssetScope(TextChoices):
 | 
			
		||||
    all = 'all', _("All assets")
 | 
			
		||||
    permed = 'permed', _("Permed assets")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,74 @@
 | 
			
		|||
# Generated by Django 4.1.13 on 2024-07-26 06:08
 | 
			
		||||
 | 
			
		||||
from django.db import migrations
 | 
			
		||||
 | 
			
		||||
import common.db.fields
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_user_attrs(name, match, value):
 | 
			
		||||
    return {
 | 
			
		||||
        "type": "attrs",
 | 
			
		||||
        "attrs": [
 | 
			
		||||
            {
 | 
			
		||||
                "name": name,
 | 
			
		||||
                "match": match,
 | 
			
		||||
                "value": value
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def migrate_assignees_to_users(apps, schema_editor):
 | 
			
		||||
    rule_model = apps.get_model('tickets', 'ApprovalRule')
 | 
			
		||||
    rules = rule_model.objects.all()
 | 
			
		||||
    objs = []
 | 
			
		||||
 | 
			
		||||
    for rule in rules:
 | 
			
		||||
        strategy = rule.strategy
 | 
			
		||||
        if strategy == 'super_admin':
 | 
			
		||||
            rule.users = generate_user_attrs(
 | 
			
		||||
                "system_roles", "m2m", ["00000000-0000-0000-0000-000000000001"]
 | 
			
		||||
            )
 | 
			
		||||
        elif strategy == 'org_admin':
 | 
			
		||||
            rule.users = generate_user_attrs(
 | 
			
		||||
                "org_roles", "m2m", ["00000000-0000-0000-0000-000000000005"]
 | 
			
		||||
            )
 | 
			
		||||
        elif strategy == 'super_org_admin':
 | 
			
		||||
            rule.users = {
 | 
			
		||||
                "type": "attrs",
 | 
			
		||||
                "attrs": [
 | 
			
		||||
                    {"name": "org_roles", "match": "m2m", "value": ["00000000-0000-0000-0000-000000000005"]},
 | 
			
		||||
                    {"name": "system_roles", "match": "m2m", "value": ["00000000-0000-0000-0000-000000000001"]}
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
        elif strategy == 'custom_user':
 | 
			
		||||
            user_ids = [str(user_id) for user_id in rule.assignees.values_list('id', flat=True)]
 | 
			
		||||
            rule.users = {"type": "ids", "ids": user_ids}
 | 
			
		||||
        else:
 | 
			
		||||
            continue
 | 
			
		||||
        objs.append(rule)
 | 
			
		||||
 | 
			
		||||
    rule_model.objects.bulk_update(objs, ['users'])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Migration(migrations.Migration):
 | 
			
		||||
    dependencies = [
 | 
			
		||||
        ('tickets', '0003_initial_ticket_flow_data'),
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    operations = [
 | 
			
		||||
        migrations.AddField(
 | 
			
		||||
            model_name='approvalrule',
 | 
			
		||||
            name='users',
 | 
			
		||||
            field=common.db.fields.JSONManyToManyField(default=dict, to='users.User', verbose_name='Users'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RunPython(migrate_assignees_to_users),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='approvalrule',
 | 
			
		||||
            name='assignees',
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.RemoveField(
 | 
			
		||||
            model_name='approvalrule',
 | 
			
		||||
            name='strategy',
 | 
			
		||||
        ),
 | 
			
		||||
    ]
 | 
			
		||||
| 
						 | 
				
			
			@ -3,31 +3,24 @@
 | 
			
		|||
from django.db import models
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
 | 
			
		||||
from common.db.fields import JSONManyToManyField, RelatedManager
 | 
			
		||||
from common.db.models import JMSBaseModel
 | 
			
		||||
from orgs.mixins.models import OrgModelMixin
 | 
			
		||||
from orgs.models import Organization
 | 
			
		||||
from orgs.utils import tmp_to_org, get_current_org_id
 | 
			
		||||
from orgs.utils import tmp_to_org, current_org
 | 
			
		||||
from users.models import User
 | 
			
		||||
from ..const import TicketType, TicketLevel, TicketApprovalStrategy
 | 
			
		||||
from ..const import TicketType, TicketLevel
 | 
			
		||||
 | 
			
		||||
__all__ = ['TicketFlow', 'ApprovalRule']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ApprovalRule(JMSBaseModel):
 | 
			
		||||
    level = models.SmallIntegerField(
 | 
			
		||||
        default=TicketLevel.one, choices=TicketLevel.choices,
 | 
			
		||||
        default=TicketLevel.one,
 | 
			
		||||
        choices=TicketLevel.choices,
 | 
			
		||||
        verbose_name=_('Approve level')
 | 
			
		||||
    )
 | 
			
		||||
    strategy = models.CharField(
 | 
			
		||||
        max_length=64, default=TicketApprovalStrategy.super_admin,
 | 
			
		||||
        choices=TicketApprovalStrategy.choices,
 | 
			
		||||
        verbose_name=_('Approve strategy')
 | 
			
		||||
    )
 | 
			
		||||
    # 受理人列表
 | 
			
		||||
    assignees = models.ManyToManyField(
 | 
			
		||||
        'users.User', related_name='assigned_ticket_flow_approval_rule',
 | 
			
		||||
        verbose_name=_("Assignees")
 | 
			
		||||
    )
 | 
			
		||||
    users = JSONManyToManyField('users.User', default=dict, verbose_name=_('Users'))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        verbose_name = _('Ticket flow approval rule')
 | 
			
		||||
| 
						 | 
				
			
			@ -36,17 +29,10 @@ class ApprovalRule(JMSBaseModel):
 | 
			
		|||
        return '{}({})'.format(self.id, self.level)
 | 
			
		||||
 | 
			
		||||
    def get_assignees(self, org_id=None):
 | 
			
		||||
        assignees = []
 | 
			
		||||
        org_id = org_id if org_id else get_current_org_id()
 | 
			
		||||
        with tmp_to_org(org_id):
 | 
			
		||||
            if self.strategy == TicketApprovalStrategy.super_admin:
 | 
			
		||||
                assignees = User.get_super_admins()
 | 
			
		||||
            elif self.strategy == TicketApprovalStrategy.org_admin:
 | 
			
		||||
                assignees = User.get_org_admins()
 | 
			
		||||
            elif self.strategy == TicketApprovalStrategy.super_org_admin:
 | 
			
		||||
                assignees = User.get_super_and_org_admins()
 | 
			
		||||
            elif self.strategy == TicketApprovalStrategy.custom_user:
 | 
			
		||||
                assignees = self.assignees.all()
 | 
			
		||||
        org = Organization.get_instance(org_id, default=current_org)
 | 
			
		||||
        user_qs = User.get_org_users(org=org)
 | 
			
		||||
        query = RelatedManager.get_to_filter_qs(self.users.value, user_qs.model)
 | 
			
		||||
        assignees = user_qs.filter(*query).distinct()
 | 
			
		||||
        return assignees
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,51 +1,23 @@
 | 
			
		|||
from django.db.transaction import atomic
 | 
			
		||||
from django.utils.translation import gettext_lazy as _
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
 | 
			
		||||
from common.serializers.fields import LabeledChoiceField
 | 
			
		||||
from common.serializers.fields import LabeledChoiceField, JSONManyToManyField
 | 
			
		||||
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
 | 
			
		||||
from orgs.models import Organization
 | 
			
		||||
from orgs.utils import get_current_org_id
 | 
			
		||||
from tickets.const import TicketApprovalStrategy, TicketType
 | 
			
		||||
from tickets.const import TicketType
 | 
			
		||||
from tickets.models import TicketFlow, ApprovalRule
 | 
			
		||||
 | 
			
		||||
__all__ = ['TicketFlowSerializer']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TicketFlowApproveSerializer(serializers.ModelSerializer):
 | 
			
		||||
    strategy = LabeledChoiceField(
 | 
			
		||||
        choices=TicketApprovalStrategy.choices, required=True, label=_('Approve strategy')
 | 
			
		||||
    )
 | 
			
		||||
    assignees_read_only = serializers.SerializerMethodField(label=_('Assignees'))
 | 
			
		||||
    assignees_display = serializers.SerializerMethodField(label=_('Assignees display'))
 | 
			
		||||
    users = JSONManyToManyField(label=_('User'))
 | 
			
		||||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = ApprovalRule
 | 
			
		||||
        fields_small = [
 | 
			
		||||
            'level', 'strategy', 'assignees_read_only', 'assignees_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)
 | 
			
		||||
        fields = ['level', 'users']
 | 
			
		||||
        read_only_fields = ['level']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TicketFlowSerializer(OrgResourceModelSerializerMixin):
 | 
			
		||||
| 
						 | 
				
			
			@ -56,15 +28,14 @@ class TicketFlowSerializer(OrgResourceModelSerializerMixin):
 | 
			
		|||
 | 
			
		||||
    class Meta:
 | 
			
		||||
        model = TicketFlow
 | 
			
		||||
        fields_mini = ['id', ]
 | 
			
		||||
        fields_mini = ['id', 'type']
 | 
			
		||||
        fields_small = fields_mini + [
 | 
			
		||||
            'type', 'approval_level', 'created_by', 'date_created', 'date_updated',
 | 
			
		||||
            'org_id', 'org_name'
 | 
			
		||||
            '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']
 | 
			
		||||
        fields = fields_small + ['rules']
 | 
			
		||||
        read_only_fields = ['created_by', 'date_created', 'date_updated']
 | 
			
		||||
        extra_kwargs = {
 | 
			
		||||
            'type': {'required': True},
 | 
			
		||||
            'approval_level': {'required': True}
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -76,31 +47,23 @@ class TicketFlowSerializer(OrgResourceModelSerializerMixin):
 | 
			
		|||
        return value
 | 
			
		||||
 | 
			
		||||
    def create_or_update(self, action, validated_data, instance=None):
 | 
			
		||||
        related = 'rules'
 | 
			
		||||
        assignees = 'assignees'
 | 
			
		||||
        childs = validated_data.pop(related, [])
 | 
			
		||||
        if not instance:
 | 
			
		||||
        children = validated_data.pop('rules', [])
 | 
			
		||||
        if instance is None:
 | 
			
		||||
            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)
 | 
			
		||||
            instance.rules.all().delete()
 | 
			
		||||
 | 
			
		||||
        child_instances = [
 | 
			
		||||
            instance.rules.model.objects.create(**data, level=level)
 | 
			
		||||
            for level, data in enumerate(children, 1)
 | 
			
		||||
        ]
 | 
			
		||||
        instance.rules.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):
 | 
			
		||||
        current_org_id = get_current_org_id()
 | 
			
		||||
        root_org_id = Organization.ROOT_ID
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue