feat: 优化工单模块1

pull/5365/head
Bai 2020-12-31 05:07:11 +08:00 committed by Jiangjie.Bai
parent 430e20a49c
commit 1a9a5c28f5
10 changed files with 240 additions and 81 deletions

View File

@ -7,10 +7,13 @@ from collections import defaultdict
from itertools import chain from itertools import chain
from django.db.models.signals import m2m_changed from django.db.models.signals import m2m_changed
from django.utils.translation import ugettext_lazy as _
from django.core.cache import cache from django.core.cache import cache
from django.http import JsonResponse from django.http import JsonResponse
from rest_framework import serializers
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from common.exceptions import JMSException
from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter
from ..utils import lazyproperty from ..utils import lazyproperty
@ -28,19 +31,200 @@ class JSONResponseMixin(object):
return JsonResponse(context) return JsonResponse(context)
class SerializerMixin: class GenericSerializerMixin:
def get_serializer_class(self): serializer_classes = None
def get_serializer_class_by_view_action(self):
if not hasattr(self, 'serializer_classes'):
return None
if not isinstance(self.serializer_classes, dict):
return None
draw = self.request.query_params.get('draw')
serializer_class = None serializer_class = None
if hasattr(self, 'serializer_classes') and isinstance(self.serializer_classes, dict): if draw and self.action in ['list', 'metadata']:
if self.action in ['list', 'metadata'] and self.request.query_params.get('draw'): serializer_class = self.serializer_classes.get('display')
serializer_class = self.serializer_classes.get('display') if serializer_class is None:
if serializer_class is None: serializer_class = self.serializer_classes.get(self.action)
serializer_class = self.serializer_classes.get( if serializer_class is None:
self.action, self.serializer_classes.get('default') serializer_class = self.serializer_classes.get('default')
) return serializer_class
if serializer_class:
def get_serializer_class(self):
serializer_class = self.get_serializer_class_by_view_action()
if serializer_class is None:
serializer_class = super().get_serializer_class()
return serializer_class
class JSONFieldsSerializerMixin:
"""
作用: 获取包含 JSONField 字段的序列类
class TestSerializer(serializers.Serializer):
pass
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': {
'class': TestSerializer,
'attrs': {'required': True},
},
'post': {
'class': TestSerializer,
'attrs': {'required': True}
},
'open': {
'class': TestSerializer,
'attrs': {'required': False}
},
'approve': {
'class': TestSerializer,
},
},
'apply_application': {
'get': {},
'post': {},
'put': {},
},
'login_confirm': {
'get': {},
'post': {},
'put': {},
}
},
'category': {}
},
'json_field_2': {},
'json_field_3': {}
}
"""
json_fields_category_mapping = {}
json_fields_serializer_classes = None
@lazyproperty
def default_json_field_serializer(self):
class DefaultJSONFieldSerializer(serializers.JSONField):
pass
if self.action in ['get', 'list', 'retrieve']:
attrs = {'readonly': False}
else:
attrs = {'readonly': True}
return DefaultJSONFieldSerializer(attrs)
def get_json_field_query_category(self, field, category):
query_category = self.request.query_params.get(category)
category_choices = self.json_fields_category_mapping[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_serializer_classes_by_query_category(self, field):
serializer_classes = None
category_collection = self.json_fields_category_mapping[field]
for category in category_collection:
query_category = self.get_json_field_query_category(field, category)
if not query_category:
continue
category_serializer_classes = self.json_fields_serializer_classes[field][category]
if query_category not in category_serializer_classes.keys():
continue
serializer_classes = category_serializer_classes[query_category]
return serializer_classes
def get_json_field_serializer_info_by_view_action(self, serializer_classes):
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_info = serializer_classes.get(action)
return serializer_info
def get_json_field_serializer_info(self, field):
category_collection = self.json_fields_category_mapping[field]
if category_collection:
serializer_classes = self.get_json_field_serializer_classes_by_query_category(field)
else:
serializer_classes = self.json_fields_serializer_classes[field]
if not serializer_classes:
return None
serializer_info = self.get_json_field_serializer_info_by_view_action(serializer_classes)
return serializer_info
@staticmethod
def new_json_field_serializer(class_info):
serializer_class = class_info['class']
serializer_attrs = class_info.get('attrs', {})
serializer = serializer_class(serializer_attrs)
return serializer
def get_json_field_serializer(self, field):
serializer_info = self.get_json_field_serializer_info(field)
if not serializer_info:
return None
serializer = self.new_json_field_serializer(serializer_info)
return serializer
def get_json_fields_serializer_mapping(self):
"""
return: {
'json_field_1': serializer1(),
'json_field_2': serializer2(),
}
"""
fields_serializer_mapping = {}
fields = self.json_fields_serializer_classes.keys()
for field in fields:
serializer = self.get_json_field_serializer(field)
if serializer is None:
serializer = self.default_json_field_serializer
fields_serializer_mapping[field] = serializer
return fields_serializer_mapping
@staticmethod
def new_include_json_fields_serializer_class(base, attrs):
serializer_class_name = ''.join([
field_serializer.__class__.__name__ for field_serializer in attrs.values()
])
serializer_class = type(serializer_class_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):
return serializer_class return serializer_class
return super().get_serializer_class() fields_serializer_mapping = self.get_json_fields_serializer_mapping()
if not fields_serializer_mapping:
return serializer_class
serializer_class = self.new_include_json_fields_serializer_class(
base=serializer_class, attrs=fields_serializer_mapping
)
return serializer_class
class SerializerMixin(JSONFieldsSerializerMixin, GenericSerializerMixin):
pass
class ExtraFilterFieldsMixin: class ExtraFilterFieldsMixin:

View File

@ -2,65 +2,44 @@ from common.exceptions import JMSException
from tickets import const, serializers from tickets import const, serializers
__all__ = ['TicketMetaSerializerViewMixin'] __all__ = ['TicketJSONFieldsSerializerViewMixin']
class TicketMetaSerializerViewMixin: class TicketJSONFieldsSerializerViewMixin:
apply_asset_meta_serializer_classes = { json_fields_category_mapping = {
'open': serializers.TicketMetaApplyAssetApplySerializer, 'meta': {
'approve': serializers.TicketMetaApplyAssetApproveSerializer, 'type': const.TicketTypeChoices.values,
},
} }
apply_application_meta_serializer_classes = { json_fields_serializer_classes = {
'open': serializers.TicketMetaApplyApplicationApplySerializer, 'meta': {
'approve': serializers.TicketMetaApplyApplicationApproveSerializer, 'type': {
const.TicketTypeChoices.apply_asset.value: {
'open': {
'class': serializers.TicketMetaApplyAssetApplySerializer,
'attrs': {'required': True}
},
'approve': {
'class': serializers.TicketMetaApplyAssetApproveSerializer,
'attrs': {'required': True}
}
},
const.TicketTypeChoices.apply_application.value: {
'open': {
'class': serializers.TicketMetaApplyApplicationApplySerializer,
'attrs': {'required': True}
},
'approve': {
'class': serializers.TicketMetaApplyApplicationApproveSerializer,
'attrs': {'required': True}
}
},
const.TicketTypeChoices.login_confirm.value: {
'open': {
'class': serializers.TicketMetaLoginConfirmApplySerializer,
'attrs': {'required': True}
}
}
}
}
} }
login_confirm_meta_serializer_classes = {
'open': serializers.TicketMetaLoginConfirmApplySerializer,
}
meta_serializer_classes = {
const.TicketTypeChoices.apply_asset.value: apply_asset_meta_serializer_classes,
const.TicketTypeChoices.apply_application.value: apply_application_meta_serializer_classes,
const.TicketTypeChoices.login_confirm.value: login_confirm_meta_serializer_classes,
}
def get_serializer_meta_field_class(self):
tp = self.request.query_params.get('type')
if not tp:
return None
tp_choices = const.TicketTypeChoices.types()
if tp not in tp_choices:
raise JMSException(
'Invalid query parameter `type`, select from the following options: {}'
''.format(tp_choices)
)
meta_class = self.meta_serializer_classes.get(tp, {}).get(self.action)
return meta_class
def get_serializer_meta_field(self):
if self.action not in ['open', 'approve']:
return None
meta_class = self.get_serializer_meta_field_class()
if not meta_class:
return None
return meta_class(required=True)
def reset_view_metadata_action(self):
if self.action not in ['metadata']:
return
view_action = self.request.query_params.get('action')
if not view_action:
raise JMSException('The `metadata` methods must carry parameter `action`')
setattr(self, 'action', view_action)
def get_serializer_class(self):
self.reset_view_metadata_action()
serializer_class = super().get_serializer_class()
if getattr(self, 'swagger_fake_view', False):
return serializer_class
meta_field = self.get_serializer_meta_field()
if not meta_field:
return serializer_class
serializer_class = type(
meta_field.__class__.__name__, (serializer_class,), {'meta': meta_field}
)
return serializer_class

View File

@ -11,13 +11,13 @@ from common.const.http import POST, PUT
from tickets import serializers from tickets import serializers
from tickets.permissions.ticket import IsAssignee, NotClosed from tickets.permissions.ticket import IsAssignee, NotClosed
from tickets.models import Ticket from tickets.models import Ticket
from tickets.api.ticket.mixin import TicketMetaSerializerViewMixin from tickets.api.ticket.mixin import TicketJSONFieldsSerializerViewMixin
__all__ = ['TicketViewSet'] __all__ = ['TicketViewSet']
class TicketViewSet(TicketMetaSerializerViewMixin, CommonApiMixin, viewsets.ModelViewSet): class TicketViewSet(TicketJSONFieldsSerializerViewMixin, CommonApiMixin, viewsets.ModelViewSet):
permission_classes = (IsValidUser,) permission_classes = (IsValidUser,)
serializer_class = serializers.TicketSerializer serializer_class = serializers.TicketSerializer
serializer_classes = { serializer_classes = {

View File

@ -10,10 +10,6 @@ class TicketTypeChoices(TextChoices):
apply_asset = 'apply_asset', _('Apply for asset') apply_asset = 'apply_asset', _('Apply for asset')
apply_application = 'apply_application', _('Apply for application') apply_application = 'apply_application', _('Apply for application')
@classmethod
def types(cls):
return set(dict(cls.choices).keys())
class TicketActionChoices(TextChoices): class TicketActionChoices(TextChoices):
open = 'open', _('Open') open = 'open', _('Open')

View File

@ -113,7 +113,7 @@ class Migration(migrations.Migration):
migrations.AlterField( migrations.AlterField(
model_name='ticket', model_name='ticket',
name='meta', name='meta',
field=models.JSONField(encoder=tickets.models.ticket.ModelJSONFieldEncoder, verbose_name='Meta'), field=models.JSONField(default=dict, encoder=tickets.models.ticket.model.ModelJSONFieldEncoder, verbose_name='Meta'),
), ),
migrations.AlterField( migrations.AlterField(
model_name='ticket', model_name='ticket',

View File

@ -1 +1 @@
from .ticket import * from .model import *

View File

@ -1 +1 @@
from .meta import * from .base import *

View File

@ -14,7 +14,7 @@ from orgs.utils import tmp_to_root_org, tmp_to_org
from tickets import const from tickets import const
from .mixin import TicketModelMixin from .mixin import TicketModelMixin
__all__ = ['Ticket'] __all__ = ['Ticket', 'ModelJSONFieldEncoder']
class ModelJSONFieldEncoder(json.JSONEncoder): class ModelJSONFieldEncoder(json.JSONEncoder):
@ -36,7 +36,7 @@ class Ticket(TicketModelMixin, CommonModelMixin, OrgModelMixin):
max_length=64, choices=const.TicketTypeChoices.choices, max_length=64, choices=const.TicketTypeChoices.choices,
default=const.TicketTypeChoices.general.value, verbose_name=_("Type") default=const.TicketTypeChoices.general.value, verbose_name=_("Type")
) )
meta = models.JSONField(encoder=ModelJSONFieldEncoder, verbose_name=_("Meta")) meta = models.JSONField(encoder=ModelJSONFieldEncoder, default=dict, verbose_name=_("Meta"))
action = models.CharField( action = models.CharField(
choices=const.TicketActionChoices.choices, max_length=16, choices=const.TicketActionChoices.choices, max_length=16,
default=const.TicketActionChoices.open.value, verbose_name=_("Action") default=const.TicketActionChoices.open.value, verbose_name=_("Action")

View File

@ -60,7 +60,7 @@ class TicketApplySerializer(TicketActionSerializer):
] ]
read_only_fields = list(set(TicketDisplaySerializer.Meta.fields) - set(required_fields)) read_only_fields = list(set(TicketDisplaySerializer.Meta.fields) - set(required_fields))
extra_kwargs = { extra_kwargs = {
'type': {'required': True} 'type': {'required': True},
} }
def validate_type(self, tp): def validate_type(self, tp):