Skip to content

Commit ac4151e

Browse files
committed
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 ac4151e

File tree

3 files changed

+83
-8
lines changed

3 files changed

+83
-8
lines changed

rest_framework/serializers.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1427,20 +1427,33 @@ def get_extra_kwargs(self):
14271427

14281428
def get_unique_together_constraints(self, model):
14291429
"""
1430-
Returns iterator of (fields, queryset, condition_fields, condition),
1430+
Returns iterator of (fields, queryset, condition_fields, condition, message, code),
14311431
each entry describes an unique together constraint on `fields` in `queryset`
1432-
with respect of constraint's `condition`.
1432+
with respect of constraint's `condition`, optional custom `violation_error_message` and `violation_error_code`.
14331433
"""
14341434
for parent_class in [model] + list(model._meta.parents):
14351435
for unique_together in parent_class._meta.unique_together:
1436-
yield unique_together, model._default_manager, [], None
1436+
yield unique_together, model._default_manager, [], None, None, None
14371437
for constraint in parent_class._meta.constraints:
14381438
if isinstance(constraint, models.UniqueConstraint) and len(constraint.fields) > 1:
14391439
if constraint.condition is None:
14401440
condition_fields = []
14411441
else:
14421442
condition_fields = list(get_referenced_base_fields_from_q(constraint.condition))
1443-
yield (constraint.fields, model._default_manager, condition_fields, constraint.condition)
1443+
1444+
violation_error_message = constraint.get_violation_error_message()
1445+
default_error_message = constraint.default_violation_error_message % {"name": constraint.name}
1446+
if violation_error_message == default_error_message:
1447+
violation_error_message = None
1448+
1449+
yield (
1450+
constraint.fields,
1451+
model._default_manager,
1452+
condition_fields,
1453+
constraint.condition,
1454+
violation_error_message,
1455+
getattr(constraint, "violation_error_code", None)
1456+
)
14441457

14451458
def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs):
14461459
"""
@@ -1473,7 +1486,7 @@ def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs
14731486

14741487
# Include each of the `unique_together` and `UniqueConstraint` field names,
14751488
# so long as all the field names are included on the serializer.
1476-
for unique_together_list, queryset, condition_fields, condition in self.get_unique_together_constraints(model):
1489+
for unique_together_list, queryset, condition_fields, condition, *__ in self.get_unique_together_constraints(model):
14771490
unique_together_list_and_condition_fields = set(unique_together_list) | set(condition_fields)
14781491
if model_fields_names.issuperset(unique_together_list_and_condition_fields):
14791492
unique_constraint_names |= unique_together_list_and_condition_fields
@@ -1598,7 +1611,7 @@ def get_unique_together_validators(self):
15981611
# Note that we make sure to check `unique_together` both on the
15991612
# base model class, but also on any parent classes.
16001613
validators = []
1601-
for unique_together, queryset, condition_fields, condition in self.get_unique_together_constraints(self.Meta.model):
1614+
for unique_together, queryset, condition_fields, condition, message, code in self.get_unique_together_constraints(self.Meta.model):
16021615
# Skip if serializer does not map to all unique together sources
16031616
unique_together_and_condition_fields = set(unique_together) | set(condition_fields)
16041617
if not set(source_map).issuperset(unique_together_and_condition_fields):
@@ -1626,6 +1639,8 @@ def get_unique_together_validators(self):
16261639
fields=field_names,
16271640
condition_fields=tuple(source_map[f][0] for f in condition_fields),
16281641
condition=condition,
1642+
message=message,
1643+
code=code,
16291644
)
16301645
validators.append(validator)
16311646
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)