Skip to content

support nested structures #776

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
May 14, 2020
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Note that in line with [Django REST Framework policy](http://www.django-rest-framework.org/topics/release-notes/),
any parts of the framework not mentioned in the documentation should generally be considered private API, and may be subject to change.

## [3.2.0] - pending

### Added

* Added support for serializiing complex structures as attributes. For details please reffer to #769

## [3.1.0] - 2020-02-08

### Added
Expand Down
25 changes: 25 additions & 0 deletions example/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,31 @@ def get_first_entry(self, obj):
return obj.entries.first()


class CommentWithNestedFieldsSerializer(serializers.ModelSerializer):
entry = EntryDRFSerializers()

included_serializers = {
'entry': 'example.serializers.EntryDRFSerializers'
}

class Meta:
model = Comment
exclude = ('created_at', 'modified_at', 'author')
# fields = ('entry', 'body', 'author',)


class AuthorWithNestedFieldsSerializer(serializers.ModelSerializer):
comments = CommentWithNestedFieldsSerializer(many=True)

included_serializers = {
'comments': 'example.serializers.CommentWithNestedFieldsSerializer'
}

class Meta:
model = Author
fields = ('name', 'email', 'comments')


class WriterSerializer(serializers.ModelSerializer):
included_serializers = {
'bio': AuthorBioSerializer
Expand Down
209 changes: 209 additions & 0 deletions example/tests/test_rendering_strategies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
from __future__ import absolute_import

import pytest
from django.utils import timezone
from rest_framework.reverse import reverse

from rest_framework_json_api.settings import JSONAPISettings, \
ATTRIBUTE_RENDERING_STRATEGY, RELATIONS_RENDERING_STRATEGY
from . import TestBase
from example.models import Author, Blog, Comment, Entry
from django.test import override_settings


class TestRenderingStrategy(TestBase):
list_url = reverse('authors-nested-list')

def setUp(self):
super(TestRenderingStrategy, self).setUp()
self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
self.entry = Entry.objects.create(
blog=self.blog,
headline='headline',
body_text='body_text',
pub_date=timezone.now(),
mod_date=timezone.now(),
n_comments=0,
n_pingbacks=0,
rating=3
)
for i in range(1, 6):
name = 'some_author{}'.format(i)
self.entry.authors.add(
Author.objects.create(name=name, email='{}@example.org'.format(name))
)

self.comment = Comment.objects.create(
entry=self.entry,
body='testing one two three',
author=Author.objects.first()
)

def test_attribute_rendering_strategy(self):
with override_settings(
JSON_API_NESTED_SERIALIZERS_RENDERING_STRATEGY=ATTRIBUTE_RENDERING_STRATEGY):
response = self.client.get(self.list_url)

expected = {
"links": {
"first": "http://testserver/authors-nested?page%5Bnumber%5D=1",
"last": "http://testserver/authors-nested?page%5Bnumber%5D=5",
"next": "http://testserver/authors-nested?page%5Bnumber%5D=2",
"prev": None
},
"data": [
{
"type": "authors",
"id": "1",
"attributes": {
"name": "some_author1",
"email": "[email protected]",
"comments": [
{
"id": 1,
"entry": {
"tags": [],
"url": "http://testserver/drf-blogs/1"
},
"body": "testing one two three"
}
]
}
}
],
"meta": {
"pagination": {
"page": 1,
"pages": 5,
"count": 5
}
}
}
assert expected == response.json()

def test_relations_rendering_strategy(self):
with override_settings(
JSON_API_NESTED_SERIALIZERS_RENDERING_STRATEGY=RELATIONS_RENDERING_STRATEGY):
response = self.client.get(self.list_url)

expected = {
"links": {
"first": "http://testserver/authors-nested?page%5Bnumber%5D=1",
"last": "http://testserver/authors-nested?page%5Bnumber%5D=5",
"next": "http://testserver/authors-nested?page%5Bnumber%5D=2",
"prev": None
},
"data": [
{
"type": "authors",
"id": "1",
"attributes": {
"name": "some_author1",
"email": "[email protected]"
},
"relationships": {
"comments": {
"data": [
{
"type": "comments",
"id": "1"
}
]
}
}
}
],
"meta": {
"pagination": {
"page": 1,
"pages": 5,
"count": 5
}
}
}
assert expected == response.json()

def test_relations_rendering_strategy_included(self):
with override_settings(
JSON_API_NESTED_SERIALIZERS_RENDERING_STRATEGY=RELATIONS_RENDERING_STRATEGY):
response = self.client.get(self.list_url, data={'include': 'comments,comments.entry'})

expected = {
"links": {
"first": "http://testserver/authors-nested?include=comments%2Ccomments.entry&page%5Bnumber%5D=1", # NoQA
"last": "http://testserver/authors-nested?include=comments%2Ccomments.entry&page%5Bnumber%5D=5", # NoQA
"next": "http://testserver/authors-nested?include=comments%2Ccomments.entry&page%5Bnumber%5D=2", # NoQA
"prev": None
},
"data": [
{
"type": "authors",
"id": "1",
"attributes": {
"name": "some_author1",
"email": "[email protected]"
},
"relationships": {
"comments": {
"data": [
{
"type": "comments",
"id": "1"
}
]
}
}
}
],
"included": [
{
"type": "comments",
"id": "1",
"attributes": {
"body": "testing one two three"
},
"relationships": {
"entry": {
"data": {
"type": "entries",
"id": "1"
}
}
}
},
{
"type": "entries",
"id": "1",
"attributes": {},
"relationships": {
"tags": {
"data": []
}
},
"links": {
"self": "http://testserver/drf-blogs/1"
}
}
],
"meta": {
"pagination": {
"page": 1,
"pages": 5,
"count": 5
}
}
}
assert expected == response.json()


class TestRenderingStrategySettings(TestBase):

def test_deprecation(self):
with pytest.deprecated_call():
JSONAPISettings()

def test_invalid_strategy(self):
class Settings:
JSON_API_NESTED_SERIALIZERS_RENDERING_STRATEGY = 'SOME_INVALID_STRATEGY'
with pytest.raises(AttributeError):
JSONAPISettings(user_settings=Settings())
4 changes: 3 additions & 1 deletion example/urls_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
NoFiltersetEntryViewSet,
NonPaginatedEntryViewSet,
ProjectTypeViewset,
ProjectViewset
ProjectViewset,
AuthorWithNestedFieldsViewSet
)

router = routers.DefaultRouter(trailing_slash=False)
Expand All @@ -32,6 +33,7 @@
router.register(r'filterset-entries', FiltersetEntryViewSet, 'filterset-entry')
router.register(r'nofilterset-entries', NoFiltersetEntryViewSet, 'nofilterset-entry')
router.register(r'authors', AuthorViewSet)
router.register(r'authors-nested', AuthorWithNestedFieldsViewSet, 'authors-nested')
router.register(r'comments', CommentViewSet)
router.register(r'companies', CompanyViewset)
router.register(r'projects', ProjectViewset)
Expand Down
9 changes: 7 additions & 2 deletions example/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
EntryDRFSerializers,
EntrySerializer,
ProjectSerializer,
ProjectTypeSerializer
)
ProjectTypeSerializer,
AuthorWithNestedFieldsSerializer)

HTTP_422_UNPROCESSABLE_ENTITY = 422

Expand Down Expand Up @@ -175,6 +175,11 @@ class AuthorViewSet(ModelViewSet):
serializer_class = AuthorSerializer


class AuthorWithNestedFieldsViewSet(ModelViewSet):
queryset = Author.objects.all()
serializer_class = AuthorWithNestedFieldsSerializer


class CommentViewSet(ModelViewSet):
queryset = Comment.objects.all()
serializer_class = CommentSerializer
Expand Down
4 changes: 2 additions & 2 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[pytest]
DJANGO_SETTINGS_MODULE=example.settings.test
filterwarnings =
error::DeprecationWarning
error::PendingDeprecationWarning
ignore::DeprecationWarning
ignore::PendingDeprecationWarning
26 changes: 22 additions & 4 deletions rest_framework_json_api/renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from rest_framework.relations import PKOnlyObject
from rest_framework.serializers import BaseSerializer, ListSerializer, Serializer
from rest_framework.settings import api_settings
from .settings import json_api_settings, RELATIONS_RENDERING_STRATEGY, ATTRIBUTE_RENDERING_STRATEGY

import rest_framework_json_api
from rest_framework_json_api import utils
Expand Down Expand Up @@ -52,6 +53,7 @@ def extract_attributes(cls, fields, resource):
Builds the `attributes` object of the JSON API resource object.
"""
data = OrderedDict()
rendering_strategy = json_api_settings.NESTED_SERIALIZERS_RENDERING_STRATEGY
for field_name, field in iter(fields.items()):
# ID is always provided in the root of JSON API so remove it from attributes
if field_name == 'id':
Expand All @@ -61,10 +63,16 @@ def extract_attributes(cls, fields, resource):
continue
# Skip fields with relations
if isinstance(
field, (relations.RelatedField, relations.ManyRelatedField, BaseSerializer)
field, (relations.RelatedField, relations.ManyRelatedField)
):
continue

if isinstance(field, BaseSerializer):
if rendering_strategy == RELATIONS_RENDERING_STRATEGY:
continue
elif rendering_strategy == ATTRIBUTE_RENDERING_STRATEGY:
pass

# Skip read_only attribute fields when `resource` is an empty
# serializer. Prevents the "Raw Data" form of the browsable API
# from rendering `"foo": null` for read only fields
Expand All @@ -89,6 +97,7 @@ def extract_relationships(cls, fields, resource, resource_instance):
from rest_framework_json_api.relations import ResourceRelatedField

data = OrderedDict()
rendering_strategy = json_api_settings.NESTED_SERIALIZERS_RENDERING_STRATEGY

# Don't try to extract relationships from a non-existent resource
if resource_instance is None:
Expand All @@ -105,10 +114,14 @@ def extract_relationships(cls, fields, resource, resource_instance):

# Skip fields without relations
if not isinstance(
field, (relations.RelatedField, relations.ManyRelatedField, BaseSerializer)
field, (relations.RelatedField, relations.ManyRelatedField)
):
continue

if isinstance(field, BaseSerializer) and \
rendering_strategy == ATTRIBUTE_RENDERING_STRATEGY:
continue

source = field.source
relation_type = utils.get_related_resource_type(field)

Expand Down Expand Up @@ -327,18 +340,23 @@ def extract_included(cls, fields, resource, resource_instance, included_resource
included_serializers = utils.get_included_serializers(current_serializer)
included_resources = copy.copy(included_resources)
included_resources = [inflection.underscore(value) for value in included_resources]
rendering_strategy = json_api_settings.NESTED_SERIALIZERS_RENDERING_STRATEGY

for field_name, field in iter(fields.items()):
# Skip URL field
if field_name == api_settings.URL_FIELD_NAME:
continue

# Skip fields without relations or serialized data
# Skip fields without relations
if not isinstance(
field, (relations.RelatedField, relations.ManyRelatedField, BaseSerializer)
field, (relations.RelatedField, relations.ManyRelatedField)
):
continue

if isinstance(field, BaseSerializer) and \
rendering_strategy == ATTRIBUTE_RENDERING_STRATEGY:
continue

try:
included_resources.remove(field_name)
except ValueError:
Expand Down
Loading