diff --git a/example/factories.py b/example/factories.py index 012fb85e..563ba30b 100644 --- a/example/factories.py +++ b/example/factories.py @@ -7,6 +7,7 @@ ArtProject, Author, AuthorBio, + AuthorType, Blog, Comment, Company, @@ -26,6 +27,13 @@ class Meta: name = factory.LazyAttribute(lambda x: faker.name()) +class AuthorTypeFactory(factory.django.DjangoModelFactory): + class Meta: + model = AuthorType + + name = factory.LazyAttribute(lambda x: faker.name()) + + class AuthorFactory(factory.django.DjangoModelFactory): class Meta: model = Author @@ -34,6 +42,7 @@ class Meta: email = factory.LazyAttribute(lambda x: faker.email()) bio = factory.RelatedFactory('example.factories.AuthorBioFactory', 'author') + type = factory.SubFactory(AuthorTypeFactory) class AuthorBioFactory(factory.django.DjangoModelFactory): diff --git a/example/migrations/0004_auto_20171011_0631.py b/example/migrations/0004_auto_20171011_0631.py new file mode 100644 index 00000000..96df2aa7 --- /dev/null +++ b/example/migrations/0004_auto_20171011_0631.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.6 on 2017-10-11 06:31 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('example', '0003_polymorphics'), + ] + + operations = [ + migrations.CreateModel( + name='AuthorType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('modified_at', models.DateTimeField(auto_now=True)), + ('name', models.CharField(max_length=50)), + ], + options={ + 'ordering': ('id',), + }, + ), + migrations.AlterModelOptions( + name='author', + options={'ordering': ('id',)}, + ), + migrations.AlterModelOptions( + name='authorbio', + options={'ordering': ('id',)}, + ), + migrations.AlterModelOptions( + name='blog', + options={'ordering': ('id',)}, + ), + migrations.AlterModelOptions( + name='comment', + options={'ordering': ('id',)}, + ), + migrations.AlterModelOptions( + name='entry', + options={'ordering': ('id',)}, + ), + migrations.AlterModelOptions( + name='taggeditem', + options={'ordering': ('id',)}, + ), + migrations.AlterField( + model_name='entry', + name='authors', + field=models.ManyToManyField(related_name='entries', to='example.Author'), + ), + migrations.AddField( + model_name='author', + name='type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='example.AuthorType'), + ), + ] diff --git a/example/models.py b/example/models.py index 5cc70b47..5395607f 100644 --- a/example/models.py +++ b/example/models.py @@ -45,10 +45,22 @@ class Meta: ordering = ('id',) +@python_2_unicode_compatible +class AuthorType(BaseModel): + name = models.CharField(max_length=50) + + def __str__(self): + return self.name + + class Meta: + ordering = ('id',) + + @python_2_unicode_compatible class Author(BaseModel): name = models.CharField(max_length=50) email = models.EmailField() + type = models.ForeignKey(AuthorType, null=True) def __str__(self): return self.name diff --git a/example/serializers.py b/example/serializers.py index accd345c..da491e7f 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -9,6 +9,7 @@ ArtProject, Author, AuthorBio, + AuthorType, Blog, Comment, Company, @@ -101,6 +102,12 @@ class JSONAPIMeta: included_resources = ['comments'] +class AuthorTypeSerializer(serializers.ModelSerializer): + class Meta: + model = AuthorType + fields = ('name', ) + + class AuthorBioSerializer(serializers.ModelSerializer): class Meta: model = AuthorBio @@ -109,12 +116,13 @@ class Meta: class AuthorSerializer(serializers.ModelSerializer): included_serializers = { - 'bio': AuthorBioSerializer + 'bio': AuthorBioSerializer, + 'type': AuthorTypeSerializer } class Meta: model = Author - fields = ('name', 'email', 'bio', 'entries') + fields = ('name', 'email', 'bio', 'entries', 'type') class WriterSerializer(serializers.ModelSerializer): diff --git a/example/tests/conftest.py b/example/tests/conftest.py index 47879630..84de7732 100644 --- a/example/tests/conftest.py +++ b/example/tests/conftest.py @@ -5,6 +5,7 @@ ArtProjectFactory, AuthorBioFactory, AuthorFactory, + AuthorTypeFactory, BlogFactory, CommentFactory, CompanyFactory, @@ -16,6 +17,7 @@ register(BlogFactory) register(AuthorFactory) register(AuthorBioFactory) +register(AuthorTypeFactory) register(EntryFactory) register(CommentFactory) register(TaggedItemFactory) diff --git a/example/tests/test_model_viewsets.py b/example/tests/test_model_viewsets.py index a3835146..36949fe1 100644 --- a/example/tests/test_model_viewsets.py +++ b/example/tests/test_model_viewsets.py @@ -1,3 +1,4 @@ +import pytest from django.conf import settings from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse @@ -226,3 +227,31 @@ def test_key_in_post(self): self.assertEqual( get_user_model().objects.get(pk=self.miles.pk).email, 'miles@trumpet.org') + + +@pytest.mark.django_db +def test_patch_allow_field_type(author, author_type_factory, client): + """ + Verify that type field may be updated. + """ + author_type = author_type_factory() + url = reverse('author-detail', args=[author.id]) + + data = { + 'data': { + 'id': author.id, + 'type': 'authors', + 'relationships': { + 'data': { + 'id': author_type.id, + 'type': 'author-type' + } + } + } + } + + response = client.patch(url, + content_type='application/vnd.api+json', + data=dump_json(data)) + + assert response.status_code == 200 diff --git a/example/tests/test_performance.py b/example/tests/test_performance.py index 3ec2f676..d9ae1db9 100644 --- a/example/tests/test_performance.py +++ b/example/tests/test_performance.py @@ -50,8 +50,9 @@ def test_query_count_include_author(self): 1. Primary resource COUNT query 2. Primary resource SELECT 3. Authors prefetched - 3. Entries prefetched + 4. Author types prefetched + 5. Entries prefetched """ - with self.assertNumQueries(4): + with self.assertNumQueries(5): response = self.client.get('/comments?include=author&page_size=25') self.assertEqual(len(response.data['results']), 25) diff --git a/example/views.py b/example/views.py index 0e1f8bb4..a08bbf61 100644 --- a/example/views.py +++ b/example/views.py @@ -78,7 +78,7 @@ class CommentViewSet(ModelViewSet): serializer_class = CommentSerializer prefetch_for_includes = { '__all__': [], - 'author': ['author', 'author__bio', 'author__entries'], + 'author': ['author', 'author__bio', 'author__entries', 'author__type'], 'entry': ['author', 'author__bio', 'author__entries'] } diff --git a/rest_framework_json_api/parsers.py b/rest_framework_json_api/parsers.py index 753c381b..86a61e74 100644 --- a/rest_framework_json_api/parsers.py +++ b/rest_framework_json_api/parsers.py @@ -6,7 +6,7 @@ from rest_framework import parsers from rest_framework.exceptions import ParseError -from . import exceptions, renderers, utils +from . import exceptions, renderers, serializers, utils class JSONParser(parsers.JSONParser): @@ -83,9 +83,10 @@ def parse(self, stream, media_type=None, parser_context=None): raise ParseError('Received document does not contain primary data') data = result.get('data') + view = parser_context['view'] from rest_framework_json_api.views import RelationshipView - if isinstance(parser_context['view'], RelationshipView): + if isinstance(view, RelationshipView): # We skip parsing the object as JSONAPI Resource Identifier Object and not a regular # Resource Object if isinstance(data, list): @@ -129,8 +130,12 @@ def parse(self, stream, media_type=None, parser_context=None): raise ParseError("The resource identifier object must contain an 'id' member") # Construct the return data + serializer_class = getattr(view, 'serializer_class', None) parsed_data = {'id': data.get('id')} if 'id' in data else {} - parsed_data['type'] = data.get('type') + # `type` field needs to be allowed in none polymorphic serializers + if serializer_class is not None: + if issubclass(serializer_class, serializers.PolymorphicModelSerializer): + parsed_data['type'] = data.get('type') parsed_data.update(self.parse_attributes(data)) parsed_data.update(self.parse_relationships(data)) parsed_data.update(self.parse_metadata(result))