From 66c1eef00f36f52facf2fca84e032ee254e815ff Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Fri, 16 Oct 2015 15:45:35 -0400 Subject: [PATCH 01/38] Use serializer `resource_name` for nested objects when present Closes #74 --- rest_framework_json_api/utils.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index a82cc2aa..5c79efa9 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -454,8 +454,7 @@ def extract_included(fields, resource, resource_instance, included_resources): if isinstance(field, ListSerializer): serializer = field.child - model = serializer.Meta.model - relation_type = format_relation_name(model.__name__) + relation_type = get_resource_type_from_serializer(serializer) relation_queryset = list(relation_instance_or_manager.all()) # Get the serializer fields @@ -476,8 +475,7 @@ def extract_included(fields, resource, resource_instance, included_resources): ) if isinstance(field, ModelSerializer): - model = field.Meta.model - relation_type = format_relation_name(model.__name__) + relation_type = get_resource_type_from_serializer(field) # Get the serializer fields serializer_fields = get_serializer_fields(field) From 33ffa01973400cd7c4af9eda54110dbc5c9a0dea Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Mon, 19 Oct 2015 09:18:06 -0400 Subject: [PATCH 02/38] Use serializer `resource_name` for nested objects in relationships too --- rest_framework_json_api/utils.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 5c79efa9..ed0833ad 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -376,7 +376,7 @@ def extract_relationships(fields, resource, resource_instance): if isinstance(serializer_data, list): for position in range(len(serializer_data)): nested_resource_instance = resource_instance_queryset[position] - nested_resource_instance_type = get_resource_type_from_instance(nested_resource_instance) + nested_resource_instance_type = get_resource_type_from_serializer(field.child) relation_data.append(OrderedDict([ ('type', nested_resource_instance_type), ('id', encoding.force_text(nested_resource_instance.pk)) @@ -386,9 +386,6 @@ def extract_relationships(fields, resource, resource_instance): continue if isinstance(field, ModelSerializer): - relation_model = field.Meta.model - relation_type = format_relation_name(relation_model.__name__) - data.update({ field_name: { 'data': ( From 73bc6d57a1321ec4d831e2a78a1b09d1d6573308 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Mon, 19 Oct 2015 09:23:44 -0400 Subject: [PATCH 03/38] Use serializer `resource_name` for nested ModelSerializers --- rest_framework_json_api/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index ed0833ad..4a3addb4 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -386,6 +386,8 @@ def extract_relationships(fields, resource, resource_instance): continue if isinstance(field, ModelSerializer): + relation_type = get_resource_type_from_serializer(field) + data.update({ field_name: { 'data': ( From 52a5b664fb5f4f232cad8174a4ddee7f3fcdcd7a Mon Sep 17 00:00:00 2001 From: Scott Fisk Date: Thu, 5 Nov 2015 10:24:21 -0500 Subject: [PATCH 04/38] added a get_resource_type_from_model replaced other usages of getting model type by __name__ --- rest_framework_json_api/serializers.py | 2 +- rest_framework_json_api/utils.py | 28 ++++++++++++++++---------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index 8fd78292..abc7a027 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -29,7 +29,7 @@ def to_representation(self, instance): } def to_internal_value(self, data): - if data['type'] != format_relation_name(self.model_class.__name__): + if data['type'] != get_resource_type_from_model(self.model_class): self.fail('incorrect_model_type', model_type=self.model_class, received_type=data['type']) pk = data['id'] try: diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index ed01b739..a4093718 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -50,7 +50,7 @@ def get_resource_name(context): return get_resource_type_from_serializer(serializer) except AttributeError: try: - resource_name = view.model.__name__ + resource_name = get_resource_type_from_model(view.model) except AttributeError: resource_name = view.__class__.__name__ @@ -171,7 +171,7 @@ def get_related_resource_type(relation): relation_model = parent_model_relation.field.related.model else: return get_related_resource_type(parent_model_relation) - return format_relation_name(relation_model.__name__) + return get_resource_type_from_model(relation_model) def get_instance_or_manager_resource_type(resource_instance_or_manager): @@ -182,25 +182,31 @@ def get_instance_or_manager_resource_type(resource_instance_or_manager): pass +def get_resource_type_from_model(model): + json_api_meta = getattr(model, 'JSONAPIMeta', None) + return getattr( + json_api_meta, + 'resource_name', + format_relation_name(model.__name__)) + + def get_resource_type_from_queryset(qs): - return format_relation_name(qs.model._meta.model.__name__) + return get_resource_type_from_model(qs.model) def get_resource_type_from_instance(instance): - return format_relation_name(instance._meta.model.__name__) + return get_resource_type_from_model(instance._meta.model) def get_resource_type_from_manager(manager): - return format_relation_name(manager.model.__name__) + return get_resource_type_from_model(manager.model) def get_resource_type_from_serializer(serializer): - try: - # Check the meta class for resource_name - return serializer.Meta.resource_name - except AttributeError: - # Use the serializer model then pluralize and format - return format_relation_name(serializer.Meta.model.__name__) + return getattr( + serializer.Meta, + 'resource_name', + get_resource_type_from_model(serializer.Meta.model)) def get_included_serializers(serializer): From e783a4322ada6513a7d8db756a115bbc1dc95a94 Mon Sep 17 00:00:00 2001 From: Scott Fisk Date: Thu, 5 Nov 2015 10:28:27 -0500 Subject: [PATCH 05/38] dropped format_relation_name from wrapping get_resource_type* This isnt required anymore as the get_resource_type calls handle this --- rest_framework_json_api/relations.py | 4 ++-- rest_framework_json_api/serializers.py | 7 ++++--- rest_framework_json_api/views.py | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 61b9ecd7..1a56464d 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -5,7 +5,7 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework_json_api.exceptions import Conflict -from rest_framework_json_api.utils import format_relation_name, Hyperlink, \ +from rest_framework_json_api.utils import Hyperlink, \ get_resource_type_from_queryset, get_resource_type_from_instance @@ -127,7 +127,7 @@ def to_representation(self, value): else: pk = value.pk - return OrderedDict([('type', format_relation_name(get_resource_type_from_instance(value))), ('id', str(pk))]) + return OrderedDict([('type', get_resource_type_from_instance(value)), ('id', str(pk))]) @property def choices(self): diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index abc7a027..6e8d377c 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -3,8 +3,9 @@ from rest_framework.serializers import * from rest_framework_json_api.relations import ResourceRelatedField -from rest_framework_json_api.utils import format_relation_name, get_resource_type_from_instance, \ - get_resource_type_from_serializer, get_included_serializers +from rest_framework_json_api.utils import ( + get_resource_type_from_model, get_resource_type_from_instance, + get_resource_type_from_serializer, get_included_serializers) class ResourceIdentifierObjectSerializer(BaseSerializer): @@ -24,7 +25,7 @@ def __init__(self, *args, **kwargs): def to_representation(self, instance): return { - 'type': format_relation_name(get_resource_type_from_instance(instance)), + 'type': get_resource_type_from_instance(instance), 'id': str(instance.pk) } diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 36c89687..a43fad89 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -12,7 +12,7 @@ from rest_framework_json_api.exceptions import Conflict from rest_framework_json_api.serializers import ResourceIdentifierObjectSerializer -from rest_framework_json_api.utils import format_relation_name, get_resource_type_from_instance, OrderedDict, Hyperlink +from rest_framework_json_api.utils import get_resource_type_from_instance, OrderedDict, Hyperlink class RelationshipView(generics.GenericAPIView): @@ -154,7 +154,7 @@ def _instantiate_serializer(self, instance): def get_resource_name(self): if not hasattr(self, '_resource_name'): instance = getattr(self.get_object(), self.kwargs['related_field']) - self._resource_name = format_relation_name(get_resource_type_from_instance(instance)) + self._resource_name = get_resource_type_from_instance(instance) return self._resource_name def set_resource_name(self, value): From e35353e69c7d7c7156f7a12d66fb4c97bcfcfc48 Mon Sep 17 00:00:00 2001 From: Scott Fisk Date: Tue, 1 Dec 2015 12:15:31 -0500 Subject: [PATCH 06/38] Added basic test for model resource_name property --- example/models.py | 12 ++++++++++++ example/serializers.py | 9 ++++++++- .../tests/integration/test_model_resource_name.py | 13 +++++++++++++ example/urls_test.py | 3 ++- example/views.py | 11 ++++++++--- 5 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 example/tests/integration/test_model_resource_name.py diff --git a/example/models.py b/example/models.py index 64291393..4738014e 100644 --- a/example/models.py +++ b/example/models.py @@ -34,6 +34,18 @@ def __str__(self): return self.name +@python_2_unicode_compatible +class RenamedAuthor(Author): + class Meta: + proxy = True + + class JSONAPIMeta: + resource_name = "super-author" + + def __str__(self): + return self.name + + @python_2_unicode_compatible class Entry(BaseModel): blog = models.ForeignKey(Blog) diff --git a/example/serializers.py b/example/serializers.py index c6b243a1..4ac43ada 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -1,5 +1,5 @@ from rest_framework_json_api import serializers, relations -from example.models import Blog, Entry, Author, Comment +from example.models import Blog, Entry, Author, Comment, RenamedAuthor class BlogSerializer(serializers.ModelSerializer): @@ -45,6 +45,13 @@ class Meta: fields = ('name', 'email',) +class RenamedAuthorSerializer(serializers.ModelSerializer): + + class Meta: + model = RenamedAuthor + fields = ('name', 'email',) + + class CommentSerializer(serializers.ModelSerializer): class Meta: diff --git a/example/tests/integration/test_model_resource_name.py b/example/tests/integration/test_model_resource_name.py new file mode 100644 index 00000000..289c8f1e --- /dev/null +++ b/example/tests/integration/test_model_resource_name.py @@ -0,0 +1,13 @@ +import pytest +from django.core.urlresolvers import reverse + +from example.tests.utils import load_json + +pytestmark = pytest.mark.django_db + + +def test_model_resource_name_on_list(single_entry, client): + response = client.get(reverse("renamed-authors-list")) + data = load_json(response.content)['data'] + # name should be super-author instead of model name RenamedAuthor + assert [x.get('type') for x in data] == ['super-author'], 'List included types are incorrect' diff --git a/example/urls_test.py b/example/urls_test.py index 96f415fd..0a4e7e75 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -2,7 +2,7 @@ from rest_framework import routers from example.views import BlogViewSet, EntryViewSet, AuthorViewSet, EntryRelationshipView, BlogRelationshipView, \ - CommentRelationshipView, AuthorRelationshipView + CommentRelationshipView, AuthorRelationshipView, RenamedAuthorViewSet from .api.resources.identity import Identity, GenericIdentity router = routers.DefaultRouter(trailing_slash=False) @@ -10,6 +10,7 @@ router.register(r'blogs', BlogViewSet) router.register(r'entries', EntryViewSet) router.register(r'authors', AuthorViewSet) +router.register(r'renamed-authors', RenamedAuthorViewSet, base_name='renamed-authors') # for the old tests router.register(r'identities', Identity) diff --git a/example/views.py b/example/views.py index 59ca1a05..c34f1401 100644 --- a/example/views.py +++ b/example/views.py @@ -1,8 +1,9 @@ from rest_framework import viewsets from rest_framework_json_api.views import RelationshipView -from example.models import Blog, Entry, Author, Comment +from example.models import Blog, Entry, Author, Comment, RenamedAuthor from example.serializers import ( - BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer) + BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer, + RenamedAuthorSerializer) class BlogViewSet(viewsets.ModelViewSet): @@ -21,6 +22,11 @@ class AuthorViewSet(viewsets.ModelViewSet): serializer_class = AuthorSerializer +class RenamedAuthorViewSet(viewsets.ModelViewSet): + queryset = RenamedAuthor.objects.all() + serializer_class = RenamedAuthorSerializer + + class CommentViewSet(viewsets.ModelViewSet): queryset = Comment.objects.all() serializer_class = CommentSerializer @@ -41,4 +47,3 @@ class CommentRelationshipView(RelationshipView): class AuthorRelationshipView(RelationshipView): queryset = Author.objects.all() self_link_view_name = 'author-relationships' - From e3291451ef4a41caf114cb1f18f60f4d773dfa8f Mon Sep 17 00:00:00 2001 From: Mojtaba Kohram Date: Fri, 11 Dec 2015 18:02:56 -0500 Subject: [PATCH 07/38] started testing all combinations of resource_name on model, serializer and view --- .../integration/test_model_resource_name.py | 78 +++++++++++++++++++ example/urls_test.py | 3 +- 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/example/tests/integration/test_model_resource_name.py b/example/tests/integration/test_model_resource_name.py index 289c8f1e..c00f5d3a 100644 --- a/example/tests/integration/test_model_resource_name.py +++ b/example/tests/integration/test_model_resource_name.py @@ -3,6 +3,8 @@ from example.tests.utils import load_json +from rest_framework.test import APITestCase +from example import models, serializers pytestmark = pytest.mark.django_db @@ -11,3 +13,79 @@ def test_model_resource_name_on_list(single_entry, client): data = load_json(response.content)['data'] # name should be super-author instead of model name RenamedAuthor assert [x.get('type') for x in data] == ['super-author'], 'List included types are incorrect' + +@pytest.mark.usefixtures("single_entry") +class ResourceNameConsistencyTest(APITestCase): + + def test_type_match_on_included_and_inline_base(self): + self._check_relationship_and_included_comment_type_are_the_same(reverse("entry-list")) + + def test_type_match_on_included_and_inline_with_JSONAPIMeta(self): + models.Comment.__bases__ += (self._PatchedModel,) + + self._check_relationship_and_included_comment_type_are_the_same(reverse("entry-list")) + + def test_type_match_on_included_and_inline_with_serializer_resource_name(self): + serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer" + + self._check_relationship_and_included_comment_type_are_the_same(reverse("entry-list")) + + def test_type_match_on_included_and_inline_with_serializer_resource_name_and_JSONAPIMeta(self): + models.Comment.__bases__ += (self._PatchedModel,) + serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer" + + self._check_relationship_and_included_comment_type_are_the_same(reverse("entry-list")) + + def test_resource_and_relationship_type_match(self): + self._check_resource_and_relationship_comment_type_match() + + def test_resource_and_relationship_type_match_with_serializer_resource_name(self): + serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer" + + self._check_resource_and_relationship_comment_type_match() + + def test_resource_and_relationship_type_match_with_JSONAPIMeta(self): + models.Comment.__bases__ += (self._PatchedModel,) + + self._check_resource_and_relationship_comment_type_match() + + def test_resource_and_relationship_type_match_with_serializer_resource_name_and_JSONAPIMeta(self): + models.Comment.__bases__ += (self._PatchedModel,) + serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer" + + self._check_resource_and_relationship_comment_type_match() + + def _check_resource_and_relationship_comment_type_match(self): + entry_response = self.client.get(reverse("entry-list")) + comment_response = self.client.get(reverse("comment-list")) + + comment_resource_type = load_json(comment_response.content).get('data')[0].get('type') + comment_relationship_type = load_json(entry_response.content).get( + 'data')[0].get('relationships').get('comments').get('data')[0].get('type') + + assert comment_resource_type == comment_relationship_type, "The resource type seen in the relationships and head resource do not match" + + def _check_relationship_and_included_comment_type_are_the_same(self, url): + response = self.client.get(url + "?include=comments") + data = load_json(response.content).get('data')[0] + comment = load_json(response.content).get('included')[0] + + comment_relationship_type = data.get('relationships').get('comments').get('data')[0].get('type') + comment_included_type = comment.get('type') + + assert comment_relationship_type == comment_included_type, "The resource type seen in the relationships and included do not match" + + def tearDown(self): + models.Comment.__bases__ = (models.Comment.__bases__[0],) + try: + delattr(serializers.CommentSerializer.Meta, "resource_name") + except AttributeError: + pass + + class _PatchedModel: + class Meta: + proxy = True + + class JSONAPIMeta: + resource_name = "resource_name_from_JSONAPIMeta" + \ No newline at end of file diff --git a/example/urls_test.py b/example/urls_test.py index 0a4e7e75..6d818cf3 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -2,7 +2,7 @@ from rest_framework import routers from example.views import BlogViewSet, EntryViewSet, AuthorViewSet, EntryRelationshipView, BlogRelationshipView, \ - CommentRelationshipView, AuthorRelationshipView, RenamedAuthorViewSet + CommentRelationshipView, AuthorRelationshipView, RenamedAuthorViewSet, CommentViewSet from .api.resources.identity import Identity, GenericIdentity router = routers.DefaultRouter(trailing_slash=False) @@ -11,6 +11,7 @@ router.register(r'entries', EntryViewSet) router.register(r'authors', AuthorViewSet) router.register(r'renamed-authors', RenamedAuthorViewSet, base_name='renamed-authors') +router.register(r'comments', CommentViewSet) # for the old tests router.register(r'identities', Identity) From 867e2099a9ed937d0454d2cd6492a5962392b3a2 Mon Sep 17 00:00:00 2001 From: Mojtaba Kohram Date: Fri, 11 Dec 2015 18:34:24 -0500 Subject: [PATCH 08/38] remove proxy=True on model --- example/tests/integration/test_model_resource_name.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/example/tests/integration/test_model_resource_name.py b/example/tests/integration/test_model_resource_name.py index c00f5d3a..733f0c83 100644 --- a/example/tests/integration/test_model_resource_name.py +++ b/example/tests/integration/test_model_resource_name.py @@ -83,8 +83,6 @@ def tearDown(self): pass class _PatchedModel: - class Meta: - proxy = True class JSONAPIMeta: resource_name = "resource_name_from_JSONAPIMeta" From fea279dc475e16aa0108683717b44ee0f1ee16ba Mon Sep 17 00:00:00 2001 From: Scott Fisk Date: Wed, 16 Dec 2015 11:11:39 -0500 Subject: [PATCH 09/38] leftover uses of model.__name__ --- rest_framework_json_api/renderers.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index b0f8b0ec..60e55f96 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -276,8 +276,7 @@ def extract_included(fields, resource, resource_instance, included_resources): if isinstance(field, ListSerializer): serializer = field.child - model = serializer.Meta.model - relation_type = utils.format_relation_name(model.__name__) + relation_type = utils.get_resource_type_from_serializer(serializer) relation_queryset = list(relation_instance_or_manager.all()) # Get the serializer fields @@ -298,15 +297,16 @@ def extract_included(fields, resource, resource_instance, included_resources): ) if isinstance(field, ModelSerializer): - model = field.Meta.model - relation_type = utils.format_relation_name(model.__name__) + + relation_type = utils.get_resource_type_from_serializer(field) # Get the serializer fields serializer_fields = utils.get_serializer_fields(field) if serializer_data: included_data.append( - JSONRenderer.build_json_resource_obj(serializer_fields, serializer_data, relation_instance_or_manager, - relation_type) + JSONRenderer.build_json_resource_obj( + serializer_fields, serializer_data, + relation_instance_or_manager, relation_type) ) included_data.extend( JSONRenderer.extract_included( From fe0056d86321255d3512fd251089b5c579633e69 Mon Sep 17 00:00:00 2001 From: Scott Fisk Date: Wed, 16 Dec 2015 12:53:29 -0500 Subject: [PATCH 10/38] use an included serializer's resource_name even when not included --- rest_framework_json_api/relations.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 1a56464d..bc44e4c1 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -6,7 +6,8 @@ from rest_framework_json_api.exceptions import Conflict from rest_framework_json_api.utils import Hyperlink, \ - get_resource_type_from_queryset, get_resource_type_from_instance + get_resource_type_from_queryset, get_resource_type_from_instance, \ + get_included_serializers, get_resource_type_from_serializer class ResourceRelatedField(PrimaryKeyRelatedField): @@ -127,7 +128,18 @@ def to_representation(self, value): else: pk = value.pk - return OrderedDict([('type', get_resource_type_from_instance(value)), ('id', str(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) + return OrderedDict([('type', resource_type), ('id', str(pk))]) @property def choices(self): From 736837f412e9e43dc59068d98f47b71dddb0b9e6 Mon Sep 17 00:00:00 2001 From: Scott Fisk Date: Wed, 16 Dec 2015 13:17:32 -0500 Subject: [PATCH 11/38] cleaned up tests --- example/models.py | 12 -------- example/serializers.py | 11 ++----- .../integration/test_model_resource_name.py | 29 ++++++++++--------- example/urls_test.py | 3 +- example/views.py | 10 ++----- 5 files changed, 20 insertions(+), 45 deletions(-) diff --git a/example/models.py b/example/models.py index 4738014e..64291393 100644 --- a/example/models.py +++ b/example/models.py @@ -34,18 +34,6 @@ def __str__(self): return self.name -@python_2_unicode_compatible -class RenamedAuthor(Author): - class Meta: - proxy = True - - class JSONAPIMeta: - resource_name = "super-author" - - def __str__(self): - return self.name - - @python_2_unicode_compatible class Entry(BaseModel): blog = models.ForeignKey(Blog) diff --git a/example/serializers.py b/example/serializers.py index 4ac43ada..cc7e624b 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -1,5 +1,5 @@ from rest_framework_json_api import serializers, relations -from example.models import Blog, Entry, Author, Comment, RenamedAuthor +from example.models import Blog, Entry, Author, Comment class BlogSerializer(serializers.ModelSerializer): @@ -35,7 +35,7 @@ def get_suggested(self, obj): class Meta: model = Entry fields = ('blog', 'headline', 'body_text', 'pub_date', 'mod_date', - 'authors', 'comments', 'suggested',) + 'authors', 'comments', 'suggested',) class AuthorSerializer(serializers.ModelSerializer): @@ -45,13 +45,6 @@ class Meta: fields = ('name', 'email',) -class RenamedAuthorSerializer(serializers.ModelSerializer): - - class Meta: - model = RenamedAuthor - fields = ('name', 'email',) - - class CommentSerializer(serializers.ModelSerializer): class Meta: diff --git a/example/tests/integration/test_model_resource_name.py b/example/tests/integration/test_model_resource_name.py index 733f0c83..793e8087 100644 --- a/example/tests/integration/test_model_resource_name.py +++ b/example/tests/integration/test_model_resource_name.py @@ -8,20 +8,27 @@ pytestmark = pytest.mark.django_db -def test_model_resource_name_on_list(single_entry, client): - response = client.get(reverse("renamed-authors-list")) +class _PatchedModel: + class JSONAPIMeta: + resource_name = "resource_name_from_JSONAPIMeta" + + +def test_match_model_resource_name_on_list(single_entry, client): + models.Comment.__bases__ += (_PatchedModel,) + response = client.get(reverse("comment-list")) data = load_json(response.content)['data'] # name should be super-author instead of model name RenamedAuthor - assert [x.get('type') for x in data] == ['super-author'], 'List included types are incorrect' + assert [x.get('type') for x in data] == ['resource_name_from_JSONAPIMeta'], 'List included types are incorrect' + @pytest.mark.usefixtures("single_entry") class ResourceNameConsistencyTest(APITestCase): - + def test_type_match_on_included_and_inline_base(self): self._check_relationship_and_included_comment_type_are_the_same(reverse("entry-list")) def test_type_match_on_included_and_inline_with_JSONAPIMeta(self): - models.Comment.__bases__ += (self._PatchedModel,) + models.Comment.__bases__ += (_PatchedModel,) self._check_relationship_and_included_comment_type_are_the_same(reverse("entry-list")) @@ -31,7 +38,7 @@ def test_type_match_on_included_and_inline_with_serializer_resource_name(self): self._check_relationship_and_included_comment_type_are_the_same(reverse("entry-list")) def test_type_match_on_included_and_inline_with_serializer_resource_name_and_JSONAPIMeta(self): - models.Comment.__bases__ += (self._PatchedModel,) + models.Comment.__bases__ += (_PatchedModel,) serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer" self._check_relationship_and_included_comment_type_are_the_same(reverse("entry-list")) @@ -45,12 +52,12 @@ def test_resource_and_relationship_type_match_with_serializer_resource_name(self self._check_resource_and_relationship_comment_type_match() def test_resource_and_relationship_type_match_with_JSONAPIMeta(self): - models.Comment.__bases__ += (self._PatchedModel,) + models.Comment.__bases__ += (_PatchedModel,) self._check_resource_and_relationship_comment_type_match() def test_resource_and_relationship_type_match_with_serializer_resource_name_and_JSONAPIMeta(self): - models.Comment.__bases__ += (self._PatchedModel,) + models.Comment.__bases__ += (_PatchedModel,) serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer" self._check_resource_and_relationship_comment_type_match() @@ -81,9 +88,3 @@ def tearDown(self): delattr(serializers.CommentSerializer.Meta, "resource_name") except AttributeError: pass - - class _PatchedModel: - - class JSONAPIMeta: - resource_name = "resource_name_from_JSONAPIMeta" - \ No newline at end of file diff --git a/example/urls_test.py b/example/urls_test.py index 6d818cf3..208334ad 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -2,7 +2,7 @@ from rest_framework import routers from example.views import BlogViewSet, EntryViewSet, AuthorViewSet, EntryRelationshipView, BlogRelationshipView, \ - CommentRelationshipView, AuthorRelationshipView, RenamedAuthorViewSet, CommentViewSet + CommentRelationshipView, AuthorRelationshipView, CommentViewSet from .api.resources.identity import Identity, GenericIdentity router = routers.DefaultRouter(trailing_slash=False) @@ -10,7 +10,6 @@ router.register(r'blogs', BlogViewSet) router.register(r'entries', EntryViewSet) router.register(r'authors', AuthorViewSet) -router.register(r'renamed-authors', RenamedAuthorViewSet, base_name='renamed-authors') router.register(r'comments', CommentViewSet) # for the old tests diff --git a/example/views.py b/example/views.py index c34f1401..6a3fb505 100644 --- a/example/views.py +++ b/example/views.py @@ -1,9 +1,8 @@ from rest_framework import viewsets from rest_framework_json_api.views import RelationshipView -from example.models import Blog, Entry, Author, Comment, RenamedAuthor +from example.models import Blog, Entry, Author, Comment from example.serializers import ( - BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer, - RenamedAuthorSerializer) + BlogSerializer, EntrySerializer, AuthorSerializer, CommentSerializer) class BlogViewSet(viewsets.ModelViewSet): @@ -22,11 +21,6 @@ class AuthorViewSet(viewsets.ModelViewSet): serializer_class = AuthorSerializer -class RenamedAuthorViewSet(viewsets.ModelViewSet): - queryset = RenamedAuthor.objects.all() - serializer_class = RenamedAuthorSerializer - - class CommentViewSet(viewsets.ModelViewSet): queryset = Comment.objects.all() serializer_class = CommentSerializer From a8ee085d27db069c27afa89141cba35e32068fe6 Mon Sep 17 00:00:00 2001 From: John Huffsmith Date: Wed, 16 Dec 2015 17:29:38 -0500 Subject: [PATCH 12/38] support many=True fields that are not Model relationships (calculated fields) --- rest_framework_json_api/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index ed01b739..7c27c799 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -139,7 +139,9 @@ def format_relation_name(value, format_type=None): def get_related_resource_type(relation): - if hasattr(relation, '_meta'): + if type(relation) == ManyRelatedField: + return get_related_resource_type(relation.child_relation) + elif hasattr(relation, '_meta'): relation_model = relation._meta.model elif hasattr(relation, 'model'): # the model type was explicitly passed as a kwarg to ResourceRelatedField From bd5a955aebae44aec1966c557e84d9ebd805c1d0 Mon Sep 17 00:00:00 2001 From: Scott Fisk Date: Wed, 16 Dec 2015 13:53:09 -0500 Subject: [PATCH 13/38] added view precedence test --- .../integration/test_model_resource_name.py | 59 ++++++++++++++++--- 1 file changed, 52 insertions(+), 7 deletions(-) diff --git a/example/tests/integration/test_model_resource_name.py b/example/tests/integration/test_model_resource_name.py index 793e8087..96d15f96 100644 --- a/example/tests/integration/test_model_resource_name.py +++ b/example/tests/integration/test_model_resource_name.py @@ -4,7 +4,7 @@ from example.tests.utils import load_json from rest_framework.test import APITestCase -from example import models, serializers +from example import models, serializers, views pytestmark = pytest.mark.django_db @@ -13,17 +13,61 @@ class JSONAPIMeta: resource_name = "resource_name_from_JSONAPIMeta" -def test_match_model_resource_name_on_list(single_entry, client): - models.Comment.__bases__ += (_PatchedModel,) - response = client.get(reverse("comment-list")) - data = load_json(response.content)['data'] - # name should be super-author instead of model name RenamedAuthor - assert [x.get('type') for x in data] == ['resource_name_from_JSONAPIMeta'], 'List included types are incorrect' +@pytest.mark.usefixtures("single_entry") +class ModelResourceNameTests(APITestCase): + def test_model_resource_name_on_list(self): + models.Comment.__bases__ += (_PatchedModel,) + response = self.client.get(reverse("comment-list")) + data = load_json(response.content)['data'][0] + # name should be super-author instead of model name RenamedAuthor + assert (data.get('type') == 'resource_name_from_JSONAPIMeta'), ( + 'resource_name from model incorrect on list') + + # Precedence tests + def test_resource_name_precendence(self): + # default + response = self.client.get(reverse("comment-list")) + data = load_json(response.content)['data'][0] + assert (data.get('type') == 'comments'), ( + 'resource_name from model incorrect on list') + + # model > default + models.Comment.__bases__ += (_PatchedModel,) + response = self.client.get(reverse("comment-list")) + data = load_json(response.content)['data'][0] + assert (data.get('type') == 'resource_name_from_JSONAPIMeta'), ( + 'resource_name from model incorrect on list') + + # serializer > model + serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer" + response = self.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' + response = self.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 tearDown(self): + 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 @pytest.mark.usefixtures("single_entry") class ResourceNameConsistencyTest(APITestCase): + # Included rename tests def test_type_match_on_included_and_inline_base(self): self._check_relationship_and_included_comment_type_are_the_same(reverse("entry-list")) @@ -43,6 +87,7 @@ def test_type_match_on_included_and_inline_with_serializer_resource_name_and_JSO self._check_relationship_and_included_comment_type_are_the_same(reverse("entry-list")) + # Relation rename tests def test_resource_and_relationship_type_match(self): self._check_resource_and_relationship_comment_type_match() From d47a7dad3d74130a319d1c9db69e676624438e24 Mon Sep 17 00:00:00 2001 From: John Huffsmith Date: Mon, 21 Dec 2015 11:09:08 -0500 Subject: [PATCH 14/38] relation_type is jacking up serialization on fields where its not needed --- rest_framework_json_api/renderers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 61ff20b8..1ae01f33 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -94,9 +94,10 @@ def extract_relationships(fields, resource, resource_instance): else: continue - relation_type = utils.get_related_resource_type(field) + #relation_type = utils.get_related_resource_type(field) if isinstance(field, relations.HyperlinkedIdentityField): + relation_type = utils.get_related_resource_type(field) # special case for HyperlinkedIdentityField relation_data = list() @@ -135,6 +136,7 @@ def extract_relationships(fields, resource, resource_instance): if isinstance(field, (relations.PrimaryKeyRelatedField, relations.HyperlinkedRelatedField)): relation_id = relation_instance_or_manager.pk if resource.get(field_name) else None + relation_type = utils.get_related_resource_type(field) relation_data = { 'data': ( From 836391f2d93d73cdab5044972206161eb01cbeb4 Mon Sep 17 00:00:00 2001 From: Scott Fisk Date: Thu, 24 Dec 2015 09:14:37 -0700 Subject: [PATCH 15/38] Use py.test client over drf client to fix Django 1.7 tests --- .../integration/test_model_resource_name.py | 98 ++++++++++--------- 1 file changed, 50 insertions(+), 48 deletions(-) diff --git a/example/tests/integration/test_model_resource_name.py b/example/tests/integration/test_model_resource_name.py index 96d15f96..979b55b5 100644 --- a/example/tests/integration/test_model_resource_name.py +++ b/example/tests/integration/test_model_resource_name.py @@ -3,7 +3,6 @@ from example.tests.utils import load_json -from rest_framework.test import APITestCase from example import models, serializers, views pytestmark = pytest.mark.django_db @@ -13,46 +12,69 @@ class JSONAPIMeta: resource_name = "resource_name_from_JSONAPIMeta" +def _check_resource_and_relationship_comment_type_match(django_client): + entry_response = django_client.get(reverse("entry-list")) + comment_response = django_client.get(reverse("comment-list")) + + comment_resource_type = load_json(comment_response.content).get('data')[0].get('type') + comment_relationship_type = load_json(entry_response.content).get( + 'data')[0].get('relationships').get('comments').get('data')[0].get('type') + + assert comment_resource_type == comment_relationship_type, "The resource type seen in the relationships and head resource do not match" + + +def _check_relationship_and_included_comment_type_are_the_same(django_client, url): + response = django_client.get(url + "?include=comments") + data = load_json(response.content).get('data')[0] + comment = load_json(response.content).get('included')[0] + + comment_relationship_type = data.get('relationships').get('comments').get('data')[0].get('type') + comment_included_type = comment.get('type') + + assert comment_relationship_type == comment_included_type, "The resource type seen in the relationships and included do not match" + + @pytest.mark.usefixtures("single_entry") -class ModelResourceNameTests(APITestCase): - def test_model_resource_name_on_list(self): +class TestModelResourceName: + + def test_model_resource_name_on_list(self, client): models.Comment.__bases__ += (_PatchedModel,) - response = self.client.get(reverse("comment-list")) + response = client.get(reverse("comment-list")) data = load_json(response.content)['data'][0] # name should be super-author instead of model name RenamedAuthor assert (data.get('type') == 'resource_name_from_JSONAPIMeta'), ( 'resource_name from model incorrect on list') # Precedence tests - def test_resource_name_precendence(self): + def test_resource_name_precendence(self, client): # default - response = self.client.get(reverse("comment-list")) + response = client.get(reverse("comment-list")) data = load_json(response.content)['data'][0] assert (data.get('type') == 'comments'), ( 'resource_name from model incorrect on list') # model > default models.Comment.__bases__ += (_PatchedModel,) - response = self.client.get(reverse("comment-list")) + response = client.get(reverse("comment-list")) data = load_json(response.content)['data'][0] assert (data.get('type') == 'resource_name_from_JSONAPIMeta'), ( 'resource_name from model incorrect on list') # serializer > model serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer" - response = self.client.get(reverse("comment-list")) + 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' - response = self.client.get(reverse("comment-list")) + 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 tearDown(self): + def teardown_method(self, method): models.Comment.__bases__ = (models.Comment.__bases__[0],) try: delattr(serializers.CommentSerializer.Meta, "resource_name") @@ -65,69 +87,49 @@ def tearDown(self): @pytest.mark.usefixtures("single_entry") -class ResourceNameConsistencyTest(APITestCase): +class TestResourceNameConsistency: # Included rename tests - def test_type_match_on_included_and_inline_base(self): - self._check_relationship_and_included_comment_type_are_the_same(reverse("entry-list")) + def test_type_match_on_included_and_inline_base(self, client): + _check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list")) - def test_type_match_on_included_and_inline_with_JSONAPIMeta(self): + def test_type_match_on_included_and_inline_with_JSONAPIMeta(self, client): models.Comment.__bases__ += (_PatchedModel,) - self._check_relationship_and_included_comment_type_are_the_same(reverse("entry-list")) + _check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list")) - def test_type_match_on_included_and_inline_with_serializer_resource_name(self): + def test_type_match_on_included_and_inline_with_serializer_resource_name(self, client): serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer" - self._check_relationship_and_included_comment_type_are_the_same(reverse("entry-list")) + _check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list")) - def test_type_match_on_included_and_inline_with_serializer_resource_name_and_JSONAPIMeta(self): + def test_type_match_on_included_and_inline_with_serializer_resource_name_and_JSONAPIMeta(self, client): models.Comment.__bases__ += (_PatchedModel,) serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer" - self._check_relationship_and_included_comment_type_are_the_same(reverse("entry-list")) + _check_relationship_and_included_comment_type_are_the_same(client, reverse("entry-list")) # Relation rename tests - def test_resource_and_relationship_type_match(self): - self._check_resource_and_relationship_comment_type_match() + def test_resource_and_relationship_type_match(self, client): + _check_resource_and_relationship_comment_type_match(client) - def test_resource_and_relationship_type_match_with_serializer_resource_name(self): + def test_resource_and_relationship_type_match_with_serializer_resource_name(self, client): serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer" - self._check_resource_and_relationship_comment_type_match() + _check_resource_and_relationship_comment_type_match(client) - def test_resource_and_relationship_type_match_with_JSONAPIMeta(self): + def test_resource_and_relationship_type_match_with_JSONAPIMeta(self, client): models.Comment.__bases__ += (_PatchedModel,) - self._check_resource_and_relationship_comment_type_match() + _check_resource_and_relationship_comment_type_match(client) - def test_resource_and_relationship_type_match_with_serializer_resource_name_and_JSONAPIMeta(self): + def test_resource_and_relationship_type_match_with_serializer_resource_name_and_JSONAPIMeta(self, client): models.Comment.__bases__ += (_PatchedModel,) serializers.CommentSerializer.Meta.resource_name = "resource_name_from_serializer" - self._check_resource_and_relationship_comment_type_match() - - def _check_resource_and_relationship_comment_type_match(self): - entry_response = self.client.get(reverse("entry-list")) - comment_response = self.client.get(reverse("comment-list")) - - comment_resource_type = load_json(comment_response.content).get('data')[0].get('type') - comment_relationship_type = load_json(entry_response.content).get( - 'data')[0].get('relationships').get('comments').get('data')[0].get('type') - - assert comment_resource_type == comment_relationship_type, "The resource type seen in the relationships and head resource do not match" - - def _check_relationship_and_included_comment_type_are_the_same(self, url): - response = self.client.get(url + "?include=comments") - data = load_json(response.content).get('data')[0] - comment = load_json(response.content).get('included')[0] - - comment_relationship_type = data.get('relationships').get('comments').get('data')[0].get('type') - comment_included_type = comment.get('type') - - assert comment_relationship_type == comment_included_type, "The resource type seen in the relationships and included do not match" + _check_resource_and_relationship_comment_type_match(client) - def tearDown(self): + def teardown_method(self, method): models.Comment.__bases__ = (models.Comment.__bases__[0],) try: delattr(serializers.CommentSerializer.Meta, "resource_name") From 8672308d99a61659c0a17549bf64984219bdae35 Mon Sep 17 00:00:00 2001 From: John Huffsmith Date: Mon, 28 Dec 2015 17:49:24 -0500 Subject: [PATCH 16/38] random tweaks --- rest_framework_json_api/mixins.py | 5 +++ rest_framework_json_api/parsers.py | 66 +++++++++++++++------------- rest_framework_json_api/relations.py | 29 ++++++------ 3 files changed, 56 insertions(+), 44 deletions(-) diff --git a/rest_framework_json_api/mixins.py b/rest_framework_json_api/mixins.py index 16af92a4..c44ffbda 100644 --- a/rest_framework_json_api/mixins.py +++ b/rest_framework_json_api/mixins.py @@ -14,7 +14,12 @@ def get_queryset(self): ids = dict(self.request.query_params).get('ids[]') else: ids = dict(self.request.QUERY_PARAMS).get('ids[]') + + if 'filter[id]' in self.request.query_params: + ids = self.request.query_params.get('filter[id]').split(',') + if ids: self.queryset = self.queryset.filter(id__in=ids) + return self.queryset diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index a918aaeb..ce0080cf 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -41,9 +41,9 @@ def parse_relationships(data): for field_name, field_data in relationships.items(): field_data = field_data.get('data') if isinstance(field_data, dict): - parsed_relationships[field_name] = field_data + parsed_relationships[field_name] = field_data['id'] elif isinstance(field_data, list): - parsed_relationships[field_name] = list(relation for relation in field_data) + parsed_relationships[field_name] = list(relation for relation in field_data['id']) return parsed_relationships def parse(self, stream, media_type=None, parser_context=None): @@ -54,38 +54,44 @@ def parse(self, stream, media_type=None, parser_context=None): data = result.get('data') if data: - from rest_framework_json_api.views import RelationshipView - if isinstance(parser_context['view'], RelationshipView): - # We skip parsing the object as JSONAPI Resource Identifier Object and not a regular Resource Object - if isinstance(data, list): - for resource_identifier_object in data: - if not (resource_identifier_object.get('id') and resource_identifier_object.get('type')): - raise ParseError( - 'Received data contains one or more malformed JSONAPI Resource Identifier Object(s)' - ) - elif not (data.get('id') and data.get('type')): - raise ParseError('Received data is not a valid JSONAPI Resource Identifier Object') + type = data.get('type') + # if type is defined, treat it like a full object, otherwise jsut pass through the data + if type: + from rest_framework_json_api.views import RelationshipView + if isinstance(parser_context['view'], RelationshipView): + # We skip parsing the object as JSONAPI Resource Identifier Object and not a regular Resource Object + if isinstance(data, list): + for resource_identifier_object in data: + if not (resource_identifier_object.get('id') and resource_identifier_object.get('type')): + raise ParseError( + 'Received data contains one or more malformed JSONAPI Resource Identifier Object(s)' + ) + elif not (data.get('id') and data.get('type')): + raise ParseError('Received data is not a valid JSONAPI Resource Identifier Object') - return data + return data - request = parser_context.get('request') + request = parser_context.get('request') - # Check for inconsistencies - resource_name = utils.get_resource_name(parser_context) - if data.get('type') != resource_name and request.method in ('PUT', 'POST', 'PATCH'): - raise exceptions.Conflict( - "The resource object's type ({data_type}) is not the type " - "that constitute the collection represented by the endpoint ({resource_type}).".format( - data_type=data.get('type'), - resource_type=resource_name + # Check for inconsistencies + resource_name = utils.get_resource_name(parser_context) + if data.get('type') != resource_name and request.method in ('PUT', 'POST', 'PATCH'): + raise exceptions.Conflict( + "The resource object's type ({data_type}) is not the type " + "that constitute the collection represented by the endpoint ({resource_type}).".format( + data_type=data.get('type'), + resource_type=resource_name + ) ) - ) - # Construct the return data - parsed_data = {'id': data.get('id')} - parsed_data.update(self.parse_attributes(data)) - parsed_data.update(self.parse_relationships(data)) - return parsed_data + # Construct the return data + parsed_data = {'id': data.get('id')} + parsed_data.update(self.parse_attributes(data)) + parsed_data.update(self.parse_relationships(data)) + return parsed_data + + else: + return data else: - raise ParseError('Received document does not contain primary data') + return {} diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index bc44e4c1..34152529 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -108,19 +108,19 @@ def get_links(self, obj=None, lookup_field='pk'): return_data.update({'related': related_link}) return return_data - def to_internal_value(self, data): - if isinstance(data, six.text_type): - try: - data = json.loads(data) - except ValueError: - # show a useful error if they send a `pk` instead of resource object - 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) - if data['type'] != expected_relation_type: - self.conflict('incorrect_relation_type', relation_type=expected_relation_type, received_type=data['type']) - return super(ResourceRelatedField, self).to_internal_value(data['id']) + # def to_internal_value(self, data): + # if isinstance(data, six.text_type): + # try: + # data = json.loads(data) + # except ValueError: + # # show a useful error if they send a `pk` instead of resource object + # 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) + # if data['type'] != expected_relation_type: + # self.conflict('incorrect_relation_type', relation_type=expected_relation_type, received_type=data['type']) + # return super(ResourceRelatedField, self).to_internal_value(data['id']) def to_representation(self, value): if getattr(self, 'pk_field', None) is not None: @@ -131,7 +131,8 @@ 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) + root = 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 9f0b0fc6a609ef83c1b52111a7859fe8db4b616c Mon Sep 17 00:00:00 2001 From: John Huffsmith Date: Sat, 2 Jan 2016 13:37:56 -0500 Subject: [PATCH 17/38] saving --- rest_framework_json_api/metadata.py | 1 + rest_framework_json_api/relations.py | 26 ++++++---- rest_framework_json_api/renderers.py | 11 ++-- rest_framework_json_api/serializers.py | 4 +- rest_framework_json_api/utils.py | 71 +++++++++++++++++++++----- 5 files changed, 83 insertions(+), 30 deletions(-) diff --git a/rest_framework_json_api/metadata.py b/rest_framework_json_api/metadata.py index 2355f2cf..9563f180 100644 --- a/rest_framework_json_api/metadata.py +++ b/rest_framework_json_api/metadata.py @@ -138,6 +138,7 @@ def get_field_info(self, field): for choice_value, choice_name in field.choices.items() ] + #Allow includes for all Relations unless explicitly if hasattr(serializer, 'included_serializers') and 'relationship_resource' in field_info: field_info['allows_include'] = field.field_name in serializer.included_serializers diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 34152529..4b66a9bb 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -7,7 +7,7 @@ from rest_framework_json_api.exceptions import Conflict from rest_framework_json_api.utils import Hyperlink, \ get_resource_type_from_queryset, get_resource_type_from_instance, \ - get_included_serializers, get_resource_type_from_serializer + get_included_serializers_override, get_resource_type_from_serializer class ResourceRelatedField(PrimaryKeyRelatedField): @@ -108,7 +108,7 @@ def get_links(self, obj=None, lookup_field='pk'): return_data.update({'related': related_link}) return return_data - # def to_internal_value(self, data): + # def to_internal_value(self, data): # if isinstance(data, six.text_type): # try: # data = json.loads(data) @@ -120,6 +120,7 @@ def get_links(self, obj=None, lookup_field='pk'): # expected_relation_type = get_resource_type_from_queryset(self.queryset) # if data['type'] != expected_relation_type: # self.conflict('incorrect_relation_type', relation_type=expected_relation_type, received_type=data['type']) + # return super(ResourceRelatedField, self).to_internal_value(data['id']) def to_representation(self, value): @@ -135,9 +136,12 @@ def to_representation(self, value): root = 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]) + include_overrides = get_included_serializers_override(root) + if field_name in include_overrides.keys(): + resource_type = get_resource_type_from_serializer(include_overrides[field_name]) + + # if included_serializers_override is defined, use that one instead of the default serializer defined on the instance model + resource_type = resource_type if resource_type else get_resource_type_from_instance(value) return OrderedDict([('type', resource_type), ('id', str(pk))]) @@ -151,12 +155,12 @@ def choices(self): return {} return OrderedDict([ - ( - json.dumps(self.to_representation(item)), - self.display_value(item) - ) - for item in queryset - ]) + ( + json.dumps(self.to_representation(item)), + self.display_value(item) + ) + for item in queryset + ]) class SerializerMethodResourceRelatedField(ResourceRelatedField): diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 42e501a4..c6957109 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -231,7 +231,7 @@ def extract_included(fields, resource, resource_instance, included_resources): included_data = list() current_serializer = fields.serializer context = current_serializer.context - included_serializers = utils.get_included_serializers(current_serializer) + include_config = utils.get_included_configuration(current_serializer) included_resources = copy.copy(included_resources) for field_name, field in six.iteritems(fields): @@ -243,6 +243,10 @@ def extract_included(fields, resource, resource_instance, included_resources): if not isinstance(field, (relations.RelatedField, relations.ManyRelatedField, BaseSerializer)): continue + # Skip disabled fields + if include_config[field_name] == False: + continue + try: included_resources.remove(field_name) except ValueError: @@ -267,12 +271,13 @@ def extract_included(fields, resource, resource_instance, included_resources): serializer_data = resource.get(field_name) if isinstance(field, relations.ManyRelatedField): - serializer_class = included_serializers.get(field_name) + serializer_class = include_config.get(field_name) field = serializer_class(relation_instance_or_manager.all(), many=True, context=context) serializer_data = field.data if isinstance(field, relations.RelatedField): - serializer_class = included_serializers.get(field_name) + #serializer_class = include_config.get(field_name) + serializer_class = utils.get_serializer_from_instance_and_serializer(relation_instance_or_manager, current_serializer, field_name) if relation_instance_or_manager is None: continue field = serializer_class(relation_instance_or_manager, context=context) diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index b993fdde..bed3d392 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -5,7 +5,7 @@ from rest_framework_json_api.relations import ResourceRelatedField from rest_framework_json_api.utils import ( get_resource_type_from_model, get_resource_type_from_instance, - get_resource_type_from_serializer, get_included_serializers) + get_resource_type_from_serializer, get_included_configuration) class ResourceIdentifierObjectSerializer(BaseSerializer): @@ -72,7 +72,7 @@ def __init__(self, *args, **kwargs): view = context.get('view') if context else None def validate_path(serializer_class, field_path, path): - serializers = get_included_serializers(serializer_class) + serializers = get_included_configuration(serializer_class) if serializers is None: raise ParseError('This endpoint does not support the include parameter') this_field_name = field_path[0] diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index f1e4618b..f5e52535 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -11,6 +11,8 @@ from django.utils.translation import ugettext_lazy as _ from rest_framework.exceptions import APIException +import string + try: from rest_framework.serializers import ManyRelatedField except ImportError: @@ -82,6 +84,7 @@ def get_serializer_fields(serializer): pass return fields + def format_keys(obj, format_type=None): """ Takes either a dict or list and returns it with camelized keys only if @@ -196,11 +199,19 @@ def get_instance_or_manager_resource_type(resource_instance_or_manager): def get_resource_type_from_model(model): + serializer_class = get_default_serializer_from_model(model) + + return get_resource_type_from_serializer(serializer_class) + + +def get_default_serializer_from_model(model): json_api_meta = getattr(model, 'JSONAPIMeta', None) - return getattr( - json_api_meta, - 'resource_name', - format_relation_name(model.__name__)) + serializer_string = getattr(json_api_meta, 'default_serializer', None) + if serializer_string is None: + # return format_relation_name(model.__name__) + raise Exception("Must define a default_serializer on %s" % model.__name__) + serializer_class = import_class_from_dotted_path(serializer_string) + return serializer_class def get_resource_type_from_queryset(qs): @@ -211,28 +222,60 @@ def get_resource_type_from_instance(instance): return get_resource_type_from_model(instance._meta.model) +def get_serializer_from_instance_and_serializer(instance, serializer, field_name): + overrides = get_included_serializers_override(serializer) + if field_name in overrides: + return overrides[field_name] + return get_default_serializer_from_model(type(instance)) + + def get_resource_type_from_manager(manager): return get_resource_type_from_model(manager.model) def get_resource_type_from_serializer(serializer): - return getattr( - serializer.Meta, - 'resource_name', - get_resource_type_from_model(serializer.Meta.model)) + resource_name = getattr( + serializer.Meta, + 'resource_name', + None) + + if not resource_name: + if isinstance(serializer, object): + resource_name = format_relation_name(string.replace(serializer.__class__.__name__, 'Serializer', '')) + else: + resource_name = format_relation_name(string.replace(serializer.__name__, 'Serializer', '')) + # resource_name = get_resource_type_from_model(serializer.Meta.model) + + return resource_name + + +def get_included_configuration(serializer): + included_resources_override = copy.copy(getattr(serializer, 'included_resources_override', dict())) + + included_resources_config = {} + + # get all the related resource fields on this serializer + for field in serializer.fields.fields.keys(): + if field in included_resources_override.keys() and included_resources_override[field] == False: + # Skip fields that are explicitly disabled + continue + + included_resources_config[field] = True + + return included_resources_config -def get_included_serializers(serializer): - included_serializers = copy.copy(getattr(serializer, 'included_serializers', dict())) +def get_included_serializers_override(serializer): + included_serializers_override = copy.copy(getattr(serializer, 'included_serializers_override', dict())) - for name, value in six.iteritems(included_serializers): + for name, value in six.iteritems(included_serializers_override): if not isinstance(value, type): if value == 'self': - included_serializers[name] = serializer if isinstance(serializer, type) else serializer.__class__ + included_serializers_override[name] = serializer if isinstance(serializer, type) else serializer.__class__ else: - included_serializers[name] = import_class_from_dotted_path(value) + included_serializers_override[name] = import_class_from_dotted_path(value) - return included_serializers + return included_serializers_override class Hyperlink(six.text_type): From dc97bf007bc220f8e59b3abdc3b677dc00844ae3 Mon Sep 17 00:00:00 2001 From: John Huffsmith Date: Mon, 4 Jan 2016 18:31:13 -0500 Subject: [PATCH 18/38] cleanup --- rest_framework_json_api/renderers.py | 2 +- rest_framework_json_api/utils.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index c6957109..46c12278 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -307,7 +307,7 @@ def extract_included(fields, resource, resource_instance, included_resources): if isinstance(field, ModelSerializer): - relation_type = utils.get_resource_type_from_serializer(field) + relation_type = utils.get_resource_type_from_serializer(field.__class__) # Get the serializer fields serializer_fields = utils.get_serializer_fields(field) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index f5e52535..086af53e 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -240,10 +240,7 @@ def get_resource_type_from_serializer(serializer): None) if not resource_name: - if isinstance(serializer, object): - resource_name = format_relation_name(string.replace(serializer.__class__.__name__, 'Serializer', '')) - else: - resource_name = format_relation_name(string.replace(serializer.__name__, 'Serializer', '')) + resource_name = format_relation_name(string.replace(serializer.__name__, 'Serializer', '')) # resource_name = get_resource_type_from_model(serializer.Meta.model) return resource_name From 0b9414277405e0403d82e9b39bc14b676fe6019d Mon Sep 17 00:00:00 2001 From: John Huffsmith Date: Wed, 6 Jan 2016 16:53:47 -0500 Subject: [PATCH 19/38] cleanup --- rest_framework_json_api/parsers.py | 2 +- rest_framework_json_api/renderers.py | 96 +++++++++++++--------------- 2 files changed, 45 insertions(+), 53 deletions(-) diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index ce0080cf..e8c0a222 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -43,7 +43,7 @@ def parse_relationships(data): if isinstance(field_data, dict): parsed_relationships[field_name] = field_data['id'] elif isinstance(field_data, list): - parsed_relationships[field_name] = list(relation for relation in field_data['id']) + parsed_relationships[field_name] = list(relation['id'] for relation in field_data) return parsed_relationships def parse(self, stream, media_type=None, parser_context=None): diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 46c12278..2f024e0e 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -5,6 +5,7 @@ from collections import OrderedDict from django.utils import six, encoding + from rest_framework import relations from rest_framework import renderers from rest_framework.serializers import BaseSerializer, ListSerializer, ModelSerializer @@ -94,7 +95,7 @@ def extract_relationships(fields, resource, resource_instance): else: continue - #relation_type = utils.get_related_resource_type(field) + # relation_type = utils.get_related_resource_type(field) if isinstance(field, relations.HyperlinkedIdentityField): relation_type = utils.get_related_resource_type(field) @@ -107,7 +108,7 @@ def extract_relationships(fields, resource, resource_instance): for related_object in relation_queryset: relation_data.append( - OrderedDict([('type', relation_type), ('id', encoding.force_text(related_object.pk))]) + OrderedDict([('type', relation_type), ('id', encoding.force_text(related_object.pk))]) ) data.update({field_name: { @@ -128,8 +129,8 @@ def extract_relationships(fields, resource, resource_instance): field_links = field.get_links(resource_instance) relation_data.update( - {'links': field_links} - if field_links else dict() + {'links': field_links} + if field_links else dict() ) data.update({field_name: relation_data}) continue @@ -145,8 +146,8 @@ def extract_relationships(fields, resource, resource_instance): } relation_data.update( - {'links': {'related': resource.get(field_name)}} - if isinstance(field, relations.HyperlinkedRelatedField) and resource.get(field_name) else dict() + {'links': {'related': resource.get(field_name)}} + if isinstance(field, relations.HyperlinkedRelatedField) and resource.get(field_name) else dict() ) data.update({field_name: relation_data}) continue @@ -161,15 +162,15 @@ def extract_relationships(fields, resource, resource_instance): field_links = field.child_relation.get_links(resource_instance) relation_data.update( - {'links': field_links} - if field_links else dict() + {'links': field_links} + if field_links else dict() ) relation_data.update( - { - 'meta': { - 'count': len(resource.get(field_name)) + { + 'meta': { + 'count': len(resource.get(field_name)) + } } - } ) data.update({field_name: relation_data}) continue @@ -270,57 +271,48 @@ def extract_included(fields, resource, resource_instance, included_resources): if field_name == key.split('.')[0]] serializer_data = resource.get(field_name) + serializer_instances = [] + if isinstance(field, relations.ManyRelatedField): - serializer_class = include_config.get(field_name) - field = serializer_class(relation_instance_or_manager.all(), many=True, context=context) - serializer_data = field.data + # # serializer_class = include_config.get(field_name) + # serializer_class = utils.get_serializer_from_instance_and_serializer() + # serializer_instance = serializer_class(relation_instance_or_manager.all(), many=True, context=context) + + iterable = field.get_attribute(current_serializer.instance) + + + + for item in iterable: + serializer_class = utils.get_serializer_from_instance_and_serializer(item, current_serializer, field_name) + serializer_instance = serializer_class(item, context=context) + serializer_instances.append(serializer_instance) if isinstance(field, relations.RelatedField): - #serializer_class = include_config.get(field_name) + # serializer_class = include_config.get(field_name) serializer_class = utils.get_serializer_from_instance_and_serializer(relation_instance_or_manager, current_serializer, field_name) if relation_instance_or_manager is None: continue - field = serializer_class(relation_instance_or_manager, context=context) - serializer_data = field.data - - if isinstance(field, ListSerializer): - serializer = field.child - relation_type = utils.get_resource_type_from_serializer(serializer) - relation_queryset = list(relation_instance_or_manager.all()) + serializer_instance = serializer_class(relation_instance_or_manager, context=context) + serializer_instances.append(serializer_instance) - # Get the serializer fields - serializer_fields = utils.get_serializer_fields(serializer) - if serializer_data: - for position in range(len(serializer_data)): - serializer_resource = serializer_data[position] - nested_resource_instance = relation_queryset[position] - included_data.append( - JSONRenderer.build_json_resource_obj( - serializer_fields, serializer_resource, nested_resource_instance, relation_type - ) - ) - included_data.extend( - JSONRenderer.extract_included( - serializer_fields, serializer_resource, nested_resource_instance, new_included_resources - ) - ) + for serializer_instance in serializer_instances: - if isinstance(field, ModelSerializer): + serializer_data = serializer_instance.data - relation_type = utils.get_resource_type_from_serializer(field.__class__) + relation_type = utils.get_resource_type_from_serializer(serializer_instance.__class__) # Get the serializer fields - serializer_fields = utils.get_serializer_fields(field) + serializer_fields = utils.get_serializer_fields(serializer_instance) if serializer_data: included_data.append( - JSONRenderer.build_json_resource_obj( - serializer_fields, serializer_data, - relation_instance_or_manager, relation_type) + JSONRenderer.build_json_resource_obj( + serializer_fields, serializer_data, + serializer_instance.instance, relation_type) ) included_data.extend( - JSONRenderer.extract_included( - serializer_fields, serializer_data, relation_instance_or_manager, new_included_resources - ) + JSONRenderer.extract_included( + serializer_fields, serializer_data, serializer_instance.instance, new_included_resources + ) ) return utils.format_keys(included_data) @@ -374,7 +366,7 @@ def render_relationship_view(self, data, accepted_media_type=None, renderer_cont if links: render_data.update({'links': links}), return super(JSONRenderer, self).render( - render_data, accepted_media_type, renderer_context + render_data, accepted_media_type, renderer_context ) def render_errors(self, data, accepted_media_type=None, renderer_context=None): @@ -382,7 +374,7 @@ def render_errors(self, data, accepted_media_type=None, renderer_context=None): if len(data) > 1 and isinstance(data, list): data.sort(key=lambda x: x.get('source', {}).get('pointer', '')) return super(JSONRenderer, self).render( - {'errors': data}, accepted_media_type, renderer_context + {'errors': data}, accepted_media_type, renderer_context ) def render(self, data, accepted_media_type=None, renderer_context=None): @@ -401,7 +393,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None): # wants to build the output format manually. if resource_name is None or resource_name is False: return super(JSONRenderer, self).render( - data, accepted_media_type, renderer_context + data, accepted_media_type, renderer_context ) # If this is an error response, skip the rest. @@ -499,5 +491,5 @@ def render(self, data, accepted_media_type=None, renderer_context=None): render_data['meta'] = utils.format_keys(json_api_meta) return super(JSONRenderer, self).render( - render_data, accepted_media_type, renderer_context + render_data, accepted_media_type, renderer_context ) From 7d058a4cf3a31d88d58eb51d3e9e5a5375bb7949 Mon Sep 17 00:00:00 2001 From: John Huffsmith Date: Mon, 25 Jan 2016 13:21:48 -0500 Subject: [PATCH 20/38] preventing explosion on skipped/empty fields --- rest_framework_json_api/renderers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 2f024e0e..deedd562 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -170,6 +170,7 @@ def extract_relationships(fields, resource, resource_instance): 'meta': { 'count': len(resource.get(field_name)) } + if resource.get(field_name) else dict() } ) data.update({field_name: relation_data}) From a0d39861714358bc1578b2e76dcf54e764ed555e Mon Sep 17 00:00:00 2001 From: John Huffsmith Date: Mon, 25 Jan 2016 13:34:36 -0500 Subject: [PATCH 21/38] better fix --- rest_framework_json_api/renderers.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index deedd562..945f05bc 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -156,6 +156,11 @@ def extract_relationships(fields, resource, resource_instance): if isinstance(field.child_relation, ResourceRelatedField): # special case for ResourceRelatedField + + if field_name not in resource: + continue + + relation_data = { 'data': resource.get(field_name) } @@ -170,7 +175,6 @@ def extract_relationships(fields, resource, resource_instance): 'meta': { 'count': len(resource.get(field_name)) } - if resource.get(field_name) else dict() } ) data.update({field_name: relation_data}) From 570e41cabcc0dad9fd0c42dab07f9791dde26d80 Mon Sep 17 00:00:00 2001 From: John Huffsmith Date: Tue, 26 Jan 2016 14:32:44 -0500 Subject: [PATCH 22/38] fixing type --- 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 4b66a9bb..c03dc4fe 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -135,7 +135,7 @@ def to_representation(self, value): #root = getattr(self.parent, 'parent', self.parent) root = 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: + if getattr(root, 'included_serializers_override', None) is not None: include_overrides = get_included_serializers_override(root) if field_name in include_overrides.keys(): resource_type = get_resource_type_from_serializer(include_overrides[field_name]) From 6ef35963047035cff1ecde46801d4222f6eb287b Mon Sep 17 00:00:00 2001 From: Audrey Kubetin Date: Tue, 26 Jan 2016 18:26:31 -0500 Subject: [PATCH 23/38] changed 'id' to 'pk' in MultipleIDMixin to be compatible w/ Ember --- rest_framework_json_api/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_json_api/mixins.py b/rest_framework_json_api/mixins.py index c44ffbda..6f03e3ce 100644 --- a/rest_framework_json_api/mixins.py +++ b/rest_framework_json_api/mixins.py @@ -19,7 +19,7 @@ def get_queryset(self): ids = self.request.query_params.get('filter[id]').split(',') if ids: - self.queryset = self.queryset.filter(id__in=ids) + self.queryset = self.queryset.filter(pk__in=ids) return self.queryset From 2069237345369af3f4576c6b0c47cff2b097eabb Mon Sep 17 00:00:00 2001 From: Anders Steinlein Date: Sat, 6 Feb 2016 22:21:39 +0100 Subject: [PATCH 24/38] Fix potential circular import edge-case Fixes #158 --- rest_framework_json_api/exceptions.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/exceptions.py b/rest_framework_json_api/exceptions.py index 1e976dfe..935fecdb 100644 --- a/rest_framework_json_api/exceptions.py +++ b/rest_framework_json_api/exceptions.py @@ -2,12 +2,18 @@ from django.utils import six, encoding from django.utils.translation import ugettext_lazy as _ from rest_framework import status, exceptions -from rest_framework.views import exception_handler as drf_exception_handler from rest_framework_json_api.utils import format_value def exception_handler(exc, context): + # Import this here to avoid potential edge-case circular imports, which + # crashes with: + # "ImportError: Could not import 'rest_framework_json_api.parsers.JSONParser' for API setting + # 'DEFAULT_PARSER_CLASSES'. ImportError: cannot import name 'exceptions'.'" + # + # Also see: https://github.com/django-json-api/django-rest-framework-json-api/issues/158 + from rest_framework.views import exception_handler as drf_exception_handler response = drf_exception_handler(exc, context) if not response: From edf22263efb4a8e9e2024325a1421471f9bedd46 Mon Sep 17 00:00:00 2001 From: Gregory Jones Date: Mon, 21 Mar 2016 17:02:04 -0400 Subject: [PATCH 25/38] Do not assume multiple queryset evals return in same order --- rest_framework_json_api/renderers.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 945f05bc..2272237d 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -5,6 +5,7 @@ from collections import OrderedDict from django.utils import six, encoding +from django.db.models.query import QuerySet from rest_framework import relations from rest_framework import renderers @@ -436,7 +437,17 @@ def render(self, data, accepted_media_type=None, renderer_context=None): json_api_data = list() for position in range(len(serializer_data)): resource = serializer_data[position] # Get current resource - resource_instance = resource_serializer.instance[position] # Get current instance + + # If this is a queryset, ensure that the query is evaluated only once, + # and do not assume the ordering of the serializer_data is in the same order, + # since queries without an 'order_by' may not return in the same order. Match + # by pk instead. + if isinstance(resource_serializer.instance, QuerySet): + pk = resource['id'] + resource_instance = filter(lambda item: item.pk == pk, + list(resource_serializer.instance))[0] + else: + resource_instance = resource_serializer.instance[position] # Get current instance json_resource_obj = self.build_json_resource_obj(fields, resource, resource_instance, resource_name) meta = self.extract_meta(resource_serializer, resource) From a1c17691d2c65763e258383de46f058ee8f3a242 Mon Sep 17 00:00:00 2001 From: Gregory Jones Date: Tue, 22 Mar 2016 10:15:10 -0400 Subject: [PATCH 26/38] Use source field of 'id' field in lookup --- rest_framework_json_api/renderers.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 2272237d..fcfcd6b8 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -443,8 +443,12 @@ def render(self, data, accepted_media_type=None, renderer_context=None): # since queries without an 'order_by' may not return in the same order. Match # by pk instead. if isinstance(resource_serializer.instance, QuerySet): - pk = resource['id'] - resource_instance = filter(lambda item: item.pk == pk, + id = resource['id'] + + # If 'id' field is sourced from another field, use the source field in the lookup + lookup_field = resource_serializer.child.fields['id'].source + + resource_instance = filter(lambda item: getattr(item, lookup_field) == id, list(resource_serializer.instance))[0] else: resource_instance = resource_serializer.instance[position] # Get current instance From 4c728bd6d0e65b91863f4c88b4315a6ef0f4c279 Mon Sep 17 00:00:00 2001 From: Cathryn Supko Date: Mon, 18 Apr 2016 12:42:49 -0400 Subject: [PATCH 27/38] fix extract_included for ManyRelatedField --- rest_framework_json_api/renderers.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index fcfcd6b8..a0ca58e4 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -2,7 +2,7 @@ Renderers """ import copy -from collections import OrderedDict +from collections import OrderedDict, Iterable from django.utils import six, encoding from django.db.models.query import QuerySet @@ -284,12 +284,19 @@ def extract_included(fields, resource, resource_instance, included_resources): # serializer_class = utils.get_serializer_from_instance_and_serializer() # serializer_instance = serializer_class(relation_instance_or_manager.all(), many=True, context=context) - iterable = field.get_attribute(current_serializer.instance) - - + if isinstance(current_serializer.instance, Iterable): + iterable = [] + for obj in current_serializer.instance: + iterable += field.get_attribute(obj) + # remove duplicates + iterable = list(set(iterable)) + else: + iterable = field.get_attribute(current_serializer.instance) for item in iterable: - serializer_class = utils.get_serializer_from_instance_and_serializer(item, current_serializer, field_name) + serializer_class = utils.get_serializer_from_instance_and_serializer(item, + current_serializer, + field_name) serializer_instance = serializer_class(item, context=context) serializer_instances.append(serializer_instance) From 793a07bd14700f990ca618bfb92aa93b98bbf3d7 Mon Sep 17 00:00:00 2001 From: Cathryn Supko Date: Mon, 18 Apr 2016 16:35:11 -0400 Subject: [PATCH 28/38] remove redundant deduping --- rest_framework_json_api/renderers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index a0ca58e4..3adf6120 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -288,8 +288,6 @@ def extract_included(fields, resource, resource_instance, included_resources): iterable = [] for obj in current_serializer.instance: iterable += field.get_attribute(obj) - # remove duplicates - iterable = list(set(iterable)) else: iterable = field.get_attribute(current_serializer.instance) From abbc805e8346b0d7df64218465b08b2d89156c2f Mon Sep 17 00:00:00 2001 From: Audrey Kubetin Date: Wed, 20 Apr 2016 16:19:06 -0400 Subject: [PATCH 29/38] changing logic in rest_framework_json_api.parsers.JSONParser.parse_relationships to preserve null/None values --- rest_framework_json_api/parsers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index e8c0a222..5c2caf86 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -44,6 +44,8 @@ def parse_relationships(data): parsed_relationships[field_name] = field_data['id'] elif isinstance(field_data, list): parsed_relationships[field_name] = list(relation['id'] for relation in field_data) + elif field_data == None: + parsed_relationships[field_name] = field_data return parsed_relationships def parse(self, stream, media_type=None, parser_context=None): @@ -51,11 +53,12 @@ def parse(self, stream, media_type=None, parser_context=None): Parses the incoming bytestream as JSON and returns the resulting data """ result = super(JSONParser, self).parse(stream, media_type=media_type, parser_context=parser_context) + data = result.get('data') if data: type = data.get('type') - # if type is defined, treat it like a full object, otherwise jsut pass through the data + # if type is defined, treat it like a full object, otherwise just pass through the data if type: from rest_framework_json_api.views import RelationshipView if isinstance(parser_context['view'], RelationshipView): From da44a929ee8692ffeb0be5e377054d2ca5e3b730 Mon Sep 17 00:00:00 2001 From: Audrey Kubetin Date: Thu, 21 Apr 2016 15:13:12 -0400 Subject: [PATCH 30/38] small change to accomodate empty dictionaries without giving 500 errors --- rest_framework_json_api/parsers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index 5c2caf86..1ef92670 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -39,11 +39,11 @@ def parse_relationships(data): # Parse the relationships parsed_relationships = dict() for field_name, field_data in relationships.items(): - field_data = field_data.get('data') + field_data = field_data.get('data', None) if isinstance(field_data, dict): - parsed_relationships[field_name] = field_data['id'] + parsed_relationships[field_name] = field_data.get('id', None) elif isinstance(field_data, list): - parsed_relationships[field_name] = list(relation['id'] for relation in field_data) + parsed_relationships[field_name] = list(relation.get('id', None) for relation in field_data) elif field_data == None: parsed_relationships[field_name] = field_data return parsed_relationships From 4eabb3c8f3531b8ef05ed0fba8fe498c8eda2c01 Mon Sep 17 00:00:00 2001 From: Gregory Jones Date: Tue, 26 Apr 2016 11:09:47 -0400 Subject: [PATCH 31/38] Default serializer can be set via method --- rest_framework_json_api/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 086af53e..56a1fe85 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -206,6 +206,10 @@ def get_resource_type_from_model(model): def get_default_serializer_from_model(model): json_api_meta = getattr(model, 'JSONAPIMeta', None) + + if hasattr(json_api_meta, 'get_default_serializer'): + return json_api_meta.get_default_serializer() + serializer_string = getattr(json_api_meta, 'default_serializer', None) if serializer_string is None: # return format_relation_name(model.__name__) From 911d34b0c2a9c1e78a2f528810d24e656171b1ac Mon Sep 17 00:00:00 2001 From: John Huffsmith Date: Mon, 20 Jun 2016 09:25:10 -0400 Subject: [PATCH 32/38] use drf get_attribute to support dot notation source lookups --- rest_framework_json_api/renderers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 3adf6120..b8e01ab8 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -11,6 +11,7 @@ from rest_framework import renderers from rest_framework.serializers import BaseSerializer, ListSerializer, ModelSerializer from rest_framework.settings import api_settings +from rest_framework.fields import get_attribute from . import utils @@ -86,7 +87,7 @@ def extract_relationships(fields, resource, resource_instance): source = field.source try: - relation_instance_or_manager = getattr(resource_instance, source) + relation_instance_or_manager = get_attribute(resource_instance, source.split('.')) except AttributeError: # if the field is not defined on the model then we check the serializer # and if no value is there we skip over the field completely From 9588b0eb880a9d354a10674437aa526f970464e1 Mon Sep 17 00:00:00 2001 From: John Huffsmith Date: Thu, 23 Jun 2016 11:09:25 -0400 Subject: [PATCH 33/38] proper source usage --- rest_framework_json_api/renderers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index b8e01ab8..088d6e0c 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -262,15 +262,15 @@ def extract_included(fields, resource, resource_instance, included_resources): continue try: - relation_instance_or_manager = getattr(resource_instance, field_name) + relation_instance_or_manager = get_attribute(resource_instance, field.source.split('.')) except AttributeError: try: # For ManyRelatedFields if `related_name` is not set we need to access `foo_set` from `source` - relation_instance_or_manager = getattr(resource_instance, field.child_relation.source) + relation_instance_or_manager = get_attribute(resource_instance, field.child_relation.source.split('.')) except AttributeError: if not hasattr(current_serializer, field.source): continue - serializer_method = getattr(current_serializer, field.source) + serializer_method = get_attribute(current_serializer, field.source.split('.')) relation_instance_or_manager = serializer_method(resource_instance) new_included_resources = [key.replace('%s.' % field_name, '', 1) From c810fd1e49d05b5444e18b1dabfc61e7b98b89b3 Mon Sep 17 00:00:00 2001 From: John Huffsmith Date: Mon, 20 Jun 2016 09:25:10 -0400 Subject: [PATCH 34/38] use drf get_attribute to support dot notation source lookups --- rest_framework_json_api/renderers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 3adf6120..b8e01ab8 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -11,6 +11,7 @@ from rest_framework import renderers from rest_framework.serializers import BaseSerializer, ListSerializer, ModelSerializer from rest_framework.settings import api_settings +from rest_framework.fields import get_attribute from . import utils @@ -86,7 +87,7 @@ def extract_relationships(fields, resource, resource_instance): source = field.source try: - relation_instance_or_manager = getattr(resource_instance, source) + relation_instance_or_manager = get_attribute(resource_instance, source.split('.')) except AttributeError: # if the field is not defined on the model then we check the serializer # and if no value is there we skip over the field completely From 7af3fa1f8531733fe1635c48b5858c43230cea24 Mon Sep 17 00:00:00 2001 From: John Huffsmith Date: Thu, 23 Jun 2016 11:09:25 -0400 Subject: [PATCH 35/38] proper source usage --- rest_framework_json_api/renderers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index b8e01ab8..088d6e0c 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -262,15 +262,15 @@ def extract_included(fields, resource, resource_instance, included_resources): continue try: - relation_instance_or_manager = getattr(resource_instance, field_name) + relation_instance_or_manager = get_attribute(resource_instance, field.source.split('.')) except AttributeError: try: # For ManyRelatedFields if `related_name` is not set we need to access `foo_set` from `source` - relation_instance_or_manager = getattr(resource_instance, field.child_relation.source) + relation_instance_or_manager = get_attribute(resource_instance, field.child_relation.source.split('.')) except AttributeError: if not hasattr(current_serializer, field.source): continue - serializer_method = getattr(current_serializer, field.source) + serializer_method = get_attribute(current_serializer, field.source.split('.')) relation_instance_or_manager = serializer_method(resource_instance) new_included_resources = [key.replace('%s.' % field_name, '', 1) From a34eac86f4d1251f404b5c36574b016180e2a8e2 Mon Sep 17 00:00:00 2001 From: John Huffsmith Date: Fri, 29 Jul 2016 16:09:19 -0400 Subject: [PATCH 36/38] passing type into serializers --- rest_framework_json_api/parsers.py | 4 ++-- rest_framework_json_api/relations.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index 1ef92670..ff9f0559 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -41,9 +41,9 @@ def parse_relationships(data): for field_name, field_data in relationships.items(): field_data = field_data.get('data', None) if isinstance(field_data, dict): - parsed_relationships[field_name] = field_data.get('id', None) + parsed_relationships[field_name] = field_data elif isinstance(field_data, list): - parsed_relationships[field_name] = list(relation.get('id', None) for relation in field_data) + parsed_relationships[field_name] = list(relation for relation in field_data) elif field_data == None: parsed_relationships[field_name] = field_data return parsed_relationships diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index c03dc4fe..e256c5cc 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -162,6 +162,18 @@ def choices(self): for item in queryset ]) + def validate_empty_values(self, data): + if isinstance(data, dict) and 'id' in data and data['id'] is None: + return super(ResourceRelatedField, self).validate_empty_values(data['id']) + + return super(ResourceRelatedField, self).validate_empty_values(data) + + def to_internal_value(self, data): + if isinstance(data, dict) and 'id' in data: + return super(ResourceRelatedField, self).to_internal_value(data['id']) + + return super(ResourceRelatedField, self).to_internal_value(data) + class SerializerMethodResourceRelatedField(ResourceRelatedField): def get_attribute(self, instance): From 49b6011915a5b293bee1a1745bb163dc09966cf5 Mon Sep 17 00:00:00 2001 From: John Huffsmith Date: Fri, 29 Jul 2016 16:24:08 -0400 Subject: [PATCH 37/38] bumping version --- rest_framework_json_api/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_json_api/__init__.py b/rest_framework_json_api/__init__.py index eeb61ce2..c1b7e75a 100644 --- a/rest_framework_json_api/__init__.py +++ b/rest_framework_json_api/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- __title__ = 'djangorestframework-jsonapi' -__version__ = '2.0.0-alpha.3' +__version__ = '3.0.0-mt' __author__ = '' __license__ = 'MIT' __copyright__ = '' From 8b32da5e9ed93cd9fa306d97aed3da25b9d21cbc Mon Sep 17 00:00:00 2001 From: John Huffsmith Date: Sun, 7 Aug 2016 16:49:13 -0400 Subject: [PATCH 38/38] skip lookup if None --- rest_framework_json_api/renderers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 088d6e0c..0a3f1de2 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -301,9 +301,10 @@ def extract_included(fields, resource, resource_instance, included_resources): if isinstance(field, relations.RelatedField): # serializer_class = include_config.get(field_name) - serializer_class = utils.get_serializer_from_instance_and_serializer(relation_instance_or_manager, current_serializer, field_name) if relation_instance_or_manager is None: continue + + serializer_class = utils.get_serializer_from_instance_and_serializer(relation_instance_or_manager, current_serializer, field_name) serializer_instance = serializer_class(relation_instance_or_manager, context=context) serializer_instances.append(serializer_instance)