From 484adc28ec5b4136ff2327bc19202af7656c7198 Mon Sep 17 00:00:00 2001 From: Christian Zosel Date: Thu, 1 Dec 2016 10:08:14 +0100 Subject: [PATCH 1/8] Fix resource_name support for ResourceRelatedField --- rest_framework_json_api/relations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index b4eefc45..697f8298 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -145,7 +145,7 @@ def to_representation(self, value): # check to see if this resource has a different resource_name when # included and use that name resource_type = None - root = getattr(self.parent, 'parent', self.parent) + root = getattr(self.parent, 'parent', self.parent) or self.parent field_name = self.field_name if self.field_name else self.parent.field_name if getattr(root, 'included_serializers', None) is not None: includes = get_included_serializers(root) From 1c8d835c36467a587ce1fa833e6fc8a1e5e732d2 Mon Sep 17 00:00:00 2001 From: Christian Zosel Date: Thu, 1 Dec 2016 17:38:40 +0100 Subject: [PATCH 2/8] Check resource name on included serializer in to_internal_value --- rest_framework_json_api/relations.py | 33 +++++++++++++++++++++------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index b4eefc45..c754670d 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -1,4 +1,5 @@ import collections +import inflection import json from rest_framework.fields import MISSING_ERROR_MESSAGE, SerializerMethodField @@ -123,7 +124,13 @@ def to_internal_value(self, data): self.fail('incorrect_type', data_type=type(data).__name__) if not isinstance(data, dict): self.fail('incorrect_type', data_type=type(data).__name__) + expected_relation_type = get_resource_type_from_queryset(self.queryset) + field_name = inflection.singularize(expected_relation_type) + serializer_resource_type = self.get_resource_type_from_serializer(field_name) + + if serializer_resource_type is not None: + expected_relation_type = serializer_resource_type if 'type' not in data: self.fail('missing_type') @@ -142,18 +149,28 @@ def to_representation(self, value): else: pk = value.pk - # check to see if this resource has a different resource_name when - # included and use that name - resource_type = None - root = getattr(self.parent, 'parent', self.parent) field_name = self.field_name if self.field_name else self.parent.field_name + + resource_type = self.get_resource_type_from_serializer(field_name) + if resource_type is None: + resource_type = get_resource_type_from_instance(value) + + return OrderedDict([('type', resource_type), ('id', str(pk))]) + + def get_resource_type_from_serializer(self, field_name): + """ + Given a field_name, check if the serializer has a + corresponding included_serializer with a Meta.resource_name property + + Returns the resource name or None + """ + root = getattr(self.parent, 'parent', self.parent) or self.parent if getattr(root, 'included_serializers', None) is not None: includes = get_included_serializers(root) if field_name in includes.keys(): - resource_type = get_resource_type_from_serializer(includes[field_name]) + return get_resource_type_from_serializer(includes[field_name]) - resource_type = resource_type if resource_type else get_resource_type_from_instance(value) - return OrderedDict([('type', resource_type), ('id', str(pk))]) + return None def get_choices(self, cutoff=None): queryset = self.get_queryset() @@ -219,4 +236,4 @@ def to_representation(self, value): if isinstance(value, collections.Iterable): base = super(SerializerMethodResourceRelatedField, self) return [base.to_representation(x) for x in value] - return super(SerializerMethodResourceRelatedField, self).to_representation(value) \ No newline at end of file + return super(SerializerMethodResourceRelatedField, self).to_representation(value) From 0700202d2f525709015ceecb22240072806a49e8 Mon Sep 17 00:00:00 2001 From: Christian Zosel Date: Fri, 2 Dec 2016 13:26:33 +0100 Subject: [PATCH 3/8] Revert "Fix resource_name support for ResourceRelatedField" This reverts commit 484adc28ec5b4136ff2327bc19202af7656c7198. --- rest_framework_json_api/relations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 697f8298..b4eefc45 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -145,7 +145,7 @@ def to_representation(self, value): # check to see if this resource has a different resource_name when # included and use that name resource_type = None - root = getattr(self.parent, 'parent', self.parent) or self.parent + root = getattr(self.parent, 'parent', self.parent) field_name = self.field_name if self.field_name else self.parent.field_name if getattr(root, 'included_serializers', None) is not None: includes = get_included_serializers(root) From 76caa820d6ce85cd4629db1c29546efbe1c268b5 Mon Sep 17 00:00:00 2001 From: Christian Zosel Date: Fri, 2 Dec 2016 13:30:53 +0100 Subject: [PATCH 4/8] Improve identification of root serializer --- rest_framework_json_api/relations.py | 32 ++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index c754670d..6d1148e3 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -126,8 +126,7 @@ def to_internal_value(self, data): self.fail('incorrect_type', data_type=type(data).__name__) expected_relation_type = get_resource_type_from_queryset(self.queryset) - field_name = inflection.singularize(expected_relation_type) - serializer_resource_type = self.get_resource_type_from_serializer(field_name) + serializer_resource_type = self.get_resource_type_from_included_serializer(expected_relation_type) if serializer_resource_type is not None: expected_relation_type = serializer_resource_type @@ -151,13 +150,13 @@ def to_representation(self, value): field_name = self.field_name if self.field_name else self.parent.field_name - resource_type = self.get_resource_type_from_serializer(field_name) + resource_type = self.get_resource_type_from_included_serializer(field_name) if resource_type is None: resource_type = get_resource_type_from_instance(value) return OrderedDict([('type', resource_type), ('id', str(pk))]) - def get_resource_type_from_serializer(self, field_name): + def get_resource_type_from_included_serializer(self, field_name): """ Given a field_name, check if the serializer has a corresponding included_serializer with a Meta.resource_name property @@ -165,13 +164,32 @@ def get_resource_type_from_serializer(self, field_name): Returns the resource name or None """ root = getattr(self.parent, 'parent', self.parent) or self.parent - if getattr(root, 'included_serializers', None) is not None: + root = self.get_root_serializer() + + if root is not None: + # accept both singular and plural versions of field_name + field_names = [ + inflection.singularize(field_name), + inflection.pluralize(field_name) + ] includes = get_included_serializers(root) - if field_name in includes.keys(): - return get_resource_type_from_serializer(includes[field_name]) + for field in field_names: + if field in includes.keys(): + return get_resource_type_from_serializer(includes[field]) return None + def get_root_serializer(self): + if hasattr(self.parent, 'parent') and self.is_serializer(self.parent.parent): + return self.parent.parent + elif self.is_serializer(self.parent): + return self.parent + + return None + + def is_serializer(self, candidate): + return hasattr(candidate, 'included_serializers') + def get_choices(self, cutoff=None): queryset = self.get_queryset() if queryset is None: From 40539017ec7161ef236af8eb341b0c46350b1788 Mon Sep 17 00:00:00 2001 From: Christian Zosel Date: Mon, 5 Dec 2016 10:53:05 +0100 Subject: [PATCH 5/8] Add tests for resource_name support --- .../integration/test_model_resource_name.py | 48 +++++++++++++++++-- rest_framework_json_api/relations.py | 7 +-- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/example/tests/integration/test_model_resource_name.py b/example/tests/integration/test_model_resource_name.py index db9f7832..ebc6d987 100644 --- a/example/tests/integration/test_model_resource_name.py +++ b/example/tests/integration/test_model_resource_name.py @@ -1,9 +1,10 @@ import pytest -from django.core.urlresolvers import reverse +from example import models, serializers, views +from example.tests.utils import dump_json, load_json +from rest_framework import status -from example.tests.utils import load_json +from django.core.urlresolvers import reverse -from example import models, serializers, views pytestmark = pytest.mark.django_db @@ -37,6 +38,24 @@ def _check_relationship_and_included_comment_type_are_the_same(django_client, ur @pytest.mark.usefixtures("single_entry") class TestModelResourceName: + create_data = { + 'data': { + 'type': 'resource_name_from_JSONAPIMeta', + 'id': None, + 'attributes': { + 'body': 'example', + }, + 'relationships': { + 'entry': { + 'data': { + 'type': 'resource_name_from_JSONAPIMeta', + 'id': 1 + } + } + } + } + } + def test_model_resource_name_on_list(self, client): models.Comment.__bases__ += (_PatchedModel,) response = client.get(reverse("comment-list")) @@ -74,10 +93,33 @@ def test_resource_name_precendence(self, client): assert (data.get('type') == 'resource_name_from_view'), ( 'resource_name from view incorrect on list') + def test_model_resource_name_create(self, client): + models.Comment.__bases__ += (_PatchedModel,) + models.Entry.__bases__ += (_PatchedModel,) + response = client.post(reverse("comment-list"), + dump_json(self.create_data), + content_type='application/vnd.api+json') + + assert response.status_code == status.HTTP_201_CREATED + + def test_serializer_resource_name_create(self, client): + serializers.CommentSerializer.Meta.resource_name = "renamed_comments" + serializers.EntrySerializer.Meta.resource_name = "renamed_entries" + self.create_data['data']['type'] = 'renamed_comments' + self.create_data['data']['relationships']['entry']['data']['type'] = 'renamed_entries' + + response = client.post(reverse("comment-list"), + dump_json(self.create_data), + content_type='application/vnd.api+json') + + assert response.status_code == status.HTTP_201_CREATED + def teardown_method(self, method): models.Comment.__bases__ = (models.Comment.__bases__[0],) + models.Entry.__bases__ = (models.Entry.__bases__[0],) try: delattr(serializers.CommentSerializer.Meta, "resource_name") + delattr(serializers.EntrySerializer.Meta, "resource_name") except AttributeError: pass try: diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 6d1148e3..cf2214ea 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -158,12 +158,9 @@ def to_representation(self, value): def get_resource_type_from_included_serializer(self, field_name): """ - Given a field_name, check if the serializer has a - corresponding included_serializer with a Meta.resource_name property - - Returns the resource name or None + Check to see it this resource has a different resource_name when + included and return that name, or None """ - root = getattr(self.parent, 'parent', self.parent) or self.parent root = self.get_root_serializer() if root is not None: From b0f0ed10f0c75d42648c70406bc29dd02e5e7596 Mon Sep 17 00:00:00 2001 From: Christian Zosel Date: Tue, 13 Dec 2016 17:12:02 +0100 Subject: [PATCH 6/8] Fix field_name support in to_internal_value --- rest_framework_json_api/relations.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index cf2214ea..2618102e 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -126,7 +126,7 @@ def to_internal_value(self, data): self.fail('incorrect_type', data_type=type(data).__name__) expected_relation_type = get_resource_type_from_queryset(self.queryset) - serializer_resource_type = self.get_resource_type_from_included_serializer(expected_relation_type) + serializer_resource_type = self.get_resource_type_from_included_serializer() if serializer_resource_type is not None: expected_relation_type = serializer_resource_type @@ -148,19 +148,18 @@ def to_representation(self, value): else: pk = value.pk - field_name = self.field_name if self.field_name else self.parent.field_name - - resource_type = self.get_resource_type_from_included_serializer(field_name) + resource_type = self.get_resource_type_from_included_serializer() if resource_type is None: resource_type = get_resource_type_from_instance(value) return OrderedDict([('type', resource_type), ('id', str(pk))]) - def get_resource_type_from_included_serializer(self, field_name): + def get_resource_type_from_included_serializer(self): """ Check to see it this resource has a different resource_name when included and return that name, or None """ + field_name = self.field_name or self.parent.field_name root = self.get_root_serializer() if root is not None: From 1001fa571b23dc3c37418bf796a3065984285e35 Mon Sep 17 00:00:00 2001 From: Christian Zosel Date: Wed, 14 Dec 2016 10:03:46 +0100 Subject: [PATCH 7/8] Refactor tests to use pytest monkeypatch --- .../integration/test_model_resource_name.py | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/example/tests/integration/test_model_resource_name.py b/example/tests/integration/test_model_resource_name.py index ebc6d987..4c3bea9e 100644 --- a/example/tests/integration/test_model_resource_name.py +++ b/example/tests/integration/test_model_resource_name.py @@ -65,7 +65,7 @@ def test_model_resource_name_on_list(self, client): 'resource_name from model incorrect on list') # Precedence tests - def test_resource_name_precendence(self, client): + def test_resource_name_precendence(self, client, monkeypatch): # default response = client.get(reverse("comment-list")) data = load_json(response.content)['data'][0] @@ -80,14 +80,14 @@ def test_resource_name_precendence(self, client): 'resource_name from model incorrect on list') # serializer > model - serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer" + monkeypatch.setattr(serializers.CommentSerializer.Meta, 'resource_name', 'resource_name_from_serializer', False) response = client.get(reverse("comment-list")) data = load_json(response.content)['data'][0] assert (data.get('type') == 'resource_name_from_serializer'), ( 'resource_name from serializer incorrect on list') # view > serializer > model - views.CommentViewSet.resource_name = 'resource_name_from_view' + monkeypatch.setattr(views.CommentViewSet, 'resource_name', 'resource_name_from_view', False) response = client.get(reverse("comment-list")) data = load_json(response.content)['data'][0] assert (data.get('type') == 'resource_name_from_view'), ( @@ -102,9 +102,9 @@ def test_model_resource_name_create(self, client): assert response.status_code == status.HTTP_201_CREATED - def test_serializer_resource_name_create(self, client): - serializers.CommentSerializer.Meta.resource_name = "renamed_comments" - serializers.EntrySerializer.Meta.resource_name = "renamed_entries" + def test_serializer_resource_name_create(self, client, monkeypatch): + monkeypatch.setattr(serializers.CommentSerializer.Meta, 'resource_name', 'renamed_comments', False) + monkeypatch.setattr(serializers.EntrySerializer.Meta, 'resource_name', 'renamed_entries', False) self.create_data['data']['type'] = 'renamed_comments' self.create_data['data']['relationships']['entry']['data']['type'] = 'renamed_entries' @@ -117,15 +117,6 @@ def test_serializer_resource_name_create(self, client): def teardown_method(self, method): models.Comment.__bases__ = (models.Comment.__bases__[0],) models.Entry.__bases__ = (models.Entry.__bases__[0],) - try: - delattr(serializers.CommentSerializer.Meta, "resource_name") - delattr(serializers.EntrySerializer.Meta, "resource_name") - except AttributeError: - pass - try: - delattr(views.CommentViewSet, "resource_name") - except AttributeError: - pass @pytest.mark.usefixtures("single_entry") From fec7e0acb4b18b099f0d337b1aec22743e66a594 Mon Sep 17 00:00:00 2001 From: Christian Zosel Date: Wed, 10 May 2017 09:52:07 +0200 Subject: [PATCH 8/8] Minor refactoring based on feedback --- AUTHORS | 1 + CHANGELOG.md | 1 + example/tests/integration/test_model_resource_name.py | 8 +++++--- rest_framework_json_api/relations.py | 11 ++++++----- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/AUTHORS b/AUTHORS index 306d1141..75d66ce1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,4 +1,5 @@ Jerel Unruh Greg Aker Adam Wróbel +Christian Zosel diff --git a/CHANGELOG.md b/CHANGELOG.md index d6e68e6e..2dd34ed2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ v2.3.0 attributes and relations to snake\_case format. This conversion was unexpected and there was no way to turn it off. * Fix for apps that don't use `django.contrib.contenttypes`. +* Fix `resource_name` support for POST requests and nested serializers v2.2.0 diff --git a/example/tests/integration/test_model_resource_name.py b/example/tests/integration/test_model_resource_name.py index 4c3bea9e..c304a670 100644 --- a/example/tests/integration/test_model_resource_name.py +++ b/example/tests/integration/test_model_resource_name.py @@ -1,4 +1,5 @@ import pytest +from copy import deepcopy from example import models, serializers, views from example.tests.utils import dump_json, load_json from rest_framework import status @@ -105,11 +106,12 @@ def test_model_resource_name_create(self, client): def test_serializer_resource_name_create(self, client, monkeypatch): monkeypatch.setattr(serializers.CommentSerializer.Meta, 'resource_name', 'renamed_comments', False) monkeypatch.setattr(serializers.EntrySerializer.Meta, 'resource_name', 'renamed_entries', False) - self.create_data['data']['type'] = 'renamed_comments' - self.create_data['data']['relationships']['entry']['data']['type'] = 'renamed_entries' + create_data = deepcopy(self.create_data) + create_data['data']['type'] = 'renamed_comments' + create_data['data']['relationships']['entry']['data']['type'] = 'renamed_entries' response = client.post(reverse("comment-list"), - dump_json(self.create_data), + dump_json(create_data), content_type='application/vnd.api+json') assert response.status_code == status.HTTP_201_CREATED diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 2618102e..18b4f4b0 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -4,6 +4,7 @@ from rest_framework.fields import MISSING_ERROR_MESSAGE, SerializerMethodField from rest_framework.relations import * +from rest_framework.serializers import Serializer from django.utils.translation import ugettext_lazy as _ from django.db.models.query import QuerySet @@ -160,22 +161,22 @@ def get_resource_type_from_included_serializer(self): included and return that name, or None """ field_name = self.field_name or self.parent.field_name - root = self.get_root_serializer() + parent = self.get_parent_serializer() - if root is not None: + if parent is not None: # accept both singular and plural versions of field_name field_names = [ inflection.singularize(field_name), inflection.pluralize(field_name) ] - includes = get_included_serializers(root) + includes = get_included_serializers(parent) for field in field_names: if field in includes.keys(): return get_resource_type_from_serializer(includes[field]) return None - def get_root_serializer(self): + def get_parent_serializer(self): if hasattr(self.parent, 'parent') and self.is_serializer(self.parent.parent): return self.parent.parent elif self.is_serializer(self.parent): @@ -184,7 +185,7 @@ def get_root_serializer(self): return None def is_serializer(self, candidate): - return hasattr(candidate, 'included_serializers') + return isinstance(candidate, Serializer) def get_choices(self, cutoff=None): queryset = self.get_queryset()