Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ coverage.*
!.github
!.gitignore
!.pre-commit-config.yaml

.idea
23 changes: 19 additions & 4 deletions rest_framework/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -1680,17 +1680,32 @@ def __init__(self, **kwargs):
self.validators.append(MinLengthValidator(self.min_length, message=message))

def get_value(self, dictionary):
if self.field_name not in dictionary:
if getattr(self.root, 'partial', False):
return empty
# We override the default field access in order to support
# lists in HTML forms.
if html.is_html_input(dictionary):
# First, try to get the value using the plain field name with getlist
# This handles standard HTML form list submissions like:
# <select multiple name="field"><option value="a">...
val = dictionary.getlist(self.field_name, [])
if len(val) > 0:
# Support QueryDict lists in HTML input.
return val
return html.parse_html_list(dictionary, prefix=self.field_name, default=empty)
# Second, check for indexed keys like field[0], field[1], etc.
# This handles form submissions with explicit indices
html_list = html.parse_html_list(dictionary, prefix=self.field_name, default=empty)
if html_list is not empty:
return html_list
# If no data found (neither plain list nor indexed keys)
# check if this is a partial update
if getattr(self.root, 'partial', False):
return empty
# For non-partial updates with no data, return empty
# This will trigger validation errors if the field is required
return empty
# Non-HTML input: standard dictionary access
if self.field_name not in dictionary:
if getattr(self.root, 'partial', False):
return empty

return dictionary.get(self.field_name, empty)

Expand Down
124 changes: 124 additions & 0 deletions tests/test_serializer_lists.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,130 @@ class MultipleChoiceSerializer(serializers.Serializer):
assert serializer.validated_data == {}
assert serializer.errors == {}

def test_partial_listfield_with_indexed_keys(self):
"""
Test that ListField respects ordered sequence in form data with partial updates.
Regression test for GitHub issue where indexed keys like field[0], field[1]
were not being parsed in partial updates.
"""
class CommunitySerializer(serializers.Serializer):
colors = serializers.ListField(
allow_null=True,
child=serializers.CharField(label='Colors', max_length=7),
required=False
)
# Simulate form data with indexed keys
data = MultiValueDict({
'colors[0]': ['#ffffff'],
'colors[1]': ['#000000']
})
serializer = CommunitySerializer(data=data, partial=True)
assert serializer.is_valid(), f"Expected valid but got errors: {serializer.errors}"
assert 'colors' in serializer.validated_data
assert serializer.validated_data['colors'] == ['#ffffff', '#000000']

def test_partial_listfield_with_non_indexed_list(self):
"""
Test that ListField still works with non-indexed list submission
in partial updates (backward compatibility check).
"""
class CommunitySerializer(serializers.Serializer):
colors = serializers.ListField(
allow_null=True,
child=serializers.CharField(label='Colors', max_length=7),
required=False
)
# Simulate standard HTML form list (e.g., multiple select)
data = MultiValueDict({
'colors': ['#ffffff', '#000000']
})
serializer = CommunitySerializer(data=data, partial=True)
assert serializer.is_valid(), f"Expected valid but got errors: {serializer.errors}"
assert 'colors' in serializer.validated_data
assert serializer.validated_data['colors'] == ['#ffffff', '#000000']

def test_non_partial_listfield_with_indexed_keys(self):
"""
Test that indexed keys work correctly in non-partial updates.
"""
class CommunitySerializer(serializers.Serializer):
colors = serializers.ListField(
allow_null=True,
child=serializers.CharField(label='Colors', max_length=7),
required=False
)
# Simulate form data with indexed keys
data = MultiValueDict({
'colors[0]': ['#ffffff'],
'colors[1]': ['#000000']
})
serializer = CommunitySerializer(data=data, partial=False)
assert serializer.is_valid(), f"Expected valid but got errors: {serializer.errors}"
assert 'colors' in serializer.validated_data
assert serializer.validated_data['colors'] == ['#ffffff', '#000000']

def test_listfield_mixed_plain_and_indexed_keys(self):
"""
Test behavior when both plain field and indexed keys are present.
Plain field should take precedence (getlist is checked first).
"""
class CommunitySerializer(serializers.Serializer):
colors = serializers.ListField(
allow_null=True,
child=serializers.CharField(label='Colors', max_length=7),
required=False
)
# When both present, getlist should win (standard HTML form behavior)
data = MultiValueDict({
'colors': ['#aaaaaa', '#bbbbbb'], # This should be used
'colors[0]': ['#ffffff'], # These should be ignored
'colors[1]': ['#000000']
})
serializer = CommunitySerializer(data=data, partial=True)
assert serializer.is_valid(), f"Expected valid but got errors: {serializer.errors}"
assert 'colors' in serializer.validated_data
# Plain field values should take precedence
assert serializer.validated_data['colors'] == ['#aaaaaa', '#bbbbbb']

def test_partial_listfield_no_data_returns_empty(self):
"""
Test that when no list data is provided in a partial update,
the field is correctly skipped (returns empty).
"""
class CommunitySerializer(serializers.Serializer):
name = serializers.CharField()
colors = serializers.ListField(
allow_null=True,
child=serializers.CharField(label='Colors', max_length=7),
required=False
)
# Only update name, not colors
data = MultiValueDict({
'name': ['Community Name']
})
serializer = CommunitySerializer(data=data, partial=True)
assert serializer.is_valid(), f"Expected valid but got errors: {serializer.errors}"
assert 'name' in serializer.validated_data
assert 'colors' not in serializer.validated_data # Should be skipped

def test_non_partial_listfield_standard_submission(self):
"""
Test standard HTML form list submission without partial=True.
Ensures backward compatibility with existing behavior.
"""
class CommunitySerializer(serializers.Serializer):
colors = serializers.ListField(
child=serializers.CharField(label='Colors', max_length=7),
required=True
)
# Standard multi-select form submission
data = MultiValueDict({
'colors': ['#ffffff', '#000000', '#ff0000']
})
serializer = CommunitySerializer(data=data, partial=False)
assert serializer.is_valid(), f"Expected valid but got errors: {serializer.errors}"
assert serializer.validated_data['colors'] == ['#ffffff', '#000000', '#ff0000']

def test_allow_empty_true(self):
class ListSerializer(serializers.Serializer):
update_field = serializers.IntegerField()
Expand Down