reactor: 增加DynamicMappingSerializer类,实现Serializer中的字段可以动态改变的功能 (#5379)

* reactor: 增加DynamicMappingSerializer类,实现Serializer中的字段可以动态改变的功能

* reactor: 增加DynamicMappingSerializer类,实现Serializer中的字段可以动态改变的功能 (2)

* reactor: 增加DynamicMappingSerializer类,实现Serializer中的字段可以动态改变的功能 (3)

Co-authored-by: Bai <bugatti_it@163.com>
This commit is contained in:
fit2bot
2021-01-05 23:39:38 +08:00
committed by GitHub
parent 3188692691
commit 17a01a12db
45 changed files with 164 additions and 1000 deletions

View File

@@ -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
# -------------------

View File

@@ -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