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 db9f7832..c304a670 100644 --- a/example/tests/integration/test_model_resource_name.py +++ b/example/tests/integration/test_model_resource_name.py @@ -1,9 +1,11 @@ import pytest -from django.core.urlresolvers import reverse +from copy import deepcopy +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 +39,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")) @@ -46,7 +66,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] @@ -61,29 +81,44 @@ 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'), ( '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, monkeypatch): + monkeypatch.setattr(serializers.CommentSerializer.Meta, 'resource_name', 'renamed_comments', False) + monkeypatch.setattr(serializers.EntrySerializer.Meta, 'resource_name', 'renamed_entries', False) + 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(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],) - try: - delattr(serializers.CommentSerializer.Meta, "resource_name") - except AttributeError: - pass - try: - delattr(views.CommentViewSet, "resource_name") - except AttributeError: - pass + models.Entry.__bases__ = (models.Entry.__bases__[0],) @pytest.mark.usefixtures("single_entry") diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index b4eefc45..18b4f4b0 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -1,8 +1,10 @@ import collections +import inflection import json 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 @@ -123,7 +125,12 @@ 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) + serializer_resource_type = self.get_resource_type_from_included_serializer() + + if serializer_resource_type is not None: + expected_relation_type = serializer_resource_type if 'type' not in data: self.fail('missing_type') @@ -142,19 +149,44 @@ 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 - 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]) - - resource_type = resource_type if resource_type else get_resource_type_from_instance(value) + 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): + """ + 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 + parent = self.get_parent_serializer() + + 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(parent) + for field in field_names: + if field in includes.keys(): + return get_resource_type_from_serializer(includes[field]) + + return None + + 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): + return self.parent + + return None + + def is_serializer(self, candidate): + return isinstance(candidate, Serializer) + def get_choices(self, cutoff=None): queryset = self.get_queryset() if queryset is None: @@ -219,4 +251,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)