mirror of https://github.com/jumpserver/jumpserver
				
				
				
			feat: 优化工单模块1
							parent
							
								
									430e20a49c
								
							
						
					
					
						commit
						1a9a5c28f5
					
				| 
						 | 
				
			
			@ -7,10 +7,13 @@ from collections import defaultdict
 | 
			
		|||
from itertools import chain
 | 
			
		||||
 | 
			
		||||
from django.db.models.signals import m2m_changed
 | 
			
		||||
from django.utils.translation import ugettext_lazy as _
 | 
			
		||||
from django.core.cache import cache
 | 
			
		||||
from django.http import JsonResponse
 | 
			
		||||
from rest_framework import serializers
 | 
			
		||||
from rest_framework.response import Response
 | 
			
		||||
from rest_framework.settings import api_settings
 | 
			
		||||
from common.exceptions import JMSException
 | 
			
		||||
 | 
			
		||||
from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter
 | 
			
		||||
from ..utils import lazyproperty
 | 
			
		||||
| 
						 | 
				
			
			@ -28,19 +31,200 @@ class JSONResponseMixin(object):
 | 
			
		|||
        return JsonResponse(context)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SerializerMixin:
 | 
			
		||||
    def get_serializer_class(self):
 | 
			
		||||
class GenericSerializerMixin:
 | 
			
		||||
    serializer_classes = None
 | 
			
		||||
 | 
			
		||||
    def get_serializer_class_by_view_action(self):
 | 
			
		||||
        if not hasattr(self, 'serializer_classes'):
 | 
			
		||||
            return None
 | 
			
		||||
        if not isinstance(self.serializer_classes, dict):
 | 
			
		||||
            return None
 | 
			
		||||
        draw = self.request.query_params.get('draw')
 | 
			
		||||
        serializer_class = None
 | 
			
		||||
        if hasattr(self, 'serializer_classes') and isinstance(self.serializer_classes, dict):
 | 
			
		||||
            if self.action in ['list', 'metadata'] and self.request.query_params.get('draw'):
 | 
			
		||||
                serializer_class = self.serializer_classes.get('display')
 | 
			
		||||
            if serializer_class is None:
 | 
			
		||||
                serializer_class = self.serializer_classes.get(
 | 
			
		||||
                    self.action, self.serializer_classes.get('default')
 | 
			
		||||
                )
 | 
			
		||||
        if serializer_class:
 | 
			
		||||
        if draw and self.action in ['list', 'metadata']:
 | 
			
		||||
            serializer_class = self.serializer_classes.get('display')
 | 
			
		||||
        if serializer_class is None:
 | 
			
		||||
            serializer_class = self.serializer_classes.get(self.action)
 | 
			
		||||
        if serializer_class is None:
 | 
			
		||||
            serializer_class = self.serializer_classes.get('default')
 | 
			
		||||
        return serializer_class
 | 
			
		||||
 | 
			
		||||
    def get_serializer_class(self):
 | 
			
		||||
        serializer_class = self.get_serializer_class_by_view_action()
 | 
			
		||||
        if serializer_class is None:
 | 
			
		||||
            serializer_class = super().get_serializer_class()
 | 
			
		||||
        return serializer_class
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class JSONFieldsSerializerMixin:
 | 
			
		||||
    """
 | 
			
		||||
    作用: 获取包含 JSONField 字段的序列类
 | 
			
		||||
 | 
			
		||||
    class TestSerializer(serializers.Serializer):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    json_fields_category_mapping = {
 | 
			
		||||
        'json_field_1': {
 | 
			
		||||
            'type': ('apply_asset', 'apply_application', 'login_confirm', ),
 | 
			
		||||
        },
 | 
			
		||||
        'json_field_2': {
 | 
			
		||||
            'type': ('chrome', 'mysql', 'oracle', 'k8s', ),
 | 
			
		||||
            'category': ('remote_app', 'db', 'cloud', ),
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
    json_fields_serializer_classes = {
 | 
			
		||||
        'json_field_1': {
 | 
			
		||||
            'type': {
 | 
			
		||||
                'apply_asset': {
 | 
			
		||||
                    'get': {
 | 
			
		||||
                        'class': TestSerializer,
 | 
			
		||||
                        'attrs': {'required': True},
 | 
			
		||||
                    },
 | 
			
		||||
                    'post': {
 | 
			
		||||
                        'class': TestSerializer,
 | 
			
		||||
                        'attrs': {'required': True}
 | 
			
		||||
                    },
 | 
			
		||||
                    'open': {
 | 
			
		||||
                        'class': TestSerializer,
 | 
			
		||||
                        'attrs': {'required': False}
 | 
			
		||||
                    },
 | 
			
		||||
                    'approve': {
 | 
			
		||||
                        'class': TestSerializer,
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
                'apply_application': {
 | 
			
		||||
                    'get': {},
 | 
			
		||||
                    'post': {},
 | 
			
		||||
                    'put': {},
 | 
			
		||||
                },
 | 
			
		||||
                'login_confirm': {
 | 
			
		||||
                    'get': {},
 | 
			
		||||
                    'post': {},
 | 
			
		||||
                    'put': {},
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            'category': {}
 | 
			
		||||
        },
 | 
			
		||||
        'json_field_2': {},
 | 
			
		||||
        'json_field_3': {}
 | 
			
		||||
    }
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    json_fields_category_mapping = {}
 | 
			
		||||
    json_fields_serializer_classes = None
 | 
			
		||||
 | 
			
		||||
    @lazyproperty
 | 
			
		||||
    def default_json_field_serializer(self):
 | 
			
		||||
        class DefaultJSONFieldSerializer(serializers.JSONField):
 | 
			
		||||
            pass
 | 
			
		||||
        if self.action in ['get', 'list', 'retrieve']:
 | 
			
		||||
            attrs = {'readonly': False}
 | 
			
		||||
        else:
 | 
			
		||||
            attrs = {'readonly': True}
 | 
			
		||||
        return DefaultJSONFieldSerializer(attrs)
 | 
			
		||||
 | 
			
		||||
    def get_json_field_query_category(self, field, category):
 | 
			
		||||
        query_category = self.request.query_params.get(category)
 | 
			
		||||
        category_choices = self.json_fields_category_mapping[field][category]
 | 
			
		||||
        if query_category and query_category not in category_choices:
 | 
			
		||||
            error = _(
 | 
			
		||||
                'Please bring the query parameter `{}`, '
 | 
			
		||||
                'the value is selected from the following options: {}'
 | 
			
		||||
                ''.format(query_category, category_choices)
 | 
			
		||||
            )
 | 
			
		||||
            raise JMSException({'query_params_error': error})
 | 
			
		||||
        return query_category
 | 
			
		||||
 | 
			
		||||
    def get_json_field_serializer_classes_by_query_category(self, field):
 | 
			
		||||
        serializer_classes = None
 | 
			
		||||
        category_collection = self.json_fields_category_mapping[field]
 | 
			
		||||
        for category in category_collection:
 | 
			
		||||
            query_category = self.get_json_field_query_category(field, category)
 | 
			
		||||
            if not query_category:
 | 
			
		||||
                continue
 | 
			
		||||
            category_serializer_classes = self.json_fields_serializer_classes[field][category]
 | 
			
		||||
            if query_category not in category_serializer_classes.keys():
 | 
			
		||||
                continue
 | 
			
		||||
            serializer_classes = category_serializer_classes[query_category]
 | 
			
		||||
 | 
			
		||||
        return serializer_classes
 | 
			
		||||
 | 
			
		||||
    def get_json_field_serializer_info_by_view_action(self, serializer_classes):
 | 
			
		||||
        if self.action in ['metadata']:
 | 
			
		||||
            action = self.request.query_params.get('action')
 | 
			
		||||
            if not action:
 | 
			
		||||
                raise JMSException('The `metadata` methods must carry query parameter `action`')
 | 
			
		||||
        else:
 | 
			
		||||
            action = self.action
 | 
			
		||||
        serializer_info = serializer_classes.get(action)
 | 
			
		||||
        return serializer_info
 | 
			
		||||
 | 
			
		||||
    def get_json_field_serializer_info(self, field):
 | 
			
		||||
        category_collection = self.json_fields_category_mapping[field]
 | 
			
		||||
        if category_collection:
 | 
			
		||||
            serializer_classes = self.get_json_field_serializer_classes_by_query_category(field)
 | 
			
		||||
        else:
 | 
			
		||||
            serializer_classes = self.json_fields_serializer_classes[field]
 | 
			
		||||
 | 
			
		||||
        if not serializer_classes:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        serializer_info = self.get_json_field_serializer_info_by_view_action(serializer_classes)
 | 
			
		||||
        return serializer_info
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def new_json_field_serializer(class_info):
 | 
			
		||||
        serializer_class = class_info['class']
 | 
			
		||||
        serializer_attrs = class_info.get('attrs', {})
 | 
			
		||||
        serializer = serializer_class(serializer_attrs)
 | 
			
		||||
        return serializer
 | 
			
		||||
 | 
			
		||||
    def get_json_field_serializer(self, field):
 | 
			
		||||
        serializer_info = self.get_json_field_serializer_info(field)
 | 
			
		||||
        if not serializer_info:
 | 
			
		||||
            return None
 | 
			
		||||
        serializer = self.new_json_field_serializer(serializer_info)
 | 
			
		||||
        return serializer
 | 
			
		||||
 | 
			
		||||
    def get_json_fields_serializer_mapping(self):
 | 
			
		||||
        """
 | 
			
		||||
        return: {
 | 
			
		||||
            'json_field_1': serializer1(),
 | 
			
		||||
            'json_field_2': serializer2(),
 | 
			
		||||
        }
 | 
			
		||||
        """
 | 
			
		||||
        fields_serializer_mapping = {}
 | 
			
		||||
        fields = self.json_fields_serializer_classes.keys()
 | 
			
		||||
        for field in fields:
 | 
			
		||||
            serializer = self.get_json_field_serializer(field)
 | 
			
		||||
            if serializer is None:
 | 
			
		||||
                serializer = self.default_json_field_serializer
 | 
			
		||||
            fields_serializer_mapping[field] = serializer
 | 
			
		||||
        return fields_serializer_mapping
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def new_include_json_fields_serializer_class(base, attrs):
 | 
			
		||||
        serializer_class_name = ''.join([
 | 
			
		||||
            field_serializer.__class__.__name__ for field_serializer in attrs.values()
 | 
			
		||||
        ])
 | 
			
		||||
        serializer_class = type(serializer_class_name, (base,), attrs)
 | 
			
		||||
        return serializer_class
 | 
			
		||||
 | 
			
		||||
    def get_serializer_class(self):
 | 
			
		||||
        serializer_class = super().get_serializer_class()
 | 
			
		||||
        if not isinstance(self.json_fields_serializer_classes, dict):
 | 
			
		||||
            return serializer_class
 | 
			
		||||
        return super().get_serializer_class()
 | 
			
		||||
        fields_serializer_mapping = self.get_json_fields_serializer_mapping()
 | 
			
		||||
        if not fields_serializer_mapping:
 | 
			
		||||
            return serializer_class
 | 
			
		||||
        serializer_class = self.new_include_json_fields_serializer_class(
 | 
			
		||||
            base=serializer_class, attrs=fields_serializer_mapping
 | 
			
		||||
        )
 | 
			
		||||
        return serializer_class
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SerializerMixin(JSONFieldsSerializerMixin, GenericSerializerMixin):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ExtraFilterFieldsMixin:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,65 +2,44 @@ from common.exceptions import JMSException
 | 
			
		|||
from tickets import const, serializers
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['TicketMetaSerializerViewMixin']
 | 
			
		||||
__all__ = ['TicketJSONFieldsSerializerViewMixin']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TicketMetaSerializerViewMixin:
 | 
			
		||||
    apply_asset_meta_serializer_classes = {
 | 
			
		||||
        'open': serializers.TicketMetaApplyAssetApplySerializer,
 | 
			
		||||
        'approve': serializers.TicketMetaApplyAssetApproveSerializer,
 | 
			
		||||
class TicketJSONFieldsSerializerViewMixin:
 | 
			
		||||
    json_fields_category_mapping = {
 | 
			
		||||
        'meta': {
 | 
			
		||||
            'type': const.TicketTypeChoices.values,
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
    apply_application_meta_serializer_classes = {
 | 
			
		||||
        'open': serializers.TicketMetaApplyApplicationApplySerializer,
 | 
			
		||||
        'approve': serializers.TicketMetaApplyApplicationApproveSerializer,
 | 
			
		||||
    json_fields_serializer_classes = {
 | 
			
		||||
        'meta': {
 | 
			
		||||
            'type': {
 | 
			
		||||
                const.TicketTypeChoices.apply_asset.value: {
 | 
			
		||||
                    'open': {
 | 
			
		||||
                        'class': serializers.TicketMetaApplyAssetApplySerializer,
 | 
			
		||||
                        'attrs': {'required': True}
 | 
			
		||||
                    },
 | 
			
		||||
                    'approve': {
 | 
			
		||||
                        'class': serializers.TicketMetaApplyAssetApproveSerializer,
 | 
			
		||||
                        'attrs': {'required': True}
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                const.TicketTypeChoices.apply_application.value: {
 | 
			
		||||
                    'open': {
 | 
			
		||||
                        'class': serializers.TicketMetaApplyApplicationApplySerializer,
 | 
			
		||||
                        'attrs': {'required': True}
 | 
			
		||||
                    },
 | 
			
		||||
                    'approve': {
 | 
			
		||||
                        'class': serializers.TicketMetaApplyApplicationApproveSerializer,
 | 
			
		||||
                        'attrs': {'required': True}
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                const.TicketTypeChoices.login_confirm.value: {
 | 
			
		||||
                    'open': {
 | 
			
		||||
                        'class': serializers.TicketMetaLoginConfirmApplySerializer,
 | 
			
		||||
                        'attrs': {'required': True}
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    login_confirm_meta_serializer_classes = {
 | 
			
		||||
        'open': serializers.TicketMetaLoginConfirmApplySerializer,
 | 
			
		||||
    }
 | 
			
		||||
    meta_serializer_classes = {
 | 
			
		||||
        const.TicketTypeChoices.apply_asset.value: apply_asset_meta_serializer_classes,
 | 
			
		||||
        const.TicketTypeChoices.apply_application.value: apply_application_meta_serializer_classes,
 | 
			
		||||
        const.TicketTypeChoices.login_confirm.value: login_confirm_meta_serializer_classes,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    def get_serializer_meta_field_class(self):
 | 
			
		||||
        tp = self.request.query_params.get('type')
 | 
			
		||||
        if not tp:
 | 
			
		||||
            return None
 | 
			
		||||
        tp_choices = const.TicketTypeChoices.types()
 | 
			
		||||
        if tp not in tp_choices:
 | 
			
		||||
            raise JMSException(
 | 
			
		||||
                'Invalid query parameter `type`, select from the following options: {}'
 | 
			
		||||
                ''.format(tp_choices)
 | 
			
		||||
            )
 | 
			
		||||
        meta_class = self.meta_serializer_classes.get(tp, {}).get(self.action)
 | 
			
		||||
        return meta_class
 | 
			
		||||
 | 
			
		||||
    def get_serializer_meta_field(self):
 | 
			
		||||
        if self.action not in ['open', 'approve']:
 | 
			
		||||
            return None
 | 
			
		||||
        meta_class = self.get_serializer_meta_field_class()
 | 
			
		||||
        if not meta_class:
 | 
			
		||||
            return None
 | 
			
		||||
        return meta_class(required=True)
 | 
			
		||||
 | 
			
		||||
    def reset_view_metadata_action(self):
 | 
			
		||||
        if self.action not in ['metadata']:
 | 
			
		||||
            return
 | 
			
		||||
        view_action = self.request.query_params.get('action')
 | 
			
		||||
        if not view_action:
 | 
			
		||||
            raise JMSException('The `metadata` methods must carry parameter `action`')
 | 
			
		||||
        setattr(self, 'action', view_action)
 | 
			
		||||
 | 
			
		||||
    def get_serializer_class(self):
 | 
			
		||||
        self.reset_view_metadata_action()
 | 
			
		||||
        serializer_class = super().get_serializer_class()
 | 
			
		||||
        if getattr(self, 'swagger_fake_view', False):
 | 
			
		||||
            return serializer_class
 | 
			
		||||
        meta_field = self.get_serializer_meta_field()
 | 
			
		||||
        if not meta_field:
 | 
			
		||||
            return serializer_class
 | 
			
		||||
        serializer_class = type(
 | 
			
		||||
            meta_field.__class__.__name__, (serializer_class,), {'meta': meta_field}
 | 
			
		||||
        )
 | 
			
		||||
        return serializer_class
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,13 +11,13 @@ from common.const.http import POST, PUT
 | 
			
		|||
from tickets import serializers
 | 
			
		||||
from tickets.permissions.ticket import IsAssignee, NotClosed
 | 
			
		||||
from tickets.models import Ticket
 | 
			
		||||
from tickets.api.ticket.mixin import TicketMetaSerializerViewMixin
 | 
			
		||||
from tickets.api.ticket.mixin import TicketJSONFieldsSerializerViewMixin
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
__all__ = ['TicketViewSet']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TicketViewSet(TicketMetaSerializerViewMixin, CommonApiMixin, viewsets.ModelViewSet):
 | 
			
		||||
class TicketViewSet(TicketJSONFieldsSerializerViewMixin, CommonApiMixin, viewsets.ModelViewSet):
 | 
			
		||||
    permission_classes = (IsValidUser,)
 | 
			
		||||
    serializer_class = serializers.TicketSerializer
 | 
			
		||||
    serializer_classes = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,10 +10,6 @@ class TicketTypeChoices(TextChoices):
 | 
			
		|||
    apply_asset = 'apply_asset', _('Apply for asset')
 | 
			
		||||
    apply_application = 'apply_application', _('Apply for application')
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def types(cls):
 | 
			
		||||
        return set(dict(cls.choices).keys())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TicketActionChoices(TextChoices):
 | 
			
		||||
    open = 'open', _('Open')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -113,7 +113,7 @@ class Migration(migrations.Migration):
 | 
			
		|||
        migrations.AlterField(
 | 
			
		||||
            model_name='ticket',
 | 
			
		||||
            name='meta',
 | 
			
		||||
            field=models.JSONField(encoder=tickets.models.ticket.ModelJSONFieldEncoder, verbose_name='Meta'),
 | 
			
		||||
            field=models.JSONField(default=dict, encoder=tickets.models.ticket.model.ModelJSONFieldEncoder, verbose_name='Meta'),
 | 
			
		||||
        ),
 | 
			
		||||
        migrations.AlterField(
 | 
			
		||||
            model_name='ticket',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
from .ticket import *
 | 
			
		||||
from .model import *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1 +1 @@
 | 
			
		|||
from .meta import *
 | 
			
		||||
from .base import *
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,7 +14,7 @@ from orgs.utils import tmp_to_root_org, tmp_to_org
 | 
			
		|||
from tickets import const
 | 
			
		||||
from .mixin import TicketModelMixin
 | 
			
		||||
 | 
			
		||||
__all__ = ['Ticket']
 | 
			
		||||
__all__ = ['Ticket', 'ModelJSONFieldEncoder']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ModelJSONFieldEncoder(json.JSONEncoder):
 | 
			
		||||
| 
						 | 
				
			
			@ -36,7 +36,7 @@ class Ticket(TicketModelMixin, CommonModelMixin, OrgModelMixin):
 | 
			
		|||
        max_length=64, choices=const.TicketTypeChoices.choices,
 | 
			
		||||
        default=const.TicketTypeChoices.general.value, verbose_name=_("Type")
 | 
			
		||||
    )
 | 
			
		||||
    meta = models.JSONField(encoder=ModelJSONFieldEncoder, verbose_name=_("Meta"))
 | 
			
		||||
    meta = models.JSONField(encoder=ModelJSONFieldEncoder, default=dict, verbose_name=_("Meta"))
 | 
			
		||||
    action = models.CharField(
 | 
			
		||||
        choices=const.TicketActionChoices.choices, max_length=16,
 | 
			
		||||
        default=const.TicketActionChoices.open.value, verbose_name=_("Action")
 | 
			
		||||
| 
						 | 
				
			
			@ -60,7 +60,7 @@ class TicketApplySerializer(TicketActionSerializer):
 | 
			
		|||
        ]
 | 
			
		||||
        read_only_fields = list(set(TicketDisplaySerializer.Meta.fields) - set(required_fields))
 | 
			
		||||
        extra_kwargs = {
 | 
			
		||||
            'type': {'required': True}
 | 
			
		||||
            'type': {'required': True},
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    def validate_type(self, tp):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue