mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-21 17:16:47 +03:00
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:
parent
ea03e95174
commit
959085c145
|
@ -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):
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user