Skip to content

Commit 089162e

Browse files
authored
Fix ModelSerializer unique_together handling for field sources (#7143)
* Fix ModelSerializer unique_together field sources Updates ModelSerializer to check for serializer fields that map to the model field sources in the unique_together lists. * Ensure field name ordering consistency
1 parent 00e6079 commit 089162e

File tree

2 files changed

+82
-12
lines changed

2 files changed

+82
-12
lines changed

rest_framework/serializers.py

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import copy
1414
import inspect
1515
import traceback
16-
from collections import OrderedDict
16+
from collections import OrderedDict, defaultdict
1717
from collections.abc import Mapping
1818

1919
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
@@ -1508,28 +1508,55 @@ def get_unique_together_validators(self):
15081508
# which may map onto a model field. Any dotted field name lookups
15091509
# cannot map to a field, and must be a traversal, so we're not
15101510
# including those.
1511-
field_names = {
1512-
field.source for field in self._writable_fields
1511+
field_sources = OrderedDict(
1512+
(field.field_name, field.source) for field in self._writable_fields
15131513
if (field.source != '*') and ('.' not in field.source)
1514-
}
1514+
)
15151515

15161516
# Special Case: Add read_only fields with defaults.
1517-
field_names |= {
1518-
field.source for field in self.fields.values()
1517+
field_sources.update(OrderedDict(
1518+
(field.field_name, field.source) for field in self.fields.values()
15191519
if (field.read_only) and (field.default != empty) and (field.source != '*') and ('.' not in field.source)
1520-
}
1520+
))
1521+
1522+
# Invert so we can find the serializer field names that correspond to
1523+
# the model field names in the unique_together sets. This also allows
1524+
# us to check that multiple fields don't map to the same source.
1525+
source_map = defaultdict(list)
1526+
for name, source in field_sources.items():
1527+
source_map[source].append(name)
15211528

15221529
# Note that we make sure to check `unique_together` both on the
15231530
# base model class, but also on any parent classes.
15241531
validators = []
15251532
for parent_class in model_class_inheritance_tree:
15261533
for unique_together in parent_class._meta.unique_together:
1527-
if field_names.issuperset(set(unique_together)):
1528-
validator = UniqueTogetherValidator(
1529-
queryset=parent_class._default_manager,
1530-
fields=unique_together
1534+
# Skip if serializer does not map to all unique together sources
1535+
if not set(source_map).issuperset(set(unique_together)):
1536+
continue
1537+
1538+
for source in unique_together:
1539+
assert len(source_map[source]) == 1, (
1540+
"Unable to create `UniqueTogetherValidator` for "
1541+
"`{model}.{field}` as `{serializer}` has multiple "
1542+
"fields ({fields}) that map to this model field. "
1543+
"Either remove the extra fields, or override "
1544+
"`Meta.validators` with a `UniqueTogetherValidator` "
1545+
"using the desired field names."
1546+
.format(
1547+
model=self.Meta.model.__name__,
1548+
serializer=self.__class__.__name__,
1549+
field=source,
1550+
fields=', '.join(source_map[source]),
1551+
)
15311552
)
1532-
validators.append(validator)
1553+
1554+
field_names = tuple(source_map[f][0] for f in unique_together)
1555+
validator = UniqueTogetherValidator(
1556+
queryset=parent_class._default_manager,
1557+
fields=field_names
1558+
)
1559+
validators.append(validator)
15331560
return validators
15341561

15351562
def get_unique_for_date_validators(self):

tests/test_validators.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,49 @@ class Meta:
344344
]
345345
}
346346

347+
def test_default_validator_with_fields_with_source(self):
348+
class TestSerializer(serializers.ModelSerializer):
349+
name = serializers.CharField(source='race_name')
350+
351+
class Meta:
352+
model = UniquenessTogetherModel
353+
fields = ['name', 'position']
354+
355+
serializer = TestSerializer()
356+
expected = dedent("""
357+
TestSerializer():
358+
name = CharField(source='race_name')
359+
position = IntegerField()
360+
class Meta:
361+
validators = [<UniqueTogetherValidator(queryset=UniquenessTogetherModel.objects.all(), fields=('name', 'position'))>]
362+
""")
363+
assert repr(serializer) == expected
364+
365+
def test_default_validator_with_multiple_fields_with_same_source(self):
366+
class TestSerializer(serializers.ModelSerializer):
367+
name = serializers.CharField(source='race_name')
368+
other_name = serializers.CharField(source='race_name')
369+
370+
class Meta:
371+
model = UniquenessTogetherModel
372+
fields = ['name', 'other_name', 'position']
373+
374+
serializer = TestSerializer(data={
375+
'name': 'foo',
376+
'other_name': 'foo',
377+
'position': 1,
378+
})
379+
with pytest.raises(AssertionError) as excinfo:
380+
serializer.is_valid()
381+
382+
expected = (
383+
"Unable to create `UniqueTogetherValidator` for "
384+
"`UniquenessTogetherModel.race_name` as `TestSerializer` has "
385+
"multiple fields (name, other_name) that map to this model field. "
386+
"Either remove the extra fields, or override `Meta.validators` "
387+
"with a `UniqueTogetherValidator` using the desired field names.")
388+
assert str(excinfo.value) == expected
389+
347390
def test_allow_explict_override(self):
348391
"""
349392
Ensure validators can be explicitly removed..

0 commit comments

Comments
 (0)