Skip to content

Commit 45a3cae

Browse files
author
Eric Hurst
committed
Merge branch 'develop' of github.com:django-json-api/django-rest-framework-json-api into develop
2 parents 2c9750e + fbe49a1 commit 45a3cae

18 files changed

+236
-101
lines changed

docs/usage.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,8 +457,53 @@ Adding `url` to `fields` on a serializer will add a `self` link to the `links` k
457457

458458
Related links will be created automatically when using the Relationship View.
459459

460+
### Included
461+
462+
JSON API can include additional resources in a single network request.
463+
The specification refers to this feature as
464+
[Compound Documents](http://jsonapi.org/format/#document-compound-documents).
465+
Compound Documents can reduce the number of network requests
466+
which can lead to a better performing web application.
467+
To accomplish this,
468+
the specification permits a top level `included` key.
469+
The list of content within this key are the extra resources
470+
that are related to the primary resource.
471+
472+
To make a Compound Document,
473+
you need to modify your `ModelSerializer`.
474+
The two required additions are `included_resources`
475+
and `included_serializers`.
476+
477+
For example,
478+
suppose you are making an app to go on quests,
479+
and you would like to fetch your chosen knight
480+
along with the quest.
481+
You could accomplish that with:
482+
483+
```python
484+
class KnightSerializer(serializers.ModelSerializer):
485+
class Meta:
486+
model = Knight
487+
fields = ('id', 'name', 'strength', 'dexterity', 'charisma')
488+
489+
490+
class QuestSerializer(serializers.ModelSerializer):
491+
included_serializers = {
492+
'knight': KnightSerializer,
493+
}
494+
495+
class Meta:
496+
model = Quest
497+
fields = ('id', 'title', 'reward', 'knight')
498+
499+
class JSONAPIMeta:
500+
included_resources = ['knight']
501+
```
502+
503+
`included_resources` informs DJA of **what** you would like to include.
504+
`included_serializers` tells DJA **how** you want to include it.
505+
460506
<!--
461507
### Relationships
462-
### Included
463508
### Errors
464509
-->

example/factories/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import factory
44
from faker import Factory as FakerFactory
5-
from example.models import Blog, Author, AuthorBio, Entry, Comment
5+
from example.models import Blog, Author, AuthorBio, Entry, Comment, TaggedItem
66

77
faker = FakerFactory.create()
88
faker.seed(983843)
@@ -58,3 +58,11 @@ class Meta:
5858
body = factory.LazyAttribute(lambda x: faker.text())
5959
author = factory.SubFactory(AuthorFactory)
6060

61+
62+
class TaggedItemFactory(factory.django.DjangoModelFactory):
63+
64+
class Meta:
65+
model = TaggedItem
66+
67+
content_object = factory.SubFactory(EntryFactory)
68+
tag = factory.LazyAttribute(lambda x: faker.word())
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.10.5 on 2017-02-01 08:34
3+
from __future__ import unicode_literals
4+
5+
from django.db import migrations, models
6+
import django.db.models.deletion
7+
8+
9+
class Migration(migrations.Migration):
10+
11+
dependencies = [
12+
('contenttypes', '0002_remove_content_type_name'),
13+
('example', '0001_initial'),
14+
]
15+
16+
operations = [
17+
migrations.CreateModel(
18+
name='TaggedItem',
19+
fields=[
20+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
21+
('created_at', models.DateTimeField(auto_now_add=True)),
22+
('modified_at', models.DateTimeField(auto_now=True)),
23+
('tag', models.SlugField()),
24+
('object_id', models.PositiveIntegerField()),
25+
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')),
26+
],
27+
options={
28+
'abstract': False,
29+
},
30+
),
31+
]

example/models.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# -*- encoding: utf-8 -*-
22
from __future__ import unicode_literals
33

4+
from django.contrib.contenttypes.models import ContentType
5+
from django.contrib.contenttypes.fields import GenericForeignKey
6+
from django.contrib.contenttypes.fields import GenericRelation
47
from django.db import models
58
from django.utils.encoding import python_2_unicode_compatible
69

@@ -16,10 +19,21 @@ class Meta:
1619
abstract = True
1720

1821

22+
class TaggedItem(BaseModel):
23+
tag = models.SlugField()
24+
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
25+
object_id = models.PositiveIntegerField()
26+
content_object = GenericForeignKey('content_type', 'object_id')
27+
28+
def __str__(self):
29+
return self.tag
30+
31+
1932
@python_2_unicode_compatible
2033
class Blog(BaseModel):
2134
name = models.CharField(max_length=100)
2235
tagline = models.TextField()
36+
tags = GenericRelation(TaggedItem)
2337

2438
def __str__(self):
2539
return self.name
@@ -54,6 +68,7 @@ class Entry(BaseModel):
5468
n_comments = models.IntegerField(default=0)
5569
n_pingbacks = models.IntegerField(default=0)
5670
rating = models.IntegerField(default=0)
71+
tags = GenericRelation(TaggedItem)
5772

5873
def __str__(self):
5974
return self.headline

example/serializers.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11
from datetime import datetime
22
from rest_framework_json_api import serializers, relations
3-
from example.models import Blog, Entry, Author, AuthorBio, Comment
3+
from example.models import Blog, Entry, Author, AuthorBio, Comment, TaggedItem
4+
5+
6+
class TaggedItemSerializer(serializers.ModelSerializer):
7+
8+
class Meta:
9+
model = TaggedItem
10+
fields = ('tag', )
411

512

613
class BlogSerializer(serializers.ModelSerializer):
714

815
copyright = serializers.SerializerMethodField()
16+
tags = TaggedItemSerializer(many=True, read_only=True)
17+
18+
include_serializers = {
19+
'tags': 'example.serializers.TaggedItemSerializer',
20+
}
921

1022
def get_copyright(self, resource):
1123
return datetime.now().year
@@ -17,7 +29,8 @@ def get_root_meta(self, resource, many):
1729

1830
class Meta:
1931
model = Blog
20-
fields = ('name', 'url',)
32+
fields = ('name', 'url', 'tags')
33+
read_only_fields = ('tags', )
2134
meta_fields = ('copyright',)
2235

2336

@@ -36,6 +49,7 @@ def __init__(self, *args, **kwargs):
3649
'comments': 'example.serializers.CommentSerializer',
3750
'featured': 'example.serializers.EntrySerializer',
3851
'suggested': 'example.serializers.EntrySerializer',
52+
'tags': 'example.serializers.TaggedItemSerializer',
3953
}
4054

4155
body_format = serializers.SerializerMethodField()
@@ -52,6 +66,7 @@ def __init__(self, *args, **kwargs):
5266
# single related from serializer
5367
featured = relations.SerializerMethodResourceRelatedField(
5468
source='get_featured', model=Entry, read_only=True)
69+
tags = TaggedItemSerializer(many=True, read_only=True)
5570

5671
def get_suggested(self, obj):
5772
return Entry.objects.exclude(pk=obj.pk)
@@ -65,9 +80,13 @@ def get_body_format(self, obj):
6580
class Meta:
6681
model = Entry
6782
fields = ('blog', 'headline', 'body_text', 'pub_date', 'mod_date',
68-
'authors', 'comments', 'featured', 'suggested',)
83+
'authors', 'comments', 'featured', 'suggested', 'tags')
84+
read_only_fields = ('tags', )
6985
meta_fields = ('body_format',)
7086

87+
class JSONAPIMeta:
88+
included_resources = ['comments']
89+
7190

7291
class AuthorBioSerializer(serializers.ModelSerializer):
7392

example/tests/conftest.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
import pytest
22
from pytest_factoryboy import register
33

4-
from example.factories import BlogFactory, AuthorFactory, AuthorBioFactory, EntryFactory, CommentFactory
4+
from example.factories import BlogFactory, AuthorFactory, AuthorBioFactory, EntryFactory, CommentFactory, \
5+
TaggedItemFactory
56

67
register(BlogFactory)
78
register(AuthorFactory)
89
register(AuthorBioFactory)
910
register(EntryFactory)
1011
register(CommentFactory)
12+
register(TaggedItemFactory)
1113

1214

1315
@pytest.fixture
14-
def single_entry(blog, author, entry_factory, comment_factory):
16+
def single_entry(blog, author, entry_factory, comment_factory, tagged_item_factory):
1517

1618
entry = entry_factory(blog=blog, authors=(author,))
1719
comment_factory(entry=entry)
20+
tagged_item_factory(content_object=entry)
1821
return entry
1922

2023

example/tests/integration/test_includes.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,9 @@
33

44
from example.tests.utils import load_json
55

6-
try:
7-
from unittest import mock
8-
except ImportError:
9-
import mock
10-
116
pytestmark = pytest.mark.django_db
127

138

14-
@mock.patch('rest_framework_json_api.utils.get_default_included_resources_from_serializer', new=lambda s: ['comments'])
159
def test_default_included_data_on_list(multiple_entries, client):
1610
return test_included_data_on_list(multiple_entries=multiple_entries, client=client, query='?page_size=5')
1711

@@ -28,7 +22,6 @@ def test_included_data_on_list(multiple_entries, client, query='?include=comment
2822
assert comment_count == expected_comment_count, 'List comment count is incorrect'
2923

3024

31-
@mock.patch('rest_framework_json_api.utils.get_default_included_resources_from_serializer', new=lambda s: ['comments'])
3225
def test_default_included_data_on_detail(single_entry, client):
3326
return test_included_data_on_detail(single_entry=single_entry, client=client, query='')
3427

example/tests/integration/test_meta.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from django.core.urlresolvers import reverse
33

44
import pytest
5-
from example.tests.utils import dump_json, redump_json
5+
from example.tests.utils import load_json
66

77
pytestmark = pytest.mark.django_db
88

@@ -19,6 +19,11 @@ def test_top_level_meta_for_list_view(blog, client):
1919
"links": {
2020
"self": 'http://testserver/blogs/1'
2121
},
22+
"relationships": {
23+
"tags": {
24+
"data": []
25+
}
26+
},
2227
"meta": {
2328
"copyright": datetime.now().year
2429
},
@@ -36,10 +41,9 @@ def test_top_level_meta_for_list_view(blog, client):
3641
}
3742

3843
response = client.get(reverse("blog-list"))
39-
content_dump = redump_json(response.content)
40-
expected_dump = dump_json(expected)
44+
parsed_content = load_json(response.content)
4145

42-
assert content_dump == expected_dump
46+
assert expected == parsed_content
4347

4448

4549
def test_top_level_meta_for_detail_view(blog, client):
@@ -51,6 +55,11 @@ def test_top_level_meta_for_detail_view(blog, client):
5155
"attributes": {
5256
"name": blog.name
5357
},
58+
"relationships": {
59+
"tags": {
60+
"data": []
61+
}
62+
},
5463
"links": {
5564
"self": "http://testserver/blogs/1"
5665
},
@@ -64,7 +73,6 @@ def test_top_level_meta_for_detail_view(blog, client):
6473
}
6574

6675
response = client.get(reverse("blog-detail", kwargs={'pk': blog.pk}))
67-
content_dump = redump_json(response.content)
68-
expected_dump = dump_json(expected)
76+
parsed_content = load_json(response.content)
6977

70-
assert content_dump == expected_dump
78+
assert expected == parsed_content

example/tests/integration/test_non_paginated_responses.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,26 @@
11
from django.core.urlresolvers import reverse
22
from django.conf import settings
33

4+
try:
5+
from unittest import mock
6+
except ImportError:
7+
import mock
8+
49
import pytest
510

611
from example.views import EntryViewSet
712
from rest_framework_json_api.pagination import PageNumberPagination
813

9-
from example.tests.utils import dump_json, redump_json
14+
from example.tests.utils import load_json
1015

1116
pytestmark = pytest.mark.django_db
1217

1318

1419
# rf == request_factory
20+
@mock.patch(
21+
'rest_framework_json_api.utils'
22+
'.get_default_included_resources_from_serializer',
23+
new=lambda s: [])
1524
def test_multiple_entries_no_pagination(multiple_entries, rf):
1625

1726
expected = {
@@ -48,6 +57,9 @@ def test_multiple_entries_no_pagination(multiple_entries, rf):
4857
"related": "http://testserver/entries/1/suggested/",
4958
"self": "http://testserver/entries/1/relationships/suggested"
5059
}
60+
},
61+
"tags": {
62+
"data": []
5163
}
5264
}
5365
},
@@ -83,6 +95,9 @@ def test_multiple_entries_no_pagination(multiple_entries, rf):
8395
"related": "http://testserver/entries/2/suggested/",
8496
"self": "http://testserver/entries/2/relationships/suggested"
8597
}
98+
},
99+
"tags": {
100+
"data": []
86101
}
87102
}
88103
},
@@ -101,7 +116,6 @@ class NonPaginatedEntryViewSet(EntryViewSet):
101116
response = view(request)
102117
response.render()
103118

104-
content_dump = redump_json(response.content)
105-
expected_dump = dump_json(expected)
119+
parsed_content = load_json(response.content)
106120

107-
assert content_dump == expected_dump
121+
assert expected == parsed_content

0 commit comments

Comments
 (0)