Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
Skip to content

Commit d565334

Browse files
committed
Improve polymorphic relations and tests.
1 parent 22829d1 commit d565334

File tree

7 files changed

+131
-52
lines changed

7 files changed

+131
-52
lines changed

docs/usage.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,14 @@ class CompanySerializer(serializers.ModelSerializer):
471471
They must be explicitely declared with the `polymorphic_serializer` (first positional argument) correctly defined.
472472
It must be a subclass of `serializers.PolymorphicModelSerializer`.
473473

474+
<div class="warning">
475+
<strong>Note:</strong>
476+
Polymorphic resources are not compatible with
477+
<code class="docutils literal">
478+
<span class="pre">resource_name</span>
479+
</code>
480+
defined on the view.
481+
</div>
474482

475483
### Meta
476484

example/serializers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,10 @@ class Meta:
115115

116116

117117
class CompanySerializer(serializers.ModelSerializer):
118-
current_project = relations.PolymorphicResourceRelatedField(ProjectSerializer, queryset=models.Project.objects.all())
119-
future_projects = relations.PolymorphicResourceRelatedField(ProjectSerializer, queryset=models.Project.objects.all(), many=True)
118+
current_project = relations.PolymorphicResourceRelatedField(
119+
ProjectSerializer, queryset=models.Project.objects.all())
120+
future_projects = relations.PolymorphicResourceRelatedField(
121+
ProjectSerializer, queryset=models.Project.objects.all(), many=True)
120122

121123
included_serializers = {
122124
'current_project': ProjectSerializer,

example/tests/integration/test_polymorphism.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,29 @@ def test_polymorphism_on_polymorphic_model_list_post(client):
7373
assert content['data']['attributes']['artist'] == test_artist
7474

7575

76+
def test_invalid_type_on_polymorphic_model(client):
77+
test_topic = 'New test topic {}'.format(random.randint(0, 999999))
78+
test_artist = 'test-{}'.format(random.randint(0, 999999))
79+
url = reverse('project-list')
80+
data = {
81+
'data': {
82+
'type': 'invalidProjects',
83+
'attributes': {
84+
'topic': test_topic,
85+
'artist': test_artist
86+
}
87+
}
88+
}
89+
response = client.post(url, data=json.dumps(data), content_type='application/vnd.api+json')
90+
assert response.status_code == 409
91+
content = load_json(response.content)
92+
assert len(content["errors"]) is 1
93+
assert content["errors"][0]["status"] == "409"
94+
assert content["errors"][0]["detail"] == \
95+
"The resource object's type (invalidProjects) is not the type that constitute the " \
96+
"collection represented by the endpoint (one of [researchProjects, artProjects])."
97+
98+
7699
def test_polymorphism_relations_update(single_company, research_project_factory, client):
77100
response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk}))
78101
content = load_json(response.content)
@@ -85,7 +108,29 @@ def test_polymorphism_relations_update(single_company, research_project_factory,
85108
}
86109
response = client.put(reverse("company-detail", kwargs={'pk': single_company.pk}),
87110
data=json.dumps(content), content_type='application/vnd.api+json')
88-
assert response.status_code is 200
111+
assert response.status_code == 200
89112
content = load_json(response.content)
90113
assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "researchProjects"
91-
assert int(content["data"]["relationships"]["currentProject"]["data"]["id"]) is research_project.pk
114+
assert int(content["data"]["relationships"]["currentProject"]["data"]["id"]) == \
115+
research_project.pk
116+
117+
118+
def test_invalid_type_on_polymorphic_relation(single_company, research_project_factory, client):
119+
response = client.get(reverse("company-detail", kwargs={'pk': single_company.pk}))
120+
content = load_json(response.content)
121+
assert content["data"]["relationships"]["currentProject"]["data"]["type"] == "artProjects"
122+
123+
research_project = research_project_factory()
124+
content["data"]["relationships"]["currentProject"]["data"] = {
125+
"type": "invalidProjects",
126+
"id": research_project.pk
127+
}
128+
response = client.put(reverse("company-detail", kwargs={'pk': single_company.pk}),
129+
data=json.dumps(content), content_type='application/vnd.api+json')
130+
assert response.status_code == 409
131+
content = load_json(response.content)
132+
assert len(content["errors"]) is 1
133+
assert content["errors"][0]["status"] == "409"
134+
assert content["errors"][0]["detail"] == \
135+
"Incorrect relation type. Expected one of [researchProjects, artProjects], " \
136+
"received invalidProjects."

rest_framework_json_api/parsers.py

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ class JSONParser(parsers.JSONParser):
3030

3131
@staticmethod
3232
def parse_attributes(data):
33-
return utils.format_keys(data.get('attributes'), 'underscore') if data.get('attributes') else dict()
33+
return utils.format_keys(
34+
data.get('attributes'), 'underscore') if data.get('attributes') else dict()
3435

3536
@staticmethod
3637
def parse_relationships(data):
@@ -59,40 +60,49 @@ def parse(self, stream, media_type=None, parser_context=None):
5960
"""
6061
Parses the incoming bytestream as JSON and returns the resulting data
6162
"""
62-
result = super(JSONParser, self).parse(stream, media_type=media_type, parser_context=parser_context)
63+
result = super(JSONParser, self).parse(
64+
stream, media_type=media_type, parser_context=parser_context)
6365
data = result.get('data')
6466

6567
if data:
6668
from rest_framework_json_api.views import RelationshipView
6769
if isinstance(parser_context['view'], RelationshipView):
68-
# We skip parsing the object as JSONAPI Resource Identifier Object and not a regular Resource Object
70+
# We skip parsing the object as JSONAPI Resource Identifier Object is not a
71+
# regular Resource Object
6972
if isinstance(data, list):
7073
for resource_identifier_object in data:
71-
if not (resource_identifier_object.get('id') and resource_identifier_object.get('type')):
72-
raise ParseError(
73-
'Received data contains one or more malformed JSONAPI Resource Identifier Object(s)'
74-
)
74+
if not (resource_identifier_object.get('id') and
75+
resource_identifier_object.get('type')):
76+
raise ParseError('Received data contains one or more malformed '
77+
'JSONAPI Resource Identifier Object(s)')
7578
elif not (data.get('id') and data.get('type')):
76-
raise ParseError('Received data is not a valid JSONAPI Resource Identifier Object')
79+
raise ParseError('Received data is not a valid '
80+
'JSONAPI Resource Identifier Object')
7781

7882
return data
7983

8084
request = parser_context.get('request')
8185

8286
# Check for inconsistencies
83-
resource_name = utils.get_resource_name(parser_context)
84-
if isinstance(resource_name, six.string_types):
85-
doesnt_match = data.get('type') != resource_name
86-
else:
87-
doesnt_match = data.get('type') not in resource_name
88-
if doesnt_match and request.method in ('PUT', 'POST', 'PATCH'):
89-
raise exceptions.Conflict(
90-
"The resource object's type ({data_type}) is not the type "
91-
"that constitute the collection represented by the endpoint ({resource_type}).".format(
92-
data_type=data.get('type'),
93-
resource_type=resource_name
94-
)
95-
)
87+
if request.method in ('PUT', 'POST', 'PATCH'):
88+
resource_name = utils.get_resource_name(
89+
parser_context, expand_polymorphic_types=True)
90+
if isinstance(resource_name, six.string_types):
91+
if data.get('type') != resource_name:
92+
raise exceptions.Conflict(
93+
"The resource object's type ({data_type}) is not the type that "
94+
"constitute the collection represented by the endpoint "
95+
"({resource_type}).".format(
96+
data_type=data.get('type'),
97+
resource_type=resource_name))
98+
else:
99+
if data.get('type') not in resource_name:
100+
raise exceptions.Conflict(
101+
"The resource object's type ({data_type}) is not the type that "
102+
"constitute the collection represented by the endpoint "
103+
"(one of [{resource_types}]).".format(
104+
data_type=data.get('type'),
105+
resource_types=", ".join(resource_name)))
96106
if not data.get('id') and request.method in ('PATCH', 'PUT'):
97107
raise ParseError("The resource identifier object must contain an 'id' member")
98108

rest_framework_json_api/relations.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ class ResourceRelatedField(PrimaryKeyRelatedField):
2222
'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'),
2323
'incorrect_type': _('Incorrect type. Expected resource identifier object, received {data_type}.'),
2424
'incorrect_relation_type': _('Incorrect relation type. Expected {relation_type}, received {received_type}.'),
25-
# 'incorrect_poly_relation_type': _('Incorrect relation type. Expected one of {relation_type}, received {received_type}.'),
2625
'missing_type': _('Invalid resource identifier object: missing \'type\' attribute'),
2726
'missing_id': _('Invalid resource identifier object: missing \'id\' attribute'),
2827
'no_match': _('Invalid hyperlink - No URL match.'),
@@ -185,7 +184,7 @@ class PolymorphicResourceRelatedField(ResourceRelatedField):
185184

186185
_skip_polymorphic_optimization = False
187186
default_error_messages = dict(ResourceRelatedField.default_error_messages, **{
188-
'incorrect_relation_type': _('Incorrect relation type. Expected one of {relation_type}, '
187+
'incorrect_relation_type': _('Incorrect relation type. Expected one of [{relation_type}], '
189188
'received {received_type}.'),
190189
})
191190

@@ -209,7 +208,7 @@ def to_internal_value(self, data):
209208
if 'id' not in data:
210209
self.fail('missing_id')
211210

212-
expected_relation_types = get_resource_type_from_serializer(self.polymorphic_serializer)
211+
expected_relation_types = self.polymorphic_serializer.get_polymorphic_types()
213212

214213
if data['type'] not in expected_relation_types:
215214
self.conflict('incorrect_relation_type', relation_type=", ".join(

rest_framework_json_api/serializers.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -187,11 +187,12 @@ def __new__(cls, name, bases, attrs):
187187
serializer: serializer.Meta.model for serializer in polymorphic_serializers}
188188
model_to_serializer = {
189189
serializer.Meta.model: serializer for serializer in polymorphic_serializers}
190-
type_to_model = {
191-
get_resource_type_from_model(model): model for model in model_to_serializer.keys()}
190+
type_to_serializer = {
191+
get_resource_type_from_serializer(serializer): serializer for
192+
serializer in polymorphic_serializers}
192193
setattr(new_class, '_poly_serializer_model_map', serializer_to_model)
193194
setattr(new_class, '_poly_model_serializer_map', model_to_serializer)
194-
setattr(new_class, '_poly_type_model_map', type_to_model)
195+
setattr(new_class, '_poly_type_serializer_map', type_to_serializer)
195196
return new_class
196197

197198

@@ -213,51 +214,62 @@ def get_fields(self):
213214
raise Exception("Cannot get fields from a polymorphic serializer given a queryset")
214215
return super(PolymorphicModelSerializer, self).get_fields()
215216

216-
def get_polymorphic_serializer_for_instance(self, instance):
217+
@classmethod
218+
def get_polymorphic_serializer_for_instance(cls, instance):
217219
"""
218220
Return the polymorphic serializer associated with the given instance/model.
219221
Raise `NotImplementedError` if no serializer is found for the given model. This usually
220222
means that a serializer is missing in the class's `polymorphic_serializers` attribute.
221223
"""
222224
try:
223-
return self._poly_model_serializer_map[instance._meta.model]
225+
return cls._poly_model_serializer_map[instance._meta.model]
224226
except KeyError:
225227
raise NotImplementedError(
226228
"No polymorphic serializer has been found for model {}".format(
227229
instance._meta.model.__name__))
228230

229-
def get_polymorphic_model_for_serializer(self, serializer):
231+
@classmethod
232+
def get_polymorphic_model_for_serializer(cls, serializer):
230233
"""
231234
Return the polymorphic model associated with the given serializer.
232235
Raise `NotImplementedError` if no model is found for the given serializer. This usually
233236
means that a serializer is missing in the class's `polymorphic_serializers` attribute.
234237
"""
235238
try:
236-
return self._poly_serializer_model_map[serializer]
239+
return cls._poly_serializer_model_map[serializer]
237240
except KeyError:
238241
raise NotImplementedError(
239242
"No polymorphic model has been found for serializer {}".format(serializer.__name__))
240243

241-
def get_polymorphic_model_for_type(self, obj_type):
244+
@classmethod
245+
def get_polymorphic_serializer_for_type(cls, obj_type):
242246
"""
243-
Return the polymorphic model associated with the given type.
244-
Raise `NotImplementedError` if no model is found for the given type. This usually
247+
Return the polymorphic serializer associated with the given type.
248+
Raise `NotImplementedError` if no serializer is found for the given type. This usually
245249
means that a serializer is missing in the class's `polymorphic_serializers` attribute.
246250
"""
247251
try:
248-
return self._poly_type_model_map[obj_type]
252+
return cls._poly_type_serializer_map[obj_type]
249253
except KeyError:
250254
raise NotImplementedError(
251-
"No polymorphic model has been found for type {}".format(obj_type))
255+
"No polymorphic serializer has been found for type {}".format(obj_type))
252256

253-
def get_polymorphic_serializer_for_type(self, obj_type):
257+
@classmethod
258+
def get_polymorphic_model_for_type(cls, obj_type):
254259
"""
255-
Return the polymorphic serializer associated with the given type.
256-
Raise `NotImplementedError` if no serializer is found for the given type. This usually
260+
Return the polymorphic model associated with the given type.
261+
Raise `NotImplementedError` if no model is found for the given type. This usually
257262
means that a serializer is missing in the class's `polymorphic_serializers` attribute.
258263
"""
259-
return self.get_polymorphic_serializer_for_instance(
260-
self.get_polymorphic_model_for_type(obj_type))
264+
return cls.get_polymorphic_model_for_serializer(
265+
cls.get_polymorphic_serializer_for_type(obj_type))
266+
267+
@classmethod
268+
def get_polymorphic_types(cls):
269+
"""
270+
Return the list of accepted types.
271+
"""
272+
return cls._poly_type_serializer_map.keys()
261273

262274
def to_representation(self, instance):
263275
"""
@@ -272,10 +284,10 @@ def to_internal_value(self, data):
272284
appropriate polymorphic serializer and use this to handle internal value.
273285
"""
274286
received_type = data.get('type')
275-
expected_types = self._poly_type_model_map.keys()
287+
expected_types = self.get_polymorphic_types()
276288
if received_type not in expected_types:
277289
raise Conflict(
278-
'Incorrect relation type. Expected on of {expected_types}, '
290+
'Incorrect relation type. Expected on of [{expected_types}], '
279291
'received {received_type}.'.format(
280292
expected_types=', '.join(expected_types), received_type=received_type))
281293
serializer_class = self.get_polymorphic_serializer_for_type(received_type)

rest_framework_json_api/utils.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@
3232
POLYMORPHIC_ANCESTORS += (ancestor_class,)
3333

3434

35-
def get_resource_name(context):
35+
def get_resource_name(context, expand_polymorphic_types=False):
3636
"""
3737
Return the name of a resource.
3838
"""
39+
from . import serializers
3940
view = context.get('view')
4041

4142
# Sanity check to make sure we have a view.
@@ -57,7 +58,11 @@ def get_resource_name(context):
5758
except AttributeError:
5859
try:
5960
serializer = view.get_serializer_class()
60-
return get_resource_type_from_serializer(serializer)
61+
if issubclass(serializer, serializers.PolymorphicModelSerializer) and \
62+
expand_polymorphic_types:
63+
return serializer.get_polymorphic_types()
64+
else:
65+
return get_resource_type_from_serializer(serializer)
6166
except AttributeError:
6267
try:
6368
resource_name = get_resource_type_from_model(view.model)
@@ -246,9 +251,7 @@ def get_resource_type_from_manager(manager):
246251
def get_resource_type_from_serializer(serializer):
247252
json_api_meta = getattr(serializer, 'JSONAPIMeta', None)
248253
meta = getattr(serializer, 'Meta', None)
249-
if hasattr(serializer, 'polymorphic_serializers'):
250-
return [get_resource_type_from_serializer(s) for s in serializer.polymorphic_serializers]
251-
elif hasattr(json_api_meta, 'resource_name'):
254+
if hasattr(json_api_meta, 'resource_name'):
252255
return json_api_meta.resource_name
253256
elif hasattr(meta, 'resource_name'):
254257
return meta.resource_name

0 commit comments

Comments
 (0)