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 contextlib
import sys import sys
from collections import OrderedDict from collections import OrderedDict
from operator import attrgetter
from urllib import parse from urllib import parse
from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist 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, instance, but still want to return an object with a .pk attribute,
in order to keep the same interface as a regular model instance. in order to keep the same interface as a regular model instance.
""" """
def __init__(self, pk): def __init__(self, pk):
self.pk = pk self.pk = pk
@ -464,7 +466,11 @@ class SlugRelatedField(RelatedField):
self.fail('invalid') self.fail('invalid')
def to_representation(self, obj): 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): class ManyRelatedField(Field):

View File

@ -342,6 +342,142 @@ class TestSlugRelatedField(APISimpleTestCase):
field.to_internal_value(self.instance.name) 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): class TestManyRelatedField(APISimpleTestCase):
def setUp(self): def setUp(self):
self.instance = MockObject(pk=1, name='foo') 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.core.exceptions import ObjectDoesNotExist
from django.urls import NoReverseMatch from django.urls import NoReverseMatch
@ -26,7 +28,7 @@ class MockQueryset:
def get(self, **lookup): def get(self, **lookup):
for item in self.items: for item in self.items:
if all([ if all([
getattr(item, key, None) == value attrgetter(key.replace('__', '.'))(item) == value
for key, value in lookup.items() for key, value in lookup.items()
]): ]):
return item return item
@ -39,6 +41,7 @@ class BadType:
will raise a `TypeError`, as occurs in Django when making will raise a `TypeError`, as occurs in Django when making
queryset lookups with an incorrect type for the lookup value. queryset lookups with an incorrect type for the lookup value.
""" """
def __eq__(self): def __eq__(self):
raise TypeError() raise TypeError()