diff --git a/CHANGELOG.md b/CHANGELOG.md index bed4287d..4e4bc23c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ any parts of the framework not mentioned in the documentation should generally b * Do not skip empty one-to-one relationships * Allow `HyperlinkRelatedField` to be used with [related urls](https://django-rest-framework-json-api.readthedocs.io/en/stable/usage.html?highlight=related%20links#related-urls) * Fixed hardcoded year 2018 in tests ([#539](https://github.com/django-json-api/django-rest-framework-json-api/issues/539)) +* Avoid exception in `AutoPrefetchMixin` when including a reverse one to one relation ([#537](https://github.com/django-json-api/django-rest-framework-json-api/issues/537)) ## [2.6.0] - 2018-09-20 diff --git a/example/factories.py b/example/factories.py index dbdd9838..9a8f4966 100644 --- a/example/factories.py +++ b/example/factories.py @@ -14,8 +14,8 @@ Entry, ProjectType, ResearchProject, - TaggedItem -) + TaggedItem, + AuthorBioMetadata) faker = FakerFactory.create() faker.seed(983843) @@ -53,6 +53,16 @@ class Meta: author = factory.SubFactory(AuthorFactory) body = factory.LazyAttribute(lambda x: faker.text()) + metadata = factory.RelatedFactory('example.factories.AuthorBioMetadataFactory', 'bio') + + +class AuthorBioMetadataFactory(factory.django.DjangoModelFactory): + class Meta: + model = AuthorBioMetadata + + bio = factory.SubFactory(AuthorBioFactory) + body = factory.LazyAttribute(lambda x: faker.text()) + class EntryFactory(factory.django.DjangoModelFactory): class Meta: diff --git a/example/migrations/0006_auto_20181228_0752.py b/example/migrations/0006_auto_20181228_0752.py new file mode 100644 index 00000000..2cfb0c29 --- /dev/null +++ b/example/migrations/0006_auto_20181228_0752.py @@ -0,0 +1,32 @@ +# Generated by Django 2.1.4 on 2018-12-28 07:52 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('example', '0005_auto_20180922_1508'), + ] + + operations = [ + migrations.CreateModel( + name='AuthorBioMetadata', + 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)), + ('body', models.TextField()), + ('bio', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='metadata', to='example.AuthorBio')), + ], + options={ + 'ordering': ('id',), + }, + ), + migrations.AlterField( + model_name='comment', + name='author', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='example.Author'), + ), + ] diff --git a/example/models.py b/example/models.py index e280c827..179cbfe5 100644 --- a/example/models.py +++ b/example/models.py @@ -81,6 +81,21 @@ class Meta: ordering = ('id',) +@python_2_unicode_compatible +class AuthorBioMetadata(BaseModel): + """ + Just a class to have a relation with author bio + """ + bio = models.OneToOneField(AuthorBio, related_name='metadata', on_delete=models.CASCADE) + body = models.TextField() + + def __str__(self): + return self.bio.author.name + + class Meta: + ordering = ('id',) + + @python_2_unicode_compatible class Entry(BaseModel): blog = models.ForeignKey(Blog, on_delete=models.CASCADE) diff --git a/example/serializers.py b/example/serializers.py index 70699987..6661dcac 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -16,8 +16,8 @@ Project, ProjectType, ResearchProject, - TaggedItem -) + TaggedItem, + AuthorBioMetadata) class TaggedItemSerializer(serializers.ModelSerializer): @@ -197,7 +197,17 @@ class Meta: class AuthorBioSerializer(serializers.ModelSerializer): class Meta: model = AuthorBio - fields = ('author', 'body') + fields = ('author', 'body', 'metadata') + + included_serializers = { + 'metadata': 'example.serializers.AuthorBioMetadataSerializer', + } + + +class AuthorBioMetadataSerializer(serializers.ModelSerializer): + class Meta: + model = AuthorBioMetadata + fields = ('body',) class AuthorSerializer(serializers.ModelSerializer): diff --git a/example/tests/conftest.py b/example/tests/conftest.py index ceaa5dcf..f2362bfd 100644 --- a/example/tests/conftest.py +++ b/example/tests/conftest.py @@ -6,6 +6,7 @@ ArtProjectFactory, AuthorBioFactory, AuthorFactory, + AuthorBioMetadataFactory, AuthorTypeFactory, BlogFactory, CommentFactory, @@ -18,6 +19,7 @@ register(BlogFactory) register(AuthorFactory) register(AuthorBioFactory) +register(AuthorBioMetadataFactory) register(AuthorTypeFactory) register(EntryFactory) register(CommentFactory) diff --git a/example/tests/integration/test_includes.py b/example/tests/integration/test_includes.py index 694e20d1..78c12539 100644 --- a/example/tests/integration/test_includes.py +++ b/example/tests/integration/test_includes.py @@ -20,6 +20,25 @@ def test_included_data_on_list(multiple_entries, client): assert comment_count == expected_comment_count, 'List comment count is incorrect' +def test_included_data_on_list_with_one_to_one_relations(multiple_entries, client): + response = client.get(reverse("entry-list"), + data={'include': 'authors.bio.metadata', 'page[size]': 5}) + included = response.json().get('included') + + assert len(response.json()['data']) == len(multiple_entries), ( + 'Incorrect entry count' + ) + expected_include_types = [ + 'authorBioMetadata', 'authorBioMetadata', + 'authorBios', 'authorBios', + 'authors', 'authors' + ] + include_types = [x.get('type') for x in included] + assert include_types == expected_include_types, ( + 'List included types are incorrect' + ) + + def test_default_included_data_on_detail(single_entry, client): return test_included_data_on_detail(single_entry=single_entry, client=client, query='') diff --git a/example/tests/test_views.py b/example/tests/test_views.py index d4a3d062..95fc92a3 100644 --- a/example/tests/test_views.py +++ b/example/tests/test_views.py @@ -1,6 +1,6 @@ import json - from datetime import datetime + from django.test import RequestFactory from django.utils import timezone from rest_framework.exceptions import NotFound @@ -337,7 +337,10 @@ def test_retrieve_related_single_reverse_lookup(self): 'data': { 'type': 'authorBios', 'id': str(self.author.bio.id), 'relationships': { - 'author': {'data': {'type': 'authors', 'id': str(self.author.id)}}}, + 'author': {'data': {'type': 'authors', 'id': str(self.author.id)}}, + 'metadata': {'data': {'id': str(self.author.bio.metadata.id), + 'type': 'authorBioMetadata'}} + }, 'attributes': { 'body': str(self.author.bio.body) }, diff --git a/rest_framework_json_api/views.py b/rest_framework_json_api/views.py index 46f3a2bc..b692844b 100644 --- a/rest_framework_json_api/views.py +++ b/rest_framework_json_api/views.py @@ -92,7 +92,11 @@ def get_queryset(self, *args, **kwargs): if level == levels[-1]: included_model = field else: - model_field = field.field + + if issubclass(field_class, ReverseOneToOneDescriptor): + model_field = field.related.field + else: + model_field = field.field if is_forward_relation: level_model = model_field.related_model