mirror of
				https://github.com/encode/django-rest-framework.git
				synced 2025-11-04 01:47:59 +03:00 
			
		
		
		
	Ensure that html forms (multipart form data) respect optional fields (#5927)
This commit is contained in:
		
							parent
							
								
									7e705246ca
								
							
						
					
					
						commit
						f148e4e259
					
				| 
						 | 
					@ -1614,7 +1614,8 @@ class ListField(Field):
 | 
				
			||||||
            if len(val) > 0:
 | 
					            if len(val) > 0:
 | 
				
			||||||
                # Support QueryDict lists in HTML input.
 | 
					                # Support QueryDict lists in HTML input.
 | 
				
			||||||
                return val
 | 
					                return val
 | 
				
			||||||
            return html.parse_html_list(dictionary, prefix=self.field_name)
 | 
					            return html.parse_html_list(dictionary, prefix=self.field_name, default=empty)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return dictionary.get(self.field_name, empty)
 | 
					        return dictionary.get(self.field_name, empty)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def to_internal_value(self, data):
 | 
					    def to_internal_value(self, data):
 | 
				
			||||||
| 
						 | 
					@ -1622,7 +1623,7 @@ class ListField(Field):
 | 
				
			||||||
        List of dicts of native values <- List of dicts of primitive datatypes.
 | 
					        List of dicts of native values <- List of dicts of primitive datatypes.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if html.is_html_input(data):
 | 
					        if html.is_html_input(data):
 | 
				
			||||||
            data = html.parse_html_list(data)
 | 
					            data = html.parse_html_list(data, default=[])
 | 
				
			||||||
        if isinstance(data, type('')) or isinstance(data, collections.Mapping) or not hasattr(data, '__iter__'):
 | 
					        if isinstance(data, type('')) or isinstance(data, collections.Mapping) or not hasattr(data, '__iter__'):
 | 
				
			||||||
            self.fail('not_a_list', input_type=type(data).__name__)
 | 
					            self.fail('not_a_list', input_type=type(data).__name__)
 | 
				
			||||||
        if not self.allow_empty and len(data) == 0:
 | 
					        if not self.allow_empty and len(data) == 0:
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -607,7 +607,7 @@ class ListSerializer(BaseSerializer):
 | 
				
			||||||
        # We override the default field access in order to support
 | 
					        # We override the default field access in order to support
 | 
				
			||||||
        # lists in HTML forms.
 | 
					        # lists in HTML forms.
 | 
				
			||||||
        if html.is_html_input(dictionary):
 | 
					        if html.is_html_input(dictionary):
 | 
				
			||||||
            return html.parse_html_list(dictionary, prefix=self.field_name)
 | 
					            return html.parse_html_list(dictionary, prefix=self.field_name, default=empty)
 | 
				
			||||||
        return dictionary.get(self.field_name, empty)
 | 
					        return dictionary.get(self.field_name, empty)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def run_validation(self, data=empty):
 | 
					    def run_validation(self, data=empty):
 | 
				
			||||||
| 
						 | 
					@ -635,7 +635,7 @@ class ListSerializer(BaseSerializer):
 | 
				
			||||||
        List of dicts of native values <- List of dicts of primitive datatypes.
 | 
					        List of dicts of native values <- List of dicts of primitive datatypes.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        if html.is_html_input(data):
 | 
					        if html.is_html_input(data):
 | 
				
			||||||
            data = html.parse_html_list(data)
 | 
					            data = html.parse_html_list(data, default=[])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not isinstance(data, list):
 | 
					        if not isinstance(data, list):
 | 
				
			||||||
            message = self.error_messages['not_a_list'].format(
 | 
					            message = self.error_messages['not_a_list'].format(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -12,7 +12,7 @@ def is_html_input(dictionary):
 | 
				
			||||||
    return hasattr(dictionary, 'getlist')
 | 
					    return hasattr(dictionary, 'getlist')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def parse_html_list(dictionary, prefix=''):
 | 
					def parse_html_list(dictionary, prefix='', default=None):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Used to support list values in HTML forms.
 | 
					    Used to support list values in HTML forms.
 | 
				
			||||||
    Supports lists of primitives and/or dictionaries.
 | 
					    Supports lists of primitives and/or dictionaries.
 | 
				
			||||||
| 
						 | 
					@ -44,6 +44,8 @@ def parse_html_list(dictionary, prefix=''):
 | 
				
			||||||
        {'foo': 'abc', 'bar': 'def'},
 | 
					        {'foo': 'abc', 'bar': 'def'},
 | 
				
			||||||
        {'foo': 'hij', 'bar': 'klm'}
 | 
					        {'foo': 'hij', 'bar': 'klm'}
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    :returns a list of objects, or the value specified in ``default`` if the list is empty
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    ret = {}
 | 
					    ret = {}
 | 
				
			||||||
    regex = re.compile(r'^%s\[([0-9]+)\](.*)$' % re.escape(prefix))
 | 
					    regex = re.compile(r'^%s\[([0-9]+)\](.*)$' % re.escape(prefix))
 | 
				
			||||||
| 
						 | 
					@ -59,7 +61,9 @@ def parse_html_list(dictionary, prefix=''):
 | 
				
			||||||
            ret[index][key] = value
 | 
					            ret[index][key] = value
 | 
				
			||||||
        else:
 | 
					        else:
 | 
				
			||||||
            ret[index] = MultiValueDict({key: [value]})
 | 
					            ret[index] = MultiValueDict({key: [value]})
 | 
				
			||||||
    return [ret[item] for item in sorted(ret)]
 | 
					
 | 
				
			||||||
 | 
					    # return the items of the ``ret`` dict, sorted by key, or ``default`` if the dict is empty
 | 
				
			||||||
 | 
					    return [ret[item] for item in sorted(ret)] if ret else default
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def parse_html_dict(dictionary, prefix=''):
 | 
					def parse_html_dict(dictionary, prefix=''):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -466,6 +466,55 @@ class TestHTMLInput:
 | 
				
			||||||
        assert serializer.is_valid()
 | 
					        assert serializer.is_valid()
 | 
				
			||||||
        assert serializer.validated_data == {'scores': [1]}
 | 
					        assert serializer.validated_data == {'scores': [1]}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_querydict_list_input_no_values_uses_default(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        When there are no values passed in, and default is set
 | 
				
			||||||
 | 
					        The field should return the default value
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        class TestSerializer(serializers.Serializer):
 | 
				
			||||||
 | 
					            a = serializers.IntegerField(required=True)
 | 
				
			||||||
 | 
					            scores = serializers.ListField(default=lambda: [1, 3])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        serializer = TestSerializer(data=QueryDict('a=1&'))
 | 
				
			||||||
 | 
					        assert serializer.is_valid()
 | 
				
			||||||
 | 
					        assert serializer.validated_data == {'a': 1, 'scores': [1, 3]}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_querydict_list_input_supports_indexed_keys(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        When data is passed in the format `scores[0]=1&scores[1]=3`
 | 
				
			||||||
 | 
					        The field should return the correct list, ignoring the default
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        class TestSerializer(serializers.Serializer):
 | 
				
			||||||
 | 
					            scores = serializers.ListField(default=lambda: [1, 3])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        serializer = TestSerializer(data=QueryDict("scores[0]=5&scores[1]=6"))
 | 
				
			||||||
 | 
					        assert serializer.is_valid()
 | 
				
			||||||
 | 
					        assert serializer.validated_data == {'scores': ['5', '6']}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_querydict_list_input_no_values_no_default_and_not_required(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        When there are no keys passed, there is no default, and required=False
 | 
				
			||||||
 | 
					        The field should be skipped
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        class TestSerializer(serializers.Serializer):
 | 
				
			||||||
 | 
					            scores = serializers.ListField(required=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        serializer = TestSerializer(data=QueryDict(''))
 | 
				
			||||||
 | 
					        assert serializer.is_valid()
 | 
				
			||||||
 | 
					        assert serializer.validated_data == {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_querydict_list_input_posts_key_but_no_values(self):
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        When there are no keys passed, there is no default, and required=False
 | 
				
			||||||
 | 
					        The field should return an array of 1 item, blank
 | 
				
			||||||
 | 
					        """
 | 
				
			||||||
 | 
					        class TestSerializer(serializers.Serializer):
 | 
				
			||||||
 | 
					            scores = serializers.ListField(required=False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        serializer = TestSerializer(data=QueryDict('scores=&'))
 | 
				
			||||||
 | 
					        assert serializer.is_valid()
 | 
				
			||||||
 | 
					        assert serializer.validated_data == {'scores': ['']}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestCreateOnlyDefault:
 | 
					class TestCreateOnlyDefault:
 | 
				
			||||||
    def setup(self):
 | 
					    def setup(self):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					from django.http import QueryDict
 | 
				
			||||||
from django.utils.datastructures import MultiValueDict
 | 
					from django.utils.datastructures import MultiValueDict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from rest_framework import serializers
 | 
					from rest_framework import serializers
 | 
				
			||||||
| 
						 | 
					@ -532,3 +533,32 @@ class TestSerializerPartialUsage:
 | 
				
			||||||
                assert value == updated_data_list[index][key]
 | 
					                assert value == updated_data_list[index][key]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert serializer.errors == {}
 | 
					        assert serializer.errors == {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestEmptyListSerializer:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Tests the behaviour of ListSerializers when there is no data passed to it
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def setup(self):
 | 
				
			||||||
 | 
					        class ExampleListSerializer(serializers.ListSerializer):
 | 
				
			||||||
 | 
					            child = serializers.IntegerField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.Serializer = ExampleListSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_nested_serializer_with_list_json(self):
 | 
				
			||||||
 | 
					        # pass an empty array to the serializer
 | 
				
			||||||
 | 
					        input_data = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        serializer = self.Serializer(data=input_data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert serializer.is_valid()
 | 
				
			||||||
 | 
					        assert serializer.validated_data == []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_nested_serializer_with_list_multipart(self):
 | 
				
			||||||
 | 
					        # pass an "empty" QueryDict to the serializer (should be the same as an empty array)
 | 
				
			||||||
 | 
					        input_data = QueryDict('')
 | 
				
			||||||
 | 
					        serializer = self.Serializer(data=input_data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert serializer.is_valid()
 | 
				
			||||||
 | 
					        assert serializer.validated_data == []
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -202,3 +202,42 @@ class TestNestedSerializerWithList:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        assert serializer.is_valid()
 | 
					        assert serializer.is_valid()
 | 
				
			||||||
        assert serializer.validated_data['nested']['example'] == {1, 2}
 | 
					        assert serializer.validated_data['nested']['example'] == {1, 2}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestNotRequiredNestedSerializerWithMany:
 | 
				
			||||||
 | 
					    def setup(self):
 | 
				
			||||||
 | 
					        class NestedSerializer(serializers.Serializer):
 | 
				
			||||||
 | 
					            one = serializers.IntegerField(max_value=10)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        class TestSerializer(serializers.Serializer):
 | 
				
			||||||
 | 
					            nested = NestedSerializer(required=False, many=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.Serializer = TestSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_json_validate(self):
 | 
				
			||||||
 | 
					        input_data = {}
 | 
				
			||||||
 | 
					        serializer = self.Serializer(data=input_data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # request is empty, therefor 'nested' should not be in serializer.data
 | 
				
			||||||
 | 
					        assert serializer.is_valid()
 | 
				
			||||||
 | 
					        assert 'nested' not in serializer.validated_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        input_data = {'nested': [{'one': '1'}, {'one': 2}]}
 | 
				
			||||||
 | 
					        serializer = self.Serializer(data=input_data)
 | 
				
			||||||
 | 
					        assert serializer.is_valid()
 | 
				
			||||||
 | 
					        assert 'nested' in serializer.validated_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_multipart_validate(self):
 | 
				
			||||||
 | 
					        # leave querydict empty
 | 
				
			||||||
 | 
					        input_data = QueryDict('')
 | 
				
			||||||
 | 
					        serializer = self.Serializer(data=input_data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # the querydict is empty, therefor 'nested' should not be in serializer.data
 | 
				
			||||||
 | 
					        assert serializer.is_valid()
 | 
				
			||||||
 | 
					        assert 'nested' not in serializer.validated_data
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        input_data = QueryDict('nested[0]one=1&nested[1]one=2')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        serializer = self.Serializer(data=input_data)
 | 
				
			||||||
 | 
					        assert serializer.is_valid()
 | 
				
			||||||
 | 
					        assert 'nested' in serializer.validated_data
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user