Skip to content

Commit 0856f91

Browse files
committed
Added SkipDataMixin, HyperLinkedMixin
1 parent 79e7738 commit 0856f91

File tree

2 files changed

+127
-64
lines changed

2 files changed

+127
-64
lines changed

rest_framework_json_api/relations.py

+102-41
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
from django.core.exceptions import ImproperlyConfigured
88
from django.urls import NoReverseMatch
99
from django.utils.translation import ugettext_lazy as _
10-
from rest_framework.fields import MISSING_ERROR_MESSAGE
11-
from rest_framework.relations import MANY_RELATION_KWARGS, PrimaryKeyRelatedField
10+
from rest_framework.fields import MISSING_ERROR_MESSAGE, SkipField
11+
from rest_framework.relations import MANY_RELATION_KWARGS
12+
from rest_framework.relations import ManyRelatedField as DRFManyRelatedField
13+
from rest_framework.relations import PrimaryKeyRelatedField, RelatedField
1214
from rest_framework.reverse import reverse
1315
from rest_framework.serializers import Serializer
1416

@@ -29,26 +31,31 @@
2931
]
3032

3133

32-
class ResourceRelatedField(PrimaryKeyRelatedField):
33-
_skip_polymorphic_optimization = True
34+
class SkipDataMixin(object):
35+
"""
36+
This workaround skips "data" rendering for relationships
37+
in order to save some sql queries and improve performance
38+
"""
39+
40+
def __init__(self, *args, **kwargs):
41+
super(SkipDataMixin, self).__init__(*args, **kwargs)
42+
43+
def get_attribute(self, instance):
44+
raise SkipField
45+
46+
def to_representation(self, *args):
47+
raise NotImplementedError
48+
49+
50+
class ManyRelatedFieldWithNoData(SkipDataMixin, DRFManyRelatedField):
51+
pass
52+
53+
54+
class HyperLinkedMixin(object):
3455
self_link_view_name = None
3556
related_link_view_name = None
3657
related_link_lookup_field = 'pk'
3758

38-
default_error_messages = {
39-
'required': _('This field is required.'),
40-
'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'),
41-
'incorrect_type': _(
42-
'Incorrect type. Expected resource identifier object, received {data_type}.'
43-
),
44-
'incorrect_relation_type': _(
45-
'Incorrect relation type. Expected {relation_type}, received {received_type}.'
46-
),
47-
'missing_type': _('Invalid resource identifier object: missing \'type\' attribute'),
48-
'missing_id': _('Invalid resource identifier object: missing \'id\' attribute'),
49-
'no_match': _('Invalid hyperlink - No URL match.'),
50-
}
51-
5259
def __init__(self, self_link_view_name=None, related_link_view_name=None, **kwargs):
5360
if self_link_view_name is not None:
5461
self.self_link_view_name = self_link_view_name
@@ -62,34 +69,12 @@ def __init__(self, self_link_view_name=None, related_link_view_name=None, **kwar
6269
'related_link_url_kwarg', self.related_link_lookup_field
6370
)
6471

65-
# check for a model class that was passed in for the relation type
66-
model = kwargs.pop('model', None)
67-
if model:
68-
self.model = model
69-
7072
# We include this simply for dependency injection in tests.
7173
# We can't add it as a class attributes or it would expect an
7274
# implicit `self` argument to be passed.
7375
self.reverse = reverse
7476

75-
super(ResourceRelatedField, self).__init__(**kwargs)
76-
77-
def use_pk_only_optimization(self):
78-
# We need the real object to determine its type...
79-
return self.get_resource_type_from_included_serializer() is not None
80-
81-
def conflict(self, key, **kwargs):
82-
"""
83-
A helper method that simply raises a validation error.
84-
"""
85-
try:
86-
msg = self.error_messages[key]
87-
except KeyError:
88-
class_name = self.__class__.__name__
89-
msg = MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key)
90-
raise AssertionError(msg)
91-
message_string = msg.format(**kwargs)
92-
raise Conflict(message_string)
77+
super(HyperLinkedMixin, self).__init__(**kwargs)
9378

9479
def get_url(self, name, view_name, kwargs, request):
9580
"""
@@ -140,6 +125,78 @@ def get_links(self, obj=None, lookup_field='pk'):
140125
return_data.update({'related': related_link})
141126
return return_data
142127

128+
129+
class HyperLinkedRelatedField(HyperLinkedMixin, SkipDataMixin, RelatedField):
130+
131+
@classmethod
132+
def many_init(cls, *args, **kwargs):
133+
"""
134+
This method handles creating a parent `ManyRelatedField` instance
135+
when the `many=True` keyword argument is passed.
136+
137+
Typically you won't need to override this method.
138+
139+
Note that we're over-cautious in passing most arguments to both parent
140+
and child classes in order to try to cover the general case. If you're
141+
overriding this method you'll probably want something much simpler, eg:
142+
143+
@classmethod
144+
def many_init(cls, *args, **kwargs):
145+
kwargs['child'] = cls()
146+
return CustomManyRelatedField(*args, **kwargs)
147+
"""
148+
list_kwargs = {'child_relation': cls(*args, **kwargs)}
149+
for key in kwargs:
150+
if key in MANY_RELATION_KWARGS:
151+
list_kwargs[key] = kwargs[key]
152+
return ManyRelatedFieldWithNoData(**list_kwargs)
153+
154+
155+
class ResourceRelatedField(HyperLinkedMixin, PrimaryKeyRelatedField):
156+
_skip_polymorphic_optimization = True
157+
self_link_view_name = None
158+
related_link_view_name = None
159+
related_link_lookup_field = 'pk'
160+
161+
default_error_messages = {
162+
'required': _('This field is required.'),
163+
'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'),
164+
'incorrect_type': _(
165+
'Incorrect type. Expected resource identifier object, received {data_type}.'
166+
),
167+
'incorrect_relation_type': _(
168+
'Incorrect relation type. Expected {relation_type}, received {received_type}.'
169+
),
170+
'missing_type': _('Invalid resource identifier object: missing \'type\' attribute'),
171+
'missing_id': _('Invalid resource identifier object: missing \'id\' attribute'),
172+
'no_match': _('Invalid hyperlink - No URL match.'),
173+
}
174+
175+
def __init__(self, **kwargs):
176+
# check for a model class that was passed in for the relation type
177+
model = kwargs.pop('model', None)
178+
if model:
179+
self.model = model
180+
181+
super(ResourceRelatedField, self).__init__(**kwargs)
182+
183+
def use_pk_only_optimization(self):
184+
# We need the real object to determine its type...
185+
return self.get_resource_type_from_included_serializer() is not None
186+
187+
def conflict(self, key, **kwargs):
188+
"""
189+
A helper method that simply raises a validation error.
190+
"""
191+
try:
192+
msg = self.error_messages[key]
193+
except KeyError:
194+
class_name = self.__class__.__name__
195+
msg = MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key)
196+
raise AssertionError(msg)
197+
message_string = msg.format(**kwargs)
198+
raise Conflict(message_string)
199+
143200
def to_internal_value(self, data):
144201
if isinstance(data, six.text_type):
145202
try:
@@ -323,3 +380,7 @@ def to_representation(self, value):
323380
base = super(SerializerMethodResourceRelatedField, self)
324381
return [base.to_representation(x) for x in value]
325382
return super(SerializerMethodResourceRelatedField, self).to_representation(value)
383+
384+
385+
class SerializerMethodHyperLinkedRelatedField(SkipDataMixin, SerializerMethodResourceRelatedField):
386+
pass

rest_framework_json_api/renderers.py

+25-23
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Renderers
33
"""
44
import copy
5-
from collections import OrderedDict, defaultdict
5+
from collections import Iterable, OrderedDict, defaultdict
66

77
import inflection
88
from django.db.models import Manager
@@ -13,7 +13,7 @@
1313

1414
import rest_framework_json_api
1515
from rest_framework_json_api import utils
16-
from rest_framework_json_api.relations import ResourceRelatedField
16+
from rest_framework_json_api.relations import HyperLinkedMixin, ResourceRelatedField, SkipDataMixin
1717

1818

1919
class JSONRenderer(renderers.JSONRenderer):
@@ -126,25 +126,23 @@ def extract_relationships(cls, fields, resource, resource_instance):
126126
}})
127127
continue
128128

129-
if isinstance(field, ResourceRelatedField):
129+
relation_data = {}
130+
if isinstance(field, HyperLinkedMixin):
131+
field_links = field.get_links(resource_instance, field.related_link_lookup_field)
132+
relation_data.update({'links': field_links} if field_links else dict())
133+
data.update({field_name: relation_data})
134+
135+
if isinstance(field, (ResourceRelatedField, )):
130136
relation_instance_id = getattr(resource_instance, source + "_id", None)
131137
if not relation_instance_id:
132138
resolved, relation_instance = utils.get_relation_instance(resource_instance,
133139
source, field.parent)
134140
if not resolved:
135141
continue
136142

137-
# special case for ResourceRelatedField
138-
relation_data = {
139-
'data': resource.get(field_name)
140-
}
143+
if not isinstance(field, SkipDataMixin):
144+
relation_data.update({'data': resource.get(field_name)})
141145

142-
field_links = field.get_links(
143-
resource_instance, field.related_link_lookup_field)
144-
relation_data.update(
145-
{'links': field_links}
146-
if field_links else dict()
147-
)
148146
data.update({field_name: relation_data})
149147
continue
150148

@@ -186,12 +184,22 @@ def extract_relationships(cls, fields, resource, resource_instance):
186184
if not resolved:
187185
continue
188186

187+
relation_data = {}
188+
189+
if isinstance(resource.get(field_name), Iterable):
190+
relation_data.update(
191+
{
192+
'meta': {'count': len(resource.get(field_name))}
193+
}
194+
)
195+
189196
if isinstance(field.child_relation, ResourceRelatedField):
190197
# special case for ResourceRelatedField
191-
relation_data = {
192-
'data': resource.get(field_name)
193-
}
198+
relation_data.update(
199+
{'data': resource.get(field_name)}
200+
)
194201

202+
if isinstance(field.child_relation, HyperLinkedMixin):
195203
field_links = field.child_relation.get_links(
196204
resource_instance,
197205
field.child_relation.related_link_lookup_field
@@ -200,13 +208,7 @@ def extract_relationships(cls, fields, resource, resource_instance):
200208
{'links': field_links}
201209
if field_links else dict()
202210
)
203-
relation_data.update(
204-
{
205-
'meta': {
206-
'count': len(resource.get(field_name))
207-
}
208-
}
209-
)
211+
210212
data.update({field_name: relation_data})
211213
continue
212214

0 commit comments

Comments
 (0)