Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 3c9ab18

Browse files
committed
Merge pull request django-json-api#210 from leo-naeka/deep_inclusion
Return intermediate resources for leaf includes. Fixes django-json-api#149
2 parents 697adf1 + c6a13ad commit 3c9ab18

File tree

6 files changed

+78
-8
lines changed

6 files changed

+78
-8
lines changed

‎example/factories/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# -*- encoding: utf-8 -*-
2-
from __future__ import unicode_literals
32

43
import factory
54
from faker import Factory as FakerFactory
@@ -22,6 +21,7 @@ class Meta:
2221
name = factory.LazyAttribute(lambda x: faker.name())
2322
email = factory.LazyAttribute(lambda x: faker.email())
2423

24+
bio = factory.RelatedFactory('example.factories.AuthorBioFactory', 'author')
2525

2626
class AuthorBioFactory(factory.django.DjangoModelFactory):
2727
class Meta:

‎example/serializers.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def __init__(self, *args, **kwargs):
3232
super(EntrySerializer, self).__init__(*args, **kwargs)
3333

3434
included_serializers = {
35+
'authors': 'example.serializers.AuthorSerializer',
3536
'comments': 'example.serializers.CommentSerializer',
3637
'suggested': 'example.serializers.EntrySerializer',
3738
}
@@ -73,6 +74,10 @@ class Meta:
7374

7475

7576
class CommentSerializer(serializers.ModelSerializer):
77+
included_serializers = {
78+
'entry': EntrySerializer,
79+
'author': AuthorSerializer
80+
}
7681

7782
class Meta:
7883
model = Comment

‎example/tests/integration/test_includes.py‎

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def test_included_data_on_detail(single_entry, client):
2828
expected_comment_count = single_entry.comment_set.count()
2929
assert comment_count == expected_comment_count, 'Detail comment count is incorrect'
3030

31+
3132
def test_dynamic_related_data_is_included(single_entry, entry_factory, client):
3233
entry_factory()
3334
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):
3940

4041
def test_missing_field_not_included(author_bio_factory, author_factory, client):
4142
# First author does not have a bio
42-
author = author_factory()
43+
author = author_factory(bio=None)
4344
response = client.get(reverse('author-detail', args=[author.pk])+'?include=bio')
4445
data = load_json(response.content)
4546
assert 'included' not in data
4647
# Second author does
47-
bio = author_bio_factory()
48-
response = client.get(reverse('author-detail', args=[bio.author.pk])+'?include=bio')
48+
author = author_factory()
49+
response = client.get(reverse('author-detail', args=[author.pk])+'?include=bio')
4950
data = load_json(response.content)
5051
assert 'included' in data
5152
assert len(data['included']) == 1
52-
assert data['included'][0]['attributes']['body'] == bio.body
53+
assert data['included'][0]['attributes']['body'] == author.bio.body
54+
55+
56+
def test_deep_included_data_on_list(multiple_entries, client):
57+
response = client.get(reverse("entry-list") + '?include=comments,comments.author,'
58+
'comments.author.bio&page_size=5')
59+
included = load_json(response.content).get('included')
60+
61+
assert len(load_json(response.content)['data']) == len(multiple_entries), 'Incorrect entry count'
62+
assert [x.get('type') for x in included] == [
63+
'authorBios', 'authorBios', 'authors', 'authors', 'comments', 'comments'
64+
], 'List included types are incorrect'
65+
66+
comment_count = len([resource for resource in included if resource["type"] == "comments"])
67+
expected_comment_count = sum([entry.comment_set.count() for entry in multiple_entries])
68+
assert comment_count == expected_comment_count, 'List comment count is incorrect'
69+
70+
author_count = len([resource for resource in included if resource["type"] == "authors"])
71+
expected_author_count = sum(
72+
[entry.comment_set.filter(author__isnull=False).count() for entry in multiple_entries])
73+
assert author_count == expected_author_count, 'List author count is incorrect'
74+
75+
author_bio_count = len([resource for resource in included if resource["type"] == "authorBios"])
76+
expected_author_bio_count = sum([entry.comment_set.filter(
77+
author__bio__isnull=False).count() for entry in multiple_entries])
78+
assert author_bio_count == expected_author_bio_count, 'List author bio count is incorrect'
79+
80+
# Also include entry authors
81+
response = client.get(reverse("entry-list") + '?include=authors,comments,comments.author,'
82+
'comments.author.bio&page_size=5')
83+
included = load_json(response.content).get('included')
84+
85+
assert len(load_json(response.content)['data']) == len(multiple_entries), 'Incorrect entry count'
86+
assert [x.get('type') for x in included] == [
87+
'authorBios', 'authorBios', 'authors', 'authors', 'authors', 'authors',
88+
'comments', 'comments'], 'List included types are incorrect'
89+
90+
author_count = len([resource for resource in included if resource["type"] == "authors"])
91+
expected_author_count = sum(
92+
[entry.authors.count() for entry in multiple_entries] +
93+
[entry.comment_set.filter(author__isnull=False).count() for entry in multiple_entries])
94+
assert author_count == expected_author_count, 'List author count is incorrect'
95+
96+
97+
def test_deep_included_data_on_detail(single_entry, client):
98+
# Same test as in list but also ensures that intermediate resources (here comments' authors)
99+
# are returned along with the leaf nodes
100+
response = client.get(reverse("entry-detail", kwargs={'pk': single_entry.pk}) +
101+
'?include=comments,comments.author.bio')
102+
included = load_json(response.content).get('included')
103+
104+
assert [x.get('type') for x in included] == ['authorBios', 'authors', 'comments'], \
105+
'Detail included types are incorrect'
106+
107+
comment_count = len([resource for resource in included if resource["type"] == "comments"])
108+
expected_comment_count = single_entry.comment_set.count()
109+
assert comment_count == expected_comment_count, 'Detail comment count is incorrect'
110+
111+
author_bio_count = len([resource for resource in included if resource["type"] == "authorBios"])
112+
expected_author_bio_count = single_entry.comment_set.filter(author__bio__isnull=False).count()
113+
assert author_bio_count == expected_author_bio_count, 'Detail author bio count is incorrect'

‎rest_framework_json_api/renderers.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,9 @@ def extract_included(fields, resource, resource_instance, included_resources):
248248
included_resources.remove(field_name)
249249
except ValueError:
250250
# Skip fields not in requested included resources
251-
continue
251+
# If no child field, directly continue with the next field
252+
if field_name not in [node.split('.')[0] for node in included_resources]:
253+
continue
252254

253255
try:
254256
relation_instance_or_manager = getattr(resource_instance, field_name)

‎rest_framework_json_api/serializers.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def validate_path(serializer_class, field_path, path):
8484
)
8585
)
8686
if len(field_path) > 1:
87-
new_included_field_path = field_path[-1:]
87+
new_included_field_path = field_path[1:]
8888
# We go down one level in the path
8989
validate_path(this_included_serializer, new_included_field_path, path)
9090

‎tox.ini‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ deps =
1414
drf33: djangorestframework>=3.3,<3.4
1515
-r{toxinidir}/requirements-development.txt
1616

17-
setenv= DJANGO_SETTINGS_MODULE=example.settings.test
17+
setenv =
18+
PYTHONPATH = {toxinidir}
19+
DJANGO_SETTINGS_MODULE=example.settings.test
1820

1921
commands =
2022
py.test --basetemp={envtmpdir}

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /