From 82de9a828d58c54f3958e7f8d0dc19ef76bdc669 Mon Sep 17 00:00:00 2001 From: Eugene Savchenko Date: Fri, 3 Aug 2018 23:21:09 +0300 Subject: [PATCH 1/3] Added three options to ModelSerializer: meta_fields, meta_exclude and meta_preset --- rest_framework/serializers.py | 130 ++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 43c7972a4..d21e92203 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -901,6 +901,136 @@ class ModelSerializer(Serializer): # "HTTP 201 Created" responses. url_field_name = None + def __init__(self, *args, **kwargs): + kwargs.pop('meta_fields', None) + kwargs.pop('meta_exclude', None) + kwargs.pop('meta_preset', None) + super(ModelSerializer, self).__init__(*args, **kwargs) + + def __new__(cls, *args, **kwargs): + if 'meta_fields' in kwargs: + fields = kwargs.pop('meta_fields') + if fields == '__all__': + fields = [] + return cls.meta_fields(*fields)(*args, **kwargs) + if 'meta_exclude' in kwargs: + return cls.meta_exclude(*kwargs.pop('meta_exclude'))(*args, **kwargs) + if 'meta_preset' in kwargs: + return cls.meta_preset(kwargs.pop('meta_preset'))(*args, **kwargs) + return super(ModelSerializer, cls).__new__(cls, *args, **kwargs) + + @classmethod + def meta_fields(cls, *fields): + """ + Create new class based on the current with overrode Meta parameters. + Option `exclude` of base serializer is going to None. + + Example of usage in serializers: + class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + + class ItemSerializer(serializers.ModelSerializer): + user = UserSerializer(meta_fields=['id', 'name']) + + class Meta: + model = Item + + Example of usage in views: + class UserViewSet(viewsets.ModelViewSet): + serializer_class = UserSerializer.meta_fields('id', 'name', 'expiration_date') + """ + if not len(fields): + fields = '__all__' + meta = getattr(cls, 'Meta', type('Meta', (), {})) + return type(cls.__name__, (cls,), { + 'Meta': type('Meta', (meta,), { + 'fields': fields, + 'exclude': None + }) + }) + + @classmethod + def meta_exclude(cls, *exclude): + """ + Create new class based on the current with overrode Meta parameters. + If base serializer has meta option `fields`, fields will exclude from its. + + Example of usage in serializers: + class UserSerializer(serializers.ModelSerializer): + products = ProductsSerializer(many=True) + + class Meta: + model = User + + class ItemSerializer(serializers.ModelSerializer): + user = UserSerializer(meta_exclude=['products']) + + class Meta: + model = Item + + Example of usage in views: + class UserViewSet(viewsets.ModelViewSet): + serializer_class = UserSerializer.meta_exclude('products') + """ + meta = getattr(cls, 'Meta', type('Meta', (), {})) + meta_fields = getattr(meta, 'fields', None) + exclude_props = [] + if isinstance(meta_fields, list) or isinstance(meta_fields, tuple): + meta_fields = [field_ for field_ in meta_fields if field_ not in exclude] + exclude = None + else: + meta_fields = None + exclude_props = [key for key, prop in cls._declared_fields.items() if isinstance(prop, Field)] + exclude = list(filter(lambda f: f not in exclude_props, exclude)) + return type(cls.__name__, (cls,), { + 'Meta': type('Meta', (meta,), { + 'fields': meta_fields, + 'exclude': exclude + }), + **{field: None for field in exclude_props} + }) + + @classmethod + def meta_preset(cls, name): + """ + Create new class based on the current with overrode Meta parameters. + It will check meta option `presets` and try to get it by name. + Presets - prepared some schemes, which simplify manipulating with meta option. + + Example of usage in serializers: + class UserSerializer(serializers.ModelSerializer): + products = ProductsSerializer(many=True) + + class Meta: + model = User + presets = { + 'short': { + 'fields': ['id', 'name'] + }, + 'light': { + 'exclude': ['products'] + } + } + + class ItemSerializer(serializers.ModelSerializer): + user = UserSerializer(meta_preset='short') + + class Meta: + model = Item + + Example of usage in views: + class UserViewSet(viewsets.ModelViewSet): + serializer_class = UserSerializer.meta_preset('light') + """ + meta = getattr(cls, 'Meta', type('Meta', (), {})) + presets = getattr(meta, 'presets', {}) + preset = presets.get(name, None) + assert preset is not None, ('Preset of `%s` with `%s` name doesn\'t exist.' % (cls.__name__, name)) + return type(cls.__name__, (cls,), { + 'Meta': type('Meta', (meta,), preset) + }) + # Default `create` and `update` behavior... def create(self, validated_data): """ From 6880725615532f74a02ea405effca514b44dc6d1 Mon Sep 17 00:00:00 2001 From: Eugene Savchenko Date: Fri, 3 Aug 2018 23:21:23 +0300 Subject: [PATCH 2/3] Added tests for meta_fields, meta_exclude and meta_preset --- tests/test_serializer_customizing_meta.py | 104 ++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 tests/test_serializer_customizing_meta.py diff --git a/tests/test_serializer_customizing_meta.py b/tests/test_serializer_customizing_meta.py new file mode 100644 index 000000000..49db93258 --- /dev/null +++ b/tests/test_serializer_customizing_meta.py @@ -0,0 +1,104 @@ +# coding: utf-8 +from __future__ import unicode_literals + +from django.db import models +from django.test import TestCase + +from rest_framework import serializers + + +class Product(models.Model): + name = models.CharField(max_length=10) + + +class Item(models.Model): + f1 = models.CharField(max_length=10) + f2 = models.CharField(max_length=10000) + user = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='items') + + +class TestCustomizingMetaOptions(TestCase): + def test_meta_fields(self): + class ItemSerializer(serializers.ModelSerializer): + class Meta: + model = Item + fields = '__all__' + + class ProductSerializer(serializers.ModelSerializer): + items = ItemSerializer(many=True, meta_fields=['f1']) + + class Meta: + model = Product + fields = '__all__' + + ShortenProductSerializer = ProductSerializer.meta_fields('id', 'name') + + self.assertEqual(len(ProductSerializer().fields['items'].child.get_fields()), 1) + self.assertEqual(len(ShortenProductSerializer().get_fields()), 2) + + def test_meta_fields_all(self): + class ItemSerializer(serializers.ModelSerializer): + class Meta: + model = Item + fields = ['f1'] + + serializer = ItemSerializer(meta_fields='__all__') + self.assertEqual(len(serializer.get_fields()), 4) + + def test_meta_exclude(self): + class ItemSerializer(serializers.ModelSerializer): + class Meta: + model = Item + fields = '__all__' + + class ProductSerializer(serializers.ModelSerializer): + items = ItemSerializer(many=True, meta_exclude=['id', 'f2', 'user']) + + class Meta: + model = Product + fields = '__all__' + + ShortenProductSerializer = ProductSerializer.meta_exclude('items') + + self.assertEqual(len(ProductSerializer().fields['items'].child.get_fields()), 1) + self.assertEqual(len(ShortenProductSerializer().get_fields()), 2) + + def test_meta_exclude_from_defined_fields(self): + class ItemSerializer(serializers.ModelSerializer): + class Meta: + model = Item + fields = ['user', 'f1', 'f2'] + + serializer = ItemSerializer(meta_exclude=['f2']) + self.assertEqual(len(serializer.get_fields()), 2) + + def test_meta_preset(self): + class ItemSerializer(serializers.ModelSerializer): + class Meta: + model = Item + fields = '__all__' + presets = { + 'short': { + 'fields': ['f1'] + } + } + + class ProductSerializer(serializers.ModelSerializer): + items = ItemSerializer(many=True, meta_preset='short') + + class Meta: + model = Product + fields = '__all__' + presets = { + 'short': { + 'fields': ['id', 'name'] + }, + 'light': { + 'exclude': ['items'] + } + } + + ShortenProductSerializer = ProductSerializer.meta_preset('short') + + self.assertEqual(len(ProductSerializer().fields['items'].child.get_fields()), 1) + self.assertEqual(len(ShortenProductSerializer().get_fields()), 2) From 0daa0adf29f400d002da33bd51f2bb370aad3e92 Mon Sep 17 00:00:00 2001 From: Eugene Savchenko Date: Sat, 4 Aug 2018 00:09:01 +0300 Subject: [PATCH 3/3] Update support python 2.7 --- rest_framework/serializers.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index d21e92203..fcb2e204d 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -942,11 +942,11 @@ class ModelSerializer(Serializer): """ if not len(fields): fields = '__all__' - meta = getattr(cls, 'Meta', type('Meta', (), {})) - return type(cls.__name__, (cls,), { - 'Meta': type('Meta', (meta,), { - 'fields': fields, - 'exclude': None + meta = getattr(cls, 'Meta', type(str('Meta'), (object,), {})) + return type(cls.__name__, (cls, object), { + six.text_type('Meta'): type(str('Meta'), (meta, object), { + six.text_type('fields'): fields, + six.text_type('exclude'): None }) }) @@ -973,7 +973,7 @@ class ModelSerializer(Serializer): class UserViewSet(viewsets.ModelViewSet): serializer_class = UserSerializer.meta_exclude('products') """ - meta = getattr(cls, 'Meta', type('Meta', (), {})) + meta = getattr(cls, 'Meta', type(str('Meta'), (object,), {})) meta_fields = getattr(meta, 'fields', None) exclude_props = [] if isinstance(meta_fields, list) or isinstance(meta_fields, tuple): @@ -983,13 +983,14 @@ class ModelSerializer(Serializer): meta_fields = None exclude_props = [key for key, prop in cls._declared_fields.items() if isinstance(prop, Field)] exclude = list(filter(lambda f: f not in exclude_props, exclude)) - return type(cls.__name__, (cls,), { - 'Meta': type('Meta', (meta,), { - 'fields': meta_fields, - 'exclude': exclude - }), - **{field: None for field in exclude_props} - }) + cls_dict = { + 'Meta': type(str('Meta'), (meta, object), { + six.text_type('fields'): meta_fields, + six.text_type('exclude'): exclude + }) + } + cls_dict.update({field: None for field in exclude_props}) + return type(cls.__name__, (cls, object), cls_dict) @classmethod def meta_preset(cls, name): @@ -1023,12 +1024,12 @@ class ModelSerializer(Serializer): class UserViewSet(viewsets.ModelViewSet): serializer_class = UserSerializer.meta_preset('light') """ - meta = getattr(cls, 'Meta', type('Meta', (), {})) + meta = getattr(cls, 'Meta', type(str('Meta'), (), {})) presets = getattr(meta, 'presets', {}) preset = presets.get(name, None) assert preset is not None, ('Preset of `%s` with `%s` name doesn\'t exist.' % (cls.__name__, name)) return type(cls.__name__, (cls,), { - 'Meta': type('Meta', (meta,), preset) + str('Meta'): type(str('Meta'), (meta, object), preset) }) # Default `create` and `update` behavior...