From cd655f3a9b505b037b1fb449cf1bf73fa8ccc12e Mon Sep 17 00:00:00 2001 From: Tomasz Glowka Date: Wed, 15 Apr 2020 19:05:23 +0200 Subject: [PATCH 1/2] Reimplement SerializerMethodResourceRelatedField Related issues: #639 #779 #780 --- AUTHORS | 1 + example/serializers.py | 10 +-- example/tests/test_relations.py | 4 - .../unit/test_serializer_method_field.py | 66 ++++++++++++++ rest_framework_json_api/relations.py | 86 +++++++++++-------- 5 files changed, 117 insertions(+), 50 deletions(-) create mode 100644 example/tests/unit/test_serializer_method_field.py diff --git a/AUTHORS b/AUTHORS index 8ecb85d2c..6e98ad71e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -28,3 +28,4 @@ Nathanael Gordon Charlie Allatson Joseba Mendivil Felix Viernickel +Tom Glowka diff --git a/example/serializers.py b/example/serializers.py index cc24efb0a..2af5eb70b 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -126,30 +126,24 @@ def __init__(self, *args, **kwargs): related_link_view_name='entry-suggested', related_link_url_kwarg='entry_pk', self_link_view_name='entry-relationships', - source='get_suggested', model=Entry, many=True, - read_only=True ) # many related hyperlinked from serializer suggested_hyperlinked = relations.SerializerMethodHyperlinkedRelatedField( related_link_view_name='entry-suggested', related_link_url_kwarg='entry_pk', self_link_view_name='entry-relationships', - source='get_suggested', model=Entry, many=True, - read_only=True ) # single related from serializer - featured = relations.SerializerMethodResourceRelatedField( - source='get_featured', model=Entry, read_only=True) + featured = relations.SerializerMethodResourceRelatedField(model=Entry) # single related hyperlinked from serializer featured_hyperlinked = relations.SerializerMethodHyperlinkedRelatedField( related_link_view_name='entry-featured', related_link_url_kwarg='entry_pk', self_link_view_name='entry-relationships', - source='get_featured', model=Entry, read_only=True ) @@ -229,8 +223,6 @@ class AuthorSerializer(serializers.ModelSerializer): related_link_view_name='author-related', self_link_view_name='author-relationships', model=Entry, - read_only=True, - source='get_first_entry' ) comments = relations.HyperlinkedRelatedField( related_link_view_name='author-related', diff --git a/example/tests/test_relations.py b/example/tests/test_relations.py index 94db188a6..ef1dfb02b 100644 --- a/example/tests/test_relations.py +++ b/example/tests/test_relations.py @@ -290,16 +290,12 @@ class EntryModelSerializerWithHyperLinks(serializers.ModelSerializer): related_link_url_kwarg='entry_pk', self_link_view_name='entry-relationships', many=True, - read_only=True, - source='get_blog' ) comments = SerializerMethodHyperlinkedRelatedField( related_link_view_name='entry-comments', related_link_url_kwarg='entry_pk', self_link_view_name='entry-relationships', many=True, - read_only=True, - source='get_comments' ) class Meta: diff --git a/example/tests/unit/test_serializer_method_field.py b/example/tests/unit/test_serializer_method_field.py new file mode 100644 index 000000000..22935ebb1 --- /dev/null +++ b/example/tests/unit/test_serializer_method_field.py @@ -0,0 +1,66 @@ +from __future__ import absolute_import + +import pytest +from rest_framework import serializers + +from rest_framework_json_api.relations import SerializerMethodResourceRelatedField + +from example.models import Blog, Entry + + +def test_method_name_default(): + class BlogSerializer(serializers.ModelSerializer): + one_entry = SerializerMethodResourceRelatedField(model=Entry) + + class Meta: + model = Blog + fields = ['one_entry'] + + def get_one_entry(self, instance): + return Entry(id=100) + + serializer = BlogSerializer(instance=Blog()) + assert serializer.data['one_entry']['id'] == '100' + + +def test_method_name_custom(): + class BlogSerializer(serializers.ModelSerializer): + one_entry = SerializerMethodResourceRelatedField( + model=Entry, + method_name='get_custom_entry' + ) + + class Meta: + model = Blog + fields = ['one_entry'] + + def get_custom_entry(self, instance): + return Entry(id=100) + + serializer = BlogSerializer(instance=Blog()) + assert serializer.data['one_entry']['id'] == '100' + + +@pytest.mark.filterwarnings("ignore::DeprecationWarning") +def test_source(): + class BlogSerializer(serializers.ModelSerializer): + one_entry = SerializerMethodResourceRelatedField( + model=Entry, + source='get_custom_entry' + ) + + class Meta: + model = Blog + fields = ['one_entry'] + + def get_custom_entry(self, instance): + return Entry(id=100) + + serializer = BlogSerializer(instance=Blog()) + assert serializer.data['one_entry']['id'] == '100' + + +@pytest.mark.filterwarnings("error::DeprecationWarning") +def test_source_is_deprecated(): + with pytest.raises(DeprecationWarning): + SerializerMethodResourceRelatedField(model=Entry, source='get_custom_entry') diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 9fbfb98fc..eb7ff3a13 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -1,12 +1,12 @@ import json +import warnings from collections import OrderedDict -from collections.abc import Iterable import inflection from django.core.exceptions import ImproperlyConfigured from django.urls import NoReverseMatch from django.utils.translation import gettext_lazy as _ -from rest_framework.fields import MISSING_ERROR_MESSAGE, SkipField +from rest_framework.fields import MISSING_ERROR_MESSAGE, Field, SkipField from rest_framework.relations import MANY_RELATION_KWARGS from rest_framework.relations import ManyRelatedField as DRFManyRelatedField from rest_framework.relations import PrimaryKeyRelatedField, RelatedField @@ -347,51 +347,63 @@ def to_internal_value(self, data): return super(ResourceRelatedField, self).to_internal_value(data['id']) -class SerializerMethodResourceRelatedField(ResourceRelatedField): +class SerializerMethodFieldBase(Field): + def __init__(self, method_name=None, **kwargs): + if not method_name and kwargs.get('source'): + method_name = kwargs.pop('source') + warnings.warn(DeprecationWarning( + "'source' argument of {cls} is deprecated, use 'method_name' " + "as in SerializerMethodField".format(cls=self.__class__.__name__)), stacklevel=3) + self.method_name = method_name + kwargs['source'] = '*' + kwargs['read_only'] = True + super().__init__(**kwargs) + + def bind(self, field_name, parent): + default_method_name = 'get_{field_name}'.format(field_name=field_name) + if self.method_name is None: + self.method_name = default_method_name + super().bind(field_name, parent) + + def get_attribute(self, instance): + serializer_method = getattr(self.parent, self.method_name) + return serializer_method(instance) + + +class ManySerializerMethodResourceRelatedField(SerializerMethodFieldBase, ResourceRelatedField): + def __init__(self, child_relation=None, *args, **kwargs): + assert child_relation is not None, '`child_relation` is a required argument.' + self.child_relation = child_relation + super().__init__(**kwargs) + self.child_relation.bind(field_name='', parent=self) + + def to_representation(self, value): + return [self.child_relation.to_representation(item) for item in value] + + +class SerializerMethodResourceRelatedField(SerializerMethodFieldBase, ResourceRelatedField): """ Allows us to use serializer method RelatedFields with return querysets """ - def __new__(cls, *args, **kwargs): - """ - We override this because getting serializer methods - fails at the base class when many=True - """ - if kwargs.pop('many', False): - return cls.many_init(*args, **kwargs) - return super(ResourceRelatedField, cls).__new__(cls, *args, **kwargs) - def __init__(self, child_relation=None, *args, **kwargs): - model = kwargs.pop('model', None) - if child_relation is not None: - self.child_relation = child_relation - if model: - self.model = model - super(SerializerMethodResourceRelatedField, self).__init__(*args, **kwargs) + many_kwargs = [*MANY_RELATION_KWARGS, *LINKS_PARAMS, 'method_name', 'model'] + many_cls = ManySerializerMethodResourceRelatedField @classmethod def many_init(cls, *args, **kwargs): - list_kwargs = {k: kwargs.pop(k) for k in LINKS_PARAMS if k in kwargs} - list_kwargs['child_relation'] = cls(*args, **kwargs) - for key in kwargs.keys(): - if key in ('model',) + MANY_RELATION_KWARGS: + list_kwargs = {'child_relation': cls(**kwargs)} + for key in kwargs: + if key in cls.many_kwargs: list_kwargs[key] = kwargs[key] - return cls(**list_kwargs) + return cls.many_cls(**list_kwargs) - def get_attribute(self, instance): - # check for a source fn defined on the serializer instead of the model - if self.source and hasattr(self.parent, self.source): - serializer_method = getattr(self.parent, self.source) - if hasattr(serializer_method, '__call__'): - return serializer_method(instance) - return super(SerializerMethodResourceRelatedField, self).get_attribute(instance) - def to_representation(self, value): - if isinstance(value, Iterable): - base = super(SerializerMethodResourceRelatedField, self) - return [base.to_representation(x) for x in value] - return super(SerializerMethodResourceRelatedField, self).to_representation(value) +class ManySerializerMethodHyperlinkedRelatedField(SkipDataMixin, + ManySerializerMethodResourceRelatedField): + pass -class SerializerMethodHyperlinkedRelatedField(SkipDataMixin, SerializerMethodResourceRelatedField): - pass +class SerializerMethodHyperlinkedRelatedField(SkipDataMixin, + SerializerMethodResourceRelatedField): + many_cls = ManySerializerMethodHyperlinkedRelatedField From 3664a01aefa23a721c7b2c5510e620462766a084 Mon Sep 17 00:00:00 2001 From: Tomasz Glowka Date: Wed, 15 Apr 2020 20:56:56 +0200 Subject: [PATCH 2/2] Fix lint --- example/tests/test_serializers.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/example/tests/test_serializers.py b/example/tests/test_serializers.py index 50a84f4d6..5f277f2fa 100644 --- a/example/tests/test_serializers.py +++ b/example/tests/test_serializers.py @@ -7,21 +7,17 @@ from rest_framework.request import Request from rest_framework.test import APIRequestFactory -from example.factories import ArtProjectFactory from rest_framework_json_api.serializers import ( DateField, ModelSerializer, ResourceIdentifierObjectSerializer, - empty, + empty ) from rest_framework_json_api.utils import format_resource_type +from example.factories import ArtProjectFactory from example.models import Author, Blog, Entry -from example.serializers import ( - BlogSerializer, - ProjectSerializer, - ArtProjectSerializer, -) +from example.serializers import ArtProjectSerializer, BlogSerializer, ProjectSerializer request_factory = APIRequestFactory() pytestmark = pytest.mark.django_db