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

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

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

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

Co-authored-by: Bai <bugatti_it@163.com>
pull/5382/head
fit2bot 2021-01-05 23:39:38 +08:00 committed by GitHub
parent 3188692691
commit 17a01a12db
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 164 additions and 1000 deletions

View File

@ -5,13 +5,12 @@ from orgs.mixins.api import OrgBulkModelViewSet
from ..hands import IsOrgAdminOrAppUser
from .. import models, serializers
from .mixin import ApplicationViewMixin
__all__ = ['ApplicationViewSet']
class ApplicationViewSet(ApplicationViewMixin, OrgBulkModelViewSet):
class ApplicationViewSet(OrgBulkModelViewSet):
model = models.Application
filter_fields = ('name', 'type', 'category')
search_fields = filter_fields

View File

@ -1,16 +1,7 @@
from orgs.models import Organization
from ..serializers.utils import get_dynamic_mapping_fields_mapping_rule_by_view
__all__ = ['ApplicationViewMixin', 'SerializeApplicationToTreeNodeMixin']
class ApplicationViewMixin:
""" 实现 `get_dynamic_mapping_fields_mapping_rule` 方法, 供其他和 Application 相关的 View 继承使用"""
def get_dynamic_mapping_fields_mapping_rule(self):
fields_mapping_rule = get_dynamic_mapping_fields_mapping_rule_by_view(view=self)
return fields_mapping_rule
__all__ = ['SerializeApplicationToTreeNodeMixin']
class SerializeApplicationToTreeNodeMixin:

View File

@ -4,20 +4,37 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
from common.drf.fields import DynamicMappingField
from .attrs import get_attrs_field_dynamic_mapping_rules
from common.drf.serializers import DynamicMappingSerializer
from .attrs import attrs_field_dynamic_mapping_serializers
from .. import models
__all__ = [
'ApplicationSerializer',
'IncludeDynamicMappingSerializerFieldApplicationSerializerMixin',
]
class ApplicationSerializer(BulkOrgResourceModelSerializer):
class IncludeDynamicMappingSerializerFieldApplicationSerializerMixin(serializers.Serializer):
attrs = DynamicMappingSerializer(mapping_serializers=attrs_field_dynamic_mapping_serializers)
def get_attrs_mapping_path(self, mapping_serializers):
request = self.context['request']
query_type = request.query_params.get('type')
query_category = request.query_params.get('category')
if query_type:
mapping_path = ['type', query_type]
elif query_category:
mapping_path = ['category', query_category]
else:
mapping_path = ['default']
return mapping_path
class ApplicationSerializer(IncludeDynamicMappingSerializerFieldApplicationSerializerMixin,
BulkOrgResourceModelSerializer):
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category'))
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type'))
attrs = DynamicMappingField(mapping_rules=get_attrs_field_dynamic_mapping_rules())
class Meta:
model = models.Application
@ -33,3 +50,4 @@ class ApplicationSerializer(BulkOrgResourceModelSerializer):
_attrs = self.instance.attrs if self.instance else {}
_attrs.update(attrs)
return _attrs

View File

@ -1,7 +1,7 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from ..category import RemoteAppSerializer
from ..application_category import RemoteAppSerializer
__all__ = ['ChromeSerializer']

View File

@ -2,7 +2,7 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from ..category import RemoteAppSerializer
from ..application_category import RemoteAppSerializer
__all__ = ['CustomSerializer']

View File

@ -1,4 +1,4 @@
from ..category import CloudSerializer
from ..application_category import CloudSerializer
__all__ = ['K8SSerializer']

View File

@ -1,7 +1,7 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from ..category import DBSerializer
from ..application_category import DBSerializer
__all__ = ['MySQLSerializer']

View File

@ -1,7 +1,7 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from ..category import RemoteAppSerializer
from ..application_category import RemoteAppSerializer
__all__ = ['MySQLWorkbenchSerializer']

View File

@ -1,7 +1,7 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from ..category import DBSerializer
from ..application_category import DBSerializer
__all__ = ['OracleSerializer']

View File

@ -1,7 +1,7 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from ..category import DBSerializer
from ..application_category import DBSerializer
__all__ = ['PostgreSerializer']

View File

@ -1,7 +1,7 @@
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from ..category import RemoteAppSerializer
from ..application_category import RemoteAppSerializer
__all__ = ['VMwareClientSerializer']

View File

@ -1,12 +1,11 @@
import copy
from rest_framework import serializers
from applications import const
from common.drf.fields import IgnoreSensitiveInfoReadOnlyJSONField
from . import category, type as application_type
from . import application_category, application_type
__all__ = [
'get_attrs_field_dynamic_mapping_rules', 'get_attrs_field_mapping_rule_by_view',
'get_serializer_by_application_type',
'attrs_field_dynamic_mapping_serializers',
'get_serializer_class_by_application_type',
]
@ -35,16 +34,15 @@ type_custom = const.ApplicationTypeChoices.custom.value
type_k8s = const.ApplicationTypeChoices.k8s.value
# define `attrs` field `DynamicMappingField` mapping_rules
# -----------------------------------------------------
# define `attrs` field `dynamic mapping serializers`
# --------------------------------------------------
__ATTRS_FIELD_DYNAMIC_MAPPING_RULES = {
'default': IgnoreSensitiveInfoReadOnlyJSONField,
attrs_field_dynamic_mapping_serializers = {
'category': {
category_db: category.DBSerializer,
category_remote_app: category.RemoteAppSerializer,
category_cloud: category.CloudSerializer,
category_db: application_category.DBSerializer,
category_remote_app: application_category.RemoteAppSerializer,
category_cloud: application_category.CloudSerializer,
},
'type': {
# db
@ -63,32 +61,5 @@ __ATTRS_FIELD_DYNAMIC_MAPPING_RULES = {
}
# Note:
# The dynamic mapping rules of `attrs` field is obtained
# through call method `get_attrs_field_dynamic_mapping_rules`
def get_attrs_field_dynamic_mapping_rules():
return copy.deepcopy(__ATTRS_FIELD_DYNAMIC_MAPPING_RULES)
# get `attrs dynamic field` mapping rule by `view object`
# ----------------------------------------------------
def get_attrs_field_mapping_rule_by_view(view):
query_type = view.request.query_params.get('type')
query_category = view.request.query_params.get('category')
if query_type:
mapping_rule = ['type', query_type]
elif query_category:
mapping_rule = ['category', query_category]
else:
mapping_rule = ['default']
return mapping_rule
# get `category` mapping `serializer`
# -----------------------------------
def get_serializer_by_application_type(app_tp):
return __ATTRS_FIELD_DYNAMIC_MAPPING_RULES['type'].get(app_tp)
def get_serializer_class_by_application_type(_application_type):
return attrs_field_dynamic_mapping_serializers['type'].get(_application_type)

View File

@ -31,8 +31,8 @@ class RemoteAppConnectionInfoSerializer(serializers.ModelSerializer):
"""
返回Guacamole需要的RemoteApp配置参数信息中的parameters参数
"""
from .attrs import get_serializer_by_application_type
serializer_class = get_serializer_by_application_type(obj.type)
from .attrs import get_serializer_class_by_application_type
serializer_class = get_serializer_class_by_application_type(obj.type)
fields = serializer_class().get_fields()
parameters = [obj.type]

View File

@ -1,16 +0,0 @@
from .attrs import get_attrs_field_mapping_rule_by_view
__all__ = [
'get_dynamic_mapping_fields_mapping_rule_by_view'
]
#
# get `dynamic fields` mapping rule by `view object`
# ----------------------------------------------------
def get_dynamic_mapping_fields_mapping_rule_by_view(view):
return {
'attrs': get_attrs_field_mapping_rule_by_view(view=view),
}

View File

@ -6,88 +6,10 @@ from rest_framework import serializers
__all__ = [
'DynamicMappingField', 'ReadableHiddenField',
'CustomMetaDictField', 'IgnoreSensitiveInfoReadOnlyJSONField',
'ReadableHiddenField', 'CustomMetaDictField',
]
#
# DynamicMappingField
# -------------------
class DynamicMappingField(serializers.Field):
"""
一个可以根据用户行为而动态改变的字段
For example, Define attribute `mapping_rules`
field_name = meta
mapping_rules = {
'default': serializers.JSONField(),
'type': {
'apply_asset': {
'default': serializers.CharField(label='default'),
'get': ApplyAssetSerializer,
'post': ApproveAssetSerializer,
},
'apply_application': ApplyApplicationSerializer,
'login_confirm': LoginConfirmSerializer,
'login_times': LoginTimesSerializer
},
'category': {
'apply': ApplySerializer,
'login': LoginSerializer
}
}
"""
def __init__(self, mapping_rules, *args, **kwargs):
assert isinstance(mapping_rules, dict), (
'`mapping_rule` argument expect type `dict`, gut get `{}`'
''.format(type(mapping_rules))
)
self.__mapping_rules = mapping_rules
super().__init__(*args, **kwargs)
@property
def mapping_rules(self):
return copy.deepcopy(self.__mapping_rules)
def to_internal_value(self, data):
""" 实际是一个虚拟字段所以不返回任何值 """
pass
def to_representation(self, value):
""" 实际是一个虚拟字段所以不返回任何值 """
pass
# A Ignore read-only fields for sensitive information
# ----------------------------------------------------------
class IgnoreSensitiveInfoReadOnlyJSONField(serializers.JSONField):
""" A ignore read-only fields for sensitive information """
def __init__(self, **kwargs):
kwargs['read_only'] = True
super().__init__(**kwargs)
def to_representation(self, value):
sensitive_ignored_value = {}
sensitive_names = ['password']
for field_name, field_value in value.items():
for sensitive_name in sensitive_names:
if sensitive_name in field_name.lower():
continue
sensitive_ignored_value[field_name] = field_value
return super().to_representation(sensitive_ignored_value)
#
# ReadableHiddenField
# -------------------

View File

@ -5,133 +5,98 @@ from rest_framework.serializers import ModelSerializer
from rest_framework_bulk.serializers import BulkListSerializer
from common.mixins import BulkListSerializerMixin
from common.drf.fields import DynamicMappingField
from django.utils.functional import cached_property
from rest_framework.utils.serializer_helpers import BindingDict
from common.mixins.serializers import BulkSerializerMixin
from common.utils import QuickLookupDict
__all__ = [
'IncludeDynamicMappingFieldSerializerMetaClass',
'DynamicMappingSerializer',
'EmptySerializer', 'BulkModelSerializer', 'AdaptedBulkListSerializer', 'CeleryTaskSerializer'
]
#
# IncludeDynamicMappingFieldSerializerMetaClass
# ---------------------------------------------
# DynamicMappingSerializer
# ------------------------
class IncludeDynamicMappingFieldSerializerMetaClass(serializers.SerializerMetaclass, type):
"""
SerializerMetaClass: 动态创建包含 `common.drf.fields.DynamicMappingField` 字段的 `SerializerClass`
* Process only fields of type `DynamicMappingField` in `_declared_fields`
* 只处理 `_declared_fields` 中类型为 `DynamicMappingField` 的字段
class DynamicMappingSerializer(serializers.Serializer):
data_type_error_messages = 'Expect get instance of type `{}`, but got instance type of `{}`'
根据 `attrs['dynamic_mapping_fields_mapping_rule']` 中指定的 `fields_mapping_rule`,
`DynamicMappingField` 中匹配出满足给定规则的字段, 并使用匹配到的字段替换自身的 `DynamicMappingField`
def __init__(self, mapping_serializers=None, get_mapping_serializers_method_name=None,
get_mapping_path_method_name=None, default_serializer=None, **kwargs):
self.mapping_serializers = mapping_serializers
self.get_mapping_serializers_method_name = get_mapping_serializers_method_name
self.get_mapping_path_method_name = get_mapping_path_method_name
self.default_serializer = default_serializer or serializers.Serializer
super().__init__(**kwargs)
* 注意: 如果未能根据给定的匹配规则获取到对应的字段先获取与给定规则同级的 `default` 字段
如果仍未获取到则再获取 `DynamicMappingField`中定义的最外层的 `default` 字段
def bind(self, field_name, parent):
# The get mapping serializers method name defaults to `get_{field_name}_mapping_serializers`
if self.get_mapping_serializers_method_name is None:
method_name = 'get_{field_name}_mapping_serializers'.format(field_name=field_name)
self.get_mapping_serializers_method_name = method_name
* 说明: 如果获取到的不是 `serializers.Field` 类型, 则返回 `DynamicMappingField()`
# The get mapping rule method name defaults to `get_{field_name}_mapping_path`.
if self.get_mapping_path_method_name is None:
method_name = 'get_{field_name}_mapping_path'.format(field_name=field_name)
self.get_mapping_path_method_name = method_name
For example, define attrs['dynamic_mapping_fields_mapping_rule']:
super().bind(field_name, parent)
mapping_rules = {
'default': serializer.JSONField,
'type': {
'apply_asset': {
'default': serializer.ChoiceField(),
'get': serializer.CharField()
}
}
}
meta = DynamicMappingField(mapping_rules=mapping_rules)
def get_mapping_serializers(self):
if self.mapping_serializers is not None:
return self.mapping_serializers
method = getattr(self.parent, self.get_mapping_serializers_method_name)
return method()
dynamic_mapping_fields_mapping_rule = {'meta': ['type', 'apply_asset', 'get'],}
=> Got `serializer.CharField()`
* or *
dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset.get',}}
=> Got `serializer.CharField()`
* or *
dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset.',}}
=> Got serializer.ChoiceField(),
* or *
dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset.xxx',}}
=> Got `serializer.ChoiceField()`
* or *
dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset.get.xxx',}}
=> Got `serializer.JSONField()`
* or *
dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset',}}
=> Got `{'get': {}}`, type is not `serializers.Field`, So `meta` is `DynamicMappingField()`
"""
def get_mapping_path(self, mapping_serializers):
method = getattr(self.parent, self.get_mapping_path_method_name)
return method(mapping_serializers)
@classmethod
def get_dynamic_mapping_fields(mcs, bases, attrs):
fields = {}
@staticmethod
def mapping(mapping_serializers, mapping_path):
quick_lookup_dict = QuickLookupDict(data=mapping_serializers)
serializer = quick_lookup_dict.get(key_path=mapping_path)
return serializer
# get `fields mapping rule` from attrs `dynamic_mapping_fields_mapping_rule`
fields_mapping_rule = attrs.get('dynamic_mapping_fields_mapping_rule')
# check `fields_mapping_rule` type
assert isinstance(fields_mapping_rule, dict), (
'`dynamic_mapping_fields_mapping_rule` must be `dict` type , but get `{}`'
''.format(type(fields_mapping_rule))
def get_mapped_serializer(self):
mapping_serializers = self.get_mapping_serializers()
assert isinstance(mapping_serializers, dict), (
self.data_type_error_messages.format('dict', type(mapping_serializers))
)
mapping_path = self.get_mapping_path(mapping_serializers)
assert isinstance(mapping_path, list), (
self.data_type_error_messages.format('list', type(mapping_path))
)
serializer = self.mapping(mapping_serializers, mapping_path)
return serializer
# get `serializer class` declared fields
declared_fields = mcs._get_declared_fields(bases, attrs)
declared_fields_names = list(declared_fields.keys())
fields_mapping_rule = copy.deepcopy(fields_mapping_rule)
for field_name, field_mapping_rule in fields_mapping_rule.items():
if field_name not in declared_fields_names:
continue
declared_field = declared_fields[field_name]
if not isinstance(declared_field, DynamicMappingField):
continue
assert isinstance(field_mapping_rule, (list, str)), (
'`dynamic_mapping_fields_mapping_rule.field_mapping_rule` '
'- can be either a list of keys, or a delimited string. '
'Such as: `["type", "apply_asset", "get"]` or `type.apply_asset.get` '
'but, get type is `{}`, `{}`'
''.format(type(field_mapping_rule), field_mapping_rule)
)
if isinstance(field_mapping_rule, str):
field_mapping_rule = field_mapping_rule.split('.')
# construct `field mapping rules` sequence list
field_mapping_rules = [
field_mapping_rule,
copy.deepcopy(field_mapping_rule)[:-1] + ['default'],
['default']
]
dynamic_field = declared_field
field_finder = QuickLookupDict(dynamic_field.mapping_rules)
field = field_finder.find_one(key_paths=field_mapping_rules)
if isinstance(field, type):
field = field()
if not isinstance(field, serializers.Field):
continue
fields[field_name] = field
@cached_property
def mapped_serializer(self):
serializer = self.get_mapped_serializer()
if serializer is None:
serializer = self.default_serializer
if isinstance(serializer, type):
serializer = serializer()
return serializer
def get_fields(self):
fields = self.mapped_serializer.get_fields()
return fields
@cached_property
def fields(self):
"""
重写此方法因为在 BindingDict 中要设置每一个 field parent `mapped_serializer`,
这样在调用 field.parent , 才会达到预期的结果
比如: serializers.SerializerMethodField
"""
fields = BindingDict(self.mapped_serializer)
for key, value in self.get_fields().items():
fields[key] = value
return fields
def __new__(mcs, name, bases, attrs):
dynamic_mapping_fields = mcs.get_dynamic_mapping_fields(bases, attrs)
attrs.update(dynamic_mapping_fields)
return super().__new__(mcs, name, bases, attrs)
#
# Other Serializer

View File

@ -13,7 +13,6 @@ from rest_framework.response import Response
from rest_framework.settings import api_settings
from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter
from common.drf.serializers import IncludeDynamicMappingFieldSerializerMetaClass
from ..utils import lazyproperty
__all__ = [
@ -29,12 +28,11 @@ class JSONResponseMixin(object):
return JsonResponse(context)
#
# GenericSerializerMixin
# SerializerMixin
# ----------------------
class GenericSerializerMixin:
class SerializerMixin:
""" 根据用户请求动作的不同,获取不同的 `serializer_class `"""
serializer_classes = None
@ -65,65 +63,6 @@ class GenericSerializerMixin:
return serializer_class
#
# IncludeDynamicMappingFieldSerializerViewMixin
# ---------------------------------------------
class IncludeDynamicMappingFieldSerializerViewMixin(GenericSerializerMixin):
"""
动态创建 `view` 使用的 `serializer_class`,
根据用户请求行为的不同, 构造出获取 `serializer_class` `common.drf.fields.DynamicMappingField` 字段
的映射规则, 并通过 `IncludeDynamicMappingFieldSerializerMetaClass` 元类
基于父类的 `serializer_class` 构造出的映射规则 `dynamic_mapping_fields_mapping_rule`
创建出满足要求的新的 `serializer_class`
* 重写 get_dynamic_mapping_fields_mapping_rule 方法:
For example,
def get_dynamic_mapping_fields_mapping_rule(self):
return {'meta': ['type', 'apply_asset', 'get']
"""
def get_dynamic_mapping_fields_mapping_rule(self):
"""
return:
{
'meta': ['type', 'apply_asset', 'get'],
'meta2': 'category.login'
}
"""
return {}
@staticmethod
def _create_serializer_class(base, attrs):
serializer_class = IncludeDynamicMappingFieldSerializerMetaClass(
base.__name__, (base, ), attrs
)
return serializer_class
def get_serializer_class(self):
serializer_class = super().get_serializer_class()
if getattr(self, 'swagger_fake_view', False):
return serializer_class
fields_mapping_rule = self.get_dynamic_mapping_fields_mapping_rule()
if not fields_mapping_rule:
return serializer_class
attrs = {'dynamic_mapping_fields_mapping_rule': fields_mapping_rule}
serializer_class = self._create_serializer_class(base=serializer_class, attrs=attrs)
return serializer_class
class SerializerMixin(IncludeDynamicMappingFieldSerializerViewMixin):
pass
class ExtraFilterFieldsMixin:
"""
额外的 api filter

View File

@ -7,7 +7,6 @@ from rest_framework.generics import ListAPIView
from common.permissions import IsOrgAdminOrAppUser
from common.mixins.api import CommonApiMixin
from applications.models import Application
from applications.api.mixin import ApplicationViewMixin
from perms import serializers
__all__ = [
@ -15,7 +14,7 @@ __all__ = [
]
class UserGroupGrantedApplicationsApi(ApplicationViewMixin, CommonApiMixin, ListAPIView):
class UserGroupGrantedApplicationsApi(CommonApiMixin, ListAPIView):
"""
获取用户组直接授权的应用
"""

View File

@ -5,7 +5,7 @@ from rest_framework.response import Response
from common.mixins.api import CommonApiMixin
from applications.api.mixin import (
SerializeApplicationToTreeNodeMixin, ApplicationViewMixin
SerializeApplicationToTreeNodeMixin
)
from perms import serializers
from perms.api.asset.user_permission.mixin import ForAdminMixin, ForUserMixin
@ -22,7 +22,7 @@ __all__ = [
]
class AllGrantedApplicationsMixin(ApplicationViewMixin, CommonApiMixin, ListAPIView):
class AllGrantedApplicationsMixin(CommonApiMixin, ListAPIView):
only_fields = serializers.ApplicationGrantedSerializer.Meta.only_fields
serializer_class = serializers.ApplicationGrantedSerializer
filter_fields = ['id', 'name', 'category', 'type', 'comment']

View File

@ -6,8 +6,7 @@ from django.utils.translation import ugettext_lazy as _
from assets.models import SystemUser
from applications.models import Application
from applications.serializers.attrs import get_attrs_field_dynamic_mapping_rules
from common.drf.fields import DynamicMappingField
from applications.serializers import IncludeDynamicMappingSerializerFieldApplicationSerializerMixin
__all__ = [
'ApplicationGrantedSerializer', 'ApplicationSystemUserSerializer'
@ -27,13 +26,13 @@ class ApplicationSystemUserSerializer(serializers.ModelSerializer):
read_only_fields = fields
class ApplicationGrantedSerializer(serializers.ModelSerializer):
class ApplicationGrantedSerializer(IncludeDynamicMappingSerializerFieldApplicationSerializerMixin,
serializers.ModelSerializer):
"""
被授权应用的数据结构
"""
category_display = serializers.ReadOnlyField(source='get_category_display', label=_('Category'))
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type'))
attrs = DynamicMappingField(mapping_rules=get_attrs_field_dynamic_mapping_rules())
class Meta:
model = Application

View File

@ -12,7 +12,6 @@ from common.permissions import IsValidUser, IsOrgAdmin
from tickets import serializers
from tickets.models import Ticket
from tickets.permissions.ticket import IsAssignee, NotClosed
from tickets.serializers.ticket.utils import get_dynamic_mapping_fields_mapping_rule_by_view
__all__ = ['TicketViewSet']
@ -66,6 +65,3 @@ class TicketViewSet(CommonApiMixin, viewsets.ModelViewSet):
def close(self, request, *args, **kwargs):
return super().update(request, *args, **kwargs)
def get_dynamic_mapping_fields_mapping_rule(self):
fields_mapping_rule = get_dynamic_mapping_fields_mapping_rule_by_view(view=self)
return fields_mapping_rule

View File

@ -1 +0,0 @@
from .ticket import *

View File

@ -4,7 +4,7 @@ from applications.models import Application
from applications.const import ApplicationCategoryChoices, ApplicationTypeChoices
from assets.models import SystemUser
from perms.models import ApplicationPermission
from tickets.utils import convert_model_data_field_name_to_verbose_name
from tickets.utils import convert_model_instance_data_field_name_to_verbose_name
class ConstructDisplayFieldMixin:
@ -67,11 +67,11 @@ class ConstructBodyMixin:
def construct_apply_application_approved_body(self):
# 审批信息
approve_applications_snapshot = self.meta['approve_applications_snapshot']
approve_applications_snapshot_display = convert_model_data_field_name_to_verbose_name(
approve_applications_snapshot_display = convert_model_instance_data_field_name_to_verbose_name(
Application, approve_applications_snapshot
)
approve_system_users_snapshot = self.meta['approve_system_users_snapshot']
approve_system_users_snapshot_display = convert_model_data_field_name_to_verbose_name(
approve_system_users_snapshot_display = convert_model_instance_data_field_name_to_verbose_name(
SystemUser, approve_system_users_snapshot
)
approve_date_start = self.meta['approve_date_start']

View File

@ -3,7 +3,7 @@ from django.utils.translation import ugettext as __
from perms.models import AssetPermission, Action
from assets.models import Asset, SystemUser
from orgs.utils import tmp_to_org, tmp_to_root_org
from tickets.utils import convert_model_data_field_name_to_verbose_name
from tickets.utils import convert_model_instance_data_field_name_to_verbose_name
class ConstructDisplayFieldMixin:
@ -69,11 +69,11 @@ class ConstructBodyMixin:
def construct_apply_asset_approved_body(self):
approve_assets_snapshot = self.meta['approve_assets_snapshot']
approve_assets_snapshot_display = convert_model_data_field_name_to_verbose_name(
approve_assets_snapshot_display = convert_model_instance_data_field_name_to_verbose_name(
Asset, approve_assets_snapshot
)
approve_system_users_snapshot = self.meta['approve_system_users_snapshot']
approve_system_users_snapshot_display = convert_model_data_field_name_to_verbose_name(
approve_system_users_snapshot_display = convert_model_instance_data_field_name_to_verbose_name(
SystemUser, approve_system_users_snapshot
)
approve_actions_display = self.meta['approve_actions_display']

View File

@ -19,7 +19,7 @@ class TicketCreatePermissionMixin(meta.CreatePermissionMixin, base.CreatePermiss
class TicketCreateCommentMixin(base.CreateCommentMixin):
""" 创建 ticket 评论"""
""" 创建 ticket 备注"""
pass

View File

@ -1,3 +1,2 @@
from .ticket import *
from .meta import *
from .utils import *

View File

@ -1,14 +1,10 @@
import copy
from common.drf.fields import IgnoreSensitiveInfoReadOnlyJSONField
from tickets import const
from . import apply_asset, apply_application, login_confirm
from .ticket_type import apply_asset, apply_application, login_confirm
__all__ = [
'get_meta_field_dynamic_mapping_rules',
'get_meta_field_mapping_rule_by_view',
'meta_field_dynamic_mapping_serializers',
]
#
# ticket type
# -----------
@ -17,7 +13,6 @@ type_apply_asset = const.TicketTypeChoices.apply_asset.value
type_apply_application = const.TicketTypeChoices.apply_application.value
type_login_confirm = const.TicketTypeChoices.login_confirm.value
#
# ticket action
# -------------
@ -29,13 +24,11 @@ action_reject = const.TicketActionChoices.reject.value
action_close = const.TicketActionChoices.close.value
#
# defines the dynamic mapping rules for the DynamicMappingField `meta`
# --------------------------------------------------------------------
# defines `meta` field dynamic mapping serializers
# ------------------------------------------------
__META_FIELD_DYNAMIC_MAPPING_RULES = {
'default': IgnoreSensitiveInfoReadOnlyJSONField,
meta_field_dynamic_mapping_serializers = {
'type': {
type_apply_asset: {
action_open: apply_asset.ApplySerializer,
@ -52,22 +45,3 @@ __META_FIELD_DYNAMIC_MAPPING_RULES = {
}
# Note:
# The dynamic mapping rules of `meta` field is obtained
# through call method `get_meta_field_dynamic_mapping_rules`
def get_meta_field_dynamic_mapping_rules():
return copy.deepcopy(__META_FIELD_DYNAMIC_MAPPING_RULES)
#
# get `meta dynamic field` mapping rule by `view object`
# ------------------------------------------------------
def get_meta_field_mapping_rule_by_view(view):
query_type = view.request.query_params.get('type')
query_action = view.request.query_params.get('action')
action = query_action if query_action else view.action
mapping_rule = ['type', query_type, action]
return mapping_rule

View File

@ -2,12 +2,11 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from perms.serializers import ActionsField
from assets.models import Asset, SystemUser
from tickets.models import Ticket
from .mixin import BaseApproveSerializerMixin
__all__ = [
'ApplyAssetTypeSerializer', 'ApplySerializer', 'ApproveSerializer',
'ApplyAssetSerializer', 'ApplySerializer', 'ApproveSerializer',
]
@ -84,5 +83,5 @@ class ApproveSerializer(BaseApproveSerializerMixin, serializers.Serializer):
return system_users_id
class ApplyAssetTypeSerializer(ApplySerializer, ApproveSerializer):
class ApplyAssetSerializer(ApplySerializer, ApproveSerializer):
pass

View File

@ -1,7 +1,7 @@
from rest_framework import serializers
from django.utils.translation import ugettext_lazy as _
from tickets.models import Ticket
__all__ = [
'ApplySerializer',

View File

@ -2,13 +2,14 @@
#
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers
from common.drf.fields import ReadableHiddenField, DynamicMappingField
from common.drf.fields import ReadableHiddenField
from common.drf.serializers import DynamicMappingSerializer
from orgs.utils import get_org_by_id
from orgs.mixins.serializers import OrgResourceModelSerializerMixin
from users.models import User
from tickets import const
from tickets.models import Ticket
from .meta import get_meta_field_dynamic_mapping_rules
from .meta import meta_field_dynamic_mapping_serializers
__all__ = [
'TicketSerializer', 'TicketDisplaySerializer',
@ -21,7 +22,7 @@ class TicketSerializer(OrgResourceModelSerializerMixin):
type_display = serializers.ReadOnlyField(source='get_type_display', label=_('Type'))
action_display = serializers.ReadOnlyField(source='get_action_display', label=_('Action'))
status_display = serializers.ReadOnlyField(source='get_status_display', label=_('Status'))
meta = DynamicMappingField(mapping_rules=get_meta_field_dynamic_mapping_rules())
meta = DynamicMappingSerializer(mapping_serializers=meta_field_dynamic_mapping_serializers)
class Meta:
model = Ticket
@ -35,6 +36,15 @@ class TicketSerializer(OrgResourceModelSerializerMixin):
'body'
]
def get_meta_mapping_path(self, mapping_serializers):
view = self.context['view']
request = self.context['request']
query_type = request.query_params.get('type')
query_action = request.query_params.get('action')
action = query_action if query_action else view.action
mapping_path = ['type', query_type, action]
return mapping_path
class TicketDisplaySerializer(TicketSerializer):

View File

@ -1,16 +0,0 @@
from .meta import get_meta_field_mapping_rule_by_view
__all__ = [
'get_dynamic_mapping_fields_mapping_rule_by_view'
]
#
# get `dynamic fields` mapping rule by `view object`
# ----------------------------------------------------
def get_dynamic_mapping_fields_mapping_rule_by_view(view):
return {
'meta': get_meta_field_mapping_rule_by_view(view=view)
}

View File

@ -1,69 +0,0 @@
"""
说明:
View 获取 serializer_class 的架构设计
问题:
View 所需的 Serializer Class 中有一个字段字段的类型并不固定
而是由 View 的行为 (比如 type, action) 来决定的.
使用 View 默认的 get_serializer_class 方法不能实现因为序列类在被定义的时候其字段及类型已经固定
所以需要一种机制来动态修改序列类中的字段及其类型MetaClass 元类
例如:
class MetaASerializer(serializers.Serializer):
name = serializers.CharField(label='Name')
class MetaBSerializer(serializers.Serializer):
age = serializers.IntegerField(label='Age')
class Serializer(serializers.Serializer):
meta = serializers.JSONField()
view 获取 serializer 无论 action 是什么, 获取到的 Serializer.meta 字段始终是
serializers.JSONField() 类型
但我们希望:
view.action = A 获取到的 Serializer.meta MetaASerializerMetaASerializer()
view.action = B 获取到的 Serializer.meta MetaBSerializerMetaASerializer()
分析:
问题关键在于数据映射规则的定义和匹配,
View 给定的规则动态去匹配 Serializer Class 中定义的规则, 从而创建出想要的序列类
当然, 使用 dict 可以很好的解决规则定义问题但要直接进行匹配, 操作起来比较复杂
所以, 决定使用以下方案实现, , dict, dict-> tree 数据类型转化, tree 搜索:
Serializer Class 中规则的定义使用 dict,
View 中指定的规则也使用 dict,
MetaClass 中进行规则匹配的过程使用 dict tree, 即将给定的 dic 转换为 tree 的数据结构再进行匹配,
* dict -> tree 的转化使用 `data-tree` 库来实现
方案:
view:
使用元类 MetaClass: View 中动态创建所需要的 Serializer Class
serializer:
实现 DynamicMappingField: 序列类的动态映射字段, 用来定义字段的映射规则
实现 IncludeDynamicMappingFieldSerializerMetaClass:
View 中用来动态创建 Serializer Class.
, View 中给定的规则去匹配 Serializer Class 里每一个 DynamicMappingField 字段定义的规则,
基于 bases, 创建并返回新的 Serializer Class
实现 IncludeDynamicMappingFieldSerializerViewMixin:
实现动态创建 Serializer Class 的逻辑,
同时定义获取 Serializer Class 中所有 DynamicMappingField 字段匹配规则的方法
=>
def get_dynamic_mapping_fields_mapping_rule(self):
return {
'dynamic_mapping_field_name1': `mapping_path`,
'dynamic_mapping_field_name2': `mapping_path',
}
View 子类重写
实现:
请看 ./serializer.py ./serializer.py
"""

View File

@ -1,226 +0,0 @@
import copy
import data_tree
from rest_framework import serializers
#
# IncludeDynamicMappingFieldSerializerMetaClass
# ---------------------------------------------
class IncludeDynamicMappingFieldSerializerMetaClass(serializers.SerializerMetaclass, type):
"""
SerializerMetaClass: 动态创建包含 `common.drf.fields.DynamicMappingField` 字段的 `SerializerClass`
* Process only fields of type `DynamicMappingField` in `_declared_fields`
* 只处理 `_declared_fields` 中类型为 `DynamicMappingField` 的字段
根据 `attrs['dynamic_mapping_fields_mapping_rule']` 中指定的 `fields_mapping_rule`,
`DynamicMappingField` 中匹配出满足给定规则的字段, 并使用匹配到的字段替换自身的 `DynamicMappingField`
* 注意: 如果未能根据给定的匹配规则获取到对应的字段先获取与给定规则同级的 `default` 字段
如果仍未获取到则再获取 `DynamicMappingField`中定义的最外层的 `default` 字段
* 说明: 如果获取到的不是 `serializers.Field` 类型, 则返回 `DynamicMappingField()`
For example, define attrs['dynamic_mapping_fields_mapping_rule']:
mapping_rules = {
'default': serializer.JSONField,
'type': {
'apply_asset': {
'default': serializer.ChoiceField(),
'get': serializer.CharField()
}
}
}
meta = DynamicMappingField(mapping_rules=mapping_rules)
dynamic_mapping_fields_mapping_rule = {'meta': ['type', 'apply_asset', 'get'],}
=> Got `serializer.CharField()`
* or *
dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset.get',}}
=> Got `serializer.CharField()`
* or *
dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset.',}}
=> Got serializer.ChoiceField(),
* or *
dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset.xxx',}}
=> Got `serializer.ChoiceField()`
* or *
dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset.get.xxx',}}
=> Got `serializer.JSONField()`
* or *
dynamic_mapping_fields_mapping_rule = {{'meta': 'type.apply_asset',}}
=> Got `{'get': {}}`, type is not `serializers.Field`, So `meta` is `DynamicMappingField()`
"""
@classmethod
def get_dynamic_mapping_fields(mcs, bases, attrs):
fields = {}
fields_mapping_rules = attrs.get('dynamic_mapping_fields_mapping_rule')
assert isinstance(fields_mapping_rules, dict), (
'`dynamic_mapping_fields_mapping_rule` must be `dict` type , but get `{}`'
''.format(type(fields_mapping_rules))
)
fields_mapping_rules = copy.deepcopy(fields_mapping_rules)
declared_fields = mcs._get_declared_fields(bases, attrs)
for field_name, field_mapping_rule in fields_mapping_rules.items():
assert isinstance(field_mapping_rule, (list, str)), (
'`dynamic_mapping_fields_mapping_rule.field_mapping_rule` '
'- can be either a list of keys, or a delimited string. '
'Such as: `["type", "apply_asset", "get"]` or `type.apply_asset.get` '
'but, get type is `{}`, `{}`'
''.format(type(field_mapping_rule), field_mapping_rule)
)
if field_name not in declared_fields.keys():
continue
declared_field = declared_fields[field_name]
if not isinstance(declared_field, DynamicMappingField):
continue
dynamic_field = declared_field
mapping_tree = dynamic_field.mapping_tree.copy()
def get_field(rule):
return mapping_tree.get(arg_path=rule)
if isinstance(field_mapping_rule, str):
field_mapping_rule = field_mapping_rule.split('.')
field_mapping_rule[-1] = field_mapping_rule[-1] or 'default'
field = get_field(rule=field_mapping_rule)
if not field:
field_mapping_rule[-1] = 'default'
field = get_field(rule=field_mapping_rule)
if field is None:
field_mapping_rule = ['default']
field = get_field(rule=field_mapping_rule)
if isinstance(field, type):
field = field()
if not isinstance(field, serializers.Field):
continue
fields[field_name] = field
return fields
def __new__(mcs, name, bases, attrs):
dynamic_mapping_fields = mcs.get_dynamic_mapping_fields(bases, attrs)
attrs.update(dynamic_mapping_fields)
return super().__new__(mcs, name, bases, attrs)
#
# DynamicMappingField
# ----------------------------------
class DynamicMappingField(serializers.Field):
""" 一个根据用户行为而动态匹配的字段 """
def __init__(self, mapping_rules, *args, **kwargs):
assert isinstance(mapping_rules, dict), (
'`mapping_rule` argument expect type `dict`, gut get `{}`'
''.format(type(mapping_rules))
)
assert 'default' in mapping_rules, (
"mapping_rules['default'] is a required, but only get `{}`"
"".format(list(mapping_rules.keys()))
)
self.mapping_rules = mapping_rules
self.mapping_tree = self._build_mapping_tree()
super().__init__(*args, **kwargs)
def _build_mapping_tree(self):
tree = data_tree.Data_tree_node(arg_data=self.mapping_rules)
return tree
def to_internal_value(self, data):
""" 实际是一个虚拟字段所以不返回任何值 """
pass
def to_representation(self, value):
""" 实际是一个虚拟字段所以不返回任何值 """
pass
#
# Test data
# ----------------------------------
# ticket type
class ApplyAssetSerializer(serializers.Serializer):
apply_asset = serializers.CharField(label='Apply Asset')
class ApproveAssetSerializer(serializers.Serializer):
approve_asset = serializers.CharField(label='Approve Asset')
class ApplyApplicationSerializer(serializers.Serializer):
apply_application = serializers.CharField(label='Application')
class LoginConfirmSerializer(serializers.Serializer):
login_ip = serializers.IPAddressField()
class LoginTimesSerializer(serializers.Serializer):
login_times = serializers.IntegerField()
# ticket category
class ApplySerializer(serializers.Serializer):
apply_datetime = serializers.DateTimeField()
class LoginSerializer(serializers.Serializer):
login_datetime = serializers.DateTimeField()
meta_mapping_rules = {
'default': serializers.JSONField(),
'type': {
'apply_asset': {
'default': serializers.CharField(label='default'),
'get': ApplyAssetSerializer,
'post': ApproveAssetSerializer,
},
'apply_application': ApplyApplicationSerializer,
'login_confirm': LoginConfirmSerializer,
'login_times': LoginTimesSerializer
},
'category': {
'apply': ApplySerializer,
'login': LoginSerializer
}
}
class TicketSerializer(serializers.Serializer):
title = serializers.CharField(label='Title')
type = serializers.ChoiceField(choices=('apply_asset', 'apply_application'), label='Type')
meta1 = DynamicMappingField(mapping_rules=meta_mapping_rules)
meta2 = DynamicMappingField(mapping_rules=meta_mapping_rules)
meta3 = DynamicMappingField(mapping_rules=meta_mapping_rules)
meta4 = DynamicMappingField(mapping_rules=meta_mapping_rules)

View File

@ -1,86 +0,0 @@
from tickets.tests.design.architecture_for_view_to_serializer_mapping.serializer import (
IncludeDynamicMappingFieldSerializerMetaClass, TicketSerializer
)
#
# IncludeDynamicMappingFieldSerializerViewMixin
# ---------------------------------------------
class IncludeDynamicMappingFieldSerializerViewMixin:
"""
动态创建 `view` 使用的 `serializer_class`,
根据用户请求行为的不同, 构造出获取 `serializer_class` `common.drf.fields.DynamicMappingField` 字段
的映射规则, 并通过 `IncludeDynamicMappingFieldSerializerMetaClass` 元类
基于父类的 `serializer_class` 构造出的映射规则 `dynamic_mapping_fields_mapping_rule`
创建出满足要求的新的 `serializer_class`
* 重写 get_dynamic_mapping_fields_mapping_rule 方法:
For example,
def get_dynamic_mapping_fields_mapping_rule(self):
return {'meta': ['type', 'apply_asset', 'get']
"""
def get_dynamic_mapping_fields_mapping_rule(self):
"""
return:
{
'meta': ['type', 'apply_asset', 'get'],
'meta2': 'category.login'
}
"""
print(self)
return {
'meta1': ['type', 'apply_asset', 'getX', 'asdf'],
'meta2': 'category.login',
'meta3': 'type.apply_asset.',
'meta4': 'category.apply'
}
@staticmethod
def _create_serializer_class(base, attrs):
serializer_class = IncludeDynamicMappingFieldSerializerMetaClass(
base.__name__, (base, ), attrs
)
return serializer_class
def get_serializer_class(self):
serializer_class = super().get_serializer_class()
fields_mapping_rule = self.get_dynamic_mapping_fields_mapping_rule()
if not fields_mapping_rule:
return serializer_class
attrs = {'dynamic_mapping_fields_mapping_rule': fields_mapping_rule}
serializer_class = self._create_serializer_class(base=serializer_class, attrs=attrs)
return serializer_class
#
# Test data
# ---------
class GenericViewSet(object):
def get_serializer_class(self):
return TicketSerializer
class TicketViewSet(IncludeDynamicMappingFieldSerializerViewMixin, GenericViewSet):
pass
view = TicketViewSet()
_serializer_class = view.get_serializer_class()
_serializer = _serializer_class()
print(_serializer_class)
print(_serializer)

View File

@ -1,196 +0,0 @@
# 测试通过 view 动态创建 serializer
class BaseSerializerMetaClass(type):
def __new__(mcs, name, bases, attrs):
attrs.update({'color': 'blank'})
return super().__new__(mcs, name, bases, attrs)
class BaseSerializer(metaclass=BaseSerializerMetaClass):
x_id = 'id_value'
class Serializer(BaseSerializer):
x_name = 'name_value'
x_hobby = {
'music': 'chinese',
'ball': 'basketball'
}
x_age = {
'real': 19,
'fake': 27
}
# custom metaclass
class SerializerMetaClass(BaseSerializerMetaClass, type):
@classmethod
def _get_declared_x_attr_value(mcs, x_types, attr_name, attr_value):
pass
@classmethod
def _get_declared_x_attrs(mcs, bases, attrs):
x_types = attrs['view'].x_types
bases_attrs = {}
for base in bases:
for k in dir(base):
if not k.startswith('x_'):
continue
v = getattr(base, k)
if isinstance(v, str):
bases_attrs[k] = v
continue
if isinstance(v, dict):
v = mcs._get_declared_x_attr_value( x_types, k, v)
bases_attrs[k] = v
attrs.update(bases_attrs)
return attrs
def __new__(mcs, name, bases, attrs):
attrs = mcs._get_declared_x_attrs(bases, attrs)
return super().__new__(mcs, name, bases, attrs)
class View(object):
x_types = ['x_age', 'fake']
serializer_class = Serializer
def get_serializer_class(self):
return self.serializer_class
def build_serializer_class(self):
serializer_class = self.get_serializer_class()
serializer_class = SerializerMetaClass(
serializer_class.__name__, (serializer_class,), {'view': self}
)
return serializer_class
view = View()
serializer = view.build_serializer_class()
print('End!')
#
from rest_framework.serializers import SerializerMetaclass
data = {
'meta': {
'type': {
'apply_asset': {
'get': 'get',
'post': 'post'
}
}
}
}
def get_value(keys_dict, data_dict):
def _get_value(_key_list, _data_dict):
if len(_key_list) == 0:
return _data_dict
for i, key in enumerate(_key_list):
_keys = _key_list[i+1:]
__data_dict = _data_dict.get(key)
if __data_dict is None:
return _data_dict
if not isinstance(__data_dict, dict):
return __data_dict
return _get_value(_keys, __data_dict)
_values_dict = {}
for field, keys in keys_dict.items():
keys.insert(0, field)
_values_dict[field] = _get_value(keys, data_dict)
return _values_dict
keys_dict_list = {
'meta': ['type', 'apply_asset', 'get']
}
values_dict = get_value(keys_dict_list, data)
print(values_dict)
keys_dict_list = {
'meta': ['type', 'apply_asset', 'post']
}
values_dict = get_value(keys_dict_list, data)
print(values_dict)
keys_dict_list = {
'meta': ['type', 'apply_asset', 'post', 'dog']
}
values_dict = get_value(keys_dict_list, data)
print(values_dict)
keys_dict_list = {
'meta': ['type', 'apply_asset', 'dog']
}
values_dict = get_value(keys_dict_list, data)
print(values_dict)
#
class A:
def __init__(self):
self.a = 'A'
get_action_serializer = 'GETSerializer'
post_action_serializer = 'POSTSerializer'
apply_action_serializer = A()
apply_asset_tree_serializer = {
'get': get_action_serializer,
'post': post_action_serializer,
'apply': apply_action_serializer
}
type_tree_serializer = {
'apply_asset': apply_asset_tree_serializer,
}
meta_tree_serializer = {
'type': type_tree_serializer,
}
json_fields_serializer_mapping = {
'meta': meta_tree_serializer
}
def data_dict_to_tree(data_dict):
import data_tree
t = data_tree.Data_tree_node(arg_data=data_dict)
return t
tree = data_dict_to_tree(json_fields_serializer_mapping)
def get_tree_node(t, path):
return t.get(path, arg_default_value_to_return='Not Found')
node = get_tree_node(tree, 'meta.type.apply_asset.get')
print(node)
node = get_tree_node(tree, 'meta.type.apply_asset.post')
print(node)
node = get_tree_node(tree, 'meta.type.apply_asset.apply')
print(node)
node = get_tree_node(tree, 'meta.type.apply_asset.xxxx')
print(node)

View File

@ -11,30 +11,23 @@ from . import const
logger = get_logger(__file__)
def convert_model_data_field_name_to_verbose_name(model, name_data):
"""将Model以field_name为key的数据转换为以field_verbose_name为key的数据"""
if isinstance(name_data, dict):
name_data = [name_data]
model_fields_name_verbose_name_mapping = {
def get_model_field_verbose_name(model, field_name):
field_name_field_verbose_name_mapping = {
field.name: field.verbose_name for field in model._meta.fields
}
field_name = field_name.split('__', 1)[0]
field_verbose_name = field_name_field_verbose_name_mapping.get(field_name, field_name)
return field_verbose_name
def get_verbose_name(field_name):
verbose_name = model_fields_name_verbose_name_mapping.get(field_name)
if not verbose_name:
other_name = field_name.split('__', 1)[0]
verbose_name = model_fields_name_verbose_name_mapping.get(other_name)
if not verbose_name:
verbose_name = field_name
return verbose_name
verbose_name_data = [
{get_verbose_name(name): value for name, value in d.items()}
for d in name_data
def convert_model_instance_data_field_name_to_verbose_name(model, data):
if isinstance(data, dict):
data = [data]
converted_data = [
{get_model_field_verbose_name(model, name): value for name, value in d.items()}
for d in data
]
return verbose_name_data
return converted_data
def send_ticket_applied_mail_to_assignees(ticket, assignees):