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 <arnab.shil@thermofisher.com>
This commit is contained in:
Arnab Kumar Shil 2023-04-08 08:27:14 +02:00 committed by GitHub
parent ea03e95174
commit 959085c145
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 147 additions and 2 deletions

View File

@ -1,6 +1,7 @@
import contextlib
import sys
from collections import OrderedDict
from operator import attrgetter
from urllib import parse
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
@ -71,6 +72,7 @@ class PKOnlyObject:
instance, but still want to return an object with a .pk attribute,
in order to keep the same interface as a regular model instance.
"""
def __init__(self, pk):
self.pk = pk
@ -464,7 +466,11 @@ class SlugRelatedField(RelatedField):
self.fail('invalid')
def to_representation(self, obj):
return getattr(obj, self.slug_field)
slug = self.slug_field
if "__" in slug:
# handling nested relationship if defined
slug = slug.replace('__', '.')
return attrgetter(slug)(obj)
class ManyRelatedField(Field):

View File

@ -342,6 +342,142 @@ class TestSlugRelatedField(APISimpleTestCase):
field.to_internal_value(self.instance.name)
class TestNestedSlugRelatedField(APISimpleTestCase):
def setUp(self):
self.queryset = MockQueryset([
MockObject(
pk=1, name='foo', nested=MockObject(
pk=2, name='bar', nested=MockObject(
pk=7, name="foobar"
)
)
),
MockObject(
pk=3, name='hello', nested=MockObject(
pk=4, name='world', nested=MockObject(
pk=8, name="helloworld"
)
)
),
MockObject(
pk=5, name='harry', nested=MockObject(
pk=6, name='potter', nested=MockObject(
pk=9, name="harrypotter"
)
)
)
])
self.instance = self.queryset.items[2]
self.field = serializers.SlugRelatedField(
slug_field='name', queryset=self.queryset
)
self.nested_field = serializers.SlugRelatedField(
slug_field='nested__name', queryset=self.queryset
)
self.nested_nested_field = serializers.SlugRelatedField(
slug_field='nested__nested__name', queryset=self.queryset
)
# testing nested inside nested relations
def test_slug_related_nested_nested_lookup_exists(self):
instance = self.nested_nested_field.to_internal_value(
self.instance.nested.nested.name
)
assert instance is self.instance
def test_slug_related_nested_nested_lookup_does_not_exist(self):
with pytest.raises(serializers.ValidationError) as excinfo:
self.nested_nested_field.to_internal_value('doesnotexist')
msg = excinfo.value.detail[0]
assert msg == \
'Object with nested__nested__name=doesnotexist does not exist.'
def test_slug_related_nested_nested_lookup_invalid_type(self):
with pytest.raises(serializers.ValidationError) as excinfo:
self.nested_nested_field.to_internal_value(BadType())
msg = excinfo.value.detail[0]
assert msg == 'Invalid value.'
def test_nested_nested_representation(self):
representation =\
self.nested_nested_field.to_representation(self.instance)
assert representation == self.instance.nested.nested.name
def test_nested_nested_overriding_get_queryset(self):
qs = self.queryset
class NoQuerySetSlugRelatedField(serializers.SlugRelatedField):
def get_queryset(self):
return qs
field = NoQuerySetSlugRelatedField(slug_field='nested__nested__name')
field.to_internal_value(self.instance.nested.nested.name)
# testing nested relations
def test_slug_related_nested_lookup_exists(self):
instance = \
self.nested_field.to_internal_value(self.instance.nested.name)
assert instance is self.instance
def test_slug_related_nested_lookup_does_not_exist(self):
with pytest.raises(serializers.ValidationError) as excinfo:
self.nested_field.to_internal_value('doesnotexist')
msg = excinfo.value.detail[0]
assert msg == 'Object with nested__name=doesnotexist does not exist.'
def test_slug_related_nested_lookup_invalid_type(self):
with pytest.raises(serializers.ValidationError) as excinfo:
self.nested_field.to_internal_value(BadType())
msg = excinfo.value.detail[0]
assert msg == 'Invalid value.'
def test_nested_representation(self):
representation = self.nested_field.to_representation(self.instance)
assert representation == self.instance.nested.name
def test_nested_overriding_get_queryset(self):
qs = self.queryset
class NoQuerySetSlugRelatedField(serializers.SlugRelatedField):
def get_queryset(self):
return qs
field = NoQuerySetSlugRelatedField(slug_field='nested__name')
field.to_internal_value(self.instance.nested.name)
# testing non-nested relations
def test_slug_related_lookup_exists(self):
instance = self.field.to_internal_value(self.instance.name)
assert instance is self.instance
def test_slug_related_lookup_does_not_exist(self):
with pytest.raises(serializers.ValidationError) as excinfo:
self.field.to_internal_value('doesnotexist')
msg = excinfo.value.detail[0]
assert msg == 'Object with name=doesnotexist does not exist.'
def test_slug_related_lookup_invalid_type(self):
with pytest.raises(serializers.ValidationError) as excinfo:
self.field.to_internal_value(BadType())
msg = excinfo.value.detail[0]
assert msg == 'Invalid value.'
def test_representation(self):
representation = self.field.to_representation(self.instance)
assert representation == self.instance.name
def test_overriding_get_queryset(self):
qs = self.queryset
class NoQuerySetSlugRelatedField(serializers.SlugRelatedField):
def get_queryset(self):
return qs
field = NoQuerySetSlugRelatedField(slug_field='name')
field.to_internal_value(self.instance.name)
class TestManyRelatedField(APISimpleTestCase):
def setUp(self):
self.instance = MockObject(pk=1, name='foo')

View File

@ -1,3 +1,5 @@
from operator import attrgetter
from django.core.exceptions import ObjectDoesNotExist
from django.urls import NoReverseMatch
@ -26,7 +28,7 @@ class MockQueryset:
def get(self, **lookup):
for item in self.items:
if all([
getattr(item, key, None) == value
attrgetter(key.replace('__', '.'))(item) == value
for key, value in lookup.items()
]):
return item
@ -39,6 +41,7 @@ class BadType:
will raise a `TypeError`, as occurs in Django when making
queryset lookups with an incorrect type for the lookup value.
"""
def __eq__(self):
raise TypeError()