diff --git a/apps/applications/api/application.py b/apps/applications/api/application.py index bf32cf679..bc37fbc35 100644 --- a/apps/applications/api/application.py +++ b/apps/applications/api/application.py @@ -5,13 +5,12 @@ from orgs.mixins.api import OrgBulkModelViewSet from ..hands import IsOrgAdminOrAppUser from .. import models, serializers -from .mixin import ApplicationViewMixin __all__ = ['ApplicationViewSet'] -class ApplicationViewSet(ApplicationViewMixin, OrgBulkModelViewSet): +class ApplicationViewSet(OrgBulkModelViewSet): model = models.Application filter_fields = ('name', 'type', 'category') search_fields = filter_fields diff --git a/apps/applications/api/mixin.py b/apps/applications/api/mixin.py index a07761a22..dd8bc64c2 100644 --- a/apps/applications/api/mixin.py +++ b/apps/applications/api/mixin.py @@ -1,16 +1,7 @@ from orgs.models import Organization -from ..serializers.utils import get_dynamic_mapping_fields_mapping_rule_by_view -__all__ = ['ApplicationViewMixin', 'SerializeApplicationToTreeNodeMixin'] - - -class ApplicationViewMixin: - """ 实现 `get_dynamic_mapping_fields_mapping_rule` 方法, 供其他和 Application 相关的 View 继承使用""" - - def get_dynamic_mapping_fields_mapping_rule(self): - fields_mapping_rule = get_dynamic_mapping_fields_mapping_rule_by_view(view=self) - return fields_mapping_rule +__all__ = ['SerializeApplicationToTreeNodeMixin'] class SerializeApplicationToTreeNodeMixin: diff --git a/apps/applications/serializers/application.py b/apps/applications/serializers/application.py index 644e3c791..fd45a6eea 100644 --- a/apps/applications/serializers/application.py +++ b/apps/applications/serializers/application.py @@ -4,20 +4,37 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from common.drf.fields import DynamicMappingField -from .attrs import get_attrs_field_dynamic_mapping_rules +from common.drf.serializers import DynamicMappingSerializer +from .attrs import attrs_field_dynamic_mapping_serializers from .. import models __all__ = [ 'ApplicationSerializer', + 'IncludeDynamicMappingSerializerFieldApplicationSerializerMixin', ] -class ApplicationSerializer(BulkOrgResourceModelSerializer): +class IncludeDynamicMappingSerializerFieldApplicationSerializerMixin(serializers.Serializer): + attrs = DynamicMappingSerializer(mapping_serializers=attrs_field_dynamic_mapping_serializers) + + def get_attrs_mapping_path(self, mapping_serializers): + request = self.context['request'] + query_type = request.query_params.get('type') + query_category = request.query_params.get('category') + if query_type: + mapping_path = ['type', query_type] + elif query_category: + mapping_path = ['category', query_category] + else: + mapping_path = ['default'] + return mapping_path + + +class ApplicationSerializer(IncludeDynamicMappingSerializerFieldApplicationSerializerMixin, + BulkOrgResourceModelSerializer): category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category')) type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type')) - attrs = DynamicMappingField(mapping_rules=get_attrs_field_dynamic_mapping_rules()) class Meta: model = models.Application @@ -33,3 +50,4 @@ class ApplicationSerializer(BulkOrgResourceModelSerializer): _attrs = self.instance.attrs if self.instance else {} _attrs.update(attrs) return _attrs + diff --git a/apps/applications/serializers/attrs/category/__init__.py b/apps/applications/serializers/attrs/application_category/__init__.py similarity index 100% rename from apps/applications/serializers/attrs/category/__init__.py rename to apps/applications/serializers/attrs/application_category/__init__.py diff --git a/apps/applications/serializers/attrs/category/cloud.py b/apps/applications/serializers/attrs/application_category/cloud.py similarity index 100% rename from apps/applications/serializers/attrs/category/cloud.py rename to apps/applications/serializers/attrs/application_category/cloud.py diff --git a/apps/applications/serializers/attrs/category/db.py b/apps/applications/serializers/attrs/application_category/db.py similarity index 100% rename from apps/applications/serializers/attrs/category/db.py rename to apps/applications/serializers/attrs/application_category/db.py diff --git a/apps/applications/serializers/attrs/category/remote_app.py b/apps/applications/serializers/attrs/application_category/remote_app.py similarity index 100% rename from apps/applications/serializers/attrs/category/remote_app.py rename to apps/applications/serializers/attrs/application_category/remote_app.py diff --git a/apps/applications/serializers/attrs/type/__init__.py b/apps/applications/serializers/attrs/application_type/__init__.py similarity index 100% rename from apps/applications/serializers/attrs/type/__init__.py rename to apps/applications/serializers/attrs/application_type/__init__.py diff --git a/apps/applications/serializers/attrs/type/chrome.py b/apps/applications/serializers/attrs/application_type/chrome.py similarity index 93% rename from apps/applications/serializers/attrs/type/chrome.py rename to apps/applications/serializers/attrs/application_type/chrome.py index eda68d0f0..13b0c21f9 100644 --- a/apps/applications/serializers/attrs/type/chrome.py +++ b/apps/applications/serializers/attrs/application_type/chrome.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from ..category import RemoteAppSerializer +from ..application_category import RemoteAppSerializer __all__ = ['ChromeSerializer'] diff --git a/apps/applications/serializers/attrs/type/custom.py b/apps/applications/serializers/attrs/application_type/custom.py similarity index 92% rename from apps/applications/serializers/attrs/type/custom.py rename to apps/applications/serializers/attrs/application_type/custom.py index 927d88c15..78e26ae69 100644 --- a/apps/applications/serializers/attrs/type/custom.py +++ b/apps/applications/serializers/attrs/application_type/custom.py @@ -2,7 +2,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from ..category import RemoteAppSerializer +from ..application_category import RemoteAppSerializer __all__ = ['CustomSerializer'] diff --git a/apps/applications/serializers/attrs/type/k8s.py b/apps/applications/serializers/attrs/application_type/k8s.py similarity index 60% rename from apps/applications/serializers/attrs/type/k8s.py rename to apps/applications/serializers/attrs/application_type/k8s.py index c7caaeda0..ac1e023d3 100644 --- a/apps/applications/serializers/attrs/type/k8s.py +++ b/apps/applications/serializers/attrs/application_type/k8s.py @@ -1,4 +1,4 @@ -from ..category import CloudSerializer +from ..application_category import CloudSerializer __all__ = ['K8SSerializer'] diff --git a/apps/applications/serializers/attrs/type/mariadb.py b/apps/applications/serializers/attrs/application_type/mariadb.py similarity index 100% rename from apps/applications/serializers/attrs/type/mariadb.py rename to apps/applications/serializers/attrs/application_type/mariadb.py diff --git a/apps/applications/serializers/attrs/type/mysql.py b/apps/applications/serializers/attrs/application_type/mysql.py similarity index 83% rename from apps/applications/serializers/attrs/type/mysql.py rename to apps/applications/serializers/attrs/application_type/mysql.py index ec6ed1b4f..f9854333e 100644 --- a/apps/applications/serializers/attrs/type/mysql.py +++ b/apps/applications/serializers/attrs/application_type/mysql.py @@ -1,7 +1,7 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from ..category import DBSerializer +from ..application_category import DBSerializer __all__ = ['MySQLSerializer'] diff --git a/apps/applications/serializers/attrs/type/mysql_workbench.py b/apps/applications/serializers/attrs/application_type/mysql_workbench.py similarity index 95% rename from apps/applications/serializers/attrs/type/mysql_workbench.py rename to apps/applications/serializers/attrs/application_type/mysql_workbench.py index a38cd34df..2eaa89858 100644 --- a/apps/applications/serializers/attrs/type/mysql_workbench.py +++ b/apps/applications/serializers/attrs/application_type/mysql_workbench.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from ..category import RemoteAppSerializer +from ..application_category import RemoteAppSerializer __all__ = ['MySQLWorkbenchSerializer'] diff --git a/apps/applications/serializers/attrs/type/oracle.py b/apps/applications/serializers/attrs/application_type/oracle.py similarity index 83% rename from apps/applications/serializers/attrs/type/oracle.py rename to apps/applications/serializers/attrs/application_type/oracle.py index 810098d21..73f637990 100644 --- a/apps/applications/serializers/attrs/type/oracle.py +++ b/apps/applications/serializers/attrs/application_type/oracle.py @@ -1,7 +1,7 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from ..category import DBSerializer +from ..application_category import DBSerializer __all__ = ['OracleSerializer'] diff --git a/apps/applications/serializers/attrs/type/pgsql.py b/apps/applications/serializers/attrs/application_type/pgsql.py similarity index 83% rename from apps/applications/serializers/attrs/type/pgsql.py rename to apps/applications/serializers/attrs/application_type/pgsql.py index 7166de21a..236cad557 100644 --- a/apps/applications/serializers/attrs/type/pgsql.py +++ b/apps/applications/serializers/attrs/application_type/pgsql.py @@ -1,7 +1,7 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from ..category import DBSerializer +from ..application_category import DBSerializer __all__ = ['PostgreSerializer'] diff --git a/apps/applications/serializers/attrs/type/vmware_client.py b/apps/applications/serializers/attrs/application_type/vmware_client.py similarity index 94% rename from apps/applications/serializers/attrs/type/vmware_client.py rename to apps/applications/serializers/attrs/application_type/vmware_client.py index c02b87a02..66b2bbcf9 100644 --- a/apps/applications/serializers/attrs/type/vmware_client.py +++ b/apps/applications/serializers/attrs/application_type/vmware_client.py @@ -1,7 +1,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from ..category import RemoteAppSerializer +from ..application_category import RemoteAppSerializer __all__ = ['VMwareClientSerializer'] diff --git a/apps/applications/serializers/attrs/attrs.py b/apps/applications/serializers/attrs/attrs.py index adbe51820..a1d6d684b 100644 --- a/apps/applications/serializers/attrs/attrs.py +++ b/apps/applications/serializers/attrs/attrs.py @@ -1,12 +1,11 @@ -import copy +from rest_framework import serializers from applications import const -from common.drf.fields import IgnoreSensitiveInfoReadOnlyJSONField -from . import category, type as application_type +from . import application_category, application_type __all__ = [ - 'get_attrs_field_dynamic_mapping_rules', 'get_attrs_field_mapping_rule_by_view', - 'get_serializer_by_application_type', + 'attrs_field_dynamic_mapping_serializers', + 'get_serializer_class_by_application_type', ] @@ -35,16 +34,15 @@ type_custom = const.ApplicationTypeChoices.custom.value type_k8s = const.ApplicationTypeChoices.k8s.value -# define `attrs` field `DynamicMappingField` mapping_rules -# ----------------------------------------------------- +# define `attrs` field `dynamic mapping serializers` +# -------------------------------------------------- -__ATTRS_FIELD_DYNAMIC_MAPPING_RULES = { - 'default': IgnoreSensitiveInfoReadOnlyJSONField, +attrs_field_dynamic_mapping_serializers = { 'category': { - category_db: category.DBSerializer, - category_remote_app: category.RemoteAppSerializer, - category_cloud: category.CloudSerializer, + category_db: application_category.DBSerializer, + category_remote_app: application_category.RemoteAppSerializer, + category_cloud: application_category.CloudSerializer, }, 'type': { # db @@ -63,32 +61,5 @@ __ATTRS_FIELD_DYNAMIC_MAPPING_RULES = { } -# Note: -# The dynamic mapping rules of `attrs` field is obtained -# through call method `get_attrs_field_dynamic_mapping_rules` - -def get_attrs_field_dynamic_mapping_rules(): - return copy.deepcopy(__ATTRS_FIELD_DYNAMIC_MAPPING_RULES) - - -# get `attrs dynamic field` mapping rule by `view object` -# ---------------------------------------------------- - - -def get_attrs_field_mapping_rule_by_view(view): - query_type = view.request.query_params.get('type') - query_category = view.request.query_params.get('category') - if query_type: - mapping_rule = ['type', query_type] - elif query_category: - mapping_rule = ['category', query_category] - else: - mapping_rule = ['default'] - return mapping_rule - - -# get `category` mapping `serializer` -# ----------------------------------- - -def get_serializer_by_application_type(app_tp): - return __ATTRS_FIELD_DYNAMIC_MAPPING_RULES['type'].get(app_tp) +def get_serializer_class_by_application_type(_application_type): + return attrs_field_dynamic_mapping_serializers['type'].get(_application_type) diff --git a/apps/applications/serializers/remote_app.py b/apps/applications/serializers/remote_app.py index 773456cae..13343ee53 100644 --- a/apps/applications/serializers/remote_app.py +++ b/apps/applications/serializers/remote_app.py @@ -31,8 +31,8 @@ class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer): """ 返回Guacamole需要的RemoteApp配置参数信息中的parameters参数 """ - from .attrs import get_serializer_by_application_type - serializer_class = get_serializer_by_application_type(obj.type) + from .attrs import get_serializer_class_by_application_type + serializer_class = get_serializer_class_by_application_type(obj.type) fields = serializer_class().get_fields() parameters = [obj.type] diff --git a/apps/applications/serializers/utils.py b/apps/applications/serializers/utils.py deleted file mode 100644 index 7de5e0c46..000000000 --- a/apps/applications/serializers/utils.py +++ /dev/null @@ -1,16 +0,0 @@ -from .attrs import get_attrs_field_mapping_rule_by_view - -__all__ = [ - 'get_dynamic_mapping_fields_mapping_rule_by_view' -] - - -# -# get `dynamic fields` mapping rule by `view object` -# ---------------------------------------------------- - - -def get_dynamic_mapping_fields_mapping_rule_by_view(view): - return { - 'attrs': get_attrs_field_mapping_rule_by_view(view=view), - } diff --git a/apps/common/drf/fields.py b/apps/common/drf/fields.py index b96d04bf8..2ee17f19a 100644 --- a/apps/common/drf/fields.py +++ b/apps/common/drf/fields.py @@ -6,88 +6,10 @@ from rest_framework import serializers __all__ = [ - 'DynamicMappingField', 'ReadableHiddenField', - 'CustomMetaDictField', 'IgnoreSensitiveInfoReadOnlyJSONField', + 'ReadableHiddenField', 'CustomMetaDictField', ] -# -# DynamicMappingField -# ------------------- - - -class DynamicMappingField(serializers.Field): - """ - 一个可以根据用户行为而动态改变的字段 - - For example, Define attribute `mapping_rules` - - field_name = meta - - mapping_rules = { - 'default': serializers.JSONField(), - 'type': { - 'apply_asset': { - 'default': serializers.CharField(label='default'), - 'get': ApplyAssetSerializer, - 'post': ApproveAssetSerializer, - }, - 'apply_application': ApplyApplicationSerializer, - 'login_confirm': LoginConfirmSerializer, - 'login_times': LoginTimesSerializer - }, - 'category': { - 'apply': ApplySerializer, - 'login': LoginSerializer - } - } - """ - - def __init__(self, mapping_rules, *args, **kwargs): - assert isinstance(mapping_rules, dict), ( - '`mapping_rule` argument expect type `dict`, gut get `{}`' - ''.format(type(mapping_rules)) - ) - - self.__mapping_rules = mapping_rules - super().__init__(*args, **kwargs) - - @property - def mapping_rules(self): - return copy.deepcopy(self.__mapping_rules) - - def to_internal_value(self, data): - """ 实际是一个虚拟字段所以不返回任何值 """ - pass - - def to_representation(self, value): - """ 实际是一个虚拟字段所以不返回任何值 """ - pass - - -# A Ignore read-only fields for sensitive information -# ---------------------------------------------------------- - - -class IgnoreSensitiveInfoReadOnlyJSONField(serializers.JSONField): - """ A ignore read-only fields for sensitive information """ - - def __init__(self, **kwargs): - kwargs['read_only'] = True - super().__init__(**kwargs) - - def to_representation(self, value): - sensitive_ignored_value = {} - sensitive_names = ['password'] - for field_name, field_value in value.items(): - for sensitive_name in sensitive_names: - if sensitive_name in field_name.lower(): - continue - sensitive_ignored_value[field_name] = field_value - return super().to_representation(sensitive_ignored_value) - - -# # ReadableHiddenField # ------------------- diff --git a/apps/common/drf/serializers.py b/apps/common/drf/serializers.py index e32f70213..817c6cd6e 100644 --- a/apps/common/drf/serializers.py +++ b/apps/common/drf/serializers.py @@ -5,133 +5,98 @@ from rest_framework.serializers import ModelSerializer from rest_framework_bulk.serializers import BulkListSerializer from common.mixins import BulkListSerializerMixin -from common.drf.fields import DynamicMappingField +from django.utils.functional import cached_property +from rest_framework.utils.serializer_helpers import BindingDict from common.mixins.serializers import BulkSerializerMixin from common.utils import QuickLookupDict __all__ = [ - 'IncludeDynamicMappingFieldSerializerMetaClass', + 'DynamicMappingSerializer', 'EmptySerializer', 'BulkModelSerializer', 'AdaptedBulkListSerializer', 'CeleryTaskSerializer' ] -# -# IncludeDynamicMappingFieldSerializerMetaClass -# --------------------------------------------- +# DynamicMappingSerializer +# ------------------------ -class IncludeDynamicMappingFieldSerializerMetaClass(serializers.SerializerMetaclass, type): - """ - SerializerMetaClass: 动态创建包含 `common.drf.fields.DynamicMappingField` 字段的 `SerializerClass` - * Process only fields of type `DynamicMappingField` in `_declared_fields` - * 只处理 `_declared_fields` 中类型为 `DynamicMappingField` 的字段 +class DynamicMappingSerializer(serializers.Serializer): + data_type_error_messages = 'Expect get instance of type `{}`, but got instance type of `{}`' - 根据 `attrs['dynamic_mapping_fields_mapping_rule']` 中指定的 `fields_mapping_rule`, - 从 `DynamicMappingField` 中匹配出满足给定规则的字段, 并使用匹配到的字段替换自身的 `DynamicMappingField` + def __init__(self, mapping_serializers=None, get_mapping_serializers_method_name=None, + get_mapping_path_method_name=None, default_serializer=None, **kwargs): + self.mapping_serializers = mapping_serializers + self.get_mapping_serializers_method_name = get_mapping_serializers_method_name + self.get_mapping_path_method_name = get_mapping_path_method_name + self.default_serializer = default_serializer or serializers.Serializer + super().__init__(**kwargs) - * 注意: 如果未能根据给定的匹配规则获取到对应的字段,先获取与给定规则同级的 `default` 字段, - 如果仍未获取到,则再获取 `DynamicMappingField`中定义的最外层的 `default` 字段 + def bind(self, field_name, parent): + # The get mapping serializers method name defaults to `get_{field_name}_mapping_serializers` + if self.get_mapping_serializers_method_name is None: + method_name = 'get_{field_name}_mapping_serializers'.format(field_name=field_name) + self.get_mapping_serializers_method_name = method_name - * 说明: 如果获取到的不是 `serializers.Field` 类型, 则返回 `DynamicMappingField()` + # The get mapping rule method name defaults to `get_{field_name}_mapping_path`. + if self.get_mapping_path_method_name is None: + method_name = 'get_{field_name}_mapping_path'.format(field_name=field_name) + self.get_mapping_path_method_name = method_name - For example, define attrs['dynamic_mapping_fields_mapping_rule']: + super().bind(field_name, parent) - mapping_rules = { - 'default': serializer.JSONField, - 'type': { - 'apply_asset': { - 'default': serializer.ChoiceField(), - 'get': serializer.CharField() - } - } - } - meta = DynamicMappingField(mapping_rules=mapping_rules) + def get_mapping_serializers(self): + if self.mapping_serializers is not None: + return self.mapping_serializers + method = getattr(self.parent, self.get_mapping_serializers_method_name) + return method() - dynamic_mapping_fields_mapping_rule = {'meta': ['type', 'apply_asset', 'get'],} - => Got `serializer.CharField()` - * or * - dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset.get',}} - => Got `serializer.CharField()` - * or * - dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset.',}} - => Got serializer.ChoiceField(), - * or * - dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset.xxx',}} - => Got `serializer.ChoiceField()` - * or * - dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset.get.xxx',}} - => Got `serializer.JSONField()` - * or * - dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset',}} - => Got `{'get': {}}`, type is not `serializers.Field`, So `meta` is `DynamicMappingField()` - """ + def get_mapping_path(self, mapping_serializers): + method = getattr(self.parent, self.get_mapping_path_method_name) + return method(mapping_serializers) - @classmethod - def get_dynamic_mapping_fields(mcs, bases, attrs): - fields = {} + @staticmethod + def mapping(mapping_serializers, mapping_path): + quick_lookup_dict = QuickLookupDict(data=mapping_serializers) + serializer = quick_lookup_dict.get(key_path=mapping_path) + return serializer - # get `fields mapping rule` from attrs `dynamic_mapping_fields_mapping_rule` - fields_mapping_rule = attrs.get('dynamic_mapping_fields_mapping_rule') - - # check `fields_mapping_rule` type - assert isinstance(fields_mapping_rule, dict), ( - '`dynamic_mapping_fields_mapping_rule` must be `dict` type , but get `{}`' - ''.format(type(fields_mapping_rule)) + def get_mapped_serializer(self): + mapping_serializers = self.get_mapping_serializers() + assert isinstance(mapping_serializers, dict), ( + self.data_type_error_messages.format('dict', type(mapping_serializers)) ) + mapping_path = self.get_mapping_path(mapping_serializers) + assert isinstance(mapping_path, list), ( + self.data_type_error_messages.format('list', type(mapping_path)) + ) + serializer = self.mapping(mapping_serializers, mapping_path) + return serializer - # get `serializer class` declared fields - declared_fields = mcs._get_declared_fields(bases, attrs) - declared_fields_names = list(declared_fields.keys()) - - fields_mapping_rule = copy.deepcopy(fields_mapping_rule) - - for field_name, field_mapping_rule in fields_mapping_rule.items(): - - if field_name not in declared_fields_names: - continue - - declared_field = declared_fields[field_name] - if not isinstance(declared_field, DynamicMappingField): - continue - - assert isinstance(field_mapping_rule, (list, str)), ( - '`dynamic_mapping_fields_mapping_rule.field_mapping_rule` ' - '- can be either a list of keys, or a delimited string. ' - 'Such as: `["type", "apply_asset", "get"]` or `type.apply_asset.get` ' - 'but, get type is `{}`, `{}`' - ''.format(type(field_mapping_rule), field_mapping_rule) - ) - - if isinstance(field_mapping_rule, str): - field_mapping_rule = field_mapping_rule.split('.') - - # construct `field mapping rules` sequence list - field_mapping_rules = [ - field_mapping_rule, - copy.deepcopy(field_mapping_rule)[:-1] + ['default'], - ['default'] - ] - - dynamic_field = declared_field - - field_finder = QuickLookupDict(dynamic_field.mapping_rules) - - field = field_finder.find_one(key_paths=field_mapping_rules) - - if isinstance(field, type): - field = field() - - if not isinstance(field, serializers.Field): - continue - - fields[field_name] = field + @cached_property + def mapped_serializer(self): + serializer = self.get_mapped_serializer() + if serializer is None: + serializer = self.default_serializer + if isinstance(serializer, type): + serializer = serializer() + return serializer + def get_fields(self): + fields = self.mapped_serializer.get_fields() + return fields + + @cached_property + def fields(self): + """ + 重写此方法因为在 BindingDict 中要设置每一个 field 的 parent 为 `mapped_serializer`, + 这样在调用 field.parent 时, 才会达到预期的结果, + 比如: serializers.SerializerMethodField + """ + fields = BindingDict(self.mapped_serializer) + for key, value in self.get_fields().items(): + fields[key] = value return fields - def __new__(mcs, name, bases, attrs): - dynamic_mapping_fields = mcs.get_dynamic_mapping_fields(bases, attrs) - attrs.update(dynamic_mapping_fields) - return super().__new__(mcs, name, bases, attrs) # # Other Serializer diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py index 706b6d619..4aa10deec 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -13,7 +13,6 @@ from rest_framework.response import Response from rest_framework.settings import api_settings from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter -from common.drf.serializers import IncludeDynamicMappingFieldSerializerMetaClass from ..utils import lazyproperty __all__ = [ @@ -29,12 +28,11 @@ class JSONResponseMixin(object): return JsonResponse(context) -# -# GenericSerializerMixin +# SerializerMixin # ---------------------- -class GenericSerializerMixin: +class SerializerMixin: """ 根据用户请求动作的不同,获取不同的 `serializer_class `""" serializer_classes = None @@ -65,65 +63,6 @@ class GenericSerializerMixin: return serializer_class -# -# IncludeDynamicMappingFieldSerializerViewMixin -# --------------------------------------------- - - -class IncludeDynamicMappingFieldSerializerViewMixin(GenericSerializerMixin): - """ - 动态创建 `view` 使用的 `serializer_class`, - - 根据用户请求行为的不同, 构造出获取 `serializer_class` 中 `common.drf.fields.DynamicMappingField` 字段 - 的映射规则, 并通过 `IncludeDynamicMappingFieldSerializerMetaClass` 元类, - 基于父类的 `serializer_class` 和 构造出的映射规则 `dynamic_mapping_fields_mapping_rule` - 创建出满足要求的新的 `serializer_class` - - * 重写 get_dynamic_mapping_fields_mapping_rule 方法: - - For example, - - def get_dynamic_mapping_fields_mapping_rule(self): - return {'meta': ['type', 'apply_asset', 'get'] - - """ - - def get_dynamic_mapping_fields_mapping_rule(self): - """ - return: - { - 'meta': ['type', 'apply_asset', 'get'], - 'meta2': 'category.login' - } - """ - return {} - - @staticmethod - def _create_serializer_class(base, attrs): - serializer_class = IncludeDynamicMappingFieldSerializerMetaClass( - base.__name__, (base, ), attrs - ) - return serializer_class - - def get_serializer_class(self): - serializer_class = super().get_serializer_class() - - if getattr(self, 'swagger_fake_view', False): - return serializer_class - - fields_mapping_rule = self.get_dynamic_mapping_fields_mapping_rule() - if not fields_mapping_rule: - return serializer_class - - attrs = {'dynamic_mapping_fields_mapping_rule': fields_mapping_rule} - serializer_class = self._create_serializer_class(base=serializer_class, attrs=attrs) - return serializer_class - - -class SerializerMixin(IncludeDynamicMappingFieldSerializerViewMixin): - pass - - class ExtraFilterFieldsMixin: """ 额外的 api filter diff --git a/apps/perms/api/application/user_group_permission.py b/apps/perms/api/application/user_group_permission.py index 661995d43..65e18ce91 100644 --- a/apps/perms/api/application/user_group_permission.py +++ b/apps/perms/api/application/user_group_permission.py @@ -7,7 +7,6 @@ from rest_framework.generics import ListAPIView from common.permissions import IsOrgAdminOrAppUser from common.mixins.api import CommonApiMixin from applications.models import Application -from applications.api.mixin import ApplicationViewMixin from perms import serializers __all__ = [ @@ -15,7 +14,7 @@ __all__ = [ ] -class UserGroupGrantedApplicationsApi(ApplicationViewMixin, CommonApiMixin, ListAPIView): +class UserGroupGrantedApplicationsApi(CommonApiMixin, ListAPIView): """ 获取用户组直接授权的应用 """ diff --git a/apps/perms/api/application/user_permission/user_permission_applications.py b/apps/perms/api/application/user_permission/user_permission_applications.py index eab6a6b97..e963783c6 100644 --- a/apps/perms/api/application/user_permission/user_permission_applications.py +++ b/apps/perms/api/application/user_permission/user_permission_applications.py @@ -5,7 +5,7 @@ from rest_framework.response import Response from common.mixins.api import CommonApiMixin from applications.api.mixin import ( - SerializeApplicationToTreeNodeMixin, ApplicationViewMixin + SerializeApplicationToTreeNodeMixin ) from perms import serializers from perms.api.asset.user_permission.mixin import ForAdminMixin, ForUserMixin @@ -22,7 +22,7 @@ __all__ = [ ] -class AllGrantedApplicationsMixin(ApplicationViewMixin, CommonApiMixin, ListAPIView): +class AllGrantedApplicationsMixin(CommonApiMixin, ListAPIView): only_fields = serializers.ApplicationGrantedSerializer.Meta.only_fields serializer_class = serializers.ApplicationGrantedSerializer filter_fields = ['id', 'name', 'category', 'type', 'comment'] diff --git a/apps/perms/serializers/application/user_permission.py b/apps/perms/serializers/application/user_permission.py index 0010e899c..c96316d71 100644 --- a/apps/perms/serializers/application/user_permission.py +++ b/apps/perms/serializers/application/user_permission.py @@ -6,8 +6,7 @@ from django.utils.translation import ugettext_lazy as _ from assets.models import SystemUser from applications.models import Application -from applications.serializers.attrs import get_attrs_field_dynamic_mapping_rules -from common.drf.fields import DynamicMappingField +from applications.serializers import IncludeDynamicMappingSerializerFieldApplicationSerializerMixin __all__ = [ 'ApplicationGrantedSerializer', 'ApplicationSystemUserSerializer' @@ -27,13 +26,13 @@ class ApplicationSystemUserSerializer(serializers.ModelSerializer): read_only_fields = fields -class ApplicationGrantedSerializer(serializers.ModelSerializer): +class ApplicationGrantedSerializer(IncludeDynamicMappingSerializerFieldApplicationSerializerMixin, + serializers.ModelSerializer): """ 被授权应用的数据结构 """ category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category')) type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type')) - attrs = DynamicMappingField(mapping_rules=get_attrs_field_dynamic_mapping_rules()) class Meta: model = Application diff --git a/apps/tickets/api/ticket/ticket.py b/apps/tickets/api/ticket.py similarity index 89% rename from apps/tickets/api/ticket/ticket.py rename to apps/tickets/api/ticket.py index f3903a15e..140bcb596 100644 --- a/apps/tickets/api/ticket/ticket.py +++ b/apps/tickets/api/ticket.py @@ -12,7 +12,6 @@ from common.permissions import IsValidUser, IsOrgAdmin from tickets import serializers from tickets.models import Ticket from tickets.permissions.ticket import IsAssignee, NotClosed -from tickets.serializers.ticket.utils import get_dynamic_mapping_fields_mapping_rule_by_view __all__ = ['TicketViewSet'] @@ -66,6 +65,3 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet): def close(self, request, *args, **kwargs): return super().update(request, *args, **kwargs) - def get_dynamic_mapping_fields_mapping_rule(self): - fields_mapping_rule = get_dynamic_mapping_fields_mapping_rule_by_view(view=self) - return fields_mapping_rule diff --git a/apps/tickets/api/ticket/__init__.py b/apps/tickets/api/ticket/__init__.py deleted file mode 100644 index 4a3a5e8c7..000000000 --- a/apps/tickets/api/ticket/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .ticket import * diff --git a/apps/tickets/models/ticket/mixin/meta/apply_application.py b/apps/tickets/models/ticket/mixin/meta/apply_application.py index 56b8c7053..35fefb024 100644 --- a/apps/tickets/models/ticket/mixin/meta/apply_application.py +++ b/apps/tickets/models/ticket/mixin/meta/apply_application.py @@ -4,7 +4,7 @@ from applications.models import Application from applications.const import ApplicationCategoryChoices, ApplicationTypeChoices from assets.models import SystemUser from perms.models import ApplicationPermission -from tickets.utils import convert_model_data_field_name_to_verbose_name +from tickets.utils import convert_model_instance_data_field_name_to_verbose_name class ConstructDisplayFieldMixin: @@ -67,11 +67,11 @@ class ConstructBodyMixin: def construct_apply_application_approved_body(self): # 审批信息 approve_applications_snapshot = self.meta['approve_applications_snapshot'] - approve_applications_snapshot_display = convert_model_data_field_name_to_verbose_name( + approve_applications_snapshot_display = convert_model_instance_data_field_name_to_verbose_name( Application, approve_applications_snapshot ) approve_system_users_snapshot = self.meta['approve_system_users_snapshot'] - approve_system_users_snapshot_display = convert_model_data_field_name_to_verbose_name( + approve_system_users_snapshot_display = convert_model_instance_data_field_name_to_verbose_name( SystemUser, approve_system_users_snapshot ) approve_date_start = self.meta['approve_date_start'] diff --git a/apps/tickets/models/ticket/mixin/meta/apply_asset.py b/apps/tickets/models/ticket/mixin/meta/apply_asset.py index b24b4b4f7..15a8a2b90 100644 --- a/apps/tickets/models/ticket/mixin/meta/apply_asset.py +++ b/apps/tickets/models/ticket/mixin/meta/apply_asset.py @@ -3,7 +3,7 @@ from django.utils.translation import ugettext as __ from perms.models import AssetPermission, Action from assets.models import Asset, SystemUser from orgs.utils import tmp_to_org, tmp_to_root_org -from tickets.utils import convert_model_data_field_name_to_verbose_name +from tickets.utils import convert_model_instance_data_field_name_to_verbose_name class ConstructDisplayFieldMixin: @@ -69,11 +69,11 @@ class ConstructBodyMixin: def construct_apply_asset_approved_body(self): approve_assets_snapshot = self.meta['approve_assets_snapshot'] - approve_assets_snapshot_display = convert_model_data_field_name_to_verbose_name( + approve_assets_snapshot_display = convert_model_instance_data_field_name_to_verbose_name( Asset, approve_assets_snapshot ) approve_system_users_snapshot = self.meta['approve_system_users_snapshot'] - approve_system_users_snapshot_display = convert_model_data_field_name_to_verbose_name( + approve_system_users_snapshot_display = convert_model_instance_data_field_name_to_verbose_name( SystemUser, approve_system_users_snapshot ) approve_actions_display = self.meta['approve_actions_display'] diff --git a/apps/tickets/models/ticket/mixin/ticket.py b/apps/tickets/models/ticket/mixin/ticket.py index 1d53effd6..055a80809 100644 --- a/apps/tickets/models/ticket/mixin/ticket.py +++ b/apps/tickets/models/ticket/mixin/ticket.py @@ -19,7 +19,7 @@ class TicketCreatePermissionMixin(meta.CreatePermissionMixin, base.CreatePermiss class TicketCreateCommentMixin(base.CreateCommentMixin): - """ 创建 ticket 评论""" + """ 创建 ticket 备注""" pass diff --git a/apps/tickets/serializers/ticket/__init__.py b/apps/tickets/serializers/ticket/__init__.py index 705837a0d..bb2ee74d6 100644 --- a/apps/tickets/serializers/ticket/__init__.py +++ b/apps/tickets/serializers/ticket/__init__.py @@ -1,3 +1,2 @@ from .ticket import * from .meta import * -from .utils import * diff --git a/apps/tickets/serializers/ticket/meta/meta.py b/apps/tickets/serializers/ticket/meta/meta.py index 63b17b0c9..6097cec4a 100644 --- a/apps/tickets/serializers/ticket/meta/meta.py +++ b/apps/tickets/serializers/ticket/meta/meta.py @@ -1,14 +1,10 @@ -import copy -from common.drf.fields import IgnoreSensitiveInfoReadOnlyJSONField from tickets import const -from . import apply_asset, apply_application, login_confirm +from .ticket_type import apply_asset, apply_application, login_confirm __all__ = [ - 'get_meta_field_dynamic_mapping_rules', - 'get_meta_field_mapping_rule_by_view', + 'meta_field_dynamic_mapping_serializers', ] -# # ticket type # ----------- @@ -17,7 +13,6 @@ type_apply_asset = const.TicketTypeChoices.apply_asset.value type_apply_application = const.TicketTypeChoices.apply_application.value type_login_confirm = const.TicketTypeChoices.login_confirm.value -# # ticket action # ------------- @@ -29,13 +24,11 @@ action_reject = const.TicketActionChoices.reject.value action_close = const.TicketActionChoices.close.value -# -# defines the dynamic mapping rules for the DynamicMappingField `meta` -# -------------------------------------------------------------------- +# defines `meta` field dynamic mapping serializers +# ------------------------------------------------ -__META_FIELD_DYNAMIC_MAPPING_RULES = { - 'default': IgnoreSensitiveInfoReadOnlyJSONField, +meta_field_dynamic_mapping_serializers = { 'type': { type_apply_asset: { action_open: apply_asset.ApplySerializer, @@ -52,22 +45,3 @@ __META_FIELD_DYNAMIC_MAPPING_RULES = { } -# Note: -# The dynamic mapping rules of `meta` field is obtained -# through call method `get_meta_field_dynamic_mapping_rules` - -def get_meta_field_dynamic_mapping_rules(): - return copy.deepcopy(__META_FIELD_DYNAMIC_MAPPING_RULES) - - -# -# get `meta dynamic field` mapping rule by `view object` -# ------------------------------------------------------ - - -def get_meta_field_mapping_rule_by_view(view): - query_type = view.request.query_params.get('type') - query_action = view.request.query_params.get('action') - action = query_action if query_action else view.action - mapping_rule = ['type', query_type, action] - return mapping_rule diff --git a/apps/tickets/tests/design/__init__.py b/apps/tickets/serializers/ticket/meta/ticket_type/__init__.py similarity index 100% rename from apps/tickets/tests/design/__init__.py rename to apps/tickets/serializers/ticket/meta/ticket_type/__init__.py diff --git a/apps/tickets/serializers/ticket/meta/apply_application.py b/apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py similarity index 100% rename from apps/tickets/serializers/ticket/meta/apply_application.py rename to apps/tickets/serializers/ticket/meta/ticket_type/apply_application.py diff --git a/apps/tickets/serializers/ticket/meta/apply_asset.py b/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py similarity index 94% rename from apps/tickets/serializers/ticket/meta/apply_asset.py rename to apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py index 7a6023979..dd983bbcf 100644 --- a/apps/tickets/serializers/ticket/meta/apply_asset.py +++ b/apps/tickets/serializers/ticket/meta/ticket_type/apply_asset.py @@ -2,12 +2,11 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers from perms.serializers import ActionsField from assets.models import Asset, SystemUser -from tickets.models import Ticket from .mixin import BaseApproveSerializerMixin __all__ = [ - 'ApplyAssetTypeSerializer', 'ApplySerializer', 'ApproveSerializer', + 'ApplyAssetSerializer', 'ApplySerializer', 'ApproveSerializer', ] @@ -84,5 +83,5 @@ class ApproveSerializer(BaseApproveSerializerMixin, serializers.Serializer): return system_users_id -class ApplyAssetTypeSerializer(ApplySerializer, ApproveSerializer): +class ApplyAssetSerializer(ApplySerializer, ApproveSerializer): pass diff --git a/apps/tickets/serializers/ticket/meta/login_confirm.py b/apps/tickets/serializers/ticket/meta/ticket_type/login_confirm.py similarity index 93% rename from apps/tickets/serializers/ticket/meta/login_confirm.py rename to apps/tickets/serializers/ticket/meta/ticket_type/login_confirm.py index d196a8f41..27bcad02f 100644 --- a/apps/tickets/serializers/ticket/meta/login_confirm.py +++ b/apps/tickets/serializers/ticket/meta/ticket_type/login_confirm.py @@ -1,7 +1,7 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from tickets.models import Ticket + __all__ = [ 'ApplySerializer', diff --git a/apps/tickets/serializers/ticket/meta/mixin.py b/apps/tickets/serializers/ticket/meta/ticket_type/mixin.py similarity index 100% rename from apps/tickets/serializers/ticket/meta/mixin.py rename to apps/tickets/serializers/ticket/meta/ticket_type/mixin.py diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py index 61370e995..0cf940e0d 100644 --- a/apps/tickets/serializers/ticket/ticket.py +++ b/apps/tickets/serializers/ticket/ticket.py @@ -2,13 +2,14 @@ # from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.drf.fields import ReadableHiddenField, DynamicMappingField +from common.drf.fields import ReadableHiddenField +from common.drf.serializers import DynamicMappingSerializer from orgs.utils import get_org_by_id from orgs.mixins.serializers import OrgResourceModelSerializerMixin from users.models import User from tickets import const from tickets.models import Ticket -from .meta import get_meta_field_dynamic_mapping_rules +from .meta import meta_field_dynamic_mapping_serializers __all__ = [ 'TicketSerializer', 'TicketDisplaySerializer', @@ -21,7 +22,7 @@ class TicketSerializer(OrgResourceModelSerializerMixin): type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type')) action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action')) status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status')) - meta = DynamicMappingField(mapping_rules=get_meta_field_dynamic_mapping_rules()) + meta = DynamicMappingSerializer(mapping_serializers=meta_field_dynamic_mapping_serializers) class Meta: model = Ticket @@ -35,6 +36,15 @@ class TicketSerializer(OrgResourceModelSerializerMixin): 'body' ] + def get_meta_mapping_path(self, mapping_serializers): + view = self.context['view'] + request = self.context['request'] + query_type = request.query_params.get('type') + query_action = request.query_params.get('action') + action = query_action if query_action else view.action + mapping_path = ['type', query_type, action] + return mapping_path + class TicketDisplaySerializer(TicketSerializer): diff --git a/apps/tickets/serializers/ticket/utils.py b/apps/tickets/serializers/ticket/utils.py deleted file mode 100644 index 88fef96d1..000000000 --- a/apps/tickets/serializers/ticket/utils.py +++ /dev/null @@ -1,16 +0,0 @@ -from .meta import get_meta_field_mapping_rule_by_view - -__all__ = [ - 'get_dynamic_mapping_fields_mapping_rule_by_view' -] - - -# -# get `dynamic fields` mapping rule by `view object` -# ---------------------------------------------------- - - -def get_dynamic_mapping_fields_mapping_rule_by_view(view): - return { - 'meta': get_meta_field_mapping_rule_by_view(view=view) - } diff --git a/apps/tickets/tests/design/architecture_for_view_to_serializer_mapping/__init__.py b/apps/tickets/tests/design/architecture_for_view_to_serializer_mapping/__init__.py deleted file mode 100644 index da59d190a..000000000 --- a/apps/tickets/tests/design/architecture_for_view_to_serializer_mapping/__init__.py +++ /dev/null @@ -1,69 +0,0 @@ -""" -说明: - View 获取 serializer_class 的架构设计 - -问题: - View 所需的 Serializer Class 中有一个字段,字段的类型并不固定, - 而是由 View 的行为 (比如 type, action) 来决定的. - 使用 View 默认的 get_serializer_class 方法不能实现,因为序列类在被定义的时候,其字段及类型已经固定, - 所以需要一种机制来动态修改序列类中的字段及其类型,即,MetaClass 元类 - - - 例如: - class MetaASerializer(serializers.Serializer): - name = serializers.CharField(label='Name') - - - class MetaBSerializer(serializers.Serializer): - age = serializers.IntegerField(label='Age') - - - class Serializer(serializers.Serializer): - meta = serializers.JSONField() - - 当 view 获取 serializer 时,无论 action 是什么, 获取到的 Serializer.meta 字段始终是 - serializers.JSONField() 类型 - - 但我们希望: - 当 view.action = A 时,获取到的 Serializer.meta 是 MetaASerializerMetaASerializer() - 当 view.action = B 时,获取到的 Serializer.meta 是 MetaBSerializerMetaASerializer() - -分析: - 问题关键在于数据映射规则的定义和匹配, - 即, 用 View 给定的规则动态去匹配 Serializer Class 中定义的规则, 从而创建出想要的序列类 - - 当然, 使用 dict 可以很好的解决规则定义问题,但要直接进行匹配, 操作起来比较复杂 - 所以, 决定使用以下方案实现, 即, dict, dict-> tree 数据类型转化, tree 搜索: - Serializer Class 中规则的定义使用 dict, - View 中指定的规则也使用 dict, - MetaClass 中进行规则匹配的过程使用 dict tree, 即将给定的 dic 转换为 tree 的数据结构,再进行匹配, - * dict -> tree 的转化使用 `data-tree` 库来实现 - - -方案: - view: - 使用元类 MetaClass: 在 View 中动态创建所需要的 Serializer Class - - serializer: - 实现 DynamicMappingField: 序列类的动态映射字段, 用来定义字段的映射规则 - - 实现 IncludeDynamicMappingFieldSerializerMetaClass: - 在 View 中用来动态创建 Serializer Class. - 即, 用 View 中给定的规则,去匹配 Serializer Class 里每一个 DynamicMappingField 字段定义的规则, - 基于 bases, 创建并返回新的 Serializer Class - - 实现 IncludeDynamicMappingFieldSerializerViewMixin: - 实现动态创建 Serializer Class 的逻辑, - 同时定义获取 Serializer Class 中所有 DynamicMappingField 字段匹配规则的方法 - => - def get_dynamic_mapping_fields_mapping_rule(self): - return { - 'dynamic_mapping_field_name1': `mapping_path`, - 'dynamic_mapping_field_name2': `mapping_path', - } - 供 View 子类重写 - -实现: - 请看 ./serializer.py 和 ./serializer.py - -""" diff --git a/apps/tickets/tests/design/architecture_for_view_to_serializer_mapping/serializer.py b/apps/tickets/tests/design/architecture_for_view_to_serializer_mapping/serializer.py deleted file mode 100644 index 73e63a406..000000000 --- a/apps/tickets/tests/design/architecture_for_view_to_serializer_mapping/serializer.py +++ /dev/null @@ -1,226 +0,0 @@ -import copy -import data_tree -from rest_framework import serializers - - -# -# IncludeDynamicMappingFieldSerializerMetaClass -# --------------------------------------------- - -class IncludeDynamicMappingFieldSerializerMetaClass(serializers.SerializerMetaclass, type): - """ - SerializerMetaClass: 动态创建包含 `common.drf.fields.DynamicMappingField` 字段的 `SerializerClass` - - * Process only fields of type `DynamicMappingField` in `_declared_fields` - * 只处理 `_declared_fields` 中类型为 `DynamicMappingField` 的字段 - - 根据 `attrs['dynamic_mapping_fields_mapping_rule']` 中指定的 `fields_mapping_rule`, - 从 `DynamicMappingField` 中匹配出满足给定规则的字段, 并使用匹配到的字段替换自身的 `DynamicMappingField` - - * 注意: 如果未能根据给定的匹配规则获取到对应的字段,先获取与给定规则同级的 `default` 字段, - 如果仍未获取到,则再获取 `DynamicMappingField`中定义的最外层的 `default` 字段 - - * 说明: 如果获取到的不是 `serializers.Field` 类型, 则返回 `DynamicMappingField()` - - For example, define attrs['dynamic_mapping_fields_mapping_rule']: - - mapping_rules = { - 'default': serializer.JSONField, - 'type': { - 'apply_asset': { - 'default': serializer.ChoiceField(), - 'get': serializer.CharField() - } - } - } - meta = DynamicMappingField(mapping_rules=mapping_rules) - - dynamic_mapping_fields_mapping_rule = {'meta': ['type', 'apply_asset', 'get'],} - => Got `serializer.CharField()` - * or * - dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset.get',}} - => Got `serializer.CharField()` - * or * - dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset.',}} - => Got serializer.ChoiceField(), - * or * - dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset.xxx',}} - => Got `serializer.ChoiceField()` - * or * - dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset.get.xxx',}} - => Got `serializer.JSONField()` - * or * - dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset',}} - => Got `{'get': {}}`, type is not `serializers.Field`, So `meta` is `DynamicMappingField()` - """ - - @classmethod - def get_dynamic_mapping_fields(mcs, bases, attrs): - fields = {} - - fields_mapping_rules = attrs.get('dynamic_mapping_fields_mapping_rule') - - assert isinstance(fields_mapping_rules, dict), ( - '`dynamic_mapping_fields_mapping_rule` must be `dict` type , but get `{}`' - ''.format(type(fields_mapping_rules)) - ) - - fields_mapping_rules = copy.deepcopy(fields_mapping_rules) - - declared_fields = mcs._get_declared_fields(bases, attrs) - - for field_name, field_mapping_rule in fields_mapping_rules.items(): - - assert isinstance(field_mapping_rule, (list, str)), ( - '`dynamic_mapping_fields_mapping_rule.field_mapping_rule` ' - '- can be either a list of keys, or a delimited string. ' - 'Such as: `["type", "apply_asset", "get"]` or `type.apply_asset.get` ' - 'but, get type is `{}`, `{}`' - ''.format(type(field_mapping_rule), field_mapping_rule) - ) - - if field_name not in declared_fields.keys(): - continue - - declared_field = declared_fields[field_name] - if not isinstance(declared_field, DynamicMappingField): - continue - - dynamic_field = declared_field - - mapping_tree = dynamic_field.mapping_tree.copy() - - def get_field(rule): - return mapping_tree.get(arg_path=rule) - - if isinstance(field_mapping_rule, str): - field_mapping_rule = field_mapping_rule.split('.') - - field_mapping_rule[-1] = field_mapping_rule[-1] or 'default' - - field = get_field(rule=field_mapping_rule) - - if not field: - field_mapping_rule[-1] = 'default' - field = get_field(rule=field_mapping_rule) - - if field is None: - field_mapping_rule = ['default'] - field = get_field(rule=field_mapping_rule) - - if isinstance(field, type): - field = field() - - if not isinstance(field, serializers.Field): - continue - - fields[field_name] = field - - return fields - - def __new__(mcs, name, bases, attrs): - dynamic_mapping_fields = mcs.get_dynamic_mapping_fields(bases, attrs) - attrs.update(dynamic_mapping_fields) - return super().__new__(mcs, name, bases, attrs) - -# -# DynamicMappingField -# ---------------------------------- - - -class DynamicMappingField(serializers.Field): - """ 一个根据用户行为而动态匹配的字段 """ - - def __init__(self, mapping_rules, *args, **kwargs): - - assert isinstance(mapping_rules, dict), ( - '`mapping_rule` argument expect type `dict`, gut get `{}`' - ''.format(type(mapping_rules)) - ) - - assert 'default' in mapping_rules, ( - "mapping_rules['default'] is a required, but only get `{}`" - "".format(list(mapping_rules.keys())) - ) - - self.mapping_rules = mapping_rules - - self.mapping_tree = self._build_mapping_tree() - - super().__init__(*args, **kwargs) - - def _build_mapping_tree(self): - tree = data_tree.Data_tree_node(arg_data=self.mapping_rules) - return tree - - def to_internal_value(self, data): - """ 实际是一个虚拟字段所以不返回任何值 """ - pass - - def to_representation(self, value): - """ 实际是一个虚拟字段所以不返回任何值 """ - pass - - -# -# Test data -# ---------------------------------- - - -# ticket type -class ApplyAssetSerializer(serializers.Serializer): - apply_asset = serializers.CharField(label='Apply Asset') - - -class ApproveAssetSerializer(serializers.Serializer): - approve_asset = serializers.CharField(label='Approve Asset') - - -class ApplyApplicationSerializer(serializers.Serializer): - apply_application = serializers.CharField(label='Application') - - -class LoginConfirmSerializer(serializers.Serializer): - login_ip = serializers.IPAddressField() - - -class LoginTimesSerializer(serializers.Serializer): - login_times = serializers.IntegerField() - - -# ticket category -class ApplySerializer(serializers.Serializer): - apply_datetime = serializers.DateTimeField() - - -class LoginSerializer(serializers.Serializer): - login_datetime = serializers.DateTimeField() - - -meta_mapping_rules = { - 'default': serializers.JSONField(), - 'type': { - 'apply_asset': { - 'default': serializers.CharField(label='default'), - 'get': ApplyAssetSerializer, - 'post': ApproveAssetSerializer, - }, - 'apply_application': ApplyApplicationSerializer, - 'login_confirm': LoginConfirmSerializer, - 'login_times': LoginTimesSerializer - }, - 'category': { - 'apply': ApplySerializer, - 'login': LoginSerializer - } -} - - -class TicketSerializer(serializers.Serializer): - title = serializers.CharField(label='Title') - type = serializers.ChoiceField(choices=('apply_asset', 'apply_application'), label='Type') - meta1 = DynamicMappingField(mapping_rules=meta_mapping_rules) - meta2 = DynamicMappingField(mapping_rules=meta_mapping_rules) - meta3 = DynamicMappingField(mapping_rules=meta_mapping_rules) - meta4 = DynamicMappingField(mapping_rules=meta_mapping_rules) - diff --git a/apps/tickets/tests/design/architecture_for_view_to_serializer_mapping/view.py b/apps/tickets/tests/design/architecture_for_view_to_serializer_mapping/view.py deleted file mode 100644 index 4cb58b52e..000000000 --- a/apps/tickets/tests/design/architecture_for_view_to_serializer_mapping/view.py +++ /dev/null @@ -1,86 +0,0 @@ -from tickets.tests.design.architecture_for_view_to_serializer_mapping.serializer import ( - IncludeDynamicMappingFieldSerializerMetaClass, TicketSerializer -) - - -# -# IncludeDynamicMappingFieldSerializerViewMixin -# --------------------------------------------- - - -class IncludeDynamicMappingFieldSerializerViewMixin: - """ - 动态创建 `view` 使用的 `serializer_class`, - - 根据用户请求行为的不同, 构造出获取 `serializer_class` 中 `common.drf.fields.DynamicMappingField` 字段 - 的映射规则, 并通过 `IncludeDynamicMappingFieldSerializerMetaClass` 元类, - 基于父类的 `serializer_class` 和 构造出的映射规则 `dynamic_mapping_fields_mapping_rule` - 创建出满足要求的新的 `serializer_class` - - * 重写 get_dynamic_mapping_fields_mapping_rule 方法: - - For example, - - def get_dynamic_mapping_fields_mapping_rule(self): - return {'meta': ['type', 'apply_asset', 'get'] - - """ - - def get_dynamic_mapping_fields_mapping_rule(self): - """ - return: - { - 'meta': ['type', 'apply_asset', 'get'], - 'meta2': 'category.login' - } - """ - print(self) - return { - 'meta1': ['type', 'apply_asset', 'getX', 'asdf'], - 'meta2': 'category.login', - 'meta3': 'type.apply_asset.', - 'meta4': 'category.apply' - } - - @staticmethod - def _create_serializer_class(base, attrs): - serializer_class = IncludeDynamicMappingFieldSerializerMetaClass( - base.__name__, (base, ), attrs - ) - return serializer_class - - def get_serializer_class(self): - serializer_class = super().get_serializer_class() - - fields_mapping_rule = self.get_dynamic_mapping_fields_mapping_rule() - if not fields_mapping_rule: - return serializer_class - - attrs = {'dynamic_mapping_fields_mapping_rule': fields_mapping_rule} - serializer_class = self._create_serializer_class(base=serializer_class, attrs=attrs) - return serializer_class - - -# -# Test data -# --------- - - -class GenericViewSet(object): - - def get_serializer_class(self): - return TicketSerializer - - -class TicketViewSet(IncludeDynamicMappingFieldSerializerViewMixin, GenericViewSet): - pass - - -view = TicketViewSet() - -_serializer_class = view.get_serializer_class() - -_serializer = _serializer_class() - -print(_serializer_class) -print(_serializer) diff --git a/apps/tickets/tests/tests.py b/apps/tickets/tests/tests.py deleted file mode 100644 index 3026012c3..000000000 --- a/apps/tickets/tests/tests.py +++ /dev/null @@ -1,196 +0,0 @@ -# 测试通过 view 动态创建 serializer - - -class BaseSerializerMetaClass(type): - - def __new__(mcs, name, bases, attrs): - attrs.update({'color': 'blank'}) - return super().__new__(mcs, name, bases, attrs) - - -class BaseSerializer(metaclass=BaseSerializerMetaClass): - x_id = 'id_value' - - -class Serializer(BaseSerializer): - - x_name = 'name_value' - x_hobby = { - 'music': 'chinese', - 'ball': 'basketball' - } - x_age = { - 'real': 19, - 'fake': 27 - } - - -# custom metaclass -class SerializerMetaClass(BaseSerializerMetaClass, type): - - @classmethod - def _get_declared_x_attr_value(mcs, x_types, attr_name, attr_value): - pass - - @classmethod - def _get_declared_x_attrs(mcs, bases, attrs): - x_types = attrs['view'].x_types - - bases_attrs = {} - for base in bases: - for k in dir(base): - if not k.startswith('x_'): - continue - v = getattr(base, k) - if isinstance(v, str): - bases_attrs[k] = v - continue - if isinstance(v, dict): - v = mcs._get_declared_x_attr_value( x_types, k, v) - bases_attrs[k] = v - attrs.update(bases_attrs) - return attrs - - def __new__(mcs, name, bases, attrs): - attrs = mcs._get_declared_x_attrs(bases, attrs) - return super().__new__(mcs, name, bases, attrs) - - -class View(object): - x_types = ['x_age', 'fake'] - serializer_class = Serializer - - def get_serializer_class(self): - return self.serializer_class - - def build_serializer_class(self): - serializer_class = self.get_serializer_class() - serializer_class = SerializerMetaClass( - serializer_class.__name__, (serializer_class,), {'view': self} - ) - return serializer_class - - -view = View() - - -serializer = view.build_serializer_class() -print('End!') - -# -from rest_framework.serializers import SerializerMetaclass - - -data = { - 'meta': { - 'type': { - 'apply_asset': { - 'get': 'get', - 'post': 'post' - } - } - } -} - - -def get_value(keys_dict, data_dict): - - def _get_value(_key_list, _data_dict): - if len(_key_list) == 0: - return _data_dict - for i, key in enumerate(_key_list): - _keys = _key_list[i+1:] - __data_dict = _data_dict.get(key) - if __data_dict is None: - return _data_dict - if not isinstance(__data_dict, dict): - return __data_dict - return _get_value(_keys, __data_dict) - - _values_dict = {} - for field, keys in keys_dict.items(): - keys.insert(0, field) - _values_dict[field] = _get_value(keys, data_dict) - return _values_dict - - -keys_dict_list = { - 'meta': ['type', 'apply_asset', 'get'] -} -values_dict = get_value(keys_dict_list, data) -print(values_dict) - -keys_dict_list = { - 'meta': ['type', 'apply_asset', 'post'] -} -values_dict = get_value(keys_dict_list, data) -print(values_dict) - -keys_dict_list = { - 'meta': ['type', 'apply_asset', 'post', 'dog'] -} -values_dict = get_value(keys_dict_list, data) -print(values_dict) - -keys_dict_list = { - 'meta': ['type', 'apply_asset', 'dog'] -} -values_dict = get_value(keys_dict_list, data) -print(values_dict) - - -# - -class A: - def __init__(self): - self.a = 'A' - - -get_action_serializer = 'GETSerializer' -post_action_serializer = 'POSTSerializer' -apply_action_serializer = A() - -apply_asset_tree_serializer = { - 'get': get_action_serializer, - 'post': post_action_serializer, - 'apply': apply_action_serializer -} - -type_tree_serializer = { - 'apply_asset': apply_asset_tree_serializer, -} - -meta_tree_serializer = { - 'type': type_tree_serializer, -} - -json_fields_serializer_mapping = { - 'meta': meta_tree_serializer -} - - -def data_dict_to_tree(data_dict): - import data_tree - t = data_tree.Data_tree_node(arg_data=data_dict) - return t - - -tree = data_dict_to_tree(json_fields_serializer_mapping) - - -def get_tree_node(t, path): - return t.get(path, arg_default_value_to_return='Not Found') - - -node = get_tree_node(tree, 'meta.type.apply_asset.get') -print(node) - -node = get_tree_node(tree, 'meta.type.apply_asset.post') -print(node) - -node = get_tree_node(tree, 'meta.type.apply_asset.apply') -print(node) - - -node = get_tree_node(tree, 'meta.type.apply_asset.xxxx') -print(node) diff --git a/apps/tickets/utils.py b/apps/tickets/utils.py index d56186e04..a3070404d 100644 --- a/apps/tickets/utils.py +++ b/apps/tickets/utils.py @@ -11,30 +11,23 @@ from . import const logger = get_logger(__file__) -def convert_model_data_field_name_to_verbose_name(model, name_data): - """将Model以field_name为key的数据转换为以field_verbose_name为key的数据""" - if isinstance(name_data, dict): - name_data = [name_data] - - model_fields_name_verbose_name_mapping = { +def get_model_field_verbose_name(model, field_name): + field_name_field_verbose_name_mapping = { field.name: field.verbose_name for field in model._meta.fields } + field_name = field_name.split('__', 1)[0] + field_verbose_name = field_name_field_verbose_name_mapping.get(field_name, field_name) + return field_verbose_name - def get_verbose_name(field_name): - verbose_name = model_fields_name_verbose_name_mapping.get(field_name) - if not verbose_name: - other_name = field_name.split('__', 1)[0] - verbose_name = model_fields_name_verbose_name_mapping.get(other_name) - if not verbose_name: - verbose_name = field_name - return verbose_name - verbose_name_data = [ - {get_verbose_name(name): value for name, value in d.items()} - for d in name_data +def convert_model_instance_data_field_name_to_verbose_name(model, data): + if isinstance(data, dict): + data = [data] + converted_data = [ + {get_model_field_verbose_name(model, name): value for name, value in d.items()} + for d in data ] - - return verbose_name_data + return converted_data def send_ticket_applied_mail_to_assignees(ticket, assignees):