diff --git a/apps/applications/serializers/database_app.py b/apps/applications/serializers/database_app.py index 8b983ed87..ab30edba4 100644 --- a/apps/applications/serializers/database_app.py +++ b/apps/applications/serializers/database_app.py @@ -4,7 +4,7 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from .. import models diff --git a/apps/applications/serializers/remote_app.py b/apps/applications/serializers/remote_app.py index 7e5a69c68..48a784e33 100644 --- a/apps/applications/serializers/remote_app.py +++ b/apps/applications/serializers/remote_app.py @@ -6,8 +6,8 @@ from django.utils.translation import ugettext_lazy as _ from django.core.exceptions import ObjectDoesNotExist from rest_framework import serializers -from common.serializers import AdaptedBulkListSerializer -from common.fields.serializer import CustomMetaDictField +from common.drf.serializers import AdaptedBulkListSerializer +from common.drf.fields import CustomMetaDictField from common.utils import get_logger from orgs.mixins.serializers import BulkOrgResourceModelSerializer from assets.models import Asset diff --git a/apps/assets/serializers/admin_user.py b/apps/assets/serializers/admin_user.py index 5e27b0c64..b8b086205 100644 --- a/apps/assets/serializers/admin_user.py +++ b/apps/assets/serializers/admin_user.py @@ -3,7 +3,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from ..models import Node, AdminUser from orgs.mixins.serializers import BulkOrgResourceModelSerializer diff --git a/apps/assets/serializers/asset_user.py b/apps/assets/serializers/asset_user.py index 1c598cbf5..7eba6ba41 100644 --- a/apps/assets/serializers/asset_user.py +++ b/apps/assets/serializers/asset_user.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext as _ from rest_framework import serializers -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import AuthBook, Asset from ..backends import AssetUserManager diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py index 5a045df9c..0c8eca3d4 100644 --- a/apps/assets/serializers/cmd_filter.py +++ b/apps/assets/serializers/cmd_filter.py @@ -3,8 +3,7 @@ import re from rest_framework import serializers -from common.fields import ChoiceDisplayField -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from ..models import CommandFilter, CommandFilterRule, SystemUser from orgs.mixins.serializers import BulkOrgResourceModelSerializer @@ -26,7 +25,6 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer): class CommandFilterRuleSerializer(BulkOrgResourceModelSerializer): - # serializer_choice_field = ChoiceDisplayField invalid_pattern = re.compile(r'[\.\*\+\[\\\?\{\}\^\$\|\(\)\#\<\>]') type_display = serializers.ReadOnlyField(source='get_type_display') action_display = serializers.ReadOnlyField(source='get_action_display') diff --git a/apps/assets/serializers/domain.py b/apps/assets/serializers/domain.py index 0a16f3910..00df0b91d 100644 --- a/apps/assets/serializers/domain.py +++ b/apps/assets/serializers/domain.py @@ -3,7 +3,7 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from common.validators import NoSpecialChars from ..models import Domain, Gateway diff --git a/apps/assets/serializers/favorite_asset.py b/apps/assets/serializers/favorite_asset.py index 8429d959e..067655b0a 100644 --- a/apps/assets/serializers/favorite_asset.py +++ b/apps/assets/serializers/favorite_asset.py @@ -4,7 +4,7 @@ from rest_framework import serializers from orgs.utils import tmp_to_root_org -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from common.mixins import BulkSerializerMixin from ..models import FavoriteAsset diff --git a/apps/assets/serializers/label.py b/apps/assets/serializers/label.py index 448018eab..bfce6e2a6 100644 --- a/apps/assets/serializers/label.py +++ b/apps/assets/serializers/label.py @@ -2,7 +2,7 @@ # from rest_framework import serializers -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import Label diff --git a/apps/assets/serializers/system_user.py b/apps/assets/serializers/system_user.py index fb8d4df97..5e8d884dd 100644 --- a/apps/assets/serializers/system_user.py +++ b/apps/assets/serializers/system_user.py @@ -2,7 +2,7 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from django.db.models import Count -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from common.mixins.serializers import BulkSerializerMixin from common.utils import ssh_pubkey_gen from orgs.mixins.serializers import BulkOrgResourceModelSerializer diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index 74c8f598c..53a17298d 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -5,7 +5,7 @@ from rest_framework import serializers from django.db.models import F from common.mixins import BulkSerializerMixin -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from terminal.models import Session from ops.models import CommandExecution from . import models diff --git a/apps/common/drf/fields.py b/apps/common/drf/fields.py index e3b333d56..80db9cd80 100644 --- a/apps/common/drf/fields.py +++ b/apps/common/drf/fields.py @@ -1,43 +1,171 @@ -from uuid import UUID +# -*- coding: utf-8 -*- +# -from rest_framework.fields import get_attribute -from rest_framework.relations import ManyRelatedField, PrimaryKeyRelatedField, MANY_RELATION_KWARGS +import data_tree +from rest_framework import serializers -class GroupConcatedManyRelatedField(ManyRelatedField): - def get_attribute(self, instance): - if hasattr(instance, 'pk') and instance.pk is None: - return [] - - attr = self.source_attrs[-1] - - # `gc` 是 `GroupConcat` 的缩写 - gc_attr = f'gc_{attr}' - if hasattr(instance, gc_attr): - gc_value = getattr(instance, gc_attr) - if isinstance(gc_value, str): - return [UUID(pk) for pk in set(gc_value.split(','))] - else: - return '' - - relationship = get_attribute(instance, self.source_attrs) - return relationship.all() if hasattr(relationship, 'all') else relationship +__all__ = [ + 'DynamicMappingField', 'ReadableHiddenField', + 'CustomMetaDictField', +] -class GroupConcatedPrimaryKeyRelatedField(PrimaryKeyRelatedField): - @classmethod - def many_init(cls, *args, **kwargs): - list_kwargs = {'child_relation': cls(*args, **kwargs)} - for key in kwargs: - if key in MANY_RELATION_KWARGS: - list_kwargs[key] = kwargs[key] - return GroupConcatedManyRelatedField(**list_kwargs) +# +# 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)) + ) + + 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): - if self.pk_field is not None: - return self.pk_field.to_representation(value.pk) + """ 实际是一个虚拟字段所以不返回任何值 """ + pass - if hasattr(value, 'pk'): - return value.pk - else: +# +# ReadableHiddenField +# ------------------- + + +class ReadableHiddenField(serializers.HiddenField): + """ 可读的 HiddenField """ + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.write_only = False + + def to_representation(self, value): + if hasattr(value, 'id'): + return getattr(value, 'id') + return value + +# +# OtherField +# ---------- + + +# TODO: DELETE 替换完成后删除 +class CustomMetaDictField(serializers.DictField): + """ + In use: + RemoteApp params field + CommandStorage meta field + ReplayStorage meta field + """ + type_fields_map = {} + default_type = None + convert_key_remove_type_prefix = False + convert_key_to_upper = False + + def filter_attribute(self, attribute, instance): + fields = self.type_fields_map.get(instance.type, []) + for field in fields: + if field.get('write_only', False): + attribute.pop(field['name'], None) + return attribute + + def get_attribute(self, instance): + """ + 序列化时调用 + """ + attribute = super().get_attribute(instance) + attribute = self.filter_attribute(attribute, instance) + return attribute + + def convert_value_key_remove_type_prefix(self, dictionary, value): + if not self.convert_key_remove_type_prefix: return value + tp = dictionary.get('type') + prefix = '{}_'.format(tp) + convert_value = {} + for k, v in value.items(): + if k.lower().startswith(prefix): + k = k.lower().split(prefix, 1)[1] + convert_value[k] = v + return convert_value + + def convert_value_key_to_upper(self, value): + if not self.convert_key_to_upper: + return value + convert_value = {k.upper(): v for k, v in value.items()} + return convert_value + + def convert_value_key(self, dictionary, value): + value = self.convert_value_key_remove_type_prefix(dictionary, value) + value = self.convert_value_key_to_upper(value) + return value + + def filter_value_key(self, dictionary, value): + tp = dictionary.get('type') + fields = self.type_fields_map.get(tp, []) + fields_names = [field['name'] for field in fields] + filter_value = {k: v for k, v in value.items() if k in fields_names} + return filter_value + + @staticmethod + def strip_value(value): + new_value = {} + for k, v in value.items(): + if isinstance(v, str): + v = v.strip() + new_value[k] = v + return new_value + + def get_value(self, dictionary): + """ + 反序列化时调用 + """ + value = super().get_value(dictionary) + value = self.convert_value_key(dictionary, value) + value = self.filter_value_key(dictionary, value) + value = self.strip_value(value) + return value + + diff --git a/apps/common/drf/serializers.py b/apps/common/drf/serializers.py index e767c32aa..9abef8ac8 100644 --- a/apps/common/drf/serializers.py +++ b/apps/common/drf/serializers.py @@ -1,12 +1,142 @@ +import copy +from rest_framework import serializers from rest_framework.serializers import Serializer from rest_framework.serializers import ModelSerializer -from rest_framework import serializers from rest_framework_bulk.serializers import BulkListSerializer -from common.mixins.serializers import BulkSerializerMixin from common.mixins import BulkListSerializerMixin +from common.drf.fields import DynamicMappingField +from common.mixins.serializers import BulkSerializerMixin -__all__ = ['EmptySerializer', 'BulkModelSerializer'] +__all__ = [ + 'IncludeDynamicMappingFieldSerializerMetaClass', + 'EmptySerializer', 'BulkModelSerializer', 'AdaptedBulkListSerializer', 'CeleryTaskSerializer' +] + + +# +# 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) + +# +# Other Serializer +# ---------------- class EmptySerializer(Serializer): @@ -23,3 +153,5 @@ class AdaptedBulkListSerializer(BulkListSerializerMixin, BulkListSerializer): class CeleryTaskSerializer(serializers.Serializer): task = serializers.CharField(read_only=True) + + diff --git a/apps/common/fields/__init__.py b/apps/common/fields/__init__.py index f918a3279..cb3ff2081 100644 --- a/apps/common/fields/__init__.py +++ b/apps/common/fields/__init__.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- # -from .form import * from .model import * -from .serializer import * diff --git a/apps/common/fields/form.py b/apps/common/fields/form.py deleted file mode 100644 index fd144ec92..000000000 --- a/apps/common/fields/form.py +++ /dev/null @@ -1,62 +0,0 @@ -# -*- coding: utf-8 -*- -# -import json - -from django import forms -import six -from django.core.exceptions import ValidationError -from django.utils.translation import ugettext as _ -from ..utils import signer - - -__all__ = [ - 'FormDictField', 'FormEncryptCharField', 'FormEncryptDictField', - 'FormEncryptMixin', -] - - -class FormDictField(forms.Field): - widget = forms.Textarea - - def to_python(self, value): - """Returns a Python boolean object.""" - # Explicitly check for the string 'False', which is what a hidden field - # will submit for False. Also check for '0', since this is what - # RadioSelect will provide. Because bool("True") == bool('1') == True, - # we don't need to handle that explicitly. - if isinstance(value, six.string_types): - value = value.replace("'", '"') - try: - value = json.loads(value) - return value - except json.JSONDecodeError: - return ValidationError(_("Not a valid json")) - else: - return ValidationError(_("Not a string type")) - - def validate(self, value): - if isinstance(value, ValidationError): - raise value - if not value and self.required: - raise ValidationError(self.error_messages['required'], code='required') - - def has_changed(self, initial, data): - # Sometimes data or initial may be a string equivalent of a boolean - # so we should run it through to_python first to get a boolean value - return self.to_python(initial) != self.to_python(data) - - -class FormEncryptMixin: - pass - - -class FormEncryptCharField(FormEncryptMixin, forms.CharField): - pass - - -class FormEncryptDictField(FormEncryptMixin, FormDictField): - pass - - - - diff --git a/apps/common/fields/serializer.py b/apps/common/fields/serializer.py deleted file mode 100644 index 8f0ae1826..000000000 --- a/apps/common/fields/serializer.py +++ /dev/null @@ -1,316 +0,0 @@ -# -*- coding: utf-8 -*- -# - -import copy -from collections import OrderedDict -from rest_framework.serializers import ALL_FIELDS -from rest_framework import serializers -import six - - -__all__ = [ - 'StringIDField', 'StringManyToManyField', 'ChoiceDisplayField', - 'CustomMetaDictField', 'ReadableHiddenField', 'JSONFieldModelSerializer' -] - - -class StringIDField(serializers.Field): - def to_representation(self, value): - return {"pk": value.pk, "name": value.__str__()} - - -class StringManyToManyField(serializers.RelatedField): - def to_representation(self, value): - return value.__str__() - - -class ChoiceDisplayField(serializers.ChoiceField): - def __init__(self, *args, **kwargs): - super(ChoiceDisplayField, self).__init__(*args, **kwargs) - self.choice_strings_to_display = { - six.text_type(key): value for key, value in self.choices.items() - } - - def to_representation(self, value): - if value is None: - return value - return { - 'value': self.choice_strings_to_values.get(six.text_type(value), value), - 'display': self.choice_strings_to_display.get(six.text_type(value), value), - } - - -class DictField(serializers.DictField): - def to_representation(self, value): - if not value or not isinstance(value, dict): - value = {} - return super().to_representation(value) - - -class ReadableHiddenField(serializers.HiddenField): - def __init__(self, **kwargs): - super().__init__(**kwargs) - self.write_only = False - - def to_representation(self, value): - if hasattr(value, 'id'): - return getattr(value, 'id') - return value - - -class CustomMetaDictField(serializers.DictField): - """ - In use: - RemoteApp params field - CommandStorage meta field - ReplayStorage meta field - """ - type_fields_map = {} - default_type = None - convert_key_remove_type_prefix = False - convert_key_to_upper = False - - def filter_attribute(self, attribute, instance): - fields = self.type_fields_map.get(instance.type, []) - for field in fields: - if field.get('write_only', False): - attribute.pop(field['name'], None) - return attribute - - def get_attribute(self, instance): - """ - 序列化时调用 - """ - attribute = super().get_attribute(instance) - attribute = self.filter_attribute(attribute, instance) - return attribute - - def convert_value_key_remove_type_prefix(self, dictionary, value): - if not self.convert_key_remove_type_prefix: - return value - tp = dictionary.get('type') - prefix = '{}_'.format(tp) - convert_value = {} - for k, v in value.items(): - if k.lower().startswith(prefix): - k = k.lower().split(prefix, 1)[1] - convert_value[k] = v - return convert_value - - def convert_value_key_to_upper(self, value): - if not self.convert_key_to_upper: - return value - convert_value = {k.upper(): v for k, v in value.items()} - return convert_value - - def convert_value_key(self, dictionary, value): - value = self.convert_value_key_remove_type_prefix(dictionary, value) - value = self.convert_value_key_to_upper(value) - return value - - def filter_value_key(self, dictionary, value): - tp = dictionary.get('type') - fields = self.type_fields_map.get(tp, []) - fields_names = [field['name'] for field in fields] - filter_value = {k: v for k, v in value.items() if k in fields_names} - return filter_value - - @staticmethod - def strip_value(value): - new_value = {} - for k, v in value.items(): - if isinstance(v, str): - v = v.strip() - new_value[k] = v - return new_value - - def get_value(self, dictionary): - """ - 反序列化时调用 - """ - value = super().get_value(dictionary) - value = self.convert_value_key(dictionary, value) - value = self.filter_value_key(dictionary, value) - value = self.strip_value(value) - return value - - -class JSONFieldModelSerializer(serializers.Serializer): - """ Model JSONField Serializer""" - - def __init__(self, *args, **kwargs): - mode_field = getattr(self.Meta, 'model_field') - if mode_field: - kwargs['label'] = mode_field.field.verbose_name - super().__init__(*args, **kwargs) - - class Meta: - model = None - model_field = None - fields = None - exclude = None - - def get_fields(self): - assert hasattr(self, 'Meta'), ( - 'Class {serializer_class} missing "Meta" attribute'.format( - serializer_class=self.__class__.__name__ - ) - ) - assert hasattr(self.Meta, 'model'), ( - 'Class {serializer_class} missing "Meta.model" attribute'.format( - serializer_class=self.__class__.__name__ - ) - ) - model_fields_mapping = {field.name: field for field in self.Meta.model._meta.fields} - assert hasattr(self.Meta, 'model_field'), ( - 'Class {serializer_class} missing "Meta.model_field" attribute'.format( - serializer_class=self.__class__.__name__ - ) - ) - - assert self.Meta.model_field.field.name in model_fields_mapping.keys(), ( - 'Class {serializer_class} "Meta.model_field" attribute not in ' - '"Meta.model._meta.fields"'.format( - serializer_class=self.__class__.__name__, - ) - ) - - declared_fields = copy.deepcopy(self._declared_fields) - - read_only_field_names = self.get_read_only_field_names() - - field_names = self.get_field_names(declared_fields) - - fields = OrderedDict() - for field_name in field_names: - if field_name not in declared_fields: - continue - field = declared_fields[field_name] - if field_name in read_only_field_names: - setattr(field, 'read_only', True) - fields[field_name] = field - return fields - - def get_field_names(self, declared_fields): - """ - Returns the list of all field names that should be created when - instantiating this serializer class. This is based on the default - set of fields, but also takes into account the `Meta.fields` or - `Meta.exclude` options if they have been specified. - """ - - fields = getattr(self.Meta, 'fields', None) - exclude = getattr(self.Meta, 'exclude', None) - - if fields and fields != ALL_FIELDS and not isinstance(fields, (list, tuple)): - raise TypeError( - 'The `fields` option must be a list or tuple or "__all__". ' - 'Got %s.' % type(fields).__name__ - ) - - if exclude and not isinstance(exclude, (list, tuple)): - raise TypeError( - 'The `exclude` option must be a list or tuple. Got %s.' % - type(exclude).__name__ - ) - - assert not (fields and exclude), ( - "Cannot set both 'fields' and 'exclude' options on " - "serializer {serializer_class}.".format( - serializer_class=self.__class__.__name__ - ) - ) - - assert not (fields is None and exclude is None), ( - "Creating a ModelSerializer without either the 'fields' attribute " - "or the 'exclude' attribute has been deprecated since 3.3.0, " - "and is now disallowed. Add an explicit fields = '__all__' to the " - "{serializer_class} serializer.".format( - serializer_class=self.__class__.__name__ - ), - ) - - if fields == ALL_FIELDS: - fields = None - - if fields is not None: - # Ensure that all declared fields have also been included in the - # `Meta.fields` option. - - # Do not require any fields that are declared in a parent class, - # in order to allow serializer subclasses to only include - # a subset of fields. - required_field_names = set(declared_fields) - for cls in self.__class__.__bases__: - required_field_names -= set(getattr(cls, '_declared_fields', [])) - - for field_name in required_field_names: - assert field_name in fields, ( - "The field '{field_name}' was declared on serializer " - "{serializer_class}, but has not been included in the " - "'fields' option.".format( - field_name=field_name, - serializer_class=self.__class__.__name__ - ) - ) - return fields - - # Use the default set of field names if `Meta.fields` is not specified. - fields = self.get_default_field_names(declared_fields) - - if exclude is not None: - # If `Meta.exclude` is included, then remove those fields. - for field_name in exclude: - assert field_name not in self._declared_fields, ( - "Cannot both declare the field '{field_name}' and include " - "it in the {serializer_class} 'exclude' option. Remove the " - "field or, if inherited from a parent serializer, disable " - "with `{field_name} = None`." - .format( - field_name=field_name, - serializer_class=self.__class__.__name__ - ) - ) - - assert field_name in fields, ( - "The field '{field_name}' was included on serializer " - "{serializer_class} in the 'exclude' option, but does " - "not match any model field.".format( - field_name=field_name, - serializer_class=self.__class__.__name__ - ) - ) - fields.remove(field_name) - return fields - - @staticmethod - def get_default_field_names(declared_fields): - return declared_fields - - def get_read_only_field_names(self): - read_only_fields = getattr(self.Meta, 'read_only_fields', None) - if read_only_fields is not None: - if not isinstance(read_only_fields, (list, tuple)): - raise TypeError( - 'The `read_only_fields` option must be a list or tuple. ' - 'Got %s.' % type(read_only_fields).__name__ - ) - return read_only_fields - - def to_internal_value(self, data): - return super().to_internal_value(data) - - def to_representation(self, instance): - if not isinstance(instance, dict): - return super().to_representation(instance) - for field_name, field in self.fields.items(): - if field_name in instance: - continue - if field.allow_null: - continue - setattr(field, 'allow_null', True) - return super().to_representation(instance) - - - - diff --git a/apps/common/mixins/api.py b/apps/common/mixins/api.py index 69e346bee..49072d518 100644 --- a/apps/common/mixins/api.py +++ b/apps/common/mixins/api.py @@ -7,15 +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 common.drf.serializers import IncludeDynamicMappingFieldSerializerMetaClass from ..utils import lazyproperty __all__ = [ @@ -31,7 +29,14 @@ class JSONResponseMixin(object): return JsonResponse(context) +# +# GenericSerializerMixin +# ---------------------- + + class GenericSerializerMixin: + """ 根据用户请求动作的不同,获取不同的 `serializer_class `""" + serializer_classes = None def get_serializer_class_by_view_action(self): @@ -39,12 +44,16 @@ class GenericSerializerMixin: return None if not isinstance(self.serializer_classes, dict): return None - draw = self.request.query_params.get('draw') + action = self.request.query_params.get('action') + serializer_class = None - if draw and self.action in ['list', 'metadata']: - serializer_class = self.serializer_classes.get('display') + if action: + # metadata方法 使用 action 参数获取 + serializer_class = self.serializer_classes.get(action) if serializer_class is None: serializer_class = self.serializer_classes.get(self.action) + if serializer_class is None: + serializer_class = self.serializer_classes.get('display') if serializer_class is None: serializer_class = self.serializer_classes.get('default') return serializer_class @@ -56,156 +65,59 @@ class GenericSerializerMixin: return serializer_class -class JSONFieldsModelSerializerMixin: +# +# IncludeDynamicMappingFieldSerializerViewMixin +# --------------------------------------------- + + +class IncludeDynamicMappingFieldSerializerViewMixin(GenericSerializerMixin): """ - 作用: 获取包含 JSONField 字段的序列类 + 动态创建 `view` 使用的 `serializer_class`, - class TestSerializer(serializers.Serializer): - pass + 根据用户请求行为的不同, 构造出获取 `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'] - 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': TestSerializer, - 'post': TestSerializer, - 'open':TestSerializer, - 'approve': TestSerializer, - }, - 'apply_application': { - 'get': TestSerializer, - 'post': TestSerializer, - 'put': TestSerializer, - }, - 'login_confirm': { - 'get': TestSerializer, - 'post': TestSerializer, - 'put': TestSerializer, - } - }, - 'category': {} - }, - 'json_field_2': {}, - 'json_field_3': {} - } """ - json_fields_category_mapping = {} - json_fields_serializer_classes = None - - # 保存当前处理的JSONField名称 - __field = None - serializer_class = None - - def get_json_field_query_category(self, category): - query_category = self.request.query_params.get(category) - category_choices = self.json_fields_category_mapping[self.__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_action_serializer_classes_by_query_category(self): - action_serializer_classes = None - category_collection = self.json_fields_category_mapping[self.__field] - for category in category_collection: - category_value = self.get_json_field_query_category(category) - if not category_value: - continue - category_serializer_classes = self.json_fields_serializer_classes[self.__field][category] - action_serializer_classes = category_serializer_classes.get(category_value) - if action_serializer_classes: - break - return action_serializer_classes - - def get_json_field_action_serializer_classes(self): - category_collection = self.json_fields_category_mapping[self.__field] - if category_collection: - serializer_classes = self.get_json_field_action_serializer_classes_by_query_category() - else: - serializer_classes = self.json_fields_serializer_classes[self.__field] - return serializer_classes - - def get_json_field_serializer_class_by_action(self, serializer_classes): - if serializer_classes is None: - return None - 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_class = serializer_classes.get(action) - return serializer_class - - @lazyproperty - def default_json_field_serializer_class(self): - readonly_json_field_serializer_class = type( - 'DefaultReadonlyJSONFieldSerializer', (serializers.JSONField,), - ) - return readonly_json_field_serializer_class - - def get_json_field_serializer(self): - serializer_classes = self.get_json_field_action_serializer_classes() - serializer_class = self.get_json_field_serializer_class_by_action(serializer_classes) - if serializer_class: - serializer = serializer_class() - return serializer - serializer_class = serializer_classes.get('default') - if serializer_class: - serializer = serializer_class(**{'read_only': True}) - return serializer - return self.default_json_field_serializer_class(**{'read_only': True}) - - def get_json_fields_serializer_mapping(self): + def get_dynamic_mapping_fields_mapping_rule(self): """ - return: { - 'json_field_1': serializer1(), - 'json_field_2': serializer2(), + return: + { + 'meta': ['type', 'apply_asset', 'get'], + 'meta2': 'category.login' } """ - fields_serializer_mapping = {} - fields = self.json_fields_serializer_classes.keys() - for field in fields: - self.__field = field - serializer = self.get_json_field_serializer() - fields_serializer_mapping[self.__field] = serializer - return fields_serializer_mapping + return {} - def build_include_json_fields_serializer_class(self, base, attrs): - serializer_class_name = ''.join([ - field_serializer.__class__.__name__ for field_serializer in attrs.values() - ]) - serializer_class = type(serializer_class_name, (base,), attrs) + @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 not isinstance(self.json_fields_serializer_classes, dict): + + fields_mapping_rule = self.get_dynamic_mapping_fields_mapping_rule() + if not fields_mapping_rule: return serializer_class - fields_serializer_mapping = self.get_json_fields_serializer_mapping() - if not fields_serializer_mapping: - return serializer_class - serializer_class = self.build_include_json_fields_serializer_class( - base=serializer_class, attrs=fields_serializer_mapping - ) + + 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(JSONFieldsModelSerializerMixin, GenericSerializerMixin): +class SerializerMixin(IncludeDynamicMappingFieldSerializerViewMixin): pass diff --git a/apps/common/serializers.py b/apps/common/serializers.py deleted file mode 100644 index 971060641..000000000 --- a/apps/common/serializers.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -老的代码统一到 `apps/common/drf/serializers.py` 中, -之后此文件废弃 -""" - -from common.drf.serializers import AdaptedBulkListSerializer, CeleryTaskSerializer diff --git a/apps/ops/api/adhoc.py b/apps/ops/api/adhoc.py index 586415c20..a09e7c9ca 100644 --- a/apps/ops/api/adhoc.py +++ b/apps/ops/api/adhoc.py @@ -7,7 +7,7 @@ from rest_framework.views import Response from common.drf.api import JMSBulkModelViewSet from common.permissions import IsOrgAdmin -from common.serializers import CeleryTaskSerializer +from common.drf.serializers import CeleryTaskSerializer from ..models import Task, AdHoc, AdHocExecution from ..serializers import ( TaskSerializer, diff --git a/apps/orgs/serializers.py b/apps/orgs/serializers.py index c4402d433..432670bb6 100644 --- a/apps/orgs/serializers.py +++ b/apps/orgs/serializers.py @@ -4,7 +4,7 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from users.models.user import User -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from common.drf.serializers import BulkModelSerializer from common.db.models import concated_display as display from .models import Organization, OrganizationMember, ROLE diff --git a/apps/perms/serializers/application/permission_relation.py b/apps/perms/serializers/application/permission_relation.py index 94ea8e608..2ec416a66 100644 --- a/apps/perms/serializers/application/permission_relation.py +++ b/apps/perms/serializers/application/permission_relation.py @@ -3,7 +3,7 @@ from rest_framework import serializers from common.mixins import BulkSerializerMixin -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from perms.models import ApplicationPermission __all__ = [ diff --git a/apps/perms/serializers/asset/permission_relation.py b/apps/perms/serializers/asset/permission_relation.py index 1081f76a0..f98c4bbf0 100644 --- a/apps/perms/serializers/asset/permission_relation.py +++ b/apps/perms/serializers/asset/permission_relation.py @@ -3,7 +3,7 @@ from rest_framework import serializers from common.mixins import BulkSerializerMixin -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from assets.models import Asset, Node from perms.models import AssetPermission from users.models import User diff --git a/apps/perms/serializers/database_app_permission.py b/apps/perms/serializers/database_app_permission.py index 3cd7f3245..0e35aff8d 100644 --- a/apps/perms/serializers/database_app_permission.py +++ b/apps/perms/serializers/database_app_permission.py @@ -4,7 +4,7 @@ from django.db.models import Count from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from .. import models diff --git a/apps/perms/serializers/database_app_permission_relation.py b/apps/perms/serializers/database_app_permission_relation.py index deb761853..5da6c88f0 100644 --- a/apps/perms/serializers/database_app_permission_relation.py +++ b/apps/perms/serializers/database_app_permission_relation.py @@ -4,7 +4,7 @@ from perms.serializers.base import PermissionAllUserSerializer from rest_framework import serializers from common.mixins import BulkSerializerMixin -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from .. import models diff --git a/apps/perms/serializers/remote_app_permission.py b/apps/perms/serializers/remote_app_permission.py index 75a20436e..a676f8028 100644 --- a/apps/perms/serializers/remote_app_permission.py +++ b/apps/perms/serializers/remote_app_permission.py @@ -4,7 +4,7 @@ from rest_framework import serializers from django.db.models import Count from django.utils.translation import ugettext_lazy as _ -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from ..models import RemoteAppPermission diff --git a/apps/perms/serializers/remote_app_permission_relation.py b/apps/perms/serializers/remote_app_permission_relation.py index 05d06a9da..1e5a30a2e 100644 --- a/apps/perms/serializers/remote_app_permission_relation.py +++ b/apps/perms/serializers/remote_app_permission_relation.py @@ -2,7 +2,7 @@ # from rest_framework import serializers -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from ..models import RemoteAppPermission diff --git a/apps/terminal/serializers/session.py b/apps/terminal/serializers/session.py index 9b1a128e7..d66c3cfa3 100644 --- a/apps/terminal/serializers/session.py +++ b/apps/terminal/serializers/session.py @@ -2,7 +2,7 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from orgs.mixins.serializers import BulkOrgResourceModelSerializer -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from ..models import Session __all__ = [ diff --git a/apps/terminal/serializers/storage.py b/apps/terminal/serializers/storage.py index ea988e597..0e7db21a2 100644 --- a/apps/terminal/serializers/storage.py +++ b/apps/terminal/serializers/storage.py @@ -3,7 +3,7 @@ import copy from rest_framework import serializers -from common.fields.serializer import CustomMetaDictField +from common.drf.fields import CustomMetaDictField from ..models import ReplayStorage, CommandStorage from .. import const diff --git a/apps/tickets/api/ticket/mixin.py b/apps/tickets/api/ticket/mixin.py deleted file mode 100644 index 21067614d..000000000 --- a/apps/tickets/api/ticket/mixin.py +++ /dev/null @@ -1,32 +0,0 @@ -from tickets import const, serializers - - -__all__ = ['TicketJSONFieldsModelSerializerViewMixin'] - - -class TicketJSONFieldsModelSerializerViewMixin: - json_fields_category_mapping = { - 'meta': { - 'type': const.TicketTypeChoices.values, - }, - } - json_fields_serializer_classes = { - 'meta': { - 'type': { - const.TicketTypeChoices.apply_asset.value: { - 'default': serializers.TicketMetaApplyAssetSerializer, - 'open': serializers.TicketMetaApplyAssetApplySerializer, - 'approve': serializers.TicketMetaApplyAssetApproveSerializer - }, - const.TicketTypeChoices.apply_application.value: { - 'default': serializers.TicketMetaApplyApplicationSerializer, - 'open': serializers.TicketMetaApplyApplicationApplySerializer, - 'approve': serializers.TicketMetaApplyApplicationApproveSerializer, - }, - const.TicketTypeChoices.login_confirm.value: { - 'default': serializers.TicketMetaLoginConfirmSerializer, - 'open': serializers.TicketMetaLoginConfirmApplySerializer - } - } - } - } diff --git a/apps/tickets/api/ticket/ticket.py b/apps/tickets/api/ticket/ticket.py index 2f9497522..658939d0d 100644 --- a/apps/tickets/api/ticket/ticket.py +++ b/apps/tickets/api/ticket/ticket.py @@ -8,16 +8,15 @@ from rest_framework.exceptions import MethodNotAllowed from common.mixins.api import CommonApiMixin from common.permissions import IsValidUser, IsOrgAdmin from common.const.http import POST, PUT -from tickets import serializers +from tickets import serializers, const from tickets.permissions.ticket import IsAssignee, NotClosed from tickets.models import Ticket -from tickets.api.ticket.mixin import TicketJSONFieldsModelSerializerViewMixin __all__ = ['TicketViewSet'] -class TicketViewSet(TicketJSONFieldsModelSerializerViewMixin, CommonApiMixin, viewsets.ModelViewSet): +class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet): permission_classes = (IsValidUser,) serializer_class = serializers.TicketSerializer serializer_classes = { @@ -64,3 +63,11 @@ class TicketViewSet(TicketJSONFieldsModelSerializerViewMixin, CommonApiMixin, vi @action(detail=True, methods=[PUT], permission_classes=[IsOrgAdmin, IsAssignee, NotClosed]) def close(self, request, *args, **kwargs): return super().update(request, *args, **kwargs) + + def get_dynamic_mapping_fields_mapping_rule(self): + from tickets.serializers.ticket.meta import get_meta_field_mapping_rule_by_view + meta_field_mapping_rule = get_meta_field_mapping_rule_by_view(self) + fields_mapping_rule = { + 'meta': meta_field_mapping_rule, + } + return fields_mapping_rule diff --git a/apps/tickets/migrations/0007_auto_20201224_1821.py b/apps/tickets/migrations/0007_auto_20201224_1821.py index 53d3a0dc1..daa771fa4 100644 --- a/apps/tickets/migrations/0007_auto_20201224_1821.py +++ b/apps/tickets/migrations/0007_auto_20201224_1821.py @@ -55,6 +55,13 @@ def migrate_field_action(old_action, old_status): return ACTION_CLOSE +def migrate_field_assignees_display(assignees_display): + if not assignees_display: + return [] + assignees_display = assignees_display.split(', ') + return assignees_display + + def migrate_tickets_fields_name(apps, schema_editor): ticket_model = apps.get_model("tickets", "Ticket") tickets = ticket_model.origin_objects.all() @@ -64,6 +71,7 @@ def migrate_tickets_fields_name(apps, schema_editor): ticket.applicant_display = ticket.user_display ticket.processor = ticket.assignee ticket.processor_display = ticket.assignee_display + ticket.assignees_display_new = migrate_field_assignees_display(ticket.assignees_display) ticket.action = migrate_field_action(ticket.action, ticket.status) ticket.type = migrate_field_type(ticket.type) ticket.meta = migrate_field_meta(ticket.type, ticket.meta) @@ -100,16 +108,16 @@ class Migration(migrations.Migration): name='processor_display', field=models.CharField(blank=True, default='No', max_length=256, null=True, verbose_name='Processor display'), ), + migrations.AddField( + model_name='ticket', + name='assignees_display_new', + field=models.JSONField(default=list, encoder=tickets.models.ticket.model.ModelJSONFieldEncoder, verbose_name='Assignees display'), + ), migrations.AlterField( model_name='ticket', name='assignees', field=models.ManyToManyField(related_name='assigned_tickets', to=settings.AUTH_USER_MODEL, verbose_name='Assignees'), ), - migrations.AlterField( - model_name='ticket', - name='assignees_display', - field=models.TextField(blank=True, default='No', verbose_name='Assignees display'), - ), migrations.AlterField( model_name='ticket', name='meta', @@ -151,6 +159,15 @@ class Migration(migrations.Migration): model_name='ticket', name='body', ), + migrations.RemoveField( + model_name='ticket', + name='assignees_display', + ), + migrations.RenameField( + model_name='ticket', + old_name='assignees_display_new', + new_name='assignees_display', + ), migrations.AlterModelManagers( name='ticket', managers=[ diff --git a/apps/tickets/models/ticket/mixin/base.py b/apps/tickets/models/ticket/mixin/base.py index c4ea304fb..373c51e28 100644 --- a/apps/tickets/models/ticket/mixin/base.py +++ b/apps/tickets/models/ticket/mixin/base.py @@ -10,8 +10,7 @@ class SetDisplayFieldMixin: def set_assignees_display(self): if self.has_applied: - assignees_display = [str(assignee) for assignee in self.assignees.all()] - self.assignees_display = ', '.join(assignees_display) + self.assignees_display = [str(assignee) for assignee in self.assignees.all()] def set_processor_display(self): if self.has_processed: diff --git a/apps/tickets/models/ticket/model.py b/apps/tickets/models/ticket/model.py index 1f5b0c5d9..f211959b2 100644 --- a/apps/tickets/models/ticket/model.py +++ b/apps/tickets/models/ticket/model.py @@ -65,8 +65,8 @@ class Ticket(TicketModelMixin, CommonModelMixin, OrgModelMixin): assignees = models.ManyToManyField( 'users.User', related_name='assigned_tickets', verbose_name=_("Assignees") ) - assignees_display = models.TextField( - blank=True, default='No', verbose_name=_("Assignees display") + assignees_display = models.JSONField( + encoder=ModelJSONFieldEncoder, default=list, verbose_name=_('Assignees display') ) # 评论 comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) diff --git a/apps/tickets/serializers/comment.py b/apps/tickets/serializers/comment.py index 30ed5144c..4eaf49cf7 100644 --- a/apps/tickets/serializers/comment.py +++ b/apps/tickets/serializers/comment.py @@ -1,6 +1,6 @@ from rest_framework import serializers from ..models import Comment -from common.fields.serializer import ReadableHiddenField +from common.drf.fields import ReadableHiddenField __all__ = ['CommentSerializer'] diff --git a/apps/tickets/serializers/ticket/meta/__init__.py b/apps/tickets/serializers/ticket/meta/__init__.py index 5b44f1c85..9b5ed21c9 100644 --- a/apps/tickets/serializers/ticket/meta/__init__.py +++ b/apps/tickets/serializers/ticket/meta/__init__.py @@ -1,3 +1 @@ -from .apply_asset import * -from .apply_application import * -from .login_confirm import * +from .base import * diff --git a/apps/tickets/serializers/ticket/meta/apply_application.py b/apps/tickets/serializers/ticket/meta/apply_application.py index f316074d5..aa6c1d4a5 100644 --- a/apps/tickets/serializers/ticket/meta/apply_application.py +++ b/apps/tickets/serializers/ticket/meta/apply_application.py @@ -3,18 +3,14 @@ from django.utils.translation import ugettext_lazy as _ from applications.models import Category, Application from assets.models import SystemUser -from .base import BaseTicketMetaApproveSerializerMixin -from common.fields.serializer import JSONFieldModelSerializer -from tickets.models import Ticket +from .mixin import BaseApproveSerializerMixin __all__ = [ - 'TicketMetaApplyApplicationSerializer', - 'TicketMetaApplyApplicationApplySerializer', - 'TicketMetaApplyApplicationApproveSerializer', + 'ApplyApplicationTypeSerializer', 'ApplySerializer', 'ApproveSerializer', ] -class TicketMetaApplyApplicationSerializer(JSONFieldModelSerializer): +class ApplySerializer(serializers.Serializer): # 申请信息 apply_category = serializers.ChoiceField( required=True, choices=Category.choices, label=_('Category') @@ -42,6 +38,19 @@ class TicketMetaApplyApplicationSerializer(JSONFieldModelSerializer): apply_date_expired = serializers.DateTimeField( required=True, label=_('Date expired') ) + + def validate_apply_type(self, tp): + category = self.root.initial_data['meta'].get('apply_category') + if not category: + return tp + valid_type_types = list((dict(Category.get_type_choices(category)).keys())) + if tp in valid_type_types: + return tp + error = _('Type `{}` is not a valid choice `({}){}`'.format(tp, category, valid_type_types)) + raise serializers.ValidationError(error) + + +class ApproveSerializer(BaseApproveSerializerMixin, serializers.Serializer): # 审批信息 approve_applications = serializers.ListField( required=True, child=serializers.UUIDField(), label=_('Approve applications') @@ -66,57 +75,6 @@ class TicketMetaApplyApplicationSerializer(JSONFieldModelSerializer): required=True, label=_('Date expired') ) - class Meta: - model = Ticket - model_field = Ticket.meta - fields = [ - 'apply_category', 'apply_category_display', - 'apply_type', 'apply_type_display', - 'apply_application_group', 'apply_system_user_group', - 'apply_date_start', 'apply_date_expired', - - 'approve_applications', 'approve_applications_snapshot', - 'approve_system_users', 'approve_system_users_snapshot', - 'approve_date_start', 'approve_date_expired' - ] - read_only_fields = fields - - -class TicketMetaApplyApplicationApplySerializer(TicketMetaApplyApplicationSerializer): - - class Meta(TicketMetaApplyApplicationSerializer.Meta): - required_fields = [ - 'apply_category', 'apply_type', - 'apply_application_group', 'apply_system_user_group', - 'apply_date_start', 'apply_date_expired', - ] - read_only_fields = list( - set(TicketMetaApplyApplicationSerializer.Meta.fields) - set(required_fields) - ) - - def validate_apply_type(self, tp): - category = self.root.initial_data['meta'].get('apply_category') - if not category: - return tp - valid_type_types = list((dict(Category.get_type_choices(category)).keys())) - if tp in valid_type_types: - return tp - error = _('Type `{}` is not a valid choice `({}){}`'.format(tp, category, valid_type_types)) - raise serializers.ValidationError(error) - - -class TicketMetaApplyApplicationApproveSerializer(BaseTicketMetaApproveSerializerMixin, - TicketMetaApplyApplicationSerializer): - - class Meta: - required_fields = { - 'approve_applications', 'approve_system_users', - 'approve_date_start', 'approve_date_expired' - } - read_only_fields = list( - set(TicketMetaApplyApplicationSerializer.Meta.fields) - set(required_fields) - ) - def validate_approve_applications(self, approve_applications): application_type = self.root.instance.meta['apply_type'] queries = {'type': application_type} @@ -131,3 +89,9 @@ class TicketMetaApplyApplicationApproveSerializer(BaseTicketMetaApproveSerialize queries = {'protocol': protocol} system_users_id = self.filter_approve_system_users(approve_system_users, queries) return system_users_id + + +class ApplyApplicationTypeSerializer(ApplySerializer, ApproveSerializer): + pass + + diff --git a/apps/tickets/serializers/ticket/meta/apply_asset.py b/apps/tickets/serializers/ticket/meta/apply_asset.py index 3aea29081..7a6023979 100644 --- a/apps/tickets/serializers/ticket/meta/apply_asset.py +++ b/apps/tickets/serializers/ticket/meta/apply_asset.py @@ -2,20 +2,16 @@ 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 .base import BaseTicketMetaApproveSerializerMixin from tickets.models import Ticket - -from common.fields.serializer import JSONFieldModelSerializer +from .mixin import BaseApproveSerializerMixin __all__ = [ - 'TicketMetaApplyAssetSerializer', - 'TicketMetaApplyAssetApplySerializer', - 'TicketMetaApplyAssetApproveSerializer', + 'ApplyAssetTypeSerializer', 'ApplySerializer', 'ApproveSerializer', ] -class TicketMetaApplyAssetSerializer(JSONFieldModelSerializer): +class ApplySerializer(serializers.Serializer): # 申请信息 apply_ip_group = serializers.ListField( required=False, child=serializers.IPAddressField(), label=_('IP group'), @@ -43,6 +39,9 @@ class TicketMetaApplyAssetSerializer(JSONFieldModelSerializer): apply_date_expired = serializers.DateTimeField( required=True, label=_('Date expired') ) + + +class ApproveSerializer(BaseApproveSerializerMixin, serializers.Serializer): # 审批信息 approve_assets = serializers.ListField( required=True, child=serializers.UUIDField(), label=_('Approve assets') @@ -75,47 +74,6 @@ class TicketMetaApplyAssetSerializer(JSONFieldModelSerializer): required=True, label=_('Date expired') ) - class Meta: - model = Ticket - model_field = Ticket.meta - fields = [ - 'apply_ip_group', - 'apply_hostname_group', 'apply_system_user_group', - 'apply_actions', 'apply_actions_display', - 'apply_date_start', 'apply_date_expired', - - 'approve_assets', 'approve_assets_snapshot', - 'approve_system_users', 'approve_system_users_snapshot', - 'approve_actions', 'approve_actions_display', - 'approve_date_start', 'approve_date_expired', - ] - read_only_fields = fields - - -class TicketMetaApplyAssetApplySerializer(TicketMetaApplyAssetSerializer): - - class Meta(TicketMetaApplyAssetSerializer.Meta): - required_fields = [ - 'apply_ip_group', 'apply_hostname_group', 'apply_system_user_group', - 'apply_actions', 'apply_date_start', 'apply_date_expired', - ] - read_only_fields = list( - set(TicketMetaApplyAssetSerializer.Meta.fields) - set(required_fields) - ) - - -class TicketMetaApplyAssetApproveSerializer(BaseTicketMetaApproveSerializerMixin, - TicketMetaApplyAssetSerializer): - - class Meta(TicketMetaApplyAssetSerializer.Meta): - required_fields = [ - 'approve_assets', 'approve_system_users', 'approve_actions', - 'approve_date_start', 'approve_date_expired', - ] - read_only_fields = list( - set(TicketMetaApplyAssetSerializer.Meta.fields) - set(required_fields) - ) - def validate_approve_assets(self, approve_assets): assets_id = self.filter_approve_resources(resource_model=Asset, resources_id=approve_assets) return assets_id @@ -124,3 +82,7 @@ class TicketMetaApplyAssetApproveSerializer(BaseTicketMetaApproveSerializerMixin queries = {'protocol__in': SystemUser.ASSET_CATEGORY_PROTOCOLS} system_users_id = self.filter_approve_system_users(approve_system_users, queries) return system_users_id + + +class ApplyAssetTypeSerializer(ApplySerializer, ApproveSerializer): + pass diff --git a/apps/tickets/serializers/ticket/meta/base.py b/apps/tickets/serializers/ticket/meta/base.py index 6dd3f0e7b..4a53776e9 100644 --- a/apps/tickets/serializers/ticket/meta/base.py +++ b/apps/tickets/serializers/ticket/meta/base.py @@ -1,42 +1,104 @@ import copy -from collections import OrderedDict from rest_framework import serializers -from rest_framework.serializers import ALL_FIELDS from django.utils.translation import ugettext_lazy as _ -from orgs.utils import tmp_to_org -from assets.models import SystemUser +from common.exceptions import JMSException +from tickets import const +from . import apply_asset, apply_application, login_confirm + +__all__ = [ + 'meta_dynamic_mapping_fields_mapping_rules', + 'get_meta_field_mapping_rule_by_view', +] + +# +# ticket type +# ----------- -class BaseTicketMetaApproveSerializerMixin: +types = const.TicketTypeChoices.values +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 type +# ----------- + + +actions = const.TicketActionChoices.values +action_open = const.TicketActionChoices.open.value +action_approve = const.TicketActionChoices.approve.value +action_reject = const.TicketActionChoices.reject.value +action_close = const.TicketActionChoices.close.value + + +# +# define meta field `DynamicMappingField` mapping_rules +# ----------------------------------------------------- + + +meta_dynamic_mapping_fields_mapping_rules = { + 'default': serializers.ReadOnlyField, + 'type': { + type_apply_asset: { + action_open: apply_asset.ApplySerializer, + action_approve: apply_asset.ApproveSerializer, + }, + type_apply_application: { + action_open: apply_application.ApplySerializer, + action_approve: apply_application.ApproveSerializer, + }, + type_login_confirm: { + action_open: login_confirm.ApplySerializer, + } + } +} + + +# +# get meta dynamic field mapping rule by view +# ------------------------------------------- + + +def get_meta_field_mapping_rule_by_view(view): + mapping_rules = copy.deepcopy(meta_dynamic_mapping_fields_mapping_rules) + request = view.request + + # type + tp = request.query_params.get('type') + if not tp: + return ['default'] + if tp not in types: + error = _('Query parameter `type` ({}) not in choices: {}'.format(tp, types)) + raise JMSException(error) + if tp not in mapping_rules['type']: + return ['default'] + + # action + action = view.action + if action in ['metadata']: + # options + action = request.query_params.get('action') + if not action: + error = _('Please carry query parameter `action`') + raise JMSException(error) + if action not in actions: + error = _('Query parameter `action` ({}) not in choices: {}'.format(action, actions)) + raise JMSException(error) + if action not in mapping_rules['type'][tp]: + return ['default'] + + # display + if action in ['list', 'retrieve']: + return ['default'] + + if not mapping_rules['type'][tp].get(action): + return ['default'] + + return ['type', tp, action] + - def _filter_approve_resources_by_org(self, model, resources_id): - with tmp_to_org(self.root.instance.org_id): - org_resources = model.objects.filter(id__in=resources_id) - if not org_resources: - error = _('None of the approved `{}` belong to Organization `{}`' - ''.format(model.__name__, self.root.instance.org_name)) - raise serializers.ValidationError(error) - return org_resources - @staticmethod - def _filter_approve_resources_by_queries(model, resources, queries=None): - if queries: - resources = resources.filter(**queries) - if not resources: - error = _('None of the approved `{}` does not comply with the filtering rules `{}`' - ''.format(model.__name__, queries)) - raise serializers.ValidationError(error) - return resources - def filter_approve_resources(self, resource_model, resources_id, queries=None): - resources = self._filter_approve_resources_by_org(resource_model, resources_id) - resources = self._filter_approve_resources_by_queries(resource_model, resources, queries) - resources_id = list(resources.values_list('id', flat=True)) - return resources_id - def filter_approve_system_users(self, system_users_id, queries=None): - system_users_id = self.filter_approve_resources( - resource_model=SystemUser, resources_id=system_users_id, queries=queries - ) - return system_users_id diff --git a/apps/tickets/serializers/ticket/meta/login_confirm.py b/apps/tickets/serializers/ticket/meta/login_confirm.py index 1dcb8ee0b..d196a8f41 100644 --- a/apps/tickets/serializers/ticket/meta/login_confirm.py +++ b/apps/tickets/serializers/ticket/meta/login_confirm.py @@ -1,15 +1,15 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ -from common.fields.serializer import JSONFieldModelSerializer from tickets.models import Ticket __all__ = [ - 'TicketMetaLoginConfirmSerializer', 'TicketMetaLoginConfirmApplySerializer', + 'ApplySerializer', ] -class TicketMetaLoginConfirmSerializer(JSONFieldModelSerializer): +class ApplySerializer(serializers.Serializer): + # 申请信息 apply_login_ip = serializers.IPAddressField( required=True, label=_('Login ip') ) @@ -20,21 +20,3 @@ class TicketMetaLoginConfirmSerializer(JSONFieldModelSerializer): required=True, label=_('Login datetime') ) - class Meta: - model = Ticket - model_field = Ticket.meta - fields = [ - 'apply_login_ip', 'apply_login_city', 'apply_login_datetime' - ] - read_only_fields = fields - - -class TicketMetaLoginConfirmApplySerializer(TicketMetaLoginConfirmSerializer): - - class Meta(TicketMetaLoginConfirmSerializer.Meta): - required_fields = [ - 'apply_login_ip', 'apply_login_city', 'apply_login_datetime' - ] - read_only_fields = list( - set(TicketMetaLoginConfirmSerializer.Meta.fields) - set(required_fields) - ) diff --git a/apps/tickets/serializers/ticket/meta/mixin.py b/apps/tickets/serializers/ticket/meta/mixin.py new file mode 100644 index 000000000..9539267fb --- /dev/null +++ b/apps/tickets/serializers/ticket/meta/mixin.py @@ -0,0 +1,39 @@ +from rest_framework import serializers +from django.utils.translation import ugettext_lazy as _ + +from orgs.utils import tmp_to_org +from assets.models import SystemUser + + +class BaseApproveSerializerMixin: + + def _filter_approve_resources_by_org(self, model, resources_id): + with tmp_to_org(self.root.instance.org_id): + org_resources = model.objects.filter(id__in=resources_id) + if not org_resources: + error = _('None of the approved `{}` belong to Organization `{}`' + ''.format(model.__name__, self.root.instance.org_name)) + raise serializers.ValidationError(error) + return org_resources + + @staticmethod + def _filter_approve_resources_by_queries(model, resources, queries=None): + if queries: + resources = resources.filter(**queries) + if not resources: + error = _('None of the approved `{}` does not comply with the filtering rules `{}`' + ''.format(model.__name__, queries)) + raise serializers.ValidationError(error) + return resources + + def filter_approve_resources(self, resource_model, resources_id, queries=None): + resources = self._filter_approve_resources_by_org(resource_model, resources_id) + resources = self._filter_approve_resources_by_queries(resource_model, resources, queries) + resources_id = list(resources.values_list('id', flat=True)) + return resources_id + + def filter_approve_system_users(self, system_users_id, queries=None): + system_users_id = self.filter_approve_resources( + resource_model=SystemUser, resources_id=system_users_id, queries=queries + ) + return system_users_id diff --git a/apps/tickets/serializers/ticket/ticket.py b/apps/tickets/serializers/ticket/ticket.py index c5c489244..cf531a353 100644 --- a/apps/tickets/serializers/ticket/ticket.py +++ b/apps/tickets/serializers/ticket/ticket.py @@ -2,12 +2,13 @@ # from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers -from common.fields.serializer import ReadableHiddenField +from common.drf.fields import ReadableHiddenField, DynamicMappingField 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 meta_dynamic_mapping_fields_mapping_rules __all__ = [ 'TicketSerializer', 'TicketDisplaySerializer', @@ -18,8 +19,9 @@ __all__ = [ class TicketSerializer(OrgResourceModelSerializerMixin): type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type')) - status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status')) action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action')) + status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status')) + meta = DynamicMappingField(mapping_rules=meta_dynamic_mapping_fields_mapping_rules) class Meta: model = Ticket diff --git a/apps/tickets/tests.py b/apps/tickets/tests/__init__.py similarity index 100% rename from apps/tickets/tests.py rename to apps/tickets/tests/__init__.py diff --git a/apps/tickets/tests/design/__init__.py b/apps/tickets/tests/design/__init__.py new file mode 100644 index 000000000..e69de29bb 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 new file mode 100644 index 000000000..e96af6e67 --- /dev/null +++ b/apps/tickets/tests/design/architecture_for_view_to_serializer_mapping/__init__.py @@ -0,0 +1,51 @@ +""" +说明: + View 获取 serializer_class 的架构设计 + +问题: + View 所需的 Serializer 中有一个 JSONField 字段,而字段的值并不固定,是由 View 的行为 (比如action) 而决定的. + 使用 View 默认的 get_serializer_class 方法不能解决 + + + 例如: + 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() + +分析: + 问题关键在于数据的映射 + 使用 dict 可以解决,但操作起来比较复杂 + 所以使用 tree 的结构来实现 + + +方案: + view: + 使用元类 MetaClass: 在 View 中动态创建所需要的 Serializer + + serializer: + 实现 DictSerializer: 在 Serializer 中定义 JSONField 字段的映射关系 + 实现 TreeSerializer: 将 DictSerializer 中定义的映射关系转换为 Tree 的结构 + 实现 DictSerializerMetaClass: 在 View 中用来动态创建 Serializer + +实现: + 1. 重写 View 的 get_serializer_class 方法, 实现动态创建 Serializer + 2. 实现 TreeSerializer, 将 DictSerializer 中的 dict 数据结构转化为 tree 的数据结构 + 3. 实现 DictSerializer, 使用 dict 类型来定义映射关系 (*注意: 继承 TreeSerializer) + 4. 实现 DictSerializerMetaClass, 定义如何创建包含字段类型为 TreeSerializer 的 Serializer + +""" + +from rest_framework.serializers import Serializer 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 new file mode 100644 index 000000000..391f956ae --- /dev/null +++ b/apps/tickets/tests/design/architecture_for_view_to_serializer_mapping/serializer.py @@ -0,0 +1,224 @@ +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 new file mode 100644 index 000000000..4cb58b52e --- /dev/null +++ b/apps/tickets/tests/design/architecture_for_view_to_serializer_mapping/view.py @@ -0,0 +1,86 @@ +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 new file mode 100644 index 000000000..3026012c3 --- /dev/null +++ b/apps/tickets/tests/tests.py @@ -0,0 +1,196 @@ +# 测试通过 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/users/serializers/group.py b/apps/users/serializers/group.py index 102b562a8..b7a6d204c 100644 --- a/apps/users/serializers/group.py +++ b/apps/users/serializers/group.py @@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _ from django.db.models import Prefetch from rest_framework import serializers -from common.serializers import AdaptedBulkListSerializer +from common.drf.serializers import AdaptedBulkListSerializer from orgs.mixins.serializers import BulkOrgResourceModelSerializer from django.db.models import Count from ..models import User, UserGroup diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 3d651fa2d..9a3eeb1b9 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -108,3 +108,4 @@ adal==1.2.5 openpyxl==3.0.5 pyexcel==0.6.6 pyexcel-xlsx==0.6.0 +data-tree==0.0.1