Skip to content

Commit 32c02c8

Browse files
committed
chore: add comments and improve tests
1 parent 6665e71 commit 32c02c8

File tree

4 files changed

+47
-6
lines changed

4 files changed

+47
-6
lines changed

docs/api-guide/validators.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,28 @@ For example:
230230
extra_kwargs = {'client': {'required': False}}
231231
validators = [] # Remove a default "unique together" constraint.
232232

233+
### UniqueConstraint with conditions
234+
235+
When using Django's `UniqueConstraint` with conditions that reference other model fields, DRF will automatically use
236+
`UniqueTogetherValidator` instead of field-level `UniqueValidator`. This ensures proper validation behavior when the constraint
237+
effectively involves multiple fields.
238+
239+
For example, a single-field constraint with a condition becomes a multi-field validation when the condition references other fields.
240+
241+
class MyModel(models.Model):
242+
name = models.CharField(max_length=100)
243+
status = models.CharField(max_length=20)
244+
245+
class Meta:
246+
constraints = [
247+
models.UniqueConstraint(
248+
fields=['name'],
249+
condition=models.Q(status='active'),
250+
name='unique_active_name'
251+
)
252+
]
253+
254+
233255
## Updating nested serializers
234256

235257
When applying an update to an existing instance, uniqueness validators will

rest_framework/serializers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1443,6 +1443,8 @@ def get_unique_together_constraints(self, model):
14431443
get_referenced_base_fields_from_q(constraint.condition)
14441444
)
14451445

1446+
# Combine constraint fields and condition fields. If the union
1447+
# involves multiple fields, treat as unique-together validation
14461448
required_fields = {*constraint.fields, *condition_fields}
14471449
if len(required_fields) > 1:
14481450
yield (

rest_framework/utils/field_mapping.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ def get_unique_validators(field_name, model_field):
8686
if condition is not None
8787
else set()
8888
)
89+
# Only use UniqueValidator if the union of field and condition fields is 1
90+
# (i.e. no additional fields referenced in conditions)
8991
if len(field_set | condition_fields) == 1:
9092
yield UniqueValidator(
9193
queryset=queryset if condition is None else queryset.filter(condition),

tests/test_validators.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ def test_is_not_unique_together(self):
248248

249249
def test_is_not_unique_together_condition_based(self):
250250
"""
251-
Failing unique together validation should result in non field errors when a condition-based
251+
Failing unique together validation should result in non-field errors when a condition-based
252252
unique together constraint is violated.
253253
"""
254254
ConditionUniquenessTogetherModel.objects.create(race_name='example', position=1)
@@ -275,10 +275,10 @@ def test_is_unique_together(self):
275275
'position': 2
276276
}
277277

278-
def test_unique_together_condition_based(self):
278+
def test_is_unique_together_condition_based(self):
279279
"""
280-
In a unique together validation, one field may be non-unique
281-
so long as the set as a whole is unique.
280+
In a condition-based unique together validation, data is valid when
281+
the constrained field differs when the condition applies`.
282282
"""
283283
ConditionUniquenessTogetherModel.objects.create(race_name='example', position=1)
284284

@@ -290,6 +290,21 @@ def test_unique_together_condition_based(self):
290290
'position': 1
291291
}
292292

293+
def test_is_unique_together_when_condition_does_not_apply(self):
294+
"""
295+
In a condition-based unique together validation, data is valid when
296+
the condition does not apply, even if constrained fields match existing records.
297+
"""
298+
ConditionUniquenessTogetherModel.objects.create(race_name='example', position=1)
299+
300+
data = {'race_name': 'example', 'position': 2}
301+
serializer = ConditionUniquenessTogetherSerializer(data=data)
302+
assert serializer.is_valid()
303+
assert serializer.validated_data == {
304+
'race_name': 'example',
305+
'position': 2
306+
}
307+
293308
def test_updated_instance_excluded_from_unique_together(self):
294309
"""
295310
When performing an update, the existing instance does not count
@@ -308,10 +323,10 @@ def test_updated_instance_excluded_from_unique_together_condition_based(self):
308323
When performing an update, the existing instance does not count
309324
as a match against uniqueness.
310325
"""
311-
ConditionUniquenessTogetherModel.objects.create(race_name='example', position=1)
326+
instance = ConditionUniquenessTogetherModel.objects.create(race_name='example', position=1)
312327

313328
data = {'race_name': 'example', 'position': 0}
314-
serializer = ConditionUniquenessTogetherSerializer(self.instance, data=data)
329+
serializer = ConditionUniquenessTogetherSerializer(instance, data=data)
315330
assert serializer.is_valid()
316331
assert serializer.validated_data == {
317332
'race_name': 'example',

0 commit comments

Comments
 (0)