Merge branch 'api_perf' into lina

pull/3948/head
ibuler 2020-04-29 15:47:46 +08:00
commit b6f5b335bd
6 changed files with 174 additions and 51 deletions

View File

@ -36,7 +36,7 @@ class AdminUserViewSet(OrgBulkModelViewSet):
def get_queryset(self): def get_queryset(self):
queryset = super().get_queryset() queryset = super().get_queryset()
queryset = queryset.annotate(_assets_amount=Count('assets')) queryset = queryset.annotate(assets_amount=Count('assets'))
return queryset return queryset
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from rest_framework import serializers from rest_framework import serializers
from django.db.models import Prefetch, F from django.db.models import Prefetch, F, Count
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -73,21 +73,35 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
class Meta: class Meta:
model = Asset model = Asset
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
fields = [ fields_mini = ['id', 'hostname', 'ip']
'id', 'ip', 'hostname', 'protocol', 'port', fields_small = fields_mini + [
'protocols', 'platform', 'is_active', 'public_ip', 'domain', 'protocol', 'port', 'protocols', 'is_active', 'public_ip',
'admin_user', 'nodes', 'labels', 'number', 'vendor', 'model', 'sn', 'number', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
'cpu_model', 'cpu_count', 'cpu_cores', 'cpu_vcpus', 'memory', 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
'disk_total', 'disk_info', 'os', 'os_version', 'os_arch', 'os', 'os_version', 'os_arch', 'hostname_raw', 'comment',
'hostname_raw', 'comment', 'created_by', 'date_created', 'created_by', 'date_created', 'hardware_info',
'hardware_info',
] ]
read_only_fields = ( fields_fk = [
'admin_user', 'domain', 'platform'
]
fk_only_fields = {
'platform': ['name']
}
fields_m2m = [
'nodes', 'labels',
]
annotates_fields = {
# 'admin_user_display': 'admin_user__name'
}
fields_as = list(annotates_fields.keys())
fields = fields_small + fields_fk + fields_m2m + fields_as
read_only_fields = [
'vendor', 'model', 'sn', 'cpu_model', 'cpu_count', 'vendor', 'model', 'sn', 'cpu_model', 'cpu_count',
'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info', 'cpu_cores', 'cpu_vcpus', 'memory', 'disk_total', 'disk_info',
'os', 'os_version', 'os_arch', 'hostname_raw', 'os', 'os_version', 'os_arch', 'hostname_raw',
'created_by', 'date_created', 'created_by', 'date_created',
) ] + fields_as
extra_kwargs = { extra_kwargs = {
'protocol': {'write_only': True}, 'protocol': {'write_only': True},
'port': {'write_only': True}, 'port': {'write_only': True},
@ -98,11 +112,7 @@ class AssetSerializer(BulkOrgResourceModelSerializer):
@classmethod @classmethod
def setup_eager_loading(cls, queryset): def setup_eager_loading(cls, queryset):
""" Perform necessary eager loading of data. """ """ Perform necessary eager loading of data. """
queryset = queryset.prefetch_related( queryset = queryset.select_related('admin_user', 'domain', 'platform')
Prefetch('nodes', queryset=Node.objects.all().only('id')),
Prefetch('labels', queryset=Label.objects.all().only('id')),
).select_related('admin_user', 'domain', 'platform') \
.annotate(platform_base=F('platform__base'))
return queryset return queryset
def compatible_with_old_protocol(self, validated_data): def compatible_with_old_protocol(self, validated_data):

View File

@ -1,13 +1,15 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# #
from collections import Iterable
from django.db.models import Prefetch, F
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from rest_framework.utils import html from rest_framework.utils import html
from rest_framework.settings import api_settings from rest_framework.settings import api_settings
from rest_framework.exceptions import ValidationError from rest_framework.exceptions import ValidationError
from rest_framework.fields import SkipField, empty from rest_framework.fields import SkipField, empty
__all__ = ['BulkSerializerMixin', 'BulkListSerializerMixin'] __all__ = ['BulkSerializerMixin', 'BulkListSerializerMixin', 'CommonSerializerMixin']
class BulkSerializerMixin(object): class BulkSerializerMixin(object):
@ -113,3 +115,121 @@ class BulkListSerializerMixin(object):
raise ValidationError(errors) raise ValidationError(errors)
return ret return ret
class BaseDynamicFieldsPlugin:
def __init__(self, serializer):
self.serializer = serializer
def can_dynamic(self):
try:
request = self.serializer.context['request']
method = request.method
except (AttributeError, TypeError, KeyError):
# The serializer was not initialized with request context.
return False
if method != 'GET':
return False
return True
def get_request(self):
return self.serializer.context['request']
def get_query_params(self):
request = self.get_request()
try:
query_params = request.query_params
except AttributeError:
# DRF 2
query_params = getattr(request, 'QUERY_PARAMS', request.GET)
return query_params
def get_exclude_field_names(self):
return set()
class QueryFieldsMixin(BaseDynamicFieldsPlugin):
# https://github.com/wimglenn/djangorestframework-queryfields/
# If using Django filters in the API, these labels mustn't conflict with any model field names.
include_arg_name = 'fields'
exclude_arg_name = 'fields!'
# Split field names by this string. It doesn't necessarily have to be a single character.
# Avoid RFC 1738 reserved characters i.e. ';', '/', '?', ':', '@', '=' and '&'
delimiter = ','
def get_exclude_field_names(self):
query_params = self.get_query_params()
includes = query_params.getlist(self.include_arg_name)
include_field_names = {name for names in includes for name in names.split(self.delimiter) if name}
excludes = query_params.getlist(self.exclude_arg_name)
exclude_field_names = {name for names in excludes for name in names.split(self.delimiter) if name}
if not include_field_names and not exclude_field_names:
# No user fields filtering was requested, we have nothing to do here.
return []
serializer_field_names = set(self.serializer.fields)
fields_to_drop = serializer_field_names & exclude_field_names
if include_field_names:
fields_to_drop |= serializer_field_names - include_field_names
return fields_to_drop
class SizedModelFieldsMixin(BaseDynamicFieldsPlugin):
arg_name = 'fields_size'
def can_dynamic(self):
if not hasattr(self.serializer, 'Meta'):
return False
can = super().can_dynamic()
return can
def get_exclude_field_names(self):
query_params = self.get_query_params()
size = query_params.get(self.arg_name)
if not size:
return []
size_fields = getattr(self.serializer.Meta, 'fields_{}'.format(size), None)
if not size_fields or not isinstance(size_fields, Iterable):
return []
serializer_field_names = set(self.serializer.fields)
fields_to_drop = serializer_field_names - set(size_fields)
return fields_to_drop
class DynamicFieldsMixin:
dynamic_fields_plugins = [QueryFieldsMixin, SizedModelFieldsMixin]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
exclude_field_names = set()
for cls in self.dynamic_fields_plugins:
plugin = cls(self)
if not plugin.can_dynamic():
continue
exclude_field_names |= set(plugin.get_exclude_field_names())
for field in exclude_field_names or []:
self.fields.pop(field, None)
class EagerLoadQuerySetFields:
def setup_eager_loading(self, queryset):
""" Perform necessary eager loading of data. """
queryset = queryset.prefetch_related(
Prefetch('nodes'),
Prefetch('labels'),
).select_related('admin_user', 'domain', 'platform') \
.annotate(platform_base=F('platform__base'))
return queryset
class CommonSerializerMixin(DynamicFieldsMixin):
pass

View File

@ -5,7 +5,7 @@ from rest_framework import serializers
from rest_framework.validators import UniqueTogetherValidator from rest_framework.validators import UniqueTogetherValidator
from common.validators import ProjectUniqueValidator from common.validators import ProjectUniqueValidator
from common.mixins import BulkSerializerMixin from common.mixins import BulkSerializerMixin, CommonSerializerMixin
from ..utils import get_current_org_id_for_serializer from ..utils import get_current_org_id_for_serializer
@ -16,7 +16,7 @@ __all__ = [
] ]
class OrgResourceSerializerMixin(serializers.Serializer): class OrgResourceSerializerMixin(CommonSerializerMixin, serializers.Serializer):
""" """
通过API批量操作资源时, 自动给每个资源添加所需属性org_id的值为current_org_id 通过API批量操作资源时, 自动给每个资源添加所需属性org_id的值为current_org_id
(同时为serializer.is_valid()对Model的unique_together校验做准备) (同时为serializer.is_valid()对Model的unique_together校验做准备)

View File

@ -31,7 +31,6 @@ class UserViewSet(CommonApiMixin, UserQuerysetMixin, BulkModelViewSet):
search_fields = filter_fields search_fields = filter_fields
serializer_classes = { serializer_classes = {
'default': serializers.UserSerializer, 'default': serializers.UserSerializer,
'display': serializers.UserDisplaySerializer
} }
permission_classes = (IsOrgAdmin, CanUpdateDeleteUser) permission_classes = (IsOrgAdmin, CanUpdateDeleteUser)

View File

@ -4,7 +4,7 @@ from django.utils.translation import ugettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from common.utils import validate_ssh_public_key from common.utils import validate_ssh_public_key
from common.mixins import BulkSerializerMixin from common.mixins import CommonSerializerMixin
from common.serializers import AdaptedBulkListSerializer from common.serializers import AdaptedBulkListSerializer
from common.permissions import CanUpdateDeleteUser from common.permissions import CanUpdateDeleteUser
from ..models import User from ..models import User
@ -13,7 +13,7 @@ from ..models import User
__all__ = [ __all__ = [
'UserSerializer', 'UserPKUpdateSerializer', 'UserSerializer', 'UserPKUpdateSerializer',
'ChangeUserPasswordSerializer', 'ResetOTPSerializer', 'ChangeUserPasswordSerializer', 'ResetOTPSerializer',
'UserProfileSerializer', 'UserDisplaySerializer', 'UserProfileSerializer',
] ]
@ -22,7 +22,7 @@ class UserOrgSerializer(serializers.Serializer):
name = serializers.CharField() name = serializers.CharField()
class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer): class UserSerializer(CommonSerializerMixin, serializers.ModelSerializer):
EMAIL_SET_PASSWORD = _('Reset link will be generated and sent to the user') EMAIL_SET_PASSWORD = _('Reset link will be generated and sent to the user')
CUSTOM_PASSWORD = _('Set password') CUSTOM_PASSWORD = _('Set password')
PASSWORD_STRATEGY_CHOICES = ( PASSWORD_STRATEGY_CHOICES = (
@ -33,18 +33,27 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
choices=PASSWORD_STRATEGY_CHOICES, required=False, initial=0, choices=PASSWORD_STRATEGY_CHOICES, required=False, initial=0,
label=_('Password strategy'), write_only=True label=_('Password strategy'), write_only=True
) )
can_update = serializers.SerializerMethodField()
can_delete = serializers.SerializerMethodField()
class Meta: class Meta:
model = User model = User
list_serializer_class = AdaptedBulkListSerializer list_serializer_class = AdaptedBulkListSerializer
fields = [ # mini 是指能识别对象的最小单元
'id', 'name', 'username', 'password', 'email', 'public_key', fields_mini = ['id', 'name', 'username']
'groups', 'role', 'wechat', 'phone', 'mfa_level', # small 指的是 不需要计算的直接能从一张表中获取到的数据
fields_small = fields_mini + [
'password', 'email', 'public_key', 'wechat', 'phone', 'mfa_level',
'comment', 'source', 'is_valid', 'is_expired', 'comment', 'source', 'is_valid', 'is_expired',
'is_active', 'created_by', 'is_first_login', 'is_active', 'created_by', 'is_first_login',
'password_strategy', 'date_password_last_updated', 'date_expired', 'password_strategy', 'date_password_last_updated', 'date_expired',
'avatar_url', 'avatar_url', 'source_display',
] ]
fields = fields_small + [
'groups', 'role', 'groups_display', 'role_display',
'can_update', 'can_delete'
]
extra_kwargs = { extra_kwargs = {
'password': {'write_only': True, 'required': False, 'allow_null': True, 'allow_blank': True}, 'password': {'write_only': True, 'required': False, 'allow_null': True, 'allow_blank': True},
'public_key': {'write_only': True}, 'public_key': {'write_only': True},
@ -53,6 +62,11 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
'is_expired': {'label': _('Is expired')}, 'is_expired': {'label': _('Is expired')},
'avatar_url': {'label': _('Avatar url')}, 'avatar_url': {'label': _('Avatar url')},
'created_by': {'read_only': True, 'allow_blank': True}, 'created_by': {'read_only': True, 'allow_blank': True},
'can_update': {'read_only': True},
'can_delete': {'read_only': True},
'groups_display': {'label': _('Groups name')},
'source_display': {'label': _('Source name')},
'role_display': {'label': _('Role name')},
} }
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -60,7 +74,9 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
self.set_role_choices() self.set_role_choices()
def set_role_choices(self): def set_role_choices(self):
role = self.fields['role'] role = self.fields.get('role')
if not role:
return
choices = role._choices choices = role._choices
choices.pop('App', None) choices.pop('App', None)
role._choices = choices role._choices = choices
@ -114,17 +130,6 @@ class UserSerializer(BulkSerializerMixin, serializers.ModelSerializer):
attrs.pop('password_strategy', None) attrs.pop('password_strategy', None)
return attrs return attrs
class UserDisplaySerializer(UserSerializer):
can_update = serializers.SerializerMethodField()
can_delete = serializers.SerializerMethodField()
class Meta(UserSerializer.Meta):
fields = UserSerializer.Meta.fields + [
'groups_display', 'role_display', 'source_display',
'can_update', 'can_delete',
]
def get_can_update(self, obj): def get_can_update(self, obj):
return CanUpdateDeleteUser.has_update_object_permission( return CanUpdateDeleteUser.has_update_object_permission(
self.context['request'], self.context['view'], obj self.context['request'], self.context['view'], obj
@ -135,17 +140,6 @@ class UserDisplaySerializer(UserSerializer):
self.context['request'], self.context['view'], obj self.context['request'], self.context['view'], obj
) )
def get_extra_kwargs(self):
kwargs = super().get_extra_kwargs()
kwargs.update({
'can_update': {'read_only': True},
'can_delete': {'read_only': True},
'groups_display': {'label': _('Groups name')},
'source_display': {'label': _('Source name')},
'role_display': {'label': _('Role name')},
})
return kwargs
class UserPKUpdateSerializer(serializers.ModelSerializer): class UserPKUpdateSerializer(serializers.ModelSerializer):
class Meta: class Meta: