Skip to content

Commit 64f8ae3

Browse files
author
Tim Csitkovics
committed
Do not lazy load forward relations
If the forward relations are included, the renderer will break, so this optimization only works if it is not included. fixup! Do not lazy load forward relations fixup! Do not lazy load forward relations fixup! Do not lazy load forward relations fixup! Do not lazy load forward relations fixup! Do not lazy load forward relations Include test case Small refactoring Make compatible with Python 2.X fixup! Make compatible with Python 2.X Fix travis Use isort Implement suggested changes Make import backward compatible Make travis happy
1 parent 96c533b commit 64f8ae3

File tree

2 files changed

+104
-28
lines changed

2 files changed

+104
-28
lines changed

example/tests/test_serializers.py

+74-4
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,42 @@
1+
from collections import OrderedDict
2+
13
import pytest
24
from django.test import TestCase
35
from django.urls import reverse
46
from django.utils import timezone
5-
6-
from rest_framework_json_api.serializers import ResourceIdentifierObjectSerializer
7+
from rest_framework.request import Request
8+
from rest_framework.test import APIRequestFactory
9+
10+
from rest_framework_json_api.serializers import (
11+
DateField,
12+
ModelSerializer,
13+
ResourceIdentifierObjectSerializer
14+
)
715
from rest_framework_json_api.utils import format_resource_type
816

917
from example.models import Author, Blog, Entry
18+
from example.serializers import BlogSerializer
19+
20+
try:
21+
from unittest import mock
22+
except ImportError:
23+
import mock
1024

25+
request_factory = APIRequestFactory()
1126
pytestmark = pytest.mark.django_db
1227

1328

1429
class TestResourceIdentifierObjectSerializer(TestCase):
1530
def setUp(self):
1631
self.blog = Blog.objects.create(name='Some Blog', tagline="It's a blog")
32+
now = timezone.now()
33+
1734
self.entry = Entry.objects.create(
1835
blog=self.blog,
1936
headline='headline',
2037
body_text='body_text',
21-
pub_date=timezone.now(),
22-
mod_date=timezone.now(),
38+
pub_date=now.date(),
39+
mod_date=now.date(),
2340
n_comments=0,
2441
n_pingbacks=0,
2542
rating=3
@@ -30,6 +47,59 @@ def setUp(self):
3047
Author.objects.create(name=name, email='{}@example.org'.format(name))
3148
)
3249

50+
def test_forward_relationship_not_loaded_when_not_included(self):
51+
to_representation_method = 'example.serializers.BlogSerializer.to_representation'
52+
with mock.patch(to_representation_method) as mocked_serializer:
53+
class EntrySerializer(ModelSerializer):
54+
blog = BlogSerializer()
55+
56+
class Meta:
57+
model = Entry
58+
fields = '__all__'
59+
60+
request_without_includes = Request(request_factory.get('/'))
61+
serializer = EntrySerializer(context={'request': request_without_includes})
62+
serializer.to_representation(self.entry)
63+
64+
mocked_serializer.assert_not_called()
65+
66+
def test_forward_relationship_optimization_correct_representation(self):
67+
class EntrySerializer(ModelSerializer):
68+
blog = BlogSerializer()
69+
70+
class Meta:
71+
model = Entry
72+
fields = '__all__'
73+
74+
request_without_includes = Request(request_factory.get('/'))
75+
serializer = EntrySerializer(context={'request': request_without_includes})
76+
result = serializer.to_representation(self.entry)
77+
78+
# Remove non deterministic fields
79+
result.pop('created_at')
80+
result.pop('modified_at')
81+
82+
expected = OrderedDict(
83+
[
84+
('id', 1),
85+
('blog', OrderedDict([('type', 'blogs'), ('id', 1)])),
86+
('headline', 'headline'),
87+
('body_text', 'body_text'),
88+
('pub_date', DateField().to_representation(self.entry.pub_date)),
89+
('mod_date', DateField().to_representation(self.entry.mod_date)),
90+
('n_comments', 0),
91+
('n_pingbacks', 0),
92+
('rating', 3),
93+
('authors',
94+
[
95+
OrderedDict([('type', 'authors'), ('id', '1')]),
96+
OrderedDict([('type', 'authors'), ('id', '2')]),
97+
OrderedDict([('type', 'authors'), ('id', '3')]),
98+
OrderedDict([('type', 'authors'), ('id', '4')]),
99+
OrderedDict([('type', 'authors'), ('id', '5')])])])
100+
101+
self.assertEqual(expected, result)
102+
33103
def test_data_in_correct_format_when_instantiated_with_blog_object(self):
34104
serializer = ResourceIdentifierObjectSerializer(instance=self.blog)
35105

rest_framework_json_api/serializers.py

+30-24
Original file line numberDiff line numberDiff line change
@@ -182,35 +182,41 @@ def to_representation(self, instance):
182182

183183
for field in readable_fields:
184184
try:
185-
186-
if isinstance(field, ModelSerializer) and hasattr(field, field.source + "_id"):
187-
attribute = getattr(instance, field.source + "_id")
188-
if attribute is None:
189-
ret[field.field_name] = None
190-
continue
191-
resource_type = get_resource_type_from_instance(field)
192-
if resource_type:
193-
ret[field.field_name] = OrderedDict([("type", resource_type),
194-
("id", attribute)])
195-
continue
196-
197-
attribute = field.get_attribute(instance)
185+
field_representation = self._get_field_representation(field, instance)
186+
ret[field.field_name] = field_representation
198187
except SkipField:
199188
continue
200189

201-
# We skip `to_representation` for `None` values so that fields do
202-
# not have to explicitly deal with that case.
203-
#
204-
# For related fields with `use_pk_only_optimization` we need to
205-
# resolve the pk value.
206-
check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
207-
if check_for_none is None:
208-
ret[field.field_name] = None
209-
else:
210-
ret[field.field_name] = field.to_representation(attribute)
211-
212190
return ret
213191

192+
def _get_field_representation(self, field, instance):
193+
request = self.context.get('request')
194+
is_included = field.source in get_included_resources(request)
195+
if not is_included and \
196+
isinstance(field, ModelSerializer) and \
197+
hasattr(instance, field.source + '_id'):
198+
attribute = getattr(instance, field.source + '_id')
199+
200+
if attribute is None:
201+
return None
202+
203+
resource_type = get_resource_type_from_serializer(field)
204+
if resource_type:
205+
return OrderedDict([('type', resource_type), ('id', attribute)])
206+
207+
attribute = field.get_attribute(instance)
208+
209+
# We skip `to_representation` for `None` values so that fields do
210+
# not have to explicitly deal with that case.
211+
#
212+
# For related fields with `use_pk_only_optimization` we need to
213+
# resolve the pk value.
214+
check_for_none = attribute.pk if isinstance(attribute, PKOnlyObject) else attribute
215+
if check_for_none is None:
216+
return None
217+
else:
218+
return field.to_representation(attribute)
219+
214220

215221
class PolymorphicSerializerMetaclass(SerializerMetaclass):
216222
"""

0 commit comments

Comments
 (0)