Skip to content

Commit 1dccbb0

Browse files
authored
Reimplement SerializerMethodResourceRelatedField (#781)
Fixes #639 - interface not consistent with `SerializerMethodField` Fixes #779 - no enforcement of `read_only` Fixes #780 - broken `parent` chain
1 parent 9fdf461 commit 1dccbb0

File tree

9 files changed

+190
-59
lines changed

9 files changed

+190
-59
lines changed

‎AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ Nathanael Gordon <nathanael.l.gordon@gmail.com>
2828
Charlie Allatson <charles.allatson@gmail.com>
2929
Joseba Mendivil <git@jma.email>
3030
Felix Viernickel <felix@gedankenspieler.org>
31+
Tom Glowka <glowka.tom@gmail.com>

‎CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ any parts of the framework not mentioned in the documentation should generally b
1414

1515
* Avoid `AttributeError` for PUT and PATCH methods when using `APIView`
1616

17+
### Changed
18+
19+
* `SerializerMethodResourceRelatedField` is now consistent with DRF `SerializerMethodField`:
20+
* Pass `method_name` argument to specify method name. If no value is provided, it defaults to `get_{field_name}`
21+
22+
### Deprecated
23+
24+
* Deprecate `source` argument of `SerializerMethodResourceRelatedField`, use `method_name` instead
25+
26+
1727
## [3.1.0] - 2020-02-08
1828

1929
### Added

‎docs/usage.md

+59-1
Original file line numberDiff line numberDiff line change
@@ -586,10 +586,68 @@ class LineItemViewSet(viewsets.ModelViewSet):
586586

587587
#### HyperlinkedRelatedField
588588

589-
`HyperlinkedRelatedField` has same functionality as `ResourceRelatedField` but does
589+
`relations.HyperlinkedRelatedField` has same functionality as `ResourceRelatedField` but does
590590
not render `data`. Use this in case you only need links of relationships and want to lower payload
591591
and increase performance.
592592

593+
#### SerializerMethodResourceRelatedField
594+
595+
`relations.SerializerMethodResourceRelatedField` combines behaviour of DRF `SerializerMethodField` and
596+
`ResourceRelatedField`, so it accepts `method_name` together with `model` and links-related arguments.
597+
`data` is rendered in `ResourceRelatedField` manner.
598+
599+
```python
600+
from rest_framework_json_api import serializers
601+
from rest_framework_json_api.relations import SerializerMethodResourceRelatedField
602+
603+
from myapp.models import Order, LineItem
604+
605+
606+
class OrderSerializer(serializers.ModelSerializer):
607+
class Meta:
608+
model = Order
609+
610+
line_items = SerializerMethodResourceRelatedField(
611+
model=LineItem,
612+
many=True,
613+
method_name='get_big_line_items'
614+
)
615+
616+
small_line_items = SerializerMethodResourceRelatedField(
617+
model=LineItem,
618+
many=True,
619+
# default to method_name='get_small_line_items'
620+
)
621+
622+
def get_big_line_items(self, instance):
623+
return LineItem.objects.filter(order=instance).filter(amount__gt=1000)
624+
625+
def get_small_line_items(self, instance):
626+
return LineItem.objects.filter(order=instance).filter(amount__lte=1000)
627+
628+
```
629+
630+
or using `related_link_*` with `HyperlinkedModelSerializer`
631+
632+
```python
633+
class OrderSerializer(serializers.HyperlinkedModelSerializer):
634+
class Meta:
635+
model = Order
636+
637+
line_items = SerializerMethodResourceRelatedField(
638+
model=LineItem,
639+
many=True,
640+
method_name='get_big_line_items',
641+
related_link_view_name='order-lineitems-list',
642+
related_link_url_kwarg='order_pk',
643+
)
644+
645+
def get_big_line_items(self, instance):
646+
return LineItem.objects.filter(order=instance).filter(amount__gt=1000)
647+
648+
```
649+
650+
593651
#### Related urls
594652

595653
There is a nice way to handle "related" urls like `/orders/3/lineitems/` or `/orders/3/customer/`.

‎example/serializers.py

+1-9
Original file line numberDiff line numberDiff line change
@@ -126,30 +126,24 @@ def __init__(self, *args, **kwargs):
126126
related_link_view_name='entry-suggested',
127127
related_link_url_kwarg='entry_pk',
128128
self_link_view_name='entry-relationships',
129-
source='get_suggested',
130129
model=Entry,
131130
many=True,
132-
read_only=True
133131
)
134132
# many related hyperlinked from serializer
135133
suggested_hyperlinked = relations.SerializerMethodHyperlinkedRelatedField(
136134
related_link_view_name='entry-suggested',
137135
related_link_url_kwarg='entry_pk',
138136
self_link_view_name='entry-relationships',
139-
source='get_suggested',
140137
model=Entry,
141138
many=True,
142-
read_only=True
143139
)
144140
# single related from serializer
145-
featured = relations.SerializerMethodResourceRelatedField(
146-
source='get_featured', model=Entry, read_only=True)
141+
featured = relations.SerializerMethodResourceRelatedField(model=Entry)
147142
# single related hyperlinked from serializer
148143
featured_hyperlinked = relations.SerializerMethodHyperlinkedRelatedField(
149144
related_link_view_name='entry-featured',
150145
related_link_url_kwarg='entry_pk',
151146
self_link_view_name='entry-relationships',
152-
source='get_featured',
153147
model=Entry,
154148
read_only=True
155149
)
@@ -229,8 +223,6 @@ class AuthorSerializer(serializers.ModelSerializer):
229223
related_link_view_name='author-related',
230224
self_link_view_name='author-relationships',
231225
model=Entry,
232-
read_only=True,
233-
source='get_first_entry'
234226
)
235227
comments = relations.HyperlinkedRelatedField(
236228
related_link_view_name='author-related',

‎example/tests/test_parsers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from django.conf.urls import url
55
from django.test import TestCase, override_settings
66
from django.urls import reverse
7-
from rest_framework import views, status
7+
from rest_framework import status, views
88
from rest_framework.exceptions import ParseError
99
from rest_framework.response import Response
1010
from rest_framework.test import APITestCase

‎example/tests/test_relations.py

-4
Original file line numberDiff line numberDiff line change
@@ -290,16 +290,12 @@ class EntryModelSerializerWithHyperLinks(serializers.ModelSerializer):
290290
related_link_url_kwarg='entry_pk',
291291
self_link_view_name='entry-relationships',
292292
many=True,
293-
read_only=True,
294-
source='get_blog'
295293
)
296294
comments = SerializerMethodHyperlinkedRelatedField(
297295
related_link_view_name='entry-comments',
298296
related_link_url_kwarg='entry_pk',
299297
self_link_view_name='entry-relationships',
300298
many=True,
301-
read_only=True,
302-
source='get_comments'
303299
)
304300

305301
class Meta:

‎example/tests/test_serializers.py

+3-7
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,17 @@
77
from rest_framework.request import Request
88
from rest_framework.test import APIRequestFactory
99

10-
from example.factories import ArtProjectFactory
1110
from rest_framework_json_api.serializers import (
1211
DateField,
1312
ModelSerializer,
1413
ResourceIdentifierObjectSerializer,
15-
empty,
14+
empty
1615
)
1716
from rest_framework_json_api.utils import format_resource_type
1817

18+
from example.factories import ArtProjectFactory
1919
from example.models import Author, Blog, Entry
20-
from example.serializers import (
21-
BlogSerializer,
22-
ProjectSerializer,
23-
ArtProjectSerializer,
24-
)
20+
from example.serializers import ArtProjectSerializer, BlogSerializer, ProjectSerializer
2521

2622
request_factory = APIRequestFactory()
2723
pytestmark = pytest.mark.django_db
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from __future__ import absolute_import
2+
3+
import pytest
4+
from rest_framework import serializers
5+
6+
from rest_framework_json_api.relations import SerializerMethodResourceRelatedField
7+
8+
from example.models import Blog, Entry
9+
10+
11+
def test_method_name_default():
12+
class BlogSerializer(serializers.ModelSerializer):
13+
one_entry = SerializerMethodResourceRelatedField(model=Entry)
14+
15+
class Meta:
16+
model = Blog
17+
fields = ['one_entry']
18+
19+
def get_one_entry(self, instance):
20+
return Entry(id=100)
21+
22+
serializer = BlogSerializer(instance=Blog())
23+
assert serializer.data['one_entry']['id'] == '100'
24+
25+
26+
def test_method_name_custom():
27+
class BlogSerializer(serializers.ModelSerializer):
28+
one_entry = SerializerMethodResourceRelatedField(
29+
model=Entry,
30+
method_name='get_custom_entry'
31+
)
32+
33+
class Meta:
34+
model = Blog
35+
fields = ['one_entry']
36+
37+
def get_custom_entry(self, instance):
38+
return Entry(id=100)
39+
40+
serializer = BlogSerializer(instance=Blog())
41+
assert serializer.data['one_entry']['id'] == '100'
42+
43+
44+
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
45+
def test_source():
46+
class BlogSerializer(serializers.ModelSerializer):
47+
one_entry = SerializerMethodResourceRelatedField(
48+
model=Entry,
49+
source='get_custom_entry'
50+
)
51+
52+
class Meta:
53+
model = Blog
54+
fields = ['one_entry']
55+
56+
def get_custom_entry(self, instance):
57+
return Entry(id=100)
58+
59+
serializer = BlogSerializer(instance=Blog())
60+
assert serializer.data['one_entry']['id'] == '100'
61+
62+
63+
@pytest.mark.filterwarnings("error::DeprecationWarning")
64+
def test_source_is_deprecated():
65+
with pytest.raises(DeprecationWarning):
66+
SerializerMethodResourceRelatedField(model=Entry, source='get_custom_entry')

‎rest_framework_json_api/relations.py

+49-37
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import json
2+
import warnings
23
from collections import OrderedDict
3-
from collections.abc import Iterable
44

55
import inflection
66
from django.core.exceptions import ImproperlyConfigured
77
from django.urls import NoReverseMatch
88
from django.utils.translation import gettext_lazy as _
9-
from rest_framework.fields import MISSING_ERROR_MESSAGE, SkipField
9+
from rest_framework.fields import MISSING_ERROR_MESSAGE, Field, SkipField
1010
from rest_framework.relations import MANY_RELATION_KWARGS
1111
from rest_framework.relations import ManyRelatedField as DRFManyRelatedField
1212
from rest_framework.relations import PrimaryKeyRelatedField, RelatedField
@@ -347,51 +347,63 @@ def to_internal_value(self, data):
347347
return super(ResourceRelatedField, self).to_internal_value(data['id'])
348348

349349

350-
class SerializerMethodResourceRelatedField(ResourceRelatedField):
350+
class SerializerMethodFieldBase(Field):
351+
def __init__(self, method_name=None, **kwargs):
352+
if not method_name and kwargs.get('source'):
353+
method_name = kwargs.pop('source')
354+
warnings.warn(DeprecationWarning(
355+
"'source' argument of {cls} is deprecated, use 'method_name' "
356+
"as in SerializerMethodField".format(cls=self.__class__.__name__)), stacklevel=3)
357+
self.method_name = method_name
358+
kwargs['source'] = '*'
359+
kwargs['read_only'] = True
360+
super().__init__(**kwargs)
361+
362+
def bind(self, field_name, parent):
363+
default_method_name = 'get_{field_name}'.format(field_name=field_name)
364+
if self.method_name is None:
365+
self.method_name = default_method_name
366+
super().bind(field_name, parent)
367+
368+
def get_attribute(self, instance):
369+
serializer_method = getattr(self.parent, self.method_name)
370+
return serializer_method(instance)
371+
372+
373+
class ManySerializerMethodResourceRelatedField(SerializerMethodFieldBase, ResourceRelatedField):
374+
def __init__(self, child_relation=None, *args, **kwargs):
375+
assert child_relation is not None, '`child_relation` is a required argument.'
376+
self.child_relation = child_relation
377+
super().__init__(**kwargs)
378+
self.child_relation.bind(field_name='', parent=self)
379+
380+
def to_representation(self, value):
381+
return [self.child_relation.to_representation(item) for item in value]
382+
383+
384+
class SerializerMethodResourceRelatedField(SerializerMethodFieldBase, ResourceRelatedField):
351385
"""
352386
Allows us to use serializer method RelatedFields
353387
with return querysets
354388
"""
355-
def __new__(cls, *args, **kwargs):
356-
"""
357-
We override this because getting serializer methods
358-
fails at the base class when many=True
359-
"""
360-
if kwargs.pop('many', False):
361-
return cls.many_init(*args, **kwargs)
362-
return super(ResourceRelatedField, cls).__new__(cls, *args, **kwargs)
363389

364-
def __init__(self, child_relation=None, *args, **kwargs):
365-
model = kwargs.pop('model', None)
366-
if child_relation is not None:
367-
self.child_relation = child_relation
368-
if model:
369-
self.model = model
370-
super(SerializerMethodResourceRelatedField, self).__init__(*args, **kwargs)
390+
many_kwargs = [*MANY_RELATION_KWARGS, *LINKS_PARAMS, 'method_name', 'model']
391+
many_cls = ManySerializerMethodResourceRelatedField
371392

372393
@classmethod
373394
def many_init(cls, *args, **kwargs):
374-
list_kwargs = {k: kwargs.pop(k) for k in LINKS_PARAMS if k in kwargs}
375-
list_kwargs['child_relation'] = cls(*args, **kwargs)
376-
for key in kwargs.keys():
377-
if key in ('model',) + MANY_RELATION_KWARGS:
395+
list_kwargs = {'child_relation': cls(**kwargs)}
396+
for key in kwargs:
397+
if key in cls.many_kwargs:
378398
list_kwargs[key] = kwargs[key]
379-
return cls(**list_kwargs)
399+
return cls.many_cls(**list_kwargs)
380400

381-
def get_attribute(self, instance):
382-
# check for a source fn defined on the serializer instead of the model
383-
if self.source and hasattr(self.parent, self.source):
384-
serializer_method = getattr(self.parent, self.source)
385-
if hasattr(serializer_method, '__call__'):
386-
return serializer_method(instance)
387-
return super(SerializerMethodResourceRelatedField, self).get_attribute(instance)
388401

389-
def to_representation(self, value):
390-
if isinstance(value, Iterable):
391-
base = super(SerializerMethodResourceRelatedField, self)
392-
return [base.to_representation(x) for x in value]
393-
return super(SerializerMethodResourceRelatedField, self).to_representation(value)
402+
class ManySerializerMethodHyperlinkedRelatedField(SkipDataMixin,
403+
ManySerializerMethodResourceRelatedField):
404+
pass
394405

395406

396-
class SerializerMethodHyperlinkedRelatedField(SkipDataMixin, SerializerMethodResourceRelatedField):
397-
pass
407+
class SerializerMethodHyperlinkedRelatedField(SkipDataMixin,
408+
SerializerMethodResourceRelatedField):
409+
many_cls = ManySerializerMethodHyperlinkedRelatedField

0 commit comments

Comments
 (0)