mirror of
https://github.com/encode/django-rest-framework.git
synced 2024-11-24 10:34:03 +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 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):
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user