chore: add comments and improve tests

This commit is contained in:
nefrob 2025-12-12 11:08:35 -07:00
parent 8043317f57
commit e09a89a1bd
4 changed files with 47 additions and 6 deletions

View File

@ -222,6 +222,28 @@ For example:
extra_kwargs = {'client': {'required': False}}
validators = [] # Remove a default "unique together" constraint.
### UniqueConstraint with conditions
When using Django's `UniqueConstraint` with conditions that reference other model fields, DRF will automatically use
`UniqueTogetherValidator` instead of field-level `UniqueValidator`. This ensures proper validation behavior when the constraint
effectively involves multiple fields.
For example, a single-field constraint with a condition becomes a multi-field validation when the condition references other fields.
class MyModel(models.Model):
name = models.CharField(max_length=100)
status = models.CharField(max_length=20)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['name'],
condition=models.Q(status='active'),
name='unique_active_name'
)
]
## Updating nested serializers
When applying an update to an existing instance, uniqueness validators will

View File

@ -1451,6 +1451,8 @@ class ModelSerializer(Serializer):
get_referenced_base_fields_from_q(constraint.condition)
)
# Combine constraint fields and condition fields. If the union
# involves multiple fields, treat as unique-together validation
required_fields = {*constraint.fields, *condition_fields}
if len(required_fields) > 1:
yield (

View File

@ -86,6 +86,8 @@ def get_unique_validators(field_name, model_field):
if condition is not None
else set()
)
# Only use UniqueValidator if the union of field and condition fields is 1
# (i.e. no additional fields referenced in conditions)
if len(field_set | condition_fields) == 1:
yield UniqueValidator(
queryset=queryset if condition is None else queryset.filter(condition),

View File

@ -248,7 +248,7 @@ class TestUniquenessTogetherValidation(TestCase):
def test_is_not_unique_together_condition_based(self):
"""
Failing unique together validation should result in non field errors when a condition-based
Failing unique together validation should result in non-field errors when a condition-based
unique together constraint is violated.
"""
ConditionUniquenessTogetherModel.objects.create(race_name='example', position=1)
@ -275,10 +275,10 @@ class TestUniquenessTogetherValidation(TestCase):
'position': 2
}
def test_unique_together_condition_based(self):
def test_is_unique_together_condition_based(self):
"""
In a unique together validation, one field may be non-unique
so long as the set as a whole is unique.
In a condition-based unique together validation, data is valid when
the constrained field differs when the condition applies`.
"""
ConditionUniquenessTogetherModel.objects.create(race_name='example', position=1)
@ -290,6 +290,21 @@ class TestUniquenessTogetherValidation(TestCase):
'position': 1
}
def test_is_unique_together_when_condition_does_not_apply(self):
"""
In a condition-based unique together validation, data is valid when
the condition does not apply, even if constrained fields match existing records.
"""
ConditionUniquenessTogetherModel.objects.create(race_name='example', position=1)
data = {'race_name': 'example', 'position': 2}
serializer = ConditionUniquenessTogetherSerializer(data=data)
assert serializer.is_valid()
assert serializer.validated_data == {
'race_name': 'example',
'position': 2
}
def test_updated_instance_excluded_from_unique_together(self):
"""
When performing an update, the existing instance does not count
@ -308,10 +323,10 @@ class TestUniquenessTogetherValidation(TestCase):
When performing an update, the existing instance does not count
as a match against uniqueness.
"""
ConditionUniquenessTogetherModel.objects.create(race_name='example', position=1)
instance = ConditionUniquenessTogetherModel.objects.create(race_name='example', position=1)
data = {'race_name': 'example', 'position': 0}
serializer = ConditionUniquenessTogetherSerializer(self.instance, data=data)
serializer = ConditionUniquenessTogetherSerializer(instance, data=data)
assert serializer.is_valid()
assert serializer.validated_data == {
'race_name': 'example',