Skip to content

Commit c54c658

Browse files
committed
fix(serializer): restore get_unique_together_constraints method signature
Extracted error message logic to a separate method. fix: conditionally include violation_error_code for Django >= 5.0 fix(validators): use custom error message and code from model constraints
1 parent 513ddb4 commit c54c658

File tree

3 files changed

+84
-2
lines changed

3 files changed

+84
-2
lines changed

rest_framework/serializers.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,6 +1569,17 @@ def get_validators(self):
15691569
self.get_unique_for_date_validators()
15701570
)
15711571

1572+
def _get_constraint_violation_error_message(self, constraint):
1573+
"""
1574+
Returns the violation error message for the UniqueConstraint,
1575+
or None if the message is the default.
1576+
"""
1577+
violation_error_message = constraint.get_violation_error_message()
1578+
default_error_message = constraint.default_violation_error_message % {"name": constraint.name}
1579+
if violation_error_message == default_error_message:
1580+
return None
1581+
return violation_error_message
1582+
15721583
def get_unique_together_validators(self):
15731584
"""
15741585
Determine a default set of validators for any unique_together constraints.
@@ -1595,6 +1606,11 @@ def get_unique_together_validators(self):
15951606
for name, source in field_sources.items():
15961607
source_map[source].append(name)
15971608

1609+
unique_constraint_by_fields = {
1610+
constraint.fields: constraint for constraint in self.Meta.model._meta.constraints
1611+
if isinstance(constraint, models.UniqueConstraint)
1612+
}
1613+
15981614
# Note that we make sure to check `unique_together` both on the
15991615
# base model class, but also on any parent classes.
16001616
validators = []
@@ -1621,11 +1637,17 @@ def get_unique_together_validators(self):
16211637
)
16221638

16231639
field_names = tuple(source_map[f][0] for f in unique_together)
1640+
1641+
constraint = unique_constraint_by_fields.get(tuple(unique_together))
1642+
violation_error_message = self._get_constraint_violation_error_message(constraint) if constraint else None
1643+
16241644
validator = UniqueTogetherValidator(
16251645
queryset=queryset,
16261646
fields=field_names,
16271647
condition_fields=tuple(source_map[f][0] for f in condition_fields),
16281648
condition=condition,
1649+
message=violation_error_message,
1650+
code=getattr(constraint, 'violation_error_code', None),
16291651
)
16301652
validators.append(validator)
16311653
return validators

rest_framework/validators.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,15 @@ class UniqueTogetherValidator:
111111
message = _('The fields {field_names} must make a unique set.')
112112
missing_message = _('This field is required.')
113113
requires_context = True
114+
code = 'unique'
114115

115-
def __init__(self, queryset, fields, message=None, condition_fields=None, condition=None):
116+
def __init__(self, queryset, fields, message=None, condition_fields=None, condition=None, code=None):
116117
self.queryset = queryset
117118
self.fields = fields
118119
self.message = message or self.message
119120
self.condition_fields = [] if condition_fields is None else condition_fields
120121
self.condition = condition
122+
self.code = code or self.code
121123

122124
def enforce_required_fields(self, attrs, serializer):
123125
"""
@@ -198,7 +200,7 @@ def __call__(self, attrs, serializer):
198200
if checked_values and None not in checked_values and qs_exists_with_condition(queryset, self.condition, condition_kwargs):
199201
field_names = ', '.join(self.fields)
200202
message = self.message.format(field_names=field_names)
201-
raise ValidationError(message, code='unique')
203+
raise ValidationError(message, code=self.code)
202204

203205
def __repr__(self):
204206
return '<{}({})>'.format(
@@ -217,6 +219,7 @@ def __eq__(self, other):
217219
and self.missing_message == other.missing_message
218220
and self.queryset == other.queryset
219221
and self.fields == other.fields
222+
and self.code == other.code
220223
)
221224

222225

tests/test_validators.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,32 @@ class Meta:
616616
]
617617

618618

619+
class UniqueConstraintCustomMessageCodeModel(models.Model):
620+
username = models.CharField(max_length=32)
621+
company_id = models.IntegerField()
622+
role = models.CharField(max_length=32)
623+
624+
class Meta:
625+
constraints = [
626+
models.UniqueConstraint(
627+
fields=("username", "company_id"),
628+
name="unique_username_company_custom_msg",
629+
violation_error_message="Username must be unique within a company.",
630+
violation_error_code="duplicate_username",
631+
)
632+
if django_version[0] >= 5
633+
else models.UniqueConstraint(
634+
fields=("username", "company_id"),
635+
name="unique_username_company_custom_msg",
636+
violation_error_message="Username must be unique within a company.",
637+
),
638+
models.UniqueConstraint(
639+
fields=("company_id", "role"),
640+
name="unique_company_role_default_msg",
641+
),
642+
]
643+
644+
619645
class UniqueConstraintSerializer(serializers.ModelSerializer):
620646
class Meta:
621647
model = UniqueConstraintModel
@@ -628,6 +654,12 @@ class Meta:
628654
fields = ('title', 'age', 'tag')
629655

630656

657+
class UniqueConstraintCustomMessageCodeSerializer(serializers.ModelSerializer):
658+
class Meta:
659+
model = UniqueConstraintCustomMessageCodeModel
660+
fields = ('username', 'company_id', 'role')
661+
662+
631663
class TestUniqueConstraintValidation(TestCase):
632664
def setUp(self):
633665
self.instance = UniqueConstraintModel.objects.create(
@@ -778,6 +810,31 @@ class Meta:
778810
)
779811
assert serializer.is_valid()
780812

813+
def test_unique_constraint_custom_message_code(self):
814+
UniqueConstraintCustomMessageCodeModel.objects.create(username="Alice", company_id=1, role="member")
815+
expected_code = "duplicate_username" if django_version[0] >= 5 else UniqueTogetherValidator.code
816+
817+
serializer = UniqueConstraintCustomMessageCodeSerializer(data={
818+
"username": "Alice",
819+
"company_id": 1,
820+
"role": "admin",
821+
})
822+
assert not serializer.is_valid()
823+
assert serializer.errors == {"non_field_errors": ["Username must be unique within a company."]}
824+
assert serializer.errors["non_field_errors"][0].code == expected_code
825+
826+
def test_unique_constraint_default_message_code(self):
827+
UniqueConstraintCustomMessageCodeModel.objects.create(username="Alice", company_id=1, role="member")
828+
serializer = UniqueConstraintCustomMessageCodeSerializer(data={
829+
"username": "John",
830+
"company_id": 1,
831+
"role": "member",
832+
})
833+
expected_message = UniqueTogetherValidator.message.format(field_names=', '.join(("company_id", "role")))
834+
assert not serializer.is_valid()
835+
assert serializer.errors == {"non_field_errors": [expected_message]}
836+
assert serializer.errors["non_field_errors"][0].code == UniqueTogetherValidator.code
837+
781838

782839
# Tests for `UniqueForDateValidator`
783840
# ----------------------------------

0 commit comments

Comments
 (0)