feat: 抽象View Mapping Serializer架构设计; 重构工单View、Serializer模块 (#5371)

* perf: 优化工单模块(修改迁移文件->Model assignees_display 字段类型为list)

* ignore: try `view` `serializer jsonfields` Map design (1)

* ignore: try `view` `serializer jsonfields` Map design (2)

* ignore: try `view` `serializer jsonfields` Map design (3)

* ignore: try `view` `serializer jsonfields` Map design (4)

* ignore: try `view` `serializer jsonfields` Map design (5)

* ignore: try `view` `serializer.DynamicMappingField` Mapping design (6)

* feat: 抽象view_mapping_serializer逻辑架构; 重构工单View、Serializer模块

* feat: 抽象view_mapping_serializer逻辑架构; 重构工单View、Serializer模块(2)

* feat: 抽象view_mapping_serializer逻辑架构; 重构工单View、Serializer模块(3)

* feat: 抽象view_mapping_serializer逻辑架构; 重构工单View、Serializer模块(4)

Co-authored-by: Bai <bugatti_it@163.com>
pull/5372/head
fit2bot 2021-01-02 07:25:23 +08:00 committed by GitHub
parent 5c483084b7
commit cef93abb2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1137 additions and 795 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
#
from .form import *
from .model import *
from .serializer import *

View File

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

View File

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

View File

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

View File

@ -1,6 +0,0 @@
"""
老的代码统一到 `apps/common/drf/serializers.py`
之后此文件废弃
"""
from common.drf.serializers import AdaptedBulkListSerializer, CeleryTaskSerializer

View File

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

View File

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

View File

@ -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__ = [

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
#
from rest_framework import serializers
from common.serializers import AdaptedBulkListSerializer
from common.drf.serializers import AdaptedBulkListSerializer
from ..models import RemoteAppPermission

View File

@ -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__ = [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1 @@
from .apply_asset import *
from .apply_application import *
from .login_confirm import *
from .base import *

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

View File

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

View File

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

View File

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

196
apps/tickets/tests/tests.py Normal file
View File

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

View File

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

View File

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