Skip to content

Commit 959085c

Browse files
ruddraArnab Shil
andauthored
Handle Nested Relation in SlugRelatedField when many=False (#8922)
* Update relations.py Currently if you define the slug field as a nested relationship in a `SlugRelatedField` while many=False, it will cause an attribute error. For example: For this code: ``` class SomeSerializer(serializers.ModelSerializer): some_field= serializers.SlugRelatedField(queryset=SomeClass.objects.all(), slug_field="foo__bar") ``` The POST request (or save operation) should work just fine, but if you use GET, then it will fail with Attribute error: > AttributeError: 'SomeClass' object has no attribute 'foo__bar' Thus I am handling nested relation here. Reference: https://stackoverflow.com/questions/75878103/drf-attributeerror-when-trying-to-creating-a-instance-with-slugrelatedfield-and/75882424#75882424 * Fixed test cases * code comment changes related to slugrelatedfield * changes based on pre-commit and removed comma which was added accidentally * fixed primary keys of the mock object * added more test cases based on review --------- Co-authored-by: Arnab Shil <[email protected]>
1 parent ea03e95 commit 959085c

File tree

3 files changed

+147
-2
lines changed

3 files changed

+147
-2
lines changed

rest_framework/relations.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import contextlib
22
import sys
33
from collections import OrderedDict
4+
from operator import attrgetter
45
from urllib import parse
56

67
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
@@ -71,6 +72,7 @@ class PKOnlyObject:
7172
instance, but still want to return an object with a .pk attribute,
7273
in order to keep the same interface as a regular model instance.
7374
"""
75+
7476
def __init__(self, pk):
7577
self.pk = pk
7678

@@ -464,7 +466,11 @@ def to_internal_value(self, data):
464466
self.fail('invalid')
465467

466468
def to_representation(self, obj):
467-
return getattr(obj, self.slug_field)
469+
slug = self.slug_field
470+
if "__" in slug:
471+
# handling nested relationship if defined
472+
slug = slug.replace('__', '.')
473+
return attrgetter(slug)(obj)
468474

469475

470476
class ManyRelatedField(Field):

tests/test_relations.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,142 @@ def get_queryset(self):
342342
field.to_internal_value(self.instance.name)
343343

344344

345+
class TestNestedSlugRelatedField(APISimpleTestCase):
346+
def setUp(self):
347+
self.queryset = MockQueryset([
348+
MockObject(
349+
pk=1, name='foo', nested=MockObject(
350+
pk=2, name='bar', nested=MockObject(
351+
pk=7, name="foobar"
352+
)
353+
)
354+
),
355+
MockObject(
356+
pk=3, name='hello', nested=MockObject(
357+
pk=4, name='world', nested=MockObject(
358+
pk=8, name="helloworld"
359+
)
360+
)
361+
),
362+
MockObject(
363+
pk=5, name='harry', nested=MockObject(
364+
pk=6, name='potter', nested=MockObject(
365+
pk=9, name="harrypotter"
366+
)
367+
)
368+
)
369+
])
370+
self.instance = self.queryset.items[2]
371+
self.field = serializers.SlugRelatedField(
372+
slug_field='name', queryset=self.queryset
373+
)
374+
self.nested_field = serializers.SlugRelatedField(
375+
slug_field='nested__name', queryset=self.queryset
376+
)
377+
378+
self.nested_nested_field = serializers.SlugRelatedField(
379+
slug_field='nested__nested__name', queryset=self.queryset
380+
)
381+
382+
# testing nested inside nested relations
383+
def test_slug_related_nested_nested_lookup_exists(self):
384+
instance = self.nested_nested_field.to_internal_value(
385+
self.instance.nested.nested.name
386+
)
387+
assert instance is self.instance
388+
389+
def test_slug_related_nested_nested_lookup_does_not_exist(self):
390+
with pytest.raises(serializers.ValidationError) as excinfo:
391+
self.nested_nested_field.to_internal_value('doesnotexist')
392+
msg = excinfo.value.detail[0]
393+
assert msg == \
394+
'Object with nested__nested__name=doesnotexist does not exist.'
395+
396+
def test_slug_related_nested_nested_lookup_invalid_type(self):
397+
with pytest.raises(serializers.ValidationError) as excinfo:
398+
self.nested_nested_field.to_internal_value(BadType())
399+
msg = excinfo.value.detail[0]
400+
assert msg == 'Invalid value.'
401+
402+
def test_nested_nested_representation(self):
403+
representation =\
404+
self.nested_nested_field.to_representation(self.instance)
405+
assert representation == self.instance.nested.nested.name
406+
407+
def test_nested_nested_overriding_get_queryset(self):
408+
qs = self.queryset
409+
410+
class NoQuerySetSlugRelatedField(serializers.SlugRelatedField):
411+
def get_queryset(self):
412+
return qs
413+
414+
field = NoQuerySetSlugRelatedField(slug_field='nested__nested__name')
415+
field.to_internal_value(self.instance.nested.nested.name)
416+
417+
# testing nested relations
418+
def test_slug_related_nested_lookup_exists(self):
419+
instance = \
420+
self.nested_field.to_internal_value(self.instance.nested.name)
421+
assert instance is self.instance
422+
423+
def test_slug_related_nested_lookup_does_not_exist(self):
424+
with pytest.raises(serializers.ValidationError) as excinfo:
425+
self.nested_field.to_internal_value('doesnotexist')
426+
msg = excinfo.value.detail[0]
427+
assert msg == 'Object with nested__name=doesnotexist does not exist.'
428+
429+
def test_slug_related_nested_lookup_invalid_type(self):
430+
with pytest.raises(serializers.ValidationError) as excinfo:
431+
self.nested_field.to_internal_value(BadType())
432+
msg = excinfo.value.detail[0]
433+
assert msg == 'Invalid value.'
434+
435+
def test_nested_representation(self):
436+
representation = self.nested_field.to_representation(self.instance)
437+
assert representation == self.instance.nested.name
438+
439+
def test_nested_overriding_get_queryset(self):
440+
qs = self.queryset
441+
442+
class NoQuerySetSlugRelatedField(serializers.SlugRelatedField):
443+
def get_queryset(self):
444+
return qs
445+
446+
field = NoQuerySetSlugRelatedField(slug_field='nested__name')
447+
field.to_internal_value(self.instance.nested.name)
448+
449+
# testing non-nested relations
450+
def test_slug_related_lookup_exists(self):
451+
instance = self.field.to_internal_value(self.instance.name)
452+
assert instance is self.instance
453+
454+
def test_slug_related_lookup_does_not_exist(self):
455+
with pytest.raises(serializers.ValidationError) as excinfo:
456+
self.field.to_internal_value('doesnotexist')
457+
msg = excinfo.value.detail[0]
458+
assert msg == 'Object with name=doesnotexist does not exist.'
459+
460+
def test_slug_related_lookup_invalid_type(self):
461+
with pytest.raises(serializers.ValidationError) as excinfo:
462+
self.field.to_internal_value(BadType())
463+
msg = excinfo.value.detail[0]
464+
assert msg == 'Invalid value.'
465+
466+
def test_representation(self):
467+
representation = self.field.to_representation(self.instance)
468+
assert representation == self.instance.name
469+
470+
def test_overriding_get_queryset(self):
471+
qs = self.queryset
472+
473+
class NoQuerySetSlugRelatedField(serializers.SlugRelatedField):
474+
def get_queryset(self):
475+
return qs
476+
477+
field = NoQuerySetSlugRelatedField(slug_field='name')
478+
field.to_internal_value(self.instance.name)
479+
480+
345481
class TestManyRelatedField(APISimpleTestCase):
346482
def setUp(self):
347483
self.instance = MockObject(pk=1, name='foo')

tests/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from operator import attrgetter
2+
13
from django.core.exceptions import ObjectDoesNotExist
24
from django.urls import NoReverseMatch
35

@@ -26,7 +28,7 @@ def __getitem__(self, val):
2628
def get(self, **lookup):
2729
for item in self.items:
2830
if all([
29-
getattr(item, key, None) == value
31+
attrgetter(key.replace('__', '.'))(item) == value
3032
for key, value in lookup.items()
3133
]):
3234
return item
@@ -39,6 +41,7 @@ class BadType:
3941
will raise a `TypeError`, as occurs in Django when making
4042
queryset lookups with an incorrect type for the lookup value.
4143
"""
44+
4245
def __eq__(self):
4346
raise TypeError()
4447

0 commit comments

Comments
 (0)