From 1a9a5c28f5f4b40bc3b282c23b6a81c1c1c710bc Mon Sep 17 00:00:00 2001 From: Bai Date: Thu, 31 Dec 2020 05:07:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E5=B7=A5=E5=8D=95?= =?UTF-8?q?=E6=A8=A1=E5=9D=971?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/common/mixins/api.py | 206 +++++++++++++++++- apps/tickets/api/ticket/mixin.py | 95 ++++---- apps/tickets/api/ticket/ticket.py | 4 +- apps/tickets/const.py | 4 - .../migrations/0007_auto_20201224_1821.py | 2 +- apps/tickets/models/ticket/__init__.py | 2 +- .../models/ticket/mixin/meta/__init__.py | 2 +- .../ticket/mixin/meta/{meta.py => base.py} | 0 .../models/ticket/{ticket.py => model.py} | 4 +- apps/tickets/serializers/ticket/ticket.py | 2 +- 10 files changed, 240 insertions(+), 81 deletions(-) rename apps/tickets/models/ticket/mixin/meta/{meta.py => base.py} (100%) rename apps/tickets/models/ticket/{ticket.py => model.py} (97%) diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py index 6754bf178..3b0b4b753 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -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: diff --git a/apps/tickets/api/ticket/mixin.py b/apps/tickets/api/ticket/mixin.py index 344a9d611..0a3598ccf 100644 --- a/apps/tickets/api/ticket/mixin.py +++ b/apps/tickets/api/ticket/mixin.py @@ -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 diff --git a/apps/tickets/api/ticket/ticket.py b/apps/tickets/api/ticket/ticket.py index 3b2c8a0cd..daa24a364 100644 --- a/apps/tickets/api/ticket/ticket.py +++ b/apps/tickets/api/ticket/ticket.py @@ -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 = { diff --git a/apps/tickets/const.py b/apps/tickets/const.py index 8bde7fc0d..742d4e7d3 100644 --- a/apps/tickets/const.py +++ b/apps/tickets/const.py @@ -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') diff --git a/apps/tickets/migrations/0007_auto_20201224_1821.py b/apps/tickets/migrations/0007_auto_20201224_1821.py index 7b19fcb58..53d3a0dc1 100644 --- a/apps/tickets/migrations/0007_auto_20201224_1821.py +++ b/apps/tickets/migrations/0007_auto_20201224_1821.py @@ -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', diff --git a/apps/tickets/models/ticket/__init__.py b/apps/tickets/models/ticket/__init__.py index 4a3a5e8c7..87cb7367b 100644 --- a/apps/tickets/models/ticket/__init__.py +++ b/apps/tickets/models/ticket/__init__.py @@ -1 +1 @@ -from .ticket import * +from .model import * diff --git a/apps/tickets/models/ticket/mixin/meta/__init__.py b/apps/tickets/models/ticket/mixin/meta/__init__.py index 7b5fbad28..9b5ed21c9 100644 --- a/apps/tickets/models/ticket/mixin/meta/__init__.py +++ b/apps/tickets/models/ticket/mixin/meta/__init__.py @@ -1 +1 @@ -from .meta import * +from .base import * diff --git a/apps/tickets/models/ticket/mixin/meta/meta.py b/apps/tickets/models/ticket/mixin/meta/base.py similarity index 100% rename from apps/tickets/models/ticket/mixin/meta/meta.py rename to apps/tickets/models/ticket/mixin/meta/base.py diff --git a/apps/tickets/models/ticket/ticket.py b/apps/tickets/models/ticket/model.py similarity index 97% rename from apps/tickets/models/ticket/ticket.py rename to apps/tickets/models/ticket/model.py index 6ef695589..1f5b0c5d9 100644 --- a/apps/tickets/models/ticket/ticket.py +++ b/apps/tickets/models/ticket/model.py @@ -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") diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py index 12591a485..d430919b1 100644 --- a/apps/tickets/serializers/ticket/ticket.py +++ b/apps/tickets/serializers/ticket/ticket.py @@ -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):