Skip to content

Commit 9a23276

Browse files
committed
Support UniqueConstraint
1 parent 599e2b1 commit 9a23276

File tree

2 files changed

+112
-36
lines changed

2 files changed

+112
-36
lines changed

rest_framework/serializers.py

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1373,6 +1373,17 @@ def get_extra_kwargs(self):
13731373

13741374
return extra_kwargs
13751375

1376+
def get_unique_together_constraints(self, model):
1377+
for parent_class in [model] + list(model._meta.parents):
1378+
for unique_together in parent_class._meta.unique_together:
1379+
yield unique_together, model._default_manager
1380+
for constraint in parent_class._meta.constraints:
1381+
if isinstance(constraint, models.UniqueConstraint):
1382+
yield (
1383+
constraint.fields,
1384+
model._default_manager.filter(constraint.condition) if constraint.condition else model._default_manager
1385+
)
1386+
13761387
def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs):
13771388
"""
13781389
Return any additional field options that need to be included as a
@@ -1401,12 +1412,11 @@ def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs
14011412

14021413
unique_constraint_names -= {None}
14031414

1404-
# Include each of the `unique_together` field names,
1415+
# Include each of the `unique_together` and `UniqueConstraint` field names,
14051416
# so long as all the field names are included on the serializer.
1406-
for parent_class in [model] + list(model._meta.parents):
1407-
for unique_together_list in parent_class._meta.unique_together:
1408-
if set(field_names).issuperset(set(unique_together_list)):
1409-
unique_constraint_names |= set(unique_together_list)
1417+
for unique_together_list, queryset in self.get_unique_together_constraints(model):
1418+
if set(field_names).issuperset(set(unique_together_list)):
1419+
unique_constraint_names |= set(unique_together_list)
14101420

14111421
# Now we have all the field names that have uniqueness constraints
14121422
# applied, we can add the extra 'required=...' or 'default=...'
@@ -1503,11 +1513,6 @@ def get_unique_together_validators(self):
15031513
"""
15041514
Determine a default set of validators for any unique_together constraints.
15051515
"""
1506-
model_class_inheritance_tree = (
1507-
[self.Meta.model] +
1508-
list(self.Meta.model._meta.parents)
1509-
)
1510-
15111516
# The field names we're passing though here only include fields
15121517
# which may map onto a model field. Any dotted field name lookups
15131518
# cannot map to a field, and must be a traversal, so we're not
@@ -1533,34 +1538,35 @@ def get_unique_together_validators(self):
15331538
# Note that we make sure to check `unique_together` both on the
15341539
# base model class, but also on any parent classes.
15351540
validators = []
1536-
for parent_class in model_class_inheritance_tree:
1537-
for unique_together in parent_class._meta.unique_together:
1538-
# Skip if serializer does not map to all unique together sources
1539-
if not set(source_map).issuperset(set(unique_together)):
1540-
continue
1541-
1542-
for source in unique_together:
1543-
assert len(source_map[source]) == 1, (
1544-
"Unable to create `UniqueTogetherValidator` for "
1545-
"`{model}.{field}` as `{serializer}` has multiple "
1546-
"fields ({fields}) that map to this model field. "
1547-
"Either remove the extra fields, or override "
1548-
"`Meta.validators` with a `UniqueTogetherValidator` "
1549-
"using the desired field names."
1550-
.format(
1551-
model=self.Meta.model.__name__,
1552-
serializer=self.__class__.__name__,
1553-
field=source,
1554-
fields=', '.join(source_map[source]),
1555-
)
1556-
)
1541+
for unique_together, queryset in self.get_unique_together_constraints(self.Meta.model):
1542+
if len(unique_together) < 2:
1543+
continue
1544+
# Skip if serializer does not map to all unique together sources
1545+
if not set(source_map).issuperset(set(unique_together)):
1546+
continue
15571547

1558-
field_names = tuple(source_map[f][0] for f in unique_together)
1559-
validator = UniqueTogetherValidator(
1560-
queryset=parent_class._default_manager,
1561-
fields=field_names
1548+
for source in unique_together:
1549+
assert len(source_map[source]) == 1, (
1550+
"Unable to create `UniqueTogetherValidator` for "
1551+
"`{model}.{field}` as `{serializer}` has multiple "
1552+
"fields ({fields}) that map to this model field. "
1553+
"Either remove the extra fields, or override "
1554+
"`Meta.validators` with a `UniqueTogetherValidator` "
1555+
"using the desired field names."
1556+
.format(
1557+
model=self.Meta.model.__name__,
1558+
serializer=self.__class__.__name__,
1559+
field=source,
1560+
fields=', '.join(source_map[source]),
1561+
)
15621562
)
1563-
validators.append(validator)
1563+
1564+
field_names = tuple(source_map[f][0] for f in unique_together)
1565+
validator = UniqueTogetherValidator(
1566+
queryset=queryset,
1567+
fields=field_names
1568+
)
1569+
validators.append(validator)
15641570
return validators
15651571

15661572
def get_unique_for_date_validators(self):

tests/test_validators.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,76 @@ def filter(self, **kwargs):
452452
assert queryset.called_with == {'race_name': 'bar', 'position': 1}
453453

454454

455+
class UniqueConstraintModel(models.Model):
456+
race_name = models.CharField(max_length=100)
457+
position = models.IntegerField()
458+
global_id = models.IntegerField()
459+
460+
class Meta:
461+
constraints = [
462+
models.UniqueConstraint(
463+
name="unique_constraint_model_global_id_uniq",
464+
fields=('global_id',),
465+
),
466+
models.UniqueConstraint(
467+
name="unique_constraint_model_together_uniq",
468+
fields=('race_name', 'position'),
469+
condition=models.Q(race_name='example'),
470+
)
471+
]
472+
473+
474+
class UniqueConstraintSerializer(serializers.ModelSerializer):
475+
class Meta:
476+
model = UniqueConstraintModel
477+
fields = '__all__'
478+
479+
480+
class TestUniqueConstraintValidation(TestCase):
481+
def setUp(self):
482+
self.instance = UniqueConstraintModel.objects.create(
483+
race_name='example',
484+
position=1,
485+
global_id=1
486+
)
487+
UniqueConstraintModel.objects.create(
488+
race_name='example',
489+
position=2,
490+
global_id=2
491+
)
492+
UniqueConstraintModel.objects.create(
493+
race_name='other',
494+
position=1,
495+
global_id=3
496+
)
497+
498+
def test_repr(self):
499+
serializer = UniqueConstraintSerializer()
500+
expected = dedent("""
501+
UniqueConstraintSerializer():
502+
id = IntegerField(label='ID', read_only=True)
503+
race_name = CharField(max_length=100, required=True)
504+
position = IntegerField(required=True)
505+
global_id = IntegerField(required=True)
506+
class Meta:
507+
validators = [<UniqueTogetherValidator(queryset=<QuerySet [<UniqueConstraintModel: UniqueConstraintModel object (1)>, <UniqueConstraintModel: UniqueConstraintModel object (2)>]>, fields=('race_name', 'position'))>]
508+
""")
509+
assert repr(serializer) == expected
510+
511+
def test_fields_and_queryset(self):
512+
"""
513+
UniqueConstraint fields and condition attributes must be passed
514+
to UniqueTogetherValidator as fields and queryset
515+
"""
516+
serializer = UniqueConstraintSerializer()
517+
assert len(serializer.validators) == 1
518+
validator = serializer.validators[0]
519+
assert validator.fields == ('race_name', 'position')
520+
assert set(validator.queryset.values_list(flat=True)) == set(
521+
UniqueConstraintModel.objects.filter(race_name='example').values_list(flat=True)
522+
)
523+
524+
455525
# Tests for `UniqueForDateValidator`
456526
# ----------------------------------
457527

0 commit comments

Comments
 (0)