Skip to content

Commit 76b60fe

Browse files
czoselmblayman
authored andcommitted
Check resource name on included serializer in to_internal_value (#306)
* Fix resource_name support for ResourceRelatedField * Check resource name on included serializer in to_internal_value * Revert "Fix resource_name support for ResourceRelatedField" This reverts commit 484adc2. * Improve identification of root serializer * Add tests for resource_name support * Fix field_name support in to_internal_value * Refactor tests to use pytest monkeypatch * Minor refactoring based on feedback
1 parent 8970398 commit 76b60fe

File tree

4 files changed

+95
-26
lines changed

4 files changed

+95
-26
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
Jerel Unruh <[email protected]>
22
Greg Aker <[email protected]>
33
Adam Wróbel <https://adamwrobel.com>
4+
Christian Zosel <https://zosel.ch>
45

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ v2.3.0
44
attributes and relations to snake\_case format. This conversion was unexpected
55
and there was no way to turn it off.
66
* Fix for apps that don't use `django.contrib.contenttypes`.
7+
* Fix `resource_name` support for POST requests and nested serializers
78

89
v2.2.0
910

example/tests/integration/test_model_resource_name.py

+49-14
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import pytest
2-
from django.core.urlresolvers import reverse
2+
from copy import deepcopy
3+
from example import models, serializers, views
4+
from example.tests.utils import dump_json, load_json
5+
from rest_framework import status
36

4-
from example.tests.utils import load_json
7+
from django.core.urlresolvers import reverse
58

6-
from example import models, serializers, views
79
pytestmark = pytest.mark.django_db
810

911

@@ -37,6 +39,24 @@ def _check_relationship_and_included_comment_type_are_the_same(django_client, ur
3739
@pytest.mark.usefixtures("single_entry")
3840
class TestModelResourceName:
3941

42+
create_data = {
43+
'data': {
44+
'type': 'resource_name_from_JSONAPIMeta',
45+
'id': None,
46+
'attributes': {
47+
'body': 'example',
48+
},
49+
'relationships': {
50+
'entry': {
51+
'data': {
52+
'type': 'resource_name_from_JSONAPIMeta',
53+
'id': 1
54+
}
55+
}
56+
}
57+
}
58+
}
59+
4060
def test_model_resource_name_on_list(self, client):
4161
models.Comment.__bases__ += (_PatchedModel,)
4262
response = client.get(reverse("comment-list"))
@@ -46,7 +66,7 @@ def test_model_resource_name_on_list(self, client):
4666
'resource_name from model incorrect on list')
4767

4868
# Precedence tests
49-
def test_resource_name_precendence(self, client):
69+
def test_resource_name_precendence(self, client, monkeypatch):
5070
# default
5171
response = client.get(reverse("comment-list"))
5272
data = load_json(response.content)['data'][0]
@@ -61,29 +81,44 @@ def test_resource_name_precendence(self, client):
6181
'resource_name from model incorrect on list')
6282

6383
# serializer > model
64-
serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer"
84+
monkeypatch.setattr(serializers.CommentSerializer.Meta, 'resource_name', 'resource_name_from_serializer', False)
6585
response = client.get(reverse("comment-list"))
6686
data = load_json(response.content)['data'][0]
6787
assert (data.get('type') == 'resource_name_from_serializer'), (
6888
'resource_name from serializer incorrect on list')
6989

7090
# view > serializer > model
71-
views.CommentViewSet.resource_name = 'resource_name_from_view'
91+
monkeypatch.setattr(views.CommentViewSet, 'resource_name', 'resource_name_from_view', False)
7292
response = client.get(reverse("comment-list"))
7393
data = load_json(response.content)['data'][0]
7494
assert (data.get('type') == 'resource_name_from_view'), (
7595
'resource_name from view incorrect on list')
7696

97+
def test_model_resource_name_create(self, client):
98+
models.Comment.__bases__ += (_PatchedModel,)
99+
models.Entry.__bases__ += (_PatchedModel,)
100+
response = client.post(reverse("comment-list"),
101+
dump_json(self.create_data),
102+
content_type='application/vnd.api+json')
103+
104+
assert response.status_code == status.HTTP_201_CREATED
105+
106+
def test_serializer_resource_name_create(self, client, monkeypatch):
107+
monkeypatch.setattr(serializers.CommentSerializer.Meta, 'resource_name', 'renamed_comments', False)
108+
monkeypatch.setattr(serializers.EntrySerializer.Meta, 'resource_name', 'renamed_entries', False)
109+
create_data = deepcopy(self.create_data)
110+
create_data['data']['type'] = 'renamed_comments'
111+
create_data['data']['relationships']['entry']['data']['type'] = 'renamed_entries'
112+
113+
response = client.post(reverse("comment-list"),
114+
dump_json(create_data),
115+
content_type='application/vnd.api+json')
116+
117+
assert response.status_code == status.HTTP_201_CREATED
118+
77119
def teardown_method(self, method):
78120
models.Comment.__bases__ = (models.Comment.__bases__[0],)
79-
try:
80-
delattr(serializers.CommentSerializer.Meta, "resource_name")
81-
except AttributeError:
82-
pass
83-
try:
84-
delattr(views.CommentViewSet, "resource_name")
85-
except AttributeError:
86-
pass
121+
models.Entry.__bases__ = (models.Entry.__bases__[0],)
87122

88123

89124
@pytest.mark.usefixtures("single_entry")

rest_framework_json_api/relations.py

+44-12
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import collections
2+
import inflection
23
import json
34

45
from rest_framework.fields import MISSING_ERROR_MESSAGE, SerializerMethodField
56
from rest_framework.relations import *
7+
from rest_framework.serializers import Serializer
68
from django.utils.translation import ugettext_lazy as _
79
from django.db.models.query import QuerySet
810

@@ -123,7 +125,12 @@ def to_internal_value(self, data):
123125
self.fail('incorrect_type', data_type=type(data).__name__)
124126
if not isinstance(data, dict):
125127
self.fail('incorrect_type', data_type=type(data).__name__)
128+
126129
expected_relation_type = get_resource_type_from_queryset(self.queryset)
130+
serializer_resource_type = self.get_resource_type_from_included_serializer()
131+
132+
if serializer_resource_type is not None:
133+
expected_relation_type = serializer_resource_type
127134

128135
if 'type' not in data:
129136
self.fail('missing_type')
@@ -142,19 +149,44 @@ def to_representation(self, value):
142149
else:
143150
pk = value.pk
144151

145-
# check to see if this resource has a different resource_name when
146-
# included and use that name
147-
resource_type = None
148-
root = getattr(self.parent, 'parent', self.parent)
149-
field_name = self.field_name if self.field_name else self.parent.field_name
150-
if getattr(root, 'included_serializers', None) is not None:
151-
includes = get_included_serializers(root)
152-
if field_name in includes.keys():
153-
resource_type = get_resource_type_from_serializer(includes[field_name])
154-
155-
resource_type = resource_type if resource_type else get_resource_type_from_instance(value)
152+
resource_type = self.get_resource_type_from_included_serializer()
153+
if resource_type is None:
154+
resource_type = get_resource_type_from_instance(value)
155+
156156
return OrderedDict([('type', resource_type), ('id', str(pk))])
157157

158+
def get_resource_type_from_included_serializer(self):
159+
"""
160+
Check to see it this resource has a different resource_name when
161+
included and return that name, or None
162+
"""
163+
field_name = self.field_name or self.parent.field_name
164+
parent = self.get_parent_serializer()
165+
166+
if parent is not None:
167+
# accept both singular and plural versions of field_name
168+
field_names = [
169+
inflection.singularize(field_name),
170+
inflection.pluralize(field_name)
171+
]
172+
includes = get_included_serializers(parent)
173+
for field in field_names:
174+
if field in includes.keys():
175+
return get_resource_type_from_serializer(includes[field])
176+
177+
return None
178+
179+
def get_parent_serializer(self):
180+
if hasattr(self.parent, 'parent') and self.is_serializer(self.parent.parent):
181+
return self.parent.parent
182+
elif self.is_serializer(self.parent):
183+
return self.parent
184+
185+
return None
186+
187+
def is_serializer(self, candidate):
188+
return isinstance(candidate, Serializer)
189+
158190
def get_choices(self, cutoff=None):
159191
queryset = self.get_queryset()
160192
if queryset is None:
@@ -219,4 +251,4 @@ def to_representation(self, value):
219251
if isinstance(value, collections.Iterable):
220252
base = super(SerializerMethodResourceRelatedField, self)
221253
return [base.to_representation(x) for x in value]
222-
return super(SerializerMethodResourceRelatedField, self).to_representation(value)
254+
return super(SerializerMethodResourceRelatedField, self).to_representation(value)

0 commit comments

Comments
 (0)