mirror of
				https://github.com/encode/django-rest-framework.git
				synced 2025-11-04 09:57:55 +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