Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
18 changes: 15 additions & 3 deletions rest_framework/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -1680,18 +1680,30 @@ 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
# For partial updates, avoid the O(n) parse_html_list scan
# if neither the plain key nor any indexed keys are present
if getattr(self.root, 'partial', False):
# Quick check: are there any keys matching field_name[*]?
prefix = self.field_name + '['
if not any(key.startswith(prefix) for key in dictionary):
return empty
# Parse indexed keys (e.g., field[0], field[1])
# This handles form submissions with explicit indices
return html.parse_html_list(dictionary, prefix=self.field_name, default=empty)

# Non-HTML input: standard dictionary access
if self.field_name not in dictionary and getattr(self.root, 'partial', False):
return empty
return dictionary.get(self.field_name, empty)

def to_internal_value(self, data):
Expand Down
61 changes: 61 additions & 0 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,6 +576,67 @@ class TestSerializer(serializers.Serializer):
assert serializer.is_valid()
assert serializer.validated_data == {'scores': ['']}

def test_partial_update_with_indexed_keys(self):
"""
Regression test for indexed HTML form keys with partial=True.
When data is passed as `colors[0]=#ffffff&colors[1]=#000000`
with partial=True, the field should parse indexed keys correctly.
"""
class TestSerializer(serializers.Serializer):
colors = serializers.ListField(
allow_null=True,
child=serializers.CharField(max_length=7),
required=False
)
name = serializers.CharField(max_length=100, required=False)

serializer = TestSerializer(
data=QueryDict('colors[0]=#ffffff&colors[1]=#000000'),
partial=True
)
assert serializer.is_valid()
assert serializer.validated_data == {'colors': ['#ffffff', '#000000']}

def test_partial_update_omitted_list_field(self):
"""
When a ListField is omitted in a partial update (and there are no
indexed keys for it), the field should return empty without triggering
an expensive O(n) scan via parse_html_list.
"""
class TestSerializer(serializers.Serializer):
colors = serializers.ListField(
child=serializers.CharField(max_length=7),
required=False
)
name = serializers.CharField(max_length=100)

# colors is omitted, only name is provided
serializer = TestSerializer(
data=QueryDict('name=Test'),
partial=True
)
assert serializer.is_valid()
assert serializer.validated_data == {'name': 'Test'}
assert 'colors' not in serializer.validated_data

def test_partial_update_indexed_keys_ordering(self):
"""
Indexed keys should preserve the correct order even when
they appear out of order in the QueryDict.
"""
class TestSerializer(serializers.Serializer):
items = serializers.ListField(
child=serializers.IntegerField(),
required=False
)

serializer = TestSerializer(
data=QueryDict('items[2]=3&items[0]=1&items[1]=2'),
partial=True
)
assert serializer.is_valid()
assert serializer.validated_data == {'items': [1, 2, 3]}


class TestCreateOnlyDefault:
def setup_method(self):
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
Loading