diff --git a/example/factories/__init__.py b/example/factories/__init__.py index 129ddf98..0119f925 100644 --- a/example/factories/__init__.py +++ b/example/factories/__init__.py @@ -1,5 +1,4 @@ # -*- encoding: utf-8 -*- -from __future__ import unicode_literals import factory from faker import Factory as FakerFactory @@ -22,6 +21,7 @@ class Meta: name = factory.LazyAttribute(lambda x: faker.name()) email = factory.LazyAttribute(lambda x: faker.email()) + bio = factory.RelatedFactory('example.factories.AuthorBioFactory', 'author') class AuthorBioFactory(factory.django.DjangoModelFactory): class Meta: diff --git a/example/serializers.py b/example/serializers.py index c16b7cdf..99cee740 100644 --- a/example/serializers.py +++ b/example/serializers.py @@ -32,6 +32,7 @@ def __init__(self, *args, **kwargs): super(EntrySerializer, self).__init__(*args, **kwargs) included_serializers = { + 'authors': 'example.serializers.AuthorSerializer', 'comments': 'example.serializers.CommentSerializer', 'suggested': 'example.serializers.EntrySerializer', } @@ -73,6 +74,10 @@ class Meta: class CommentSerializer(serializers.ModelSerializer): + included_serializers = { + 'entry': EntrySerializer, + 'author': AuthorSerializer + } class Meta: model = Comment diff --git a/example/tests/integration/test_includes.py b/example/tests/integration/test_includes.py index 4e8c79ce..8c4cb587 100644 --- a/example/tests/integration/test_includes.py +++ b/example/tests/integration/test_includes.py @@ -28,6 +28,7 @@ def test_included_data_on_detail(single_entry, client): expected_comment_count = single_entry.comment_set.count() assert comment_count == expected_comment_count, 'Detail comment count is incorrect' + def test_dynamic_related_data_is_included(single_entry, entry_factory, client): entry_factory() response = client.get(reverse("entry-detail", kwargs={'pk': single_entry.pk}) + '?include=suggested') @@ -39,14 +40,74 @@ def test_dynamic_related_data_is_included(single_entry, entry_factory, client): def test_missing_field_not_included(author_bio_factory, author_factory, client): # First author does not have a bio - author = author_factory() + author = author_factory(bio=None) response = client.get(reverse('author-detail', args=[author.pk])+'?include=bio') data = load_json(response.content) assert 'included' not in data # Second author does - bio = author_bio_factory() - response = client.get(reverse('author-detail', args=[bio.author.pk])+'?include=bio') + author = author_factory() + response = client.get(reverse('author-detail', args=[author.pk])+'?include=bio') data = load_json(response.content) assert 'included' in data assert len(data['included']) == 1 - assert data['included'][0]['attributes']['body'] == bio.body + assert data['included'][0]['attributes']['body'] == author.bio.body + + +def test_deep_included_data_on_list(multiple_entries, client): + response = client.get(reverse("entry-list") + '?include=comments,comments.author,' + 'comments.author.bio&page_size=5') + included = load_json(response.content).get('included') + + assert len(load_json(response.content)['data']) == len(multiple_entries), 'Incorrect entry count' + assert [x.get('type') for x in included] == [ + 'authorBios', 'authorBios', 'authors', 'authors', 'comments', 'comments' + ], 'List included types are incorrect' + + comment_count = len([resource for resource in included if resource["type"] == "comments"]) + expected_comment_count = sum([entry.comment_set.count() for entry in multiple_entries]) + assert comment_count == expected_comment_count, 'List comment count is incorrect' + + author_count = len([resource for resource in included if resource["type"] == "authors"]) + expected_author_count = sum( + [entry.comment_set.filter(author__isnull=False).count() for entry in multiple_entries]) + assert author_count == expected_author_count, 'List author count is incorrect' + + author_bio_count = len([resource for resource in included if resource["type"] == "authorBios"]) + expected_author_bio_count = sum([entry.comment_set.filter( + author__bio__isnull=False).count() for entry in multiple_entries]) + assert author_bio_count == expected_author_bio_count, 'List author bio count is incorrect' + + # Also include entry authors + response = client.get(reverse("entry-list") + '?include=authors,comments,comments.author,' + 'comments.author.bio&page_size=5') + included = load_json(response.content).get('included') + + assert len(load_json(response.content)['data']) == len(multiple_entries), 'Incorrect entry count' + assert [x.get('type') for x in included] == [ + 'authorBios', 'authorBios', 'authors', 'authors', 'authors', 'authors', + 'comments', 'comments'], 'List included types are incorrect' + + author_count = len([resource for resource in included if resource["type"] == "authors"]) + expected_author_count = sum( + [entry.authors.count() for entry in multiple_entries] + + [entry.comment_set.filter(author__isnull=False).count() for entry in multiple_entries]) + assert author_count == expected_author_count, 'List author count is incorrect' + + +def test_deep_included_data_on_detail(single_entry, client): + # Same test as in list but also ensures that intermediate resources (here comments' authors) + # are returned along with the leaf nodes + response = client.get(reverse("entry-detail", kwargs={'pk': single_entry.pk}) + + '?include=comments,comments.author.bio') + included = load_json(response.content).get('included') + + assert [x.get('type') for x in included] == ['authorBios', 'authors', 'comments'], \ + 'Detail included types are incorrect' + + comment_count = len([resource for resource in included if resource["type"] == "comments"]) + expected_comment_count = single_entry.comment_set.count() + assert comment_count == expected_comment_count, 'Detail comment count is incorrect' + + author_bio_count = len([resource for resource in included if resource["type"] == "authorBios"]) + expected_author_bio_count = single_entry.comment_set.filter(author__bio__isnull=False).count() + assert author_bio_count == expected_author_bio_count, 'Detail author bio count is incorrect' diff --git a/rest_framework_json_api/renderers.py b/rest_framework_json_api/renderers.py index a8e852cf..877ccf8e 100644 --- a/rest_framework_json_api/renderers.py +++ b/rest_framework_json_api/renderers.py @@ -248,7 +248,9 @@ def extract_included(fields, resource, resource_instance, included_resources): included_resources.remove(field_name) except ValueError: # Skip fields not in requested included resources - continue + # If no child field, directly continue with the next field + if field_name not in [node.split('.')[0] for node in included_resources]: + continue try: relation_instance_or_manager = getattr(resource_instance, field_name) diff --git a/rest_framework_json_api/serializers.py b/rest_framework_json_api/serializers.py index f68d984e..045da2de 100644 --- a/rest_framework_json_api/serializers.py +++ b/rest_framework_json_api/serializers.py @@ -84,7 +84,7 @@ def validate_path(serializer_class, field_path, path): ) ) if len(field_path) > 1: - new_included_field_path = field_path[-1:] + new_included_field_path = field_path[1:] # We go down one level in the path validate_path(this_included_serializer, new_included_field_path, path) diff --git a/tox.ini b/tox.ini index fbb33a91..9ee8fafb 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,9 @@ deps = drf33: djangorestframework>=3.3,<3.4 -r{toxinidir}/requirements-development.txt -setenv= DJANGO_SETTINGS_MODULE=example.settings.test +setenv = + PYTHONPATH = {toxinidir} + DJANGO_SETTINGS_MODULE=example.settings.test commands = py.test --basetemp={envtmpdir}