From 96a6484299e2cffb2a796480ad60945cae82ca1a Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Wed, 16 Sep 2015 19:38:21 -0400 Subject: [PATCH 01/26] Skip read_only attribute fields when the resource is non-existent --- rest_framework_json_api/utils.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 36c42e33..b3e75126 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -2,7 +2,6 @@ Utils. """ import inflection -from django.core import urlresolvers from django.conf import settings from django.utils import six, encoding from django.utils.translation import ugettext_lazy as _ @@ -12,8 +11,6 @@ from rest_framework.settings import api_settings from rest_framework.exceptions import APIException -from django.utils.six.moves.urllib.parse import urlparse - try: from rest_framework.compat import OrderedDict except ImportError: @@ -191,6 +188,12 @@ def extract_attributes(fields, resource): # Skip fields with relations if isinstance(field, (RelatedField, BaseSerializer, ManyRelatedField)): continue + + # Skip read_only attribute fields when the resource is non-existent + # Needed for the "Raw data" form of the browseable API + if resource.get('id') is None and fields[field_name].read_only: + continue + data.update({ field_name: resource.get(field_name) }) From 1aa6d3998deb0205cfa8877a6adad666fbb79197 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Wed, 16 Sep 2015 23:16:11 -0400 Subject: [PATCH 02/26] Created JSONAPIRelatedField --- rest_framework_json_api/relations.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 rest_framework_json_api/relations.py diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py new file mode 100644 index 00000000..7c036e6b --- /dev/null +++ b/rest_framework_json_api/relations.py @@ -0,0 +1,26 @@ +from django.core.exceptions import ObjectDoesNotExist +from rest_framework.relations import HyperlinkedRelatedField + + +class JSONAPIRelatedField(HyperlinkedRelatedField): + """ + This field exists for the sole purpose of accepting PKs as well as URLs + when data is submitted back to the serializer + """ + + def __init__(self, **kwargs): + self.pk_field = kwargs.pop('pk_field', None) + super(JSONAPIRelatedField, self).__init__(**kwargs) + + def to_internal_value(self, data): + try: + super(JSONAPIRelatedField, self).to_internal_value(data) + except AssertionError: + if self.pk_field is not None: + data = self.pk_field.to_internal_value(data) + try: + return self.get_queryset().get(pk=data) + except ObjectDoesNotExist: + self.fail('does_not_exist', pk_value=data) + except (TypeError, ValueError): + self.fail('incorrect_type', data_type=type(data).__name__) \ No newline at end of file From f9c4fdf79998f2d88b95bb261b9e8c5d962f77e6 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Wed, 16 Sep 2015 23:19:40 -0400 Subject: [PATCH 03/26] Created JSONAPIModelSerializer --- rest_framework_json_api/serializers.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 rest_framework_json_api/serializers.py diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py new file mode 100644 index 00000000..fe83910f --- /dev/null +++ b/rest_framework_json_api/serializers.py @@ -0,0 +1,15 @@ +from rest_framework.serializers import HyperlinkedModelSerializer + +from rest_framework_json_api.relations import JSONAPIRelatedField + + +class JSONAPIModelSerializer(HyperlinkedModelSerializer): + """ + A type of `ModelSerializer` that uses hyperlinked relationships instead + of primary key relationships. Specifically: + + * A 'url' field is included instead of the 'id' field. + * Relationships to other instances are hyperlinks, instead of primary keys. + * Uses django-rest-framework-json-api JSONAPIRelatedField instead of the default HyperlinkedRelatedField + """ + serializer_related_field = JSONAPIRelatedField From 8caebbb1cedb9bff3c3617bc1b0082559ca264c7 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Thu, 17 Sep 2015 09:52:11 -0400 Subject: [PATCH 04/26] Renamed the classes so that they can replace their original equivalent transparently. --- rest_framework_json_api/relations.py | 9 ++++----- rest_framework_json_api/serializers.py | 10 +++++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 7c036e6b..9e4e9504 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -1,8 +1,7 @@ -from django.core.exceptions import ObjectDoesNotExist -from rest_framework.relations import HyperlinkedRelatedField +from rest_framework.relations import * -class JSONAPIRelatedField(HyperlinkedRelatedField): +class HyperlinkedRelatedField(HyperlinkedRelatedField): """ This field exists for the sole purpose of accepting PKs as well as URLs when data is submitted back to the serializer @@ -10,11 +9,11 @@ class JSONAPIRelatedField(HyperlinkedRelatedField): def __init__(self, **kwargs): self.pk_field = kwargs.pop('pk_field', None) - super(JSONAPIRelatedField, self).__init__(**kwargs) + super(HyperlinkedRelatedField, self).__init__(**kwargs) def to_internal_value(self, data): try: - super(JSONAPIRelatedField, self).to_internal_value(data) + super(HyperlinkedRelatedField, self).to_internal_value(data) except AssertionError: if self.pk_field is not None: data = self.pk_field.to_internal_value(data) diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index fe83910f..874dc37d 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -1,15 +1,15 @@ -from rest_framework.serializers import HyperlinkedModelSerializer +from rest_framework.serializers import * -from rest_framework_json_api.relations import JSONAPIRelatedField +from rest_framework_json_api.relations import HyperlinkedRelatedField -class JSONAPIModelSerializer(HyperlinkedModelSerializer): +class HyperlinkedModelSerializer(HyperlinkedModelSerializer): """ A type of `ModelSerializer` that uses hyperlinked relationships instead of primary key relationships. Specifically: * A 'url' field is included instead of the 'id' field. * Relationships to other instances are hyperlinks, instead of primary keys. - * Uses django-rest-framework-json-api JSONAPIRelatedField instead of the default HyperlinkedRelatedField + * Uses django-rest-framework-json-api HyperlinkedRelatedField instead of the default HyperlinkedRelatedField """ - serializer_related_field = JSONAPIRelatedField + serializer_related_field = HyperlinkedRelatedField From ea6c6bc24102a86e47cdc372142e4796d94b5f94 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Thu, 17 Sep 2015 10:22:56 -0400 Subject: [PATCH 05/26] Thou shalt return something --- 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 9e4e9504..921eb1ea 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -13,7 +13,7 @@ def __init__(self, **kwargs): def to_internal_value(self, data): try: - super(HyperlinkedRelatedField, self).to_internal_value(data) + return super(HyperlinkedRelatedField, self).to_internal_value(data) except AssertionError: if self.pk_field is not None: data = self.pk_field.to_internal_value(data) From 64ce3765a4220056a3f15309bece832362527ae1 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Thu, 17 Sep 2015 10:28:21 -0400 Subject: [PATCH 06/26] Wrong exception class in except --- rest_framework_json_api/relations.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 921eb1ea..22969b59 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -1,3 +1,4 @@ +from rest_framework.exceptions import ValidationError from rest_framework.relations import * @@ -13,8 +14,9 @@ def __init__(self, **kwargs): def to_internal_value(self, data): try: + # Try parsing links first for the browseable API return super(HyperlinkedRelatedField, self).to_internal_value(data) - except AssertionError: + except ValidationError: if self.pk_field is not None: data = self.pk_field.to_internal_value(data) try: From 2202e268582533f2d0d7a5337c7565f81f1db39a Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Thu, 17 Sep 2015 10:37:30 -0400 Subject: [PATCH 07/26] Proper error messages --- rest_framework_json_api/relations.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 22969b59..dd818df8 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -1,5 +1,6 @@ from rest_framework.exceptions import ValidationError from rest_framework.relations import * +from django.utils.translation import ugettext_lazy as _ class HyperlinkedRelatedField(HyperlinkedRelatedField): @@ -7,6 +8,15 @@ class HyperlinkedRelatedField(HyperlinkedRelatedField): This field exists for the sole purpose of accepting PKs as well as URLs when data is submitted back to the serializer """ + default_error_messages = { + 'required': _('This field is required.'), + 'no_match': _('Invalid hyperlink - No URL match.'), + 'incorrect_match': _('Invalid hyperlink - Incorrect URL match.'), + 'does_not_exist': _('Invalid hyperlink - Object does not exist.'), + 'incorrect_type': _('Incorrect type. Expected URL string, received {data_type}.'), + 'pk_does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'), + 'incorrect_pk_type': _('Incorrect type. Expected pk value, received {data_type}.'), + } def __init__(self, **kwargs): self.pk_field = kwargs.pop('pk_field', None) @@ -22,6 +32,6 @@ def to_internal_value(self, data): try: return self.get_queryset().get(pk=data) except ObjectDoesNotExist: - self.fail('does_not_exist', pk_value=data) + self.fail('pk_does_not_exist', pk_value=data) except (TypeError, ValueError): - self.fail('incorrect_type', data_type=type(data).__name__) \ No newline at end of file + self.fail('incorrect_pk_type', data_type=type(data).__name__) From 5df8ee3456170560d1161f612887be788eaab9d9 Mon Sep 17 00:00:00 2001 From: Leifur Halldor Asgeirsson Date: Thu, 17 Sep 2015 15:57:27 -0400 Subject: [PATCH 08/26] ResourceRelatedField & tests Collaborative effort of @leifurhauks & @jsenecal --- example/models.py | 10 ++ example/tests/test_relations.py | 131 +++++++++++++++++++++++++++ rest_framework_json_api/relations.py | 23 +++++ rest_framework_json_api/utils.py | 4 + 4 files changed, 168 insertions(+) create mode 100644 example/tests/test_relations.py diff --git a/example/models.py b/example/models.py index 7dfbc1ab..7fd8a3d7 100644 --- a/example/models.py +++ b/example/models.py @@ -48,3 +48,13 @@ class Entry(BaseModel): def __str__(self): return self.headline + + +@python_2_unicode_compatible +class Comment(BaseModel): + entry = models.ForeignKey(Entry) + body = models.TextField() + author = models.ForeignKey(Author) + + def __str__(self): + return self.body diff --git a/example/tests/test_relations.py b/example/tests/test_relations.py new file mode 100644 index 00000000..ea60ae9a --- /dev/null +++ b/example/tests/test_relations.py @@ -0,0 +1,131 @@ +from __future__ import absolute_import + +from django.utils import timezone + +from rest_framework import serializers + +from . import TestBase +from rest_framework_json_api.utils import format_relation_name +from example.models import Blog, Entry, Comment, Author +from rest_framework_json_api.relations import ResourceRelatedField + + +class TestResourceRelatedField(TestBase): + + def setUp(self): + super(TestResourceRelatedField, self).setUp() + self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog") + self.entry = Entry.objects.create( + blog=self.blog, + headline='headline', + body_text='body_text', + pub_date=timezone.now(), + mod_date=timezone.now(), + n_comments=0, + n_pingbacks=0, + rating=3 + ) + for i in range(1,6): + name = 'some_author{}'.format(i) + self.entry.authors.add( + Author.objects.create(name=name, email='{}@example.org'.format(name)) + ) + + self.comment = Comment.objects.create( + entry=self.entry, + body='testing one two three', + author=Author.objects.first() + ) + + def test_data_in_correct_format_when_instantiated_with_blog_object(self): + serializer = BlogFKSerializer(instance={'blog': self.blog}) + + expected_data = { + 'type': format_relation_name('Blog'), + 'id': str(self.blog.id) + } + + actual_data = serializer.data['blog'] + + self.assertEqual(actual_data, expected_data) + + def test_data_in_correct_format_when_instantiated_with_entry_object(self): + serializer = EntryFKSerializer(instance={'entry': self.entry}) + + expected_data = { + 'type': format_relation_name('Entry'), + 'id': str(self.entry.id) + } + + actual_data = serializer.data['entry'] + + self.assertEqual(actual_data, expected_data) + + def test_deserialize_primitive_data_blog(self): + serializer = BlogFKSerializer(data={ + 'blog': { + 'type': format_relation_name('Blog'), + 'id': str(self.blog.id) + } + } + ) + + self.assertTrue(serializer.is_valid()) + self.assertEqual(serializer.validated_data['blog'], self.blog) + + def test_validation_fails_for_wrong_type(self): + serializer = BlogFKSerializer(data={ + 'blog': { + 'type': 'Entries', + 'id': str(self.blog.id) + } + } + ) + + self.assertFalse(serializer.is_valid()) + + def test_serialize_many_to_many_relation(self): + serializer = EntryModelSerializer(instance=self.entry) + + type_string = format_relation_name('Author') + author_pks = Author.objects.values_list('pk', flat=True) + expected_data = [{'type': type_string, 'id': str(pk)} for pk in author_pks] + + self.assertEqual( + serializer.data['authors'], + expected_data + ) + + def test_deserialize_many_to_many_relation(self): + type_string = format_relation_name('Author') + author_pks = Author.objects.values_list('pk', flat=True) + authors = [{'type': type_string, 'id': pk} for pk in author_pks] + + serializer = EntryModelSerializer(data={'authors': authors, 'comment_set': []}) + + self.assertTrue(serializer.is_valid()) + self.assertEqual(len(serializer.validated_data['authors']), Author.objects.count()) + for author in serializer.validated_data['authors']: + self.assertIsInstance(author, Author) + + def test_read_only(self): + serializer = EntryModelSerializer(data={'authors': [], 'comment_set': [{'type': 'Comments', 'id': 2}]}) + serializer.is_valid(raise_exception=True) + self.assertNotIn('comment_set', serializer.validated_data) + + +class BlogFKSerializer(serializers.Serializer): + blog = ResourceRelatedField(queryset=Blog.objects) + + +class EntryFKSerializer(serializers.Serializer): + entry = ResourceRelatedField(queryset=Entry.objects) + + +class EntryModelSerializer(serializers.ModelSerializer): + authors = ResourceRelatedField(many=True, queryset=Author.objects) + comment_set = ResourceRelatedField(many=True, read_only=True) + + class Meta: + model = Entry + fields = ('authors', 'comment_set') diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index dd818df8..4bf6896f 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -1,5 +1,6 @@ from rest_framework.exceptions import ValidationError from rest_framework.relations import * +from rest_framework_json_api.utils import format_relation_name, get_related_resource_type from django.utils.translation import ugettext_lazy as _ @@ -35,3 +36,25 @@ def to_internal_value(self, data): self.fail('pk_does_not_exist', pk_value=data) except (TypeError, ValueError): self.fail('incorrect_pk_type', data_type=type(data).__name__) + + +class ResourceRelatedField(PrimaryKeyRelatedField): + default_error_messages = { + 'required': _('This field is required.'), + 'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'), + 'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'), + 'incorrect_relation_type': _('Incorrect relation type. Expected {relation_type}, received {received_type}.'), + } + + def to_internal_value(self, data): + expected_relation_type = format_relation_name(get_related_resource_type(self)) + if data['type'] != expected_relation_type: + self.fail('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): + return { + 'type': format_relation_name(get_related_resource_type(self)), + 'id': str(value.pk) + } + diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index b3e75126..007d387a 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -179,6 +179,10 @@ def get_related_resource_type(relation): return format_relation_name(relation_model.__name__) +def get_model_name_from_queryset(qs): + return qs.model._meta.model_name + + def extract_attributes(fields, resource): data = OrderedDict() for field_name, field in six.iteritems(fields): From b74c4e0b3f81dcc31b32b67a8d8fb5ee5225c8f5 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Thu, 17 Sep 2015 17:38:26 -0400 Subject: [PATCH 09/26] Getting rid of get_related_resource_type for relations.ResourceRelatedField and utils.extract_relationships --- rest_framework_json_api/relations.py | 7 ++++--- rest_framework_json_api/utils.py | 30 +++++++++++++++++++++------- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/rest_framework_json_api/relations.py b/rest_framework_json_api/relations.py index 4bf6896f..2f33af0a 100644 --- a/rest_framework_json_api/relations.py +++ b/rest_framework_json_api/relations.py @@ -1,6 +1,7 @@ from rest_framework.exceptions import ValidationError from rest_framework.relations import * -from rest_framework_json_api.utils import format_relation_name, get_related_resource_type +from rest_framework_json_api.utils import format_relation_name, get_related_resource_type, \ + get_resource_type_from_queryset, get_resource_type_from_instance from django.utils.translation import ugettext_lazy as _ @@ -47,14 +48,14 @@ class ResourceRelatedField(PrimaryKeyRelatedField): } def to_internal_value(self, data): - expected_relation_type = format_relation_name(get_related_resource_type(self)) + expected_relation_type = get_resource_type_from_queryset(self.queryset) if data['type'] != expected_relation_type: self.fail('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): return { - 'type': format_relation_name(get_related_resource_type(self)), + 'type': format_relation_name(get_resource_type_from_instance(value)), 'id': str(value.pk) } diff --git a/rest_framework_json_api/utils.py b/rest_framework_json_api/utils.py index 2c119340..5bb2973a 100644 --- a/rest_framework_json_api/utils.py +++ b/rest_framework_json_api/utils.py @@ -187,14 +187,31 @@ def get_related_resource_type(relation): if hasattr(parent_model_relation, 'related'): relation_model = parent_model_relation.related.related_model elif hasattr(parent_model_relation, 'field'): - relation_model = parent_model_relation.field.related_model + relation_model = parent_model_relation.field.related.model else: raise APIException('Unable to find related model for relation {relation}'.format(relation=relation)) return format_relation_name(relation_model.__name__) -def get_model_name_from_queryset(qs): - return qs.model._meta.model_name +def get_instance_or_manager_resource_type(resource_instance_or_manager): + + if hasattr(resource_instance_or_manager, 'model'): + return get_resource_type_from_manager(resource_instance_or_manager) + if hasattr(resource_instance_or_manager, '_meta'): + return get_resource_type_from_instance(resource_instance_or_manager) + pass + + +def get_resource_type_from_queryset(qs): + return format_relation_name(qs.model._meta.model.__name__) + + +def get_resource_type_from_instance(instance): + return format_relation_name(instance._meta.model.__name__) + + +def get_resource_type_from_manager(manager): + return format_relation_name(manager.model.__name__) def extract_attributes(fields, resource): @@ -235,8 +252,8 @@ def extract_relationships(fields, resource, resource_instance): if not isinstance(field, (RelatedField, ManyRelatedField, BaseSerializer)): continue - relation_type = get_related_resource_type(field) relation_instance_or_manager = getattr(resource_instance, field_name) + relation_type = get_instance_or_manager_resource_type(relation_instance_or_manager) if isinstance(field, HyperlinkedIdentityField): # special case for HyperlinkedIdentityField @@ -280,7 +297,7 @@ def extract_relationships(fields, resource, resource_instance): if isinstance(field, ManyRelatedField): relation_data = list() for related_object in relation_instance_or_manager.all(): - related_object_type = get_related_resource_type(related_object) + related_object_type = get_instance_or_manager_resource_type(relation_instance_or_manager) relation_data.append(OrderedDict([ ('type', related_object_type), ('id', encoding.force_text(related_object.pk)) @@ -303,8 +320,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_related_resource_type( - nested_resource_instance) + nested_resource_instance_type = get_resource_type_from_instance(nested_resource_instance) relation_data.append(OrderedDict([ ('type', nested_resource_instance_type), ('id', encoding.force_text(nested_resource_instance.pk)) From 1fa74e72833e029271e61863689b5a8ac6b9143d Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Fri, 18 Sep 2015 14:38:55 -0400 Subject: [PATCH 10/26] Created a generic _relationship_ endpoint view --- rest_framework_json_api/views.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 rest_framework_json_api/views.py diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py new file mode 100644 index 00000000..dcd59276 --- /dev/null +++ b/rest_framework_json_api/views.py @@ -0,0 +1,19 @@ +from rest_framework import generics +from rest_framework.response import Response + + +class RelationshipView(generics.GenericAPIView): + def get(self, request, *args, **kwargs): + return Response() + + def put(self, request, *args, **kwargs): + return Response() + + def patch(self, request, *args, **kwargs): + return Response() + + def post(self, request, *args, **kwargs): + return Response() + + def delete(self, request, *args, **kwargs): + return Response() From ba985efa2c87b55c4d7c10e955afd05d0a326c9c Mon Sep 17 00:00:00 2001 From: Leifur Halldor Asgeirsson Date: Fri, 18 Sep 2015 15:30:40 -0400 Subject: [PATCH 11/26] Merge stashed changes --- example/tests/test_serializers.py | 73 ++++++++++++++++++++++++++ example/tests/test_views.py | 37 +++++++++++++ example/urls.py | 7 ++- example/views.py | 8 +++ rest_framework_json_api/serializers.py | 33 ++++++++++++ rest_framework_json_api/views.py | 15 +++++- 6 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 example/tests/test_serializers.py create mode 100644 example/tests/test_views.py diff --git a/example/tests/test_serializers.py b/example/tests/test_serializers.py new file mode 100644 index 00000000..6712ec7e --- /dev/null +++ b/example/tests/test_serializers.py @@ -0,0 +1,73 @@ +from django.test import TestCase +from django.utils import timezone + +from rest_framework_json_api.utils import format_relation_name +from rest_framework_json_api.serializers import ResourceIdentifierObjectSerializer + +from example.models import Blog, Entry, Author + + +class TestResourceIdentifierObjectSerializer(TestCase): + def setUp(self): + self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog") + self.entry = Entry.objects.create( + blog=self.blog, + headline='headline', + body_text='body_text', + pub_date=timezone.now(), + mod_date=timezone.now(), + n_comments=0, + n_pingbacks=0, + rating=3 + ) + for i in range(1,6): + name = 'some_author{}'.format(i) + self.entry.authors.add( + Author.objects.create(name=name, email='{}@example.org'.format(name)) + ) + + def test_data_in_correct_format_when_instantiated_with_blog_object(self): + serializer = ResourceIdentifierObjectSerializer(instance=self.blog) + + expected_data = {'type': format_relation_name('Blog'), 'id': str(self.blog.id)} + + assert serializer.data == expected_data + + def test_data_in_correct_format_when_instantiated_with_entry_object(self): + serializer = ResourceIdentifierObjectSerializer(instance=self.entry) + + expected_data = {'type': format_relation_name('Entry'), 'id': str(self.entry.id)} + + assert serializer.data == expected_data + + def test_deserialize_primitive_data_blog(self): + initial_data = { + 'type': format_relation_name('Blog'), + 'id': str(self.blog.id) + } + serializer = ResourceIdentifierObjectSerializer(data=initial_data, model_class=Blog) + + self.assertTrue(serializer.is_valid(), msg=serializer.errors) + assert serializer.validated_data == self.blog + + def test_data_in_correct_format_when_instantiated_with_queryset(self): + qs = Author.objects.all() + serializer = ResourceIdentifierObjectSerializer(instance=qs, many=True) + + type_string = format_relation_name('Author') + author_pks = Author.objects.values_list('pk', flat=True) + expected_data = [{'type': type_string, 'id': str(pk)} for pk in author_pks] + + assert serializer.data == expected_data + + def test_deserialize_many(self): + type_string = format_relation_name('Author') + author_pks = Author.objects.values_list('pk', flat=True) + initial_data = [{'type': type_string, 'id': str(pk)} for pk in author_pks] + + serializer = ResourceIdentifierObjectSerializer(data=initial_data, model_class=Author, many=True) + + self.assertTrue(serializer.is_valid(), msg=serializer.errors) + + print(serializer.data) + diff --git a/example/tests/test_views.py b/example/tests/test_views.py new file mode 100644 index 00000000..88c2a0ac --- /dev/null +++ b/example/tests/test_views.py @@ -0,0 +1,37 @@ +from django.utils import timezone +from rest_framework.test import APITestCase + +from rest_framework_json_api.utils import format_relation_name + +from example.models import Blog, Entry +from example.views import EntryRelationshipView, BlogRelationshipView + + +class TestRelationshipView(APITestCase): + + def setUp(self): + self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog") + self.entry = Entry.objects.create( + blog=self.blog, + headline='headline', + body_text='body_text', + pub_date=timezone.now(), + mod_date=timezone.now(), + n_comments=0, + n_pingbacks=0, + rating=3 + ) + + def test_get_entry_relationship_blog(self): + response = self.client.get('/entries/{}/relationships/blog'.format(self.entry.id)) + expected_data = {'type': format_relation_name('Blog'), 'id': str(self.entry.blog.id)} + + assert response.data == expected_data + + def test_get_entry_relationship_invalid_field(self): + response = self.client.get('/entries/{}/relationships/invalid_field'.format(self.entry.id)) + + assert response.status_code == 404 + + def test_get_blog_relationship_entry_set(self): + response = self.client.get('/blogs/{}/relationships/entry_set'.format(self.blog.id)) diff --git a/example/urls.py b/example/urls.py index 554c4b05..762a427e 100644 --- a/example/urls.py +++ b/example/urls.py @@ -1,7 +1,8 @@ from django.conf.urls import include, url from rest_framework import routers -from example.views import BlogViewSet, EntryViewSet, AuthorViewSet +from example.views import BlogViewSet, EntryViewSet, AuthorViewSet, EntryRelationshipView, BlogRelationshipView + router = routers.DefaultRouter(trailing_slash=False) @@ -11,4 +12,8 @@ urlpatterns = [ url(r'^', include(router.urls)), + + url(r'^entries/(?P[^/.]+)/relationships/(?P\w+)', EntryRelationshipView.as_view()), + url(r'^blogs/(?P[^/.]+)/relationships/(?P\w+)', BlogRelationshipView.as_view()) + ] diff --git a/example/views.py b/example/views.py index 4a41f5d8..fb66a601 100644 --- a/example/views.py +++ b/example/views.py @@ -1,4 +1,5 @@ from rest_framework import viewsets +from rest_framework_json_api.views import RelationshipView from example.models import Blog, Entry, Author from example.serializers import BlogSerializer, EntrySerializer, AuthorSerializer @@ -19,3 +20,10 @@ class AuthorViewSet(viewsets.ModelViewSet): queryset = Author.objects.all() serializer_class = AuthorSerializer + + +class EntryRelationshipView(RelationshipView): + queryset = Entry.objects + +class BlogRelationshipView(RelationshipView): + queryset = Blog.objects diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index 874dc37d..fafdb2d4 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -1,4 +1,6 @@ +from django.utils.translation import ugettext_lazy as _ from rest_framework.serializers import * +from rest_framework_json_api.utils import format_relation_name, get_resource_type_from_instance from rest_framework_json_api.relations import HyperlinkedRelatedField @@ -13,3 +15,34 @@ class HyperlinkedModelSerializer(HyperlinkedModelSerializer): * Uses django-rest-framework-json-api HyperlinkedRelatedField instead of the default HyperlinkedRelatedField """ serializer_related_field = HyperlinkedRelatedField + + +class ResourceIdentifierObjectSerializer(BaseSerializer): + default_error_messages = { + 'incorrect_model_type': _('Incorrect model type. Expected {model_type}, received {received_type}.'), + 'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'), + 'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'), + } + + def __init__(self, *args, **kwargs): + self.model_class = kwargs.pop('model_class', None) + if 'instance' not in kwargs and not self.model_class: + raise RuntimeError('ResourceIdentifierObjectsSerializer must be initialized with a model class.') + super(ResourceIdentifierObjectSerializer, self).__init__(*args, **kwargs) + + def to_representation(self, instance): + return { + 'type': format_relation_name(get_resource_type_from_instance(instance)), + 'id': str(instance.pk) + } + + def to_internal_value(self, data): + if data['type'] != format_relation_name(self.model_class.__name__): + self.fail('incorrect_model_type', model_type=self.model_class, received_type=data['type']) + pk = data['id'] + try: + return self.model_class.objects.get(pk=pk) + except ObjectDoesNotExist: + self.fail('does_not_exist', pk_value=pk) + except (TypeError, ValueError): + self.fail('incorrect_type', data_type=type(data['pk']).__name__) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index dcd59276..d60407ea 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -1,10 +1,17 @@ from rest_framework import generics from rest_framework.response import Response +from rest_framework_json_api.serializers import ResourceIdentifierObjectSerializer +from rest_framework.exceptions import NotFound class RelationshipView(generics.GenericAPIView): + serializer_class = ResourceIdentifierObjectSerializer + def get(self, request, *args, **kwargs): - return Response() + related_instance = self.get_related_instance(kwargs) + serializer_class = self.get_serializer_class() + serializer = serializer_class(instance=related_instance) + return Response(serializer.data) def put(self, request, *args, **kwargs): return Response() @@ -17,3 +24,9 @@ def post(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs): return Response() + + def get_related_instance(self, kwargs): + try: + return getattr(self.get_object(), kwargs['related_field']) + except AttributeError: + raise NotFound From 16d4fa5509ab3cf304e8932eb49e1300c5e11f88 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Sun, 20 Sep 2015 06:58:12 -0400 Subject: [PATCH 12/26] Working on relationships views --- example/tests/test_views.py | 5 +++-- example/urls.py | 7 +------ example/urls_test.py | 10 +++++++++- rest_framework_json_api/views.py | 21 ++++++++++++++++++--- 4 files changed, 31 insertions(+), 12 deletions(-) diff --git a/example/tests/test_views.py b/example/tests/test_views.py index 88c2a0ac..0630450f 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -1,4 +1,5 @@ from django.utils import timezone +from rest_framework.reverse import reverse from rest_framework.test import APITestCase from rest_framework_json_api.utils import format_relation_name @@ -8,7 +9,6 @@ class TestRelationshipView(APITestCase): - def setUp(self): self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog") self.entry = Entry.objects.create( @@ -23,7 +23,8 @@ def setUp(self): ) def test_get_entry_relationship_blog(self): - response = self.client.get('/entries/{}/relationships/blog'.format(self.entry.id)) + url = reverse('entry-relationships', kwargs={'pk': self.entry.id, 'related_field': 'blog'}) + response = self.client.get(url) expected_data = {'type': format_relation_name('Blog'), 'id': str(self.entry.blog.id)} assert response.data == expected_data diff --git a/example/urls.py b/example/urls.py index 762a427e..554c4b05 100644 --- a/example/urls.py +++ b/example/urls.py @@ -1,8 +1,7 @@ from django.conf.urls import include, url from rest_framework import routers -from example.views import BlogViewSet, EntryViewSet, AuthorViewSet, EntryRelationshipView, BlogRelationshipView - +from example.views import BlogViewSet, EntryViewSet, AuthorViewSet router = routers.DefaultRouter(trailing_slash=False) @@ -12,8 +11,4 @@ urlpatterns = [ url(r'^', include(router.urls)), - - url(r'^entries/(?P[^/.]+)/relationships/(?P\w+)', EntryRelationshipView.as_view()), - url(r'^blogs/(?P[^/.]+)/relationships/(?P\w+)', BlogRelationshipView.as_view()) - ] diff --git a/example/urls_test.py b/example/urls_test.py index a31670b7..bce686f4 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -1,7 +1,7 @@ from django.conf.urls import include, url from rest_framework import routers -from example.views import BlogViewSet, EntryViewSet, AuthorViewSet +from example.views import BlogViewSet, EntryViewSet, AuthorViewSet, EntryRelationshipView, BlogRelationshipView from .api.resources.identity import Identity, GenericIdentity router = routers.DefaultRouter(trailing_slash=False) @@ -19,5 +19,13 @@ # old tests url(r'identities/default/(?P\d+)', GenericIdentity.as_view(), name='user-default'), + + + url(r'^entries/(?P[^/.]+)/relationships/(?P\w+)', + EntryRelationshipView.as_view(), + name='entry-relationships'), + url(r'^blogs/(?P[^/.]+)/relationships/(?P\w+)', + BlogRelationshipView.as_view(), + name='blog-relationships'), ] diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index d60407ea..374ceb9e 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -1,17 +1,20 @@ +from django.db.models import Model, QuerySet +from django.db.models.manager import BaseManager from rest_framework import generics from rest_framework.response import Response +from rest_framework.renderers import JSONRenderer from rest_framework_json_api.serializers import ResourceIdentifierObjectSerializer from rest_framework.exceptions import NotFound class RelationshipView(generics.GenericAPIView): serializer_class = ResourceIdentifierObjectSerializer + renderer_classes = (JSONRenderer, ) def get(self, request, *args, **kwargs): related_instance = self.get_related_instance(kwargs) - serializer_class = self.get_serializer_class() - serializer = serializer_class(instance=related_instance) - return Response(serializer.data) + serializer_instance = self.instantiate_serializer(related_instance) + return Response(serializer_instance.data) def put(self, request, *args, **kwargs): return Response() @@ -30,3 +33,15 @@ def get_related_instance(self, kwargs): return getattr(self.get_object(), kwargs['related_field']) except AttributeError: raise NotFound + + def instantiate_serializer(self, instance): + serializer_class = self.get_serializer_class() + if isinstance(instance, Model): + return serializer_class(instance=instance) + else: + if isinstance(instance, (QuerySet, BaseManager)): + instance = instance.all() + + return serializer_class(instance=instance, many=True) + + From 7979db0c591dab1083950ca9664c7f030510a260 Mon Sep 17 00:00:00 2001 From: Leifur Halldor Asgeirsson Date: Mon, 21 Sep 2015 14:30:16 -0400 Subject: [PATCH 13/26] WIP RelationshipView --- example/tests/test_views.py | 20 +++++++++++++++- rest_framework_json_api/parsers.py | 3 +++ rest_framework_json_api/views.py | 37 ++++++++++++++++++++---------- 3 files changed, 47 insertions(+), 13 deletions(-) diff --git a/example/tests/test_views.py b/example/tests/test_views.py index 0630450f..22bb14ec 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -1,6 +1,7 @@ from django.utils import timezone from rest_framework.reverse import reverse -from rest_framework.test import APITestCase +from rest_framework.test import APITestCase, APIRequestFactory +import json from rest_framework_json_api.utils import format_relation_name @@ -11,6 +12,7 @@ class TestRelationshipView(APITestCase): def setUp(self): self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog") + self.other_blog = Blog.objects.create(name='Other blog', tagline="It's another blog") self.entry = Entry.objects.create( blog=self.blog, headline='headline', @@ -36,3 +38,19 @@ def test_get_entry_relationship_invalid_field(self): def test_get_blog_relationship_entry_set(self): response = self.client.get('/blogs/{}/relationships/entry_set'.format(self.blog.id)) + + def test_put_entry_relationship_blog_returns_405(self): + url = '/entries/{}/relationships/blog'.format(self.entry.id) + response = self.client.put(url, data={}) + assert response.status_code == 405 + + def test_patch_to_one_relationship(self): + url = '/entries/{}/relationships/blog'.format(self.entry.id) + request_data = { + 'data': {'type': format_relation_name('Blog'), 'id': str(self.other_blog.id)} + } + response = self.client.patch(url, data=json.dumps(request_data), content_type='application/vnd.api+json') + assert response.status_code == 200, response.content.decode() + + response = self.client.get(url) + assert response.data == request_data['data'] diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index 10d61041..4dbcfb85 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -34,6 +34,9 @@ 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): + return data # temporary workaround # Check for inconsistencies resource_name = utils.get_resource_name(parser_context) if data.get('type') != resource_name: diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 374ceb9e..db395efb 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -4,6 +4,7 @@ from rest_framework.response import Response from rest_framework.renderers import JSONRenderer from rest_framework_json_api.serializers import ResourceIdentifierObjectSerializer +from rest_framework_json_api.utils import format_relation_name, get_resource_type_from_instance from rest_framework.exceptions import NotFound @@ -12,15 +13,19 @@ class RelationshipView(generics.GenericAPIView): renderer_classes = (JSONRenderer, ) def get(self, request, *args, **kwargs): - related_instance = self.get_related_instance(kwargs) - serializer_instance = self.instantiate_serializer(related_instance) + related_instance = self.get_related_instance() + serializer_instance = self._instantiate_serializer(related_instance) return Response(serializer_instance.data) - def put(self, request, *args, **kwargs): - return Response() - def patch(self, request, *args, **kwargs): - return Response() + parent_obj = self.get_object() + if hasattr(parent_obj, kwargs['related_field']): + related_model_class = self.get_related_instance().__class__ + serializer = self.get_serializer(data=request.data, model_class=related_model_class) + serializer.is_valid(raise_exception=True) + setattr(parent_obj, kwargs['related_field'], serializer.validated_data) + parent_obj.save() + return Response(serializer.data) def post(self, request, *args, **kwargs): return Response() @@ -28,20 +33,28 @@ def post(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs): return Response() - def get_related_instance(self, kwargs): + def get_related_instance(self): try: - return getattr(self.get_object(), kwargs['related_field']) + return getattr(self.get_object(), self.kwargs['related_field']) except AttributeError: raise NotFound - def instantiate_serializer(self, instance): - serializer_class = self.get_serializer_class() + def _instantiate_serializer(self, instance): if isinstance(instance, Model): - return serializer_class(instance=instance) + return self.get_serializer(instance=instance) else: if isinstance(instance, (QuerySet, BaseManager)): instance = instance.all() - return serializer_class(instance=instance, many=True) + return self.get_serializer(instance=instance, many=True) + + 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)) + return self._resource_name + def set_resource_name(self, value): + self._resource_name = value + resource_name = property(get_resource_name, set_resource_name) From dc64c20191711df2b2209e14d6d2e31d4fe66106 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Mon, 21 Sep 2015 15:46:59 -0400 Subject: [PATCH 14/26] Fixed issue with empty "ToOne" relationship being returned as empty lists. --- rest_framework_json_api/views.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index db395efb..cfc4a780 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -2,15 +2,14 @@ from django.db.models.manager import BaseManager from rest_framework import generics from rest_framework.response import Response -from rest_framework.renderers import JSONRenderer +from rest_framework.exceptions import NotFound + from rest_framework_json_api.serializers import ResourceIdentifierObjectSerializer from rest_framework_json_api.utils import format_relation_name, get_resource_type_from_instance -from rest_framework.exceptions import NotFound class RelationshipView(generics.GenericAPIView): serializer_class = ResourceIdentifierObjectSerializer - renderer_classes = (JSONRenderer, ) def get(self, request, *args, **kwargs): related_instance = self.get_related_instance() @@ -40,7 +39,7 @@ def get_related_instance(self): raise NotFound def _instantiate_serializer(self, instance): - if isinstance(instance, Model): + if isinstance(instance, Model) or instance is None: return self.get_serializer(instance=instance) else: if isinstance(instance, (QuerySet, BaseManager)): From 081d3a22ab135afec4956b3b6adb620a2031d7c5 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Mon, 21 Sep 2015 17:23:38 -0400 Subject: [PATCH 15/26] Handling RelationshipView --- rest_framework_json_api/parsers.py | 16 ++++++++++++++-- rest_framework_json_api/renderers.py | 12 ++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index 4dbcfb85..575fabcb 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -2,6 +2,7 @@ Parsers """ from rest_framework import parsers +from rest_framework.exceptions import ParseError from . import utils, renderers, exceptions @@ -31,12 +32,20 @@ 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', {}) + data = result.get('data') if data: from rest_framework_json_api.views import RelationshipView if isinstance(parser_context['view'], RelationshipView): - return data # temporary workaround + # 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 a malformed JSONAPI Resource Identifier Object') + elif not (data.get('id') and data.get('type')): + raise ParseError('Received data is not a valid JSONAPI Resource Identifier Object') + + return data # Check for inconsistencies resource_name = utils.get_resource_name(parser_context) if data.get('type') != resource_name: @@ -70,3 +79,6 @@ def parse(self, stream, media_type=None, parser_context=None): parsed_data.update(attributes) parsed_data.update(parsed_relationships) return parsed_data + + else: + raise ParseError('Received document does not contain primary data') diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 55cd7a3d..eca68324 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -55,11 +55,15 @@ def render(self, data, accepted_media_type=None, renderer_context=None): json_api_included = list() - # If detail view then json api spec expects dict, otherwise a list - # - http://jsonapi.org/format/#document-top-level - # The `results` key may be missing if unpaginated or an OPTIONS request - if view and hasattr(view, 'action') and view.action == 'list' and \ + from rest_framework_json_api.views import RelationshipView + if isinstance(view, RelationshipView): + # Special case for RelationshipView + json_api_data = data + elif view and hasattr(view, 'action') and view.action == 'list' and \ isinstance(data, dict) and 'results' in data: + # If detail view then json api spec expects dict, otherwise a list + # - http://jsonapi.org/format/#document-top-level + # The `results` key may be missing if unpaginated or an OPTIONS request results = data["results"] From 610469ddf91899316c87aae2d4d35915b6718c9b Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Mon, 21 Sep 2015 17:24:52 -0400 Subject: [PATCH 16/26] Better handling of various HTTP methods for RelationshipView --- rest_framework_json_api/views.py | 51 ++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index cfc4a780..441a95ee 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -2,8 +2,9 @@ from django.db.models.manager import BaseManager from rest_framework import generics from rest_framework.response import Response -from rest_framework.exceptions import NotFound +from rest_framework.exceptions import NotFound, MethodNotAllowed +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 @@ -18,19 +19,57 @@ def get(self, request, *args, **kwargs): def patch(self, request, *args, **kwargs): parent_obj = self.get_object() - if hasattr(parent_obj, kwargs['related_field']): - related_model_class = self.get_related_instance().__class__ + related_instance_or_manager = self.get_related_instance() + + if isinstance(related_instance_or_manager, BaseManager): + related_model_class = related_instance_or_manager.model + serializer = self.get_serializer(data=request.data, model_class=related_model_class, many=True) + serializer.is_valid(raise_exception=True) + related_instance_or_manager.all().delete() + related_instance_or_manager.add(*serializer.validated_data) + else: + related_model_class = related_instance_or_manager.__class__ serializer = self.get_serializer(data=request.data, model_class=related_model_class) serializer.is_valid(raise_exception=True) setattr(parent_obj, kwargs['related_field'], serializer.validated_data) parent_obj.save() - return Response(serializer.data) + result_serializer = self._instantiate_serializer(related_instance_or_manager) + return Response(result_serializer.data) def post(self, request, *args, **kwargs): - return Response() + related_instance_or_manager = self.get_related_instance() + + if isinstance(related_instance_or_manager, BaseManager): + related_model_class = related_instance_or_manager.model + serializer = self.get_serializer(data=request.data, model_class=related_model_class, many=True) + serializer.is_valid(raise_exception=True) + if frozenset(serializer.validated_data) <= frozenset(related_instance_or_manager.all()): + return Response(status=204) + related_instance_or_manager.add(*serializer.validated_data) + else: + raise MethodNotAllowed('POST') + result_serializer = self._instantiate_serializer(related_instance_or_manager) + return Response(result_serializer.data) def delete(self, request, *args, **kwargs): - return Response() + related_instance_or_manager = self.get_related_instance() + + if isinstance(related_instance_or_manager, BaseManager): + related_model_class = related_instance_or_manager.model + serializer = self.get_serializer(data=request.data, model_class=related_model_class, many=True) + serializer.is_valid(raise_exception=True) + if frozenset(serializer.validated_data).isdisjoint(frozenset(related_instance_or_manager.all())): + return Response(status=204) + try: + related_instance_or_manager.remove(*serializer.validated_data) + except AttributeError: + raise Conflict( + 'This object cannot be removed from this relationship without being added to another' + ) + else: + raise MethodNotAllowed('DELETE') + result_serializer = self._instantiate_serializer(related_instance_or_manager) + return Response(result_serializer.data) def get_related_instance(self): try: From 854a8e0ab0eb45ed9399389f612f5a519e62b59f Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Mon, 21 Sep 2015 17:25:06 -0400 Subject: [PATCH 17/26] More tests for RelationshipView --- example/models.py | 6 +- example/tests/test_views.py | 126 ++++++++++++++++++++++++++++++++---- example/urls_test.py | 9 ++- example/views.py | 18 ++++-- 4 files changed, 139 insertions(+), 20 deletions(-) diff --git a/example/models.py b/example/models.py index 0c545c43..a86cc8b1 100644 --- a/example/models.py +++ b/example/models.py @@ -54,7 +54,11 @@ def __str__(self): class Comment(BaseModel): entry = models.ForeignKey(Entry) body = models.TextField() - author = models.ForeignKey(Author) + author = models.ForeignKey( + Author, + null=True, + blank=True + ) def __str__(self): return self.body diff --git a/example/tests/test_views.py b/example/tests/test_views.py index 22bb14ec..150c11d5 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -1,38 +1,55 @@ +import json + from django.utils import timezone from rest_framework.reverse import reverse -from rest_framework.test import APITestCase, APIRequestFactory -import json -from rest_framework_json_api.utils import format_relation_name +from rest_framework.test import APITestCase -from example.models import Blog, Entry -from example.views import EntryRelationshipView, BlogRelationshipView +from rest_framework_json_api.utils import format_relation_name +from example.models import Blog, Entry, Comment, Author class TestRelationshipView(APITestCase): def setUp(self): + self.author = Author.objects.create(name='Super powerful superhero', email='i.am@lost.com') self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog") self.other_blog = Blog.objects.create(name='Other blog', tagline="It's another blog") - self.entry = Entry.objects.create( + self.first_entry = Entry.objects.create( blog=self.blog, - headline='headline', - body_text='body_text', + headline='headline one', + body_text='body_text two', pub_date=timezone.now(), mod_date=timezone.now(), n_comments=0, n_pingbacks=0, rating=3 ) + self.second_entry = Entry.objects.create( + blog=self.blog, + headline='headline two', + body_text='body_text one', + pub_date=timezone.now(), + mod_date=timezone.now(), + n_comments=0, + n_pingbacks=0, + rating=1 + ) + self.first_comment = Comment.objects.create(entry=self.first_entry, body="This entry is cool", author=None) + self.second_comment = Comment.objects.create( + entry=self.second_entry, + body="This entry is not cool", + author=self.author + ) def test_get_entry_relationship_blog(self): - url = reverse('entry-relationships', kwargs={'pk': self.entry.id, 'related_field': 'blog'}) + url = reverse('entry-relationships', kwargs={'pk': self.first_entry.id, 'related_field': 'blog'}) response = self.client.get(url) - expected_data = {'type': format_relation_name('Blog'), 'id': str(self.entry.blog.id)} + expected_data = {'type': format_relation_name('Blog'), 'id': str(self.first_entry.blog.id)} assert response.data == expected_data def test_get_entry_relationship_invalid_field(self): - response = self.client.get('/entries/{}/relationships/invalid_field'.format(self.entry.id)) + response = self.client.get('/entries/{}/relationships/invalid_field'.format(self.first_entry.id)) assert response.status_code == 404 @@ -40,12 +57,26 @@ def test_get_blog_relationship_entry_set(self): response = self.client.get('/blogs/{}/relationships/entry_set'.format(self.blog.id)) def test_put_entry_relationship_blog_returns_405(self): - url = '/entries/{}/relationships/blog'.format(self.entry.id) + url = '/entries/{}/relationships/blog'.format(self.first_entry.id) response = self.client.put(url, data={}) assert response.status_code == 405 + def test_patch_invalid_entry_relationship_blog_returns_400(self): + url = '/entries/{}/relationships/blog'.format(self.first_entry.id) + response = self.client.patch(url, + data=json.dumps({'data': {'invalid': ''}}), + content_type='application/vnd.api+json') + assert response.status_code == 400 + + def test_get_empty_to_one_relationship(self): + url = '/comments/{}/relationships/author'.format(self.first_entry.id) + response = self.client.get(url) + expected_data = None + + assert response.data == expected_data + def test_patch_to_one_relationship(self): - url = '/entries/{}/relationships/blog'.format(self.entry.id) + url = '/entries/{}/relationships/blog'.format(self.first_entry.id) request_data = { 'data': {'type': format_relation_name('Blog'), 'id': str(self.other_blog.id)} } @@ -54,3 +85,72 @@ def test_patch_to_one_relationship(self): response = self.client.get(url) assert response.data == request_data['data'] + + def test_patch_to_many_relationship(self): + url = '/blogs/{}/relationships/entry_set'.format(self.first_entry.id) + request_data = { + 'data': [{'type': format_relation_name('Entry'), 'id': str(self.first_entry.id)}, ] + } + response = self.client.patch(url, data=json.dumps(request_data), content_type='application/vnd.api+json') + assert response.status_code == 200, response.content.decode() + + response = self.client.get(url) + assert response.data == request_data['data'] + + def test_post_to_one_relationship_should_fail(self): + url = '/entries/{}/relationships/blog'.format(self.first_entry.id) + request_data = { + 'data': {'type': format_relation_name('Blog'), 'id': str(self.other_blog.id)} + } + response = self.client.post(url, data=json.dumps(request_data), content_type='application/vnd.api+json') + assert response.status_code == 405, response.content.decode() + + def test_post_to_many_relationship_with_no_change(self): + url = '/entries/{}/relationships/comment_set'.format(self.first_entry.id) + request_data = { + 'data': [{'type': format_relation_name('Comment'), 'id': str(self.first_comment.id)}, ] + } + response = self.client.post(url, data=json.dumps(request_data), content_type='application/vnd.api+json') + assert response.status_code == 204, response.content.decode() + + def test_post_to_many_relationship_with_change(self): + url = '/entries/{}/relationships/comment_set'.format(self.first_entry.id) + request_data = { + 'data': [{'type': format_relation_name('Comment'), 'id': str(self.second_comment.id)}, ] + } + response = self.client.post(url, data=json.dumps(request_data), content_type='application/vnd.api+json') + assert response.status_code == 200, response.content.decode() + + assert request_data['data'][0] in response.data + + def test_delete_to_one_relationship_should_fail(self): + url = '/entries/{}/relationships/blog'.format(self.first_entry.id) + request_data = { + 'data': {'type': format_relation_name('Blog'), 'id': str(self.other_blog.id)} + } + response = self.client.delete(url, data=json.dumps(request_data), content_type='application/vnd.api+json') + assert response.status_code == 405, response.content.decode() + + def test_delete_to_many_relationship_with_no_change(self): + url = '/entries/{}/relationships/comment_set'.format(self.first_entry.id) + request_data = { + 'data': [{'type': format_relation_name('Comment'), 'id': str(self.second_comment.id)}, ] + } + response = self.client.delete(url, data=json.dumps(request_data), content_type='application/vnd.api+json') + assert response.status_code == 204, response.content.decode() + + def test_delete_one_to_many_relationship_with_not_null_constraint(self): + url = '/entries/{}/relationships/comment_set'.format(self.first_entry.id) + request_data = { + 'data': [{'type': format_relation_name('Comment'), 'id': str(self.first_comment.id)}, ] + } + response = self.client.delete(url, data=json.dumps(request_data), content_type='application/vnd.api+json') + assert response.status_code == 409, response.content.decode() + + def test_delete_to_many_relationship_with_change(self): + url = '/authors/{}/relationships/comment_set'.format(self.author.id) + request_data = { + 'data': [{'type': format_relation_name('Comment'), 'id': str(self.second_comment.id)}, ] + } + response = self.client.delete(url, data=json.dumps(request_data), content_type='application/vnd.api+json') + assert response.status_code == 200, response.content.decode() diff --git a/example/urls_test.py b/example/urls_test.py index bce686f4..96f415fd 100644 --- a/example/urls_test.py +++ b/example/urls_test.py @@ -1,7 +1,8 @@ from django.conf.urls import include, url from rest_framework import routers -from example.views import BlogViewSet, EntryViewSet, AuthorViewSet, EntryRelationshipView, BlogRelationshipView +from example.views import BlogViewSet, EntryViewSet, AuthorViewSet, EntryRelationshipView, BlogRelationshipView, \ + CommentRelationshipView, AuthorRelationshipView from .api.resources.identity import Identity, GenericIdentity router = routers.DefaultRouter(trailing_slash=False) @@ -27,5 +28,11 @@ url(r'^blogs/(?P[^/.]+)/relationships/(?P\w+)', BlogRelationshipView.as_view(), name='blog-relationships'), + url(r'^comments/(?P[^/.]+)/relationships/(?P\w+)', + CommentRelationshipView.as_view(), + name='comment-relationships'), + url(r'^authors/(?P[^/.]+)/relationships/(?P\w+)', + AuthorRelationshipView.as_view(), + name='author-relationships'), ] diff --git a/example/views.py b/example/views.py index fb66a601..3e62599d 100644 --- a/example/views.py +++ b/example/views.py @@ -1,29 +1,37 @@ from rest_framework import viewsets + from rest_framework_json_api.views import RelationshipView -from example.models import Blog, Entry, Author +from example.models import Blog, Entry, Author, Comment from example.serializers import BlogSerializer, EntrySerializer, AuthorSerializer class BlogViewSet(viewsets.ModelViewSet): - queryset = Blog.objects.all() serializer_class = BlogSerializer -class EntryViewSet(viewsets.ModelViewSet): +class EntryViewSet(viewsets.ModelViewSet): queryset = Entry.objects.all() serializer_class = EntrySerializer resource_name = 'posts' -class AuthorViewSet(viewsets.ModelViewSet): +class AuthorViewSet(viewsets.ModelViewSet): queryset = Author.objects.all() serializer_class = AuthorSerializer - class EntryRelationshipView(RelationshipView): queryset = Entry.objects + class BlogRelationshipView(RelationshipView): queryset = Blog.objects + + +class CommentRelationshipView(RelationshipView): + queryset = Comment.objects + + +class AuthorRelationshipView(RelationshipView): + queryset = Author.objects From d3e4243ea57c05378aa8381a5deb0ee5a0c38eaf Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Tue, 22 Sep 2015 09:37:11 -0400 Subject: [PATCH 18/26] Updated error message --- rest_framework_json_api/parsers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index 575fabcb..0afc3551 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -41,7 +41,9 @@ def parse(self, stream, media_type=None, parser_context=None): 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 a malformed JSONAPI Resource Identifier Object') + 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') From 0d6230f748be09a5216df26ff68e06c6336fa685 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Tue, 22 Sep 2015 09:38:46 -0400 Subject: [PATCH 19/26] WIP support for links --- rest_framework_json_api/views.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 441a95ee..00daddd0 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -6,11 +6,29 @@ 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 +from rest_framework_json_api.utils import format_relation_name, get_resource_type_from_instance, OrderedDict class RelationshipView(generics.GenericAPIView): serializer_class = ResourceIdentifierObjectSerializer + self_view_name = None + related_view_name = None + + def get_self_link(self): + return 'self_link' + + def get_related_link(self): + return 'related_link' + + def get_links(self): + return_data = OrderedDict() + self_link = self.get_self_link() + related_link = self.get_related_link() + if self_link: + return_data.update({'self': self_link}) + if related_link: + return_data.update({'related': related_link}) + return return_data def get(self, request, *args, **kwargs): related_instance = self.get_related_instance() From 502cc431c02220f5d2a7f065fcbcb675d2f8c996 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Tue, 22 Sep 2015 09:38:59 -0400 Subject: [PATCH 20/26] Handle RelationshipViews --- rest_framework_json_api/renderers.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index 579a9cdc..acf45a91 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -32,12 +32,25 @@ class JSONRenderer(renderers.JSONRenderer): format = 'vnd.api+json' def render(self, data, accepted_media_type=None, renderer_context=None): - # Get the resource name. - resource_name = utils.get_resource_name(renderer_context) view = renderer_context.get("view", None) request = renderer_context.get("request", None) + from rest_framework_json_api.views import RelationshipView + if isinstance(view, RelationshipView): + # Special case for RelationshipView + links = view.get_links() + render_data = OrderedDict([ + ('data', data), + (('links', links) if links else None), + ]) + return super(JSONRenderer, self).render( + render_data, accepted_media_type, renderer_context + ) + + # Get the resource name. + resource_name = utils.get_resource_name(renderer_context) + # If `resource_name` is set to None then render default as the dev # wants to build the output format manually. if resource_name is None or resource_name is False: @@ -55,11 +68,7 @@ def render(self, data, accepted_media_type=None, renderer_context=None): json_api_included = list() - from rest_framework_json_api.views import RelationshipView - if isinstance(view, RelationshipView): - # Special case for RelationshipView - json_api_data = data - elif view and hasattr(view, 'action') and view.action == 'list' and \ + if view and hasattr(view, 'action') and view.action == 'list' and \ isinstance(data, dict) and 'results' in data: # If detail view then json api spec expects dict, otherwise a list # - http://jsonapi.org/format/#document-top-level From ea65003ae29353d52505a8fe0cfa0455fe26df23 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Tue, 22 Sep 2015 10:58:23 -0400 Subject: [PATCH 21/26] Fixed missed empty test --- example/tests/test_views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/example/tests/test_views.py b/example/tests/test_views.py index 150c11d5..12e4de8a 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -55,6 +55,10 @@ def test_get_entry_relationship_invalid_field(self): def test_get_blog_relationship_entry_set(self): response = self.client.get('/blogs/{}/relationships/entry_set'.format(self.blog.id)) + expected_data = [{'type': format_relation_name('Entry'), 'id': str(self.first_entry.id)}, + {'type': format_relation_name('Entry'), 'id': str(self.second_entry.id)}] + + assert response.data == expected_data def test_put_entry_relationship_blog_returns_405(self): url = '/entries/{}/relationships/blog'.format(self.first_entry.id) From 2797b9df4f8c0ae092c2769793f77c8dc58bc4b2 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Tue, 22 Sep 2015 11:27:20 -0400 Subject: [PATCH 22/26] Now supporting links for RelationshipView --- example/tests/test_views.py | 10 ++++++ example/views.py | 1 + rest_framework_json_api/renderers.py | 7 ++-- rest_framework_json_api/views.py | 53 +++++++++++++++++++++++----- 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/example/tests/test_views.py b/example/tests/test_views.py index 12e4de8a..3f14c031 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -79,6 +79,16 @@ def test_get_empty_to_one_relationship(self): assert response.data == expected_data + def test_get_to_many_relationship_self_link(self): + url = '/authors/{}/relationships/comment_set'.format(self.author.id) + + response = self.client.get(url) + expected_data = { + 'links': {'self': 'http://testserver/authors/1/relationships/comment_set'}, + 'data': [{'id': str(self.second_comment.id), 'type': format_relation_name('Comment')}] + } + assert json.loads(response.content.decode('utf-8')) == expected_data + def test_patch_to_one_relationship(self): url = '/entries/{}/relationships/blog'.format(self.first_entry.id) request_data = { diff --git a/example/views.py b/example/views.py index 3e62599d..28e2b245 100644 --- a/example/views.py +++ b/example/views.py @@ -35,3 +35,4 @@ class CommentRelationshipView(RelationshipView): class AuthorRelationshipView(RelationshipView): queryset = Author.objects + self_link_view_name = 'author-relationships' diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index acf45a91..3cea2bb8 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -39,11 +39,12 @@ def render(self, data, accepted_media_type=None, renderer_context=None): from rest_framework_json_api.views import RelationshipView if isinstance(view, RelationshipView): # Special case for RelationshipView - links = view.get_links() render_data = OrderedDict([ - ('data', data), - (('links', links) if links else None), + ('data', data) ]) + links = view.get_links() + if links: + render_data.update({'links': links}), return super(JSONRenderer, self).render( render_data, accepted_media_type, renderer_context ) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 00daddd0..6e14319c 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -1,8 +1,12 @@ +from django.core.exceptions import ImproperlyConfigured +from django.core.urlresolvers import NoReverseMatch from django.db.models import Model, QuerySet from django.db.models.manager import BaseManager from rest_framework import generics +from rest_framework.relations import Hyperlink from rest_framework.response import Response from rest_framework.exceptions import NotFound, MethodNotAllowed +from rest_framework.reverse import reverse from rest_framework_json_api.exceptions import Conflict from rest_framework_json_api.serializers import ResourceIdentifierObjectSerializer @@ -11,19 +15,52 @@ class RelationshipView(generics.GenericAPIView): serializer_class = ResourceIdentifierObjectSerializer - self_view_name = None - related_view_name = None + self_link_view_name = None + related_link_view_name = None - def get_self_link(self): - return 'self_link' + def __init__(self, **kwargs): + super(RelationshipView, self).__init__(**kwargs) + # We include this simply for dependency injection in tests. + # We can't add it as a class attributes or it would expect an + # implicit `self` argument to be passed. + self.reverse = reverse - def get_related_link(self): - return 'related_link' + + + def get_url(self, name, view_name, kwargs, request): + """ + Given an object, return the URL that hyperlinks to the object. + + May raise a `NoReverseMatch` if the `view_name` and `lookup_field` + attributes are not configured to correctly match the URL conf. + """ + + # Return None if the view name is not supplied + if not view_name: + return None + + # Return the hyperlink, or error if incorrectly configured. + try: + url = self.reverse(view_name, kwargs=kwargs, request=request) + except NoReverseMatch: + msg = ( + 'Could not resolve URL for hyperlinked relationship using ' + 'view name "%s". You may have failed to include the related ' + 'model in your API, or incorrectly configured the ' + '`lookup_field` attribute on this field.' + ) + raise ImproperlyConfigured(msg % view_name) + + if url is None: + return None + + return Hyperlink(url, name) def get_links(self): return_data = OrderedDict() - self_link = self.get_self_link() - related_link = self.get_related_link() + self_link = self.get_url('self', self.self_link_view_name, self.kwargs, self.request) + related_kwargs = {self.lookup_field: self.kwargs.get(self.lookup_field)} + related_link = self.get_url('related', self.related_link_view_name, related_kwargs, self.request) if self_link: return_data.update({'self': self_link}) if related_link: From 5f0c2b4d5ba8e038c4c71973b7f933a36ca70d8c Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Tue, 22 Sep 2015 11:35:31 -0400 Subject: [PATCH 23/26] Compat fix for Django 1.6 --- rest_framework_json_api/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 6e14319c..78b72438 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -1,6 +1,7 @@ from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import NoReverseMatch -from django.db.models import Model, QuerySet +from django.db.models import Model +from django.db.models.query import QuerySet from django.db.models.manager import BaseManager from rest_framework import generics from rest_framework.relations import Hyperlink From 212c44a67fc2ca50ac8362986a66754a67a5937c Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Tue, 22 Sep 2015 11:45:56 -0400 Subject: [PATCH 24/26] Compat fix for Django 1.6 --- rest_framework_json_api/views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 78b72438..bdc56630 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -2,7 +2,7 @@ from django.core.urlresolvers import NoReverseMatch from django.db.models import Model from django.db.models.query import QuerySet -from django.db.models.manager import BaseManager +from django.db.models.manager import Manager from rest_framework import generics from rest_framework.relations import Hyperlink from rest_framework.response import Response @@ -77,7 +77,7 @@ def patch(self, request, *args, **kwargs): parent_obj = self.get_object() related_instance_or_manager = self.get_related_instance() - if isinstance(related_instance_or_manager, BaseManager): + if isinstance(related_instance_or_manager, Manager): related_model_class = related_instance_or_manager.model serializer = self.get_serializer(data=request.data, model_class=related_model_class, many=True) serializer.is_valid(raise_exception=True) @@ -95,7 +95,7 @@ def patch(self, request, *args, **kwargs): def post(self, request, *args, **kwargs): related_instance_or_manager = self.get_related_instance() - if isinstance(related_instance_or_manager, BaseManager): + if isinstance(related_instance_or_manager, Manager): related_model_class = related_instance_or_manager.model serializer = self.get_serializer(data=request.data, model_class=related_model_class, many=True) serializer.is_valid(raise_exception=True) @@ -110,7 +110,7 @@ def post(self, request, *args, **kwargs): def delete(self, request, *args, **kwargs): related_instance_or_manager = self.get_related_instance() - if isinstance(related_instance_or_manager, BaseManager): + if isinstance(related_instance_or_manager, Manager): related_model_class = related_instance_or_manager.model serializer = self.get_serializer(data=request.data, model_class=related_model_class, many=True) serializer.is_valid(raise_exception=True) @@ -137,7 +137,7 @@ def _instantiate_serializer(self, instance): if isinstance(instance, Model) or instance is None: return self.get_serializer(instance=instance) else: - if isinstance(instance, (QuerySet, BaseManager)): + if isinstance(instance, (QuerySet, Manager)): instance = instance.all() return self.get_serializer(instance=instance, many=True) From 6c5413c0e66c0f606fc60b6ff334440b018631e1 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Tue, 22 Sep 2015 11:53:17 -0400 Subject: [PATCH 25/26] Compat fix for DRF 3.1 --- rest_framework_json_api/views.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index bdc56630..94389d22 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -4,10 +4,10 @@ from django.db.models.query import QuerySet from django.db.models.manager import Manager from rest_framework import generics -from rest_framework.relations import Hyperlink from rest_framework.response import Response from rest_framework.exceptions import NotFound, MethodNotAllowed from rest_framework.reverse import reverse +import six from rest_framework_json_api.exceptions import Conflict from rest_framework_json_api.serializers import ResourceIdentifierObjectSerializer @@ -26,8 +26,6 @@ def __init__(self, **kwargs): # implicit `self` argument to be passed. self.reverse = reverse - - def get_url(self, name, view_name, kwargs, request): """ Given an object, return the URL that hyperlinks to the object. @@ -36,6 +34,23 @@ def get_url(self, name, view_name, kwargs, request): attributes are not configured to correctly match the URL conf. """ + class Hyperlink(six.text_type): + """ + A string like object that additionally has an associated name. + We use this for hyperlinked URLs that may render as a named link + in some contexts, or render as a plain URL in others. + + Comes from Django REST framework 3.2 + https://github.com/tomchristie/django-rest-framework + """ + + def __new__(self, url, name): + ret = six.text_type.__new__(self, url) + ret.name = name + return ret + + is_hyperlink = True + # Return None if the view name is not supplied if not view_name: return None From bc63e9fcc6bc880d0d157565b93ea2b0cf1d3030 Mon Sep 17 00:00:00 2001 From: Jonathan Senecal Date: Tue, 22 Sep 2015 16:25:07 -0400 Subject: [PATCH 26/26] Use six from Django instead --- rest_framework_json_api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 94389d22..9fc2e67a 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -3,11 +3,11 @@ from django.db.models import Model from django.db.models.query import QuerySet from django.db.models.manager import Manager +from django.utils import six from rest_framework import generics from rest_framework.response import Response from rest_framework.exceptions import NotFound, MethodNotAllowed from rest_framework.reverse import reverse -import six from rest_framework_json_api.exceptions import Conflict from rest_framework_json_api.serializers import ResourceIdentifierObjectSerializer