Skip to content

Commit 9fe6a10

Browse files
committed
Merge pull request encode#694 from craigds/master
fix function names and dotted lookups for use in PrimaryKeyRelatedField
2 parents a73c16b + c992b60 commit 9fe6a10

File tree

5 files changed

+193
-23
lines changed

5 files changed

+193
-23
lines changed

rest_framework/relations.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -221,12 +221,20 @@ def from_native(self, data):
221221
def field_to_native(self, obj, field_name):
222222
if self.many:
223223
# To-many relationship
224-
try:
224+
225+
queryset = None
226+
if not self.source:
225227
# Prefer obj.serializable_value for performance reasons
226-
queryset = obj.serializable_value(self.source or field_name)
227-
except AttributeError:
228+
try:
229+
queryset = obj.serializable_value(field_name)
230+
except AttributeError:
231+
pass
232+
if queryset is None:
228233
# RelatedManager (reverse relationship)
229-
queryset = getattr(obj, self.source or field_name)
234+
source = self.source or field_name
235+
queryset = obj
236+
for component in source.split('.'):
237+
queryset = get_component(queryset, component)
230238

231239
# Forward relationship
232240
return [self.to_native(item.pk) for item in queryset.all()]

rest_framework/tests/relations.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from django.db import models
66
from django.test import TestCase
77
from rest_framework import serializers
8+
from rest_framework.tests.models import BlogPost
89

910

1011
class NullModel(models.Model):
@@ -33,7 +34,7 @@ def test_slug_related_field_with_empty_string(self):
3334
self.assertRaises(serializers.ValidationError, field.from_native, [])
3435

3536

36-
class TestManyRelateMixin(TestCase):
37+
class TestManyRelatedMixin(TestCase):
3738
def test_missing_many_to_many_related_field(self):
3839
'''
3940
Regression test for #632
@@ -45,3 +46,55 @@ def test_missing_many_to_many_related_field(self):
4546
into = {}
4647
field.field_from_native({}, None, 'field_name', into)
4748
self.assertEqual(into['field_name'], [])
49+
50+
51+
# Regression tests for #694 (`source` attribute on related fields)
52+
53+
class RelatedFieldSourceTests(TestCase):
54+
def test_related_manager_source(self):
55+
"""
56+
Relational fields should be able to use manager-returning methods as their source.
57+
"""
58+
BlogPost.objects.create(title='blah')
59+
field = serializers.RelatedField(many=True, source='get_blogposts_manager')
60+
61+
class ClassWithManagerMethod(object):
62+
def get_blogposts_manager(self):
63+
return BlogPost.objects
64+
65+
obj = ClassWithManagerMethod()
66+
value = field.field_to_native(obj, 'field_name')
67+
self.assertEqual(value, ['BlogPost object'])
68+
69+
def test_related_queryset_source(self):
70+
"""
71+
Relational fields should be able to use queryset-returning methods as their source.
72+
"""
73+
BlogPost.objects.create(title='blah')
74+
field = serializers.RelatedField(many=True, source='get_blogposts_queryset')
75+
76+
class ClassWithQuerysetMethod(object):
77+
def get_blogposts_queryset(self):
78+
return BlogPost.objects.all()
79+
80+
obj = ClassWithQuerysetMethod()
81+
value = field.field_to_native(obj, 'field_name')
82+
self.assertEqual(value, ['BlogPost object'])
83+
84+
def test_dotted_source(self):
85+
"""
86+
Source argument should support dotted.source notation.
87+
"""
88+
BlogPost.objects.create(title='blah')
89+
field = serializers.RelatedField(many=True, source='a.b.c')
90+
91+
class ClassWithQuerysetMethod(object):
92+
a = {
93+
'b': {
94+
'c': BlogPost.objects.all()
95+
}
96+
}
97+
98+
obj = ClassWithQuerysetMethod()
99+
value = field.field_to_native(obj, 'field_name')
100+
self.assertEqual(value, ['BlogPost object'])

rest_framework/tests/relations_hyperlink.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from rest_framework import serializers
55
from rest_framework.compat import patterns, url
66
from rest_framework.tests.models import (
7+
BlogPost,
78
ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
89
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
910
)
@@ -16,6 +17,7 @@ def dummy_view(request, pk):
1617
pass
1718

1819
urlpatterns = patterns('',
20+
url(r'^dummyurl/(?P<pk>[0-9]+)/$', dummy_view, name='dummy-url'),
1921
url(r'^manytomanysource/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanysource-detail'),
2022
url(r'^manytomanytarget/(?P<pk>[0-9]+)/$', dummy_view, name='manytomanytarget-detail'),
2123
url(r'^foreignkeysource/(?P<pk>[0-9]+)/$', dummy_view, name='foreignkeysource-detail'),
@@ -451,3 +453,72 @@ def test_reverse_foreign_key_retrieve_with_null(self):
451453
{'url': 'http://testserver/onetoonetarget/2/', 'name': 'target-2', 'nullable_source': None},
452454
]
453455
self.assertEqual(serializer.data, expected)
456+
457+
458+
# Regression tests for #694 (`source` attribute on related fields)
459+
460+
class HyperlinkedRelatedFieldSourceTests(TestCase):
461+
urls = 'rest_framework.tests.relations_hyperlink'
462+
463+
def test_related_manager_source(self):
464+
"""
465+
Relational fields should be able to use manager-returning methods as their source.
466+
"""
467+
BlogPost.objects.create(title='blah')
468+
field = serializers.HyperlinkedRelatedField(
469+
many=True,
470+
source='get_blogposts_manager',
471+
view_name='dummy-url',
472+
)
473+
field.context = {'request': request}
474+
475+
class ClassWithManagerMethod(object):
476+
def get_blogposts_manager(self):
477+
return BlogPost.objects
478+
479+
obj = ClassWithManagerMethod()
480+
value = field.field_to_native(obj, 'field_name')
481+
self.assertEqual(value, ['http://testserver/dummyurl/1/'])
482+
483+
def test_related_queryset_source(self):
484+
"""
485+
Relational fields should be able to use queryset-returning methods as their source.
486+
"""
487+
BlogPost.objects.create(title='blah')
488+
field = serializers.HyperlinkedRelatedField(
489+
many=True,
490+
source='get_blogposts_queryset',
491+
view_name='dummy-url',
492+
)
493+
field.context = {'request': request}
494+
495+
class ClassWithQuerysetMethod(object):
496+
def get_blogposts_queryset(self):
497+
return BlogPost.objects.all()
498+
499+
obj = ClassWithQuerysetMethod()
500+
value = field.field_to_native(obj, 'field_name')
501+
self.assertEqual(value, ['http://testserver/dummyurl/1/'])
502+
503+
def test_dotted_source(self):
504+
"""
505+
Source argument should support dotted.source notation.
506+
"""
507+
BlogPost.objects.create(title='blah')
508+
field = serializers.HyperlinkedRelatedField(
509+
many=True,
510+
source='a.b.c',
511+
view_name='dummy-url',
512+
)
513+
field.context = {'request': request}
514+
515+
class ClassWithQuerysetMethod(object):
516+
a = {
517+
'b': {
518+
'c': BlogPost.objects.all()
519+
}
520+
}
521+
522+
obj = ClassWithQuerysetMethod()
523+
value = field.field_to_native(obj, 'field_name')
524+
self.assertEqual(value, ['http://testserver/dummyurl/1/'])

rest_framework/tests/relations_pk.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from __future__ import unicode_literals
22
from django.test import TestCase
33
from rest_framework import serializers
4-
from rest_framework.tests.models import ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource, NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource
4+
from rest_framework.tests.models import (
5+
BlogPost, ManyToManyTarget, ManyToManySource, ForeignKeyTarget, ForeignKeySource,
6+
NullableForeignKeySource, OneToOneTarget, NullableOneToOneSource,
7+
)
58
from rest_framework.compat import six
69

710

@@ -421,3 +424,55 @@ def test_reverse_foreign_key_retrieve_with_null(self):
421424
{'id': 2, 'name': 'target-2', 'nullable_source': 1},
422425
]
423426
self.assertEqual(serializer.data, expected)
427+
428+
429+
# Regression tests for #694 (`source` attribute on related fields)
430+
431+
class PrimaryKeyRelatedFieldSourceTests(TestCase):
432+
def test_related_manager_source(self):
433+
"""
434+
Relational fields should be able to use manager-returning methods as their source.
435+
"""
436+
BlogPost.objects.create(title='blah')
437+
field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_manager')
438+
439+
class ClassWithManagerMethod(object):
440+
def get_blogposts_manager(self):
441+
return BlogPost.objects
442+
443+
obj = ClassWithManagerMethod()
444+
value = field.field_to_native(obj, 'field_name')
445+
self.assertEqual(value, [1])
446+
447+
def test_related_queryset_source(self):
448+
"""
449+
Relational fields should be able to use queryset-returning methods as their source.
450+
"""
451+
BlogPost.objects.create(title='blah')
452+
field = serializers.PrimaryKeyRelatedField(many=True, source='get_blogposts_queryset')
453+
454+
class ClassWithQuerysetMethod(object):
455+
def get_blogposts_queryset(self):
456+
return BlogPost.objects.all()
457+
458+
obj = ClassWithQuerysetMethod()
459+
value = field.field_to_native(obj, 'field_name')
460+
self.assertEqual(value, [1])
461+
462+
def test_dotted_source(self):
463+
"""
464+
Source argument should support dotted.source notation.
465+
"""
466+
BlogPost.objects.create(title='blah')
467+
field = serializers.PrimaryKeyRelatedField(many=True, source='a.b.c')
468+
469+
class ClassWithQuerysetMethod(object):
470+
a = {
471+
'b': {
472+
'c': BlogPost.objects.all()
473+
}
474+
}
475+
476+
obj = ClassWithQuerysetMethod()
477+
value = field.field_to_native(obj, 'field_name')
478+
self.assertEqual(value, [1])

rest_framework/tests/serializer.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -871,23 +871,6 @@ class NullableSourceSerializer(serializers.Serializer):
871871

872872
self.assertEqual(serializer.data, expected)
873873

874-
def test_queryset_nested_traversal(self):
875-
"""
876-
Relational fields should be able to use methods as their source.
877-
"""
878-
BlogPost.objects.create(title='blah')
879-
880-
class QuerysetMethodSerializer(serializers.Serializer):
881-
blogposts = serializers.RelatedField(many=True, source='get_all_blogposts')
882-
883-
class ClassWithQuerysetMethod(object):
884-
def get_all_blogposts(self):
885-
return BlogPost.objects
886-
887-
obj = ClassWithQuerysetMethod()
888-
serializer = QuerysetMethodSerializer(obj)
889-
self.assertEqual(serializer.data, {'blogposts': ['BlogPost object']})
890-
891874

892875
class SerializerMethodFieldTests(TestCase):
893876
def setUp(self):

0 commit comments

Comments
 (0)