Skip to content

Commit f148e4e

Browse files
anx-ckreuzbergercarltongibson
authored andcommitted
Ensure that html forms (multipart form data) respect optional fields (#5927)
1 parent 7e70524 commit f148e4e

File tree

6 files changed

+129
-6
lines changed

6 files changed

+129
-6
lines changed

rest_framework/fields.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -1614,15 +1614,16 @@ def get_value(self, dictionary):
16141614
if len(val) > 0:
16151615
# Support QueryDict lists in HTML input.
16161616
return val
1617-
return html.parse_html_list(dictionary, prefix=self.field_name)
1617+
return html.parse_html_list(dictionary, prefix=self.field_name, default=empty)
1618+
16181619
return dictionary.get(self.field_name, empty)
16191620

16201621
def to_internal_value(self, data):
16211622
"""
16221623
List of dicts of native values <- List of dicts of primitive datatypes.
16231624
"""
16241625
if html.is_html_input(data):
1625-
data = html.parse_html_list(data)
1626+
data = html.parse_html_list(data, default=[])
16261627
if isinstance(data, type('')) or isinstance(data, collections.Mapping) or not hasattr(data, '__iter__'):
16271628
self.fail('not_a_list', input_type=type(data).__name__)
16281629
if not self.allow_empty and len(data) == 0:

rest_framework/serializers.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,7 @@ def get_value(self, dictionary):
607607
# We override the default field access in order to support
608608
# lists in HTML forms.
609609
if html.is_html_input(dictionary):
610-
return html.parse_html_list(dictionary, prefix=self.field_name)
610+
return html.parse_html_list(dictionary, prefix=self.field_name, default=empty)
611611
return dictionary.get(self.field_name, empty)
612612

613613
def run_validation(self, data=empty):
@@ -635,7 +635,7 @@ def to_internal_value(self, data):
635635
List of dicts of native values <- List of dicts of primitive datatypes.
636636
"""
637637
if html.is_html_input(data):
638-
data = html.parse_html_list(data)
638+
data = html.parse_html_list(data, default=[])
639639

640640
if not isinstance(data, list):
641641
message = self.error_messages['not_a_list'].format(

rest_framework/utils/html.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def is_html_input(dictionary):
1212
return hasattr(dictionary, 'getlist')
1313

1414

15-
def parse_html_list(dictionary, prefix=''):
15+
def parse_html_list(dictionary, prefix='', default=None):
1616
"""
1717
Used to support list values in HTML forms.
1818
Supports lists of primitives and/or dictionaries.
@@ -44,6 +44,8 @@ def parse_html_list(dictionary, prefix=''):
4444
{'foo': 'abc', 'bar': 'def'},
4545
{'foo': 'hij', 'bar': 'klm'}
4646
]
47+
48+
:returns a list of objects, or the value specified in ``default`` if the list is empty
4749
"""
4850
ret = {}
4951
regex = re.compile(r'^%s\[([0-9]+)\](.*)$' % re.escape(prefix))
@@ -59,7 +61,9 @@ def parse_html_list(dictionary, prefix=''):
5961
ret[index][key] = value
6062
else:
6163
ret[index] = MultiValueDict({key: [value]})
62-
return [ret[item] for item in sorted(ret)]
64+
65+
# return the items of the ``ret`` dict, sorted by key, or ``default`` if the dict is empty
66+
return [ret[item] for item in sorted(ret)] if ret else default
6367

6468

6569
def parse_html_dict(dictionary, prefix=''):

tests/test_fields.py

+49
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,55 @@ class TestSerializer(serializers.Serializer):
466466
assert serializer.is_valid()
467467
assert serializer.validated_data == {'scores': [1]}
468468

469+
def test_querydict_list_input_no_values_uses_default(self):
470+
"""
471+
When there are no values passed in, and default is set
472+
The field should return the default value
473+
"""
474+
class TestSerializer(serializers.Serializer):
475+
a = serializers.IntegerField(required=True)
476+
scores = serializers.ListField(default=lambda: [1, 3])
477+
478+
serializer = TestSerializer(data=QueryDict('a=1&'))
479+
assert serializer.is_valid()
480+
assert serializer.validated_data == {'a': 1, 'scores': [1, 3]}
481+
482+
def test_querydict_list_input_supports_indexed_keys(self):
483+
"""
484+
When data is passed in the format `scores[0]=1&scores[1]=3`
485+
The field should return the correct list, ignoring the default
486+
"""
487+
class TestSerializer(serializers.Serializer):
488+
scores = serializers.ListField(default=lambda: [1, 3])
489+
490+
serializer = TestSerializer(data=QueryDict("scores[0]=5&scores[1]=6"))
491+
assert serializer.is_valid()
492+
assert serializer.validated_data == {'scores': ['5', '6']}
493+
494+
def test_querydict_list_input_no_values_no_default_and_not_required(self):
495+
"""
496+
When there are no keys passed, there is no default, and required=False
497+
The field should be skipped
498+
"""
499+
class TestSerializer(serializers.Serializer):
500+
scores = serializers.ListField(required=False)
501+
502+
serializer = TestSerializer(data=QueryDict(''))
503+
assert serializer.is_valid()
504+
assert serializer.validated_data == {}
505+
506+
def test_querydict_list_input_posts_key_but_no_values(self):
507+
"""
508+
When there are no keys passed, there is no default, and required=False
509+
The field should return an array of 1 item, blank
510+
"""
511+
class TestSerializer(serializers.Serializer):
512+
scores = serializers.ListField(required=False)
513+
514+
serializer = TestSerializer(data=QueryDict('scores=&'))
515+
assert serializer.is_valid()
516+
assert serializer.validated_data == {'scores': ['']}
517+
469518

470519
class TestCreateOnlyDefault:
471520
def setup(self):

tests/test_serializer_lists.py

+30
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from django.http import QueryDict
12
from django.utils.datastructures import MultiValueDict
23

34
from rest_framework import serializers
@@ -532,3 +533,32 @@ class Serializer(serializers.Serializer):
532533
assert value == updated_data_list[index][key]
533534

534535
assert serializer.errors == {}
536+
537+
538+
class TestEmptyListSerializer:
539+
"""
540+
Tests the behaviour of ListSerializers when there is no data passed to it
541+
"""
542+
543+
def setup(self):
544+
class ExampleListSerializer(serializers.ListSerializer):
545+
child = serializers.IntegerField()
546+
547+
self.Serializer = ExampleListSerializer
548+
549+
def test_nested_serializer_with_list_json(self):
550+
# pass an empty array to the serializer
551+
input_data = []
552+
553+
serializer = self.Serializer(data=input_data)
554+
555+
assert serializer.is_valid()
556+
assert serializer.validated_data == []
557+
558+
def test_nested_serializer_with_list_multipart(self):
559+
# pass an "empty" QueryDict to the serializer (should be the same as an empty array)
560+
input_data = QueryDict('')
561+
serializer = self.Serializer(data=input_data)
562+
563+
assert serializer.is_valid()
564+
assert serializer.validated_data == []

tests/test_serializer_nested.py

+39
Original file line numberDiff line numberDiff line change
@@ -202,3 +202,42 @@ def test_nested_serializer_with_list_multipart(self):
202202

203203
assert serializer.is_valid()
204204
assert serializer.validated_data['nested']['example'] == {1, 2}
205+
206+
207+
class TestNotRequiredNestedSerializerWithMany:
208+
def setup(self):
209+
class NestedSerializer(serializers.Serializer):
210+
one = serializers.IntegerField(max_value=10)
211+
212+
class TestSerializer(serializers.Serializer):
213+
nested = NestedSerializer(required=False, many=True)
214+
215+
self.Serializer = TestSerializer
216+
217+
def test_json_validate(self):
218+
input_data = {}
219+
serializer = self.Serializer(data=input_data)
220+
221+
# request is empty, therefor 'nested' should not be in serializer.data
222+
assert serializer.is_valid()
223+
assert 'nested' not in serializer.validated_data
224+
225+
input_data = {'nested': [{'one': '1'}, {'one': 2}]}
226+
serializer = self.Serializer(data=input_data)
227+
assert serializer.is_valid()
228+
assert 'nested' in serializer.validated_data
229+
230+
def test_multipart_validate(self):
231+
# leave querydict empty
232+
input_data = QueryDict('')
233+
serializer = self.Serializer(data=input_data)
234+
235+
# the querydict is empty, therefor 'nested' should not be in serializer.data
236+
assert serializer.is_valid()
237+
assert 'nested' not in serializer.validated_data
238+
239+
input_data = QueryDict('nested[0]one=1&nested[1]one=2')
240+
241+
serializer = self.Serializer(data=input_data)
242+
assert serializer.is_valid()
243+
assert 'nested' in serializer.validated_data

0 commit comments

Comments
 (0)