Skip to content

Commit 23d0af6

Browse files
committed
Completed support for ResourceRelatedField
* self and related links are supported and added by the renderer if their corresponding view names are defined * meta count is added by the renderer
1 parent d654c75 commit 23d0af6

File tree

2 files changed

+120
-24
lines changed

2 files changed

+120
-24
lines changed

rest_framework_json_api/relations.py

Lines changed: 79 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from rest_framework.exceptions import ValidationError
2+
from rest_framework.fields import MISSING_ERROR_MESSAGE
23
from rest_framework.relations import *
3-
from rest_framework_json_api.utils import format_relation_name, get_related_resource_type, \
4-
get_resource_type_from_queryset, get_resource_type_from_instance
54
from django.utils.translation import ugettext_lazy as _
65

6+
from rest_framework_json_api.exceptions import Conflict
7+
from rest_framework_json_api.utils import format_relation_name, Hyperlink, \
8+
get_resource_type_from_queryset, get_resource_type_from_instance
9+
710

811
class HyperlinkedRelatedField(HyperlinkedRelatedField):
912
"""
@@ -40,23 +43,26 @@ def to_internal_value(self, data):
4043

4144

4245
class ResourceRelatedField(PrimaryKeyRelatedField):
43-
lookup_field = 'pk'
44-
view_name = None
45-
46-
47-
46+
self_link_view_name = None
47+
related_link_view_name = None
48+
related_link_lookup_field = 'pk'
4849

4950
default_error_messages = {
5051
'required': _('This field is required.'),
5152
'does_not_exist': _('Invalid pk "{pk_value}" - object does not exist.'),
52-
'incorrect_type': _('Incorrect type. Expected pk value, received {data_type}.'),
53+
'incorrect_type': _('Incorrect type. Expected resource identifier object, received {data_type}.'),
5354
'incorrect_relation_type': _('Incorrect relation type. Expected {relation_type}, received {received_type}.'),
5455
'no_match': _('Invalid hyperlink - No URL match.'),
5556
}
5657

57-
def __init__(self, view_name=None, **kwargs):
58-
self.lookup_field = kwargs.pop('lookup_field', self.lookup_field)
59-
self.lookup_url_kwarg = kwargs.pop('lookup_url_kwarg', self.lookup_field)
58+
def __init__(self, self_link_view_name=None, related_link_view_name=None, **kwargs):
59+
if self_link_view_name is not None:
60+
self.self_link_view_name = self_link_view_name
61+
if related_link_view_name is not None:
62+
self.related_link_view_name = related_link_view_name
63+
64+
self.related_link_lookup_field = kwargs.pop('related_link_lookup_field', self.related_link_lookup_field)
65+
self.related_link_url_kwarg = kwargs.pop('related_link_url_kwarg', self.related_link_lookup_field)
6066

6167
# We include this simply for dependency injection in tests.
6268
# We can't add it as a class attributes or it would expect an
@@ -65,30 +71,80 @@ def __init__(self, view_name=None, **kwargs):
6571

6672
super(ResourceRelatedField, self).__init__(**kwargs)
6773

68-
def get_url(self, obj, view_name, request):
74+
def use_pk_only_optimization(self):
75+
# We need the real object to determine its type...
76+
return False
77+
78+
def conflict(self, key, **kwargs):
6979
"""
70-
Given an object, return the URL that hyperlinks to the object.
80+
A helper method that simply raises a validation error.
81+
"""
82+
try:
83+
msg = self.error_messages[key]
84+
except KeyError:
85+
class_name = self.__class__.__name__
86+
msg = MISSING_ERROR_MESSAGE.format(class_name=class_name, key=key)
87+
raise AssertionError(msg)
88+
message_string = msg.format(**kwargs)
89+
raise Conflict(message_string)
90+
91+
def get_url(self, name, view_name, kwargs, request):
92+
"""
93+
Given a name, view name and kwargs, return the URL that hyperlinks to the object.
7194
7295
May raise a `NoReverseMatch` if the `view_name` and `lookup_field`
7396
attributes are not configured to correctly match the URL conf.
7497
"""
75-
# Unsaved objects will not yet have a valid URL.
76-
if hasattr(obj, 'pk') and obj.pk is None:
98+
99+
# Return None if the view name is not supplied
100+
if not view_name:
101+
return None
102+
103+
# Return the hyperlink, or error if incorrectly configured.
104+
try:
105+
url = self.reverse(view_name, kwargs=kwargs, request=request)
106+
except NoReverseMatch:
107+
msg = (
108+
'Could not resolve URL for hyperlinked relationship using '
109+
'view name "%s".'
110+
)
111+
raise ImproperlyConfigured(msg % view_name)
112+
113+
if url is None:
77114
return None
78115

79-
lookup_value = getattr(obj, self.lookup_field)
80-
kwargs = {self.lookup_url_kwarg: lookup_value}
81-
return self.reverse(view_name, kwargs=kwargs, request=request)
116+
return Hyperlink(url, name)
117+
118+
def get_links(self):
119+
request = self.context.get('request', None)
120+
view = self.context.get('view', None)
121+
return_data = OrderedDict()
122+
self_kwargs = view.kwargs.copy()
123+
self_kwargs.update({'related_field': self.field_name if self.field_name else self.parent.field_name})
124+
self_link = self.get_url('self', self.self_link_view_name, self_kwargs, request)
125+
126+
related_kwargs = {self.related_link_url_kwarg: view.kwargs[self.related_link_lookup_field]}
127+
related_link = self.get_url('related', self.related_link_view_name, related_kwargs, request)
128+
129+
if self_link:
130+
return_data.update({'self': self_link})
131+
if related_link:
132+
return_data.update({'related': related_link})
133+
return return_data
82134

83135
def to_internal_value(self, data):
84136
expected_relation_type = get_resource_type_from_queryset(self.queryset)
137+
if not isinstance(data, dict):
138+
self.fail('incorrect_type', data_type=type(data).__name__)
85139
if data['type'] != expected_relation_type:
86-
self.fail('incorrect_relation_type', relation_type=expected_relation_type, received_type=data['type'])
140+
self.conflict('incorrect_relation_type', relation_type=expected_relation_type, received_type=data['type'])
87141
return super(ResourceRelatedField, self).to_internal_value(data['id'])
88142

89143
def to_representation(self, value):
90-
return {
91-
'type': format_relation_name(get_resource_type_from_instance(value)),
92-
'id': str(value.pk)
93-
}
144+
if self.pk_field is not None:
145+
pk = self.pk_field.to_representation(value.pk)
146+
else:
147+
pk = value.pk
148+
149+
return OrderedDict([('type', format_relation_name(get_resource_type_from_instance(value))), ('id', str(pk))])
94150

rest_framework_json_api/utils.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from rest_framework.settings import api_settings
1212
from rest_framework.exceptions import APIException
1313

14+
1415
try:
1516
from rest_framework.compat import OrderedDict
1617
except ImportError:
@@ -237,6 +238,9 @@ def extract_attributes(fields, resource):
237238

238239

239240
def extract_relationships(fields, resource, resource_instance):
241+
# Avoid circular deps
242+
from rest_framework_json_api.relations import ResourceRelatedField
243+
240244
data = OrderedDict()
241245

242246
# Don't try to extract relationships from a non-existent resource
@@ -254,7 +258,7 @@ def extract_relationships(fields, resource, resource_instance):
254258

255259
try:
256260
relation_instance_or_manager = getattr(resource_instance, field_name)
257-
except AttributeError: # Skip fields defined on the serializer that don't correspond to a field on the model
261+
except AttributeError: # Skip fields defined on the serializer that don't correspond to a field on the model
258262
continue
259263

260264
relation_type = get_related_resource_type(field)
@@ -282,6 +286,20 @@ def extract_relationships(fields, resource, resource_instance):
282286
}})
283287
continue
284288

289+
if isinstance(field, ResourceRelatedField):
290+
# special case for ResourceRelatedField
291+
relation_data = {
292+
'data': resource.get(field_name)
293+
}
294+
295+
field_links = field.get_links()
296+
relation_data.update(
297+
{'links': field_links}
298+
if field_links else dict()
299+
)
300+
data.update({field_name: relation_data})
301+
continue
302+
285303
if isinstance(field, (PrimaryKeyRelatedField, HyperlinkedRelatedField)):
286304
relation_id = relation_instance_or_manager.pk if resource.get(field_name) else None
287305

@@ -299,6 +317,28 @@ def extract_relationships(fields, resource, resource_instance):
299317
continue
300318

301319
if isinstance(field, ManyRelatedField):
320+
321+
if isinstance(field.child_relation, ResourceRelatedField):
322+
# special case for ResourceRelatedField
323+
relation_data = {
324+
'data': resource.get(field_name)
325+
}
326+
327+
field_links = field.child_relation.get_links()
328+
relation_data.update(
329+
{'links': field_links}
330+
if field_links else dict()
331+
)
332+
relation_data.update(
333+
{
334+
'meta': {
335+
'count': len(resource.get(field_name))
336+
}
337+
}
338+
)
339+
data.update({field_name: relation_data})
340+
continue
341+
302342
relation_data = list()
303343
for related_object in relation_instance_or_manager.all():
304344
related_object_type = get_instance_or_manager_resource_type(relation_instance_or_manager)

0 commit comments

Comments
 (0)