From 950e0567cd28a46265f5aec49fd54bc2c808be92 Mon Sep 17 00:00:00 2001 From: Christian Kreuzberger Date: Mon, 9 Apr 2018 11:20:15 +0200 Subject: [PATCH 1/4] Fixed issue #5926: Ensure that html forms (multipart form data) respect optional fields --- rest_framework/fields.py | 3 ++- rest_framework/serializers.py | 4 ++-- rest_framework/utils/html.py | 6 +++-- tests/test_serializer_nested.py | 39 +++++++++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 58e28ed4ce..38aac36408 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1614,7 +1614,8 @@ def get_value(self, dictionary): if len(val) > 0: # Support QueryDict lists in HTML input. 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) def to_internal_value(self, data): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 7e84372def..34cb74a86c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -607,7 +607,7 @@ def get_value(self, dictionary): # We override the default field access in order to support # lists in HTML forms. 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) def run_validation(self, data=empty): @@ -635,7 +635,7 @@ def to_internal_value(self, data): List of dicts of native values <- List of dicts of primitive datatypes. """ if html.is_html_input(data): - data = html.parse_html_list(data) + data = html.parse_html_list(data, default=empty) if not isinstance(data, list): message = self.error_messages['not_a_list'].format( diff --git a/rest_framework/utils/html.py b/rest_framework/utils/html.py index 77167e4708..8fd6af567d 100644 --- a/rest_framework/utils/html.py +++ b/rest_framework/utils/html.py @@ -12,7 +12,7 @@ def is_html_input(dictionary): return hasattr(dictionary, 'getlist') -def parse_html_list(dictionary, prefix=''): +def parse_html_list(dictionary, prefix='', default=[]): """ Used to support list values in HTML forms. Supports lists of primitives and/or dictionaries. @@ -44,6 +44,8 @@ def parse_html_list(dictionary, prefix=''): {'foo': 'abc', 'bar': 'def'}, {'foo': 'hij', 'bar': 'klm'} ] + + :returns a list of objects, or the value specified in ``default`` if the list is empty """ ret = {} regex = re.compile(r'^%s\[([0-9]+)\](.*)$' % re.escape(prefix)) @@ -59,7 +61,7 @@ def parse_html_list(dictionary, prefix=''): ret[index][key] = value else: ret[index] = MultiValueDict({key: [value]}) - return [ret[item] for item in sorted(ret)] + return [ret[item] for item in sorted(ret)] if len(ret.keys()) > 0 else default def parse_html_dict(dictionary, prefix=''): diff --git a/tests/test_serializer_nested.py b/tests/test_serializer_nested.py index 09b8dd1052..1cd0caf854 100644 --- a/tests/test_serializer_nested.py +++ b/tests/test_serializer_nested.py @@ -202,3 +202,42 @@ def test_nested_serializer_with_list_multipart(self): assert serializer.is_valid() 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 From 823454da39415ec4c87213dfb39823756fadaf86 Mon Sep 17 00:00:00 2001 From: Christian Kreuzberger Date: Tue, 10 Apr 2018 11:55:34 +0200 Subject: [PATCH 2/4] Added tests of @awbacker provided in PR #5812 (includes review by @carltongibson) --- tests/test_fields.py | 49 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test_fields.py b/tests/test_fields.py index eee794eaab..ca4e4ecc52 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -466,6 +466,55 @@ class TestSerializer(serializers.Serializer): assert serializer.is_valid() 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: def setup(self): From 1cfd42d46c13bc2ad7639cc8e642ee79e8e30af7 Mon Sep 17 00:00:00 2001 From: Christian Kreuzberger Date: Tue, 17 Apr 2018 16:06:17 +0200 Subject: [PATCH 3/4] Avoid using a mutable as a default parameter --- rest_framework/fields.py | 2 +- rest_framework/utils/html.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 38aac36408..39050ff878 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1623,7 +1623,7 @@ def to_internal_value(self, data): List of dicts of native values <- List of dicts of primitive datatypes. """ 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__'): self.fail('not_a_list', input_type=type(data).__name__) if not self.allow_empty and len(data) == 0: diff --git a/rest_framework/utils/html.py b/rest_framework/utils/html.py index 8fd6af567d..c7ede78035 100644 --- a/rest_framework/utils/html.py +++ b/rest_framework/utils/html.py @@ -12,7 +12,7 @@ def is_html_input(dictionary): return hasattr(dictionary, 'getlist') -def parse_html_list(dictionary, prefix='', default=[]): +def parse_html_list(dictionary, prefix='', default=None): """ Used to support list values in HTML forms. Supports lists of primitives and/or dictionaries. @@ -61,7 +61,9 @@ def parse_html_list(dictionary, prefix='', default=[]): ret[index][key] = value else: ret[index] = MultiValueDict({key: [value]}) - return [ret[item] for item in sorted(ret)] if len(ret.keys()) > 0 else default + + # 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=''): From 02b0c1118a46523473a545d95b776ba7320f18c6 Mon Sep 17 00:00:00 2001 From: Christian Kreuzberger Date: Fri, 20 Apr 2018 08:36:57 +0200 Subject: [PATCH 4/4] Fixed an issue with ListSerializer.to_internal_value and also added a test for it --- rest_framework/serializers.py | 2 +- tests/test_serializer_lists.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 34cb74a86c..43c7972a4c 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -635,7 +635,7 @@ def to_internal_value(self, data): List of dicts of native values <- List of dicts of primitive datatypes. """ if html.is_html_input(data): - data = html.parse_html_list(data, default=empty) + data = html.parse_html_list(data, default=[]) if not isinstance(data, list): message = self.error_messages['not_a_list'].format( diff --git a/tests/test_serializer_lists.py b/tests/test_serializer_lists.py index 34c3d100b4..12ed78b84a 100644 --- a/tests/test_serializer_lists.py +++ b/tests/test_serializer_lists.py @@ -1,3 +1,4 @@ +from django.http import QueryDict from django.utils.datastructures import MultiValueDict from rest_framework import serializers @@ -532,3 +533,32 @@ class Serializer(serializers.Serializer): assert value == updated_data_list[index][key] 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 == [] pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy