Skip to content

Commit e09a89a

Browse files
committed
chore: add comments and improve tests
1 parent 8043317 commit e09a89a

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
@@ -222,6 +222,28 @@ For example:
222222
extra_kwargs = {'client': {'required': False}}
223223
validators = [] # Remove a default "unique together" constraint.
224224

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

227249
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
@@ -1451,6 +1451,8 @@ def get_unique_together_constraints(self, model):
14511451
get_referenced_base_fields_from_q(constraint.condition)
14521452
)
14531453

1454+
# Combine constraint fields and condition fields. If the union
1455+
# involves multiple fields, treat as unique-together validation
14541456
required_fields = {*constraint.fields, *condition_fields}
14551457
if len(required_fields) > 1:
14561458
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)