This commit is contained in:
Sergei Aleshin 2025-09-15 20:46:18 +01:00 committed by GitHub
commit 767d870d85
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 80 additions and 2 deletions

View File

@ -1569,6 +1569,17 @@ class ModelSerializer(Serializer):
self.get_unique_for_date_validators() self.get_unique_for_date_validators()
) )
def _get_constraint_violation_error_message(self, constraint):
"""
Returns the violation error message for the UniqueConstraint,
or None if the message is the default.
"""
violation_error_message = constraint.get_violation_error_message()
default_error_message = constraint.default_violation_error_message % {"name": constraint.name}
if violation_error_message == default_error_message:
return None
return violation_error_message
def get_unique_together_validators(self): def get_unique_together_validators(self):
""" """
Determine a default set of validators for any unique_together constraints. Determine a default set of validators for any unique_together constraints.
@ -1595,6 +1606,13 @@ class ModelSerializer(Serializer):
for name, source in field_sources.items(): for name, source in field_sources.items():
source_map[source].append(name) source_map[source].append(name)
unique_constraint_by_fields = {
constraint.fields: constraint
for model_cls in (self.Meta.model, *self.Meta.model._meta.parents)
for constraint in model_cls._meta.constraints
if isinstance(constraint, models.UniqueConstraint)
}
# Note that we make sure to check `unique_together` both on the # Note that we make sure to check `unique_together` both on the
# base model class, but also on any parent classes. # base model class, but also on any parent classes.
validators = [] validators = []
@ -1621,11 +1639,17 @@ class ModelSerializer(Serializer):
) )
field_names = tuple(source_map[f][0] for f in unique_together) field_names = tuple(source_map[f][0] for f in unique_together)
constraint = unique_constraint_by_fields.get(tuple(unique_together))
violation_error_message = self._get_constraint_violation_error_message(constraint) if constraint else None
validator = UniqueTogetherValidator( validator = UniqueTogetherValidator(
queryset=queryset, queryset=queryset,
fields=field_names, fields=field_names,
condition_fields=tuple(source_map[f][0] for f in condition_fields), condition_fields=tuple(source_map[f][0] for f in condition_fields),
condition=condition, condition=condition,
message=violation_error_message,
code=getattr(constraint, 'violation_error_code', None),
) )
validators.append(validator) validators.append(validator)
return validators return validators

View File

@ -111,13 +111,15 @@ class UniqueTogetherValidator:
message = _('The fields {field_names} must make a unique set.') message = _('The fields {field_names} must make a unique set.')
missing_message = _('This field is required.') missing_message = _('This field is required.')
requires_context = True requires_context = True
code = 'unique'
def __init__(self, queryset, fields, message=None, condition_fields=None, condition=None): def __init__(self, queryset, fields, message=None, condition_fields=None, condition=None, code=None):
self.queryset = queryset self.queryset = queryset
self.fields = fields self.fields = fields
self.message = message or self.message self.message = message or self.message
self.condition_fields = [] if condition_fields is None else condition_fields self.condition_fields = [] if condition_fields is None else condition_fields
self.condition = condition self.condition = condition
self.code = code or self.code
def enforce_required_fields(self, attrs, serializer): def enforce_required_fields(self, attrs, serializer):
""" """
@ -198,7 +200,7 @@ class UniqueTogetherValidator:
if checked_values and None not in checked_values and qs_exists_with_condition(queryset, self.condition, condition_kwargs): if checked_values and None not in checked_values and qs_exists_with_condition(queryset, self.condition, condition_kwargs):
field_names = ', '.join(self.fields) field_names = ', '.join(self.fields)
message = self.message.format(field_names=field_names) message = self.message.format(field_names=field_names)
raise ValidationError(message, code='unique') raise ValidationError(message, code=self.code)
def __repr__(self): def __repr__(self):
return '<{}({})>'.format( return '<{}({})>'.format(
@ -217,6 +219,7 @@ class UniqueTogetherValidator:
and self.missing_message == other.missing_message and self.missing_message == other.missing_message
and self.queryset == other.queryset and self.queryset == other.queryset
and self.fields == other.fields and self.fields == other.fields
and self.code == other.code
) )

View File

@ -616,6 +616,26 @@ class UniqueConstraintNullableModel(models.Model):
] ]
class UniqueConstraintCustomMessageCodeModel(models.Model):
username = models.CharField(max_length=32)
company_id = models.IntegerField()
role = models.CharField(max_length=32)
class Meta:
constraints = [
models.UniqueConstraint(
fields=("username", "company_id"),
name="unique_username_company_custom_msg",
violation_error_message="Username must be unique within a company.",
**(dict(violation_error_code="duplicate_username") if django_version[0] >= 5 else {}),
),
models.UniqueConstraint(
fields=("company_id", "role"),
name="unique_company_role_default_msg",
),
]
class UniqueConstraintSerializer(serializers.ModelSerializer): class UniqueConstraintSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = UniqueConstraintModel model = UniqueConstraintModel
@ -628,6 +648,12 @@ class UniqueConstraintNullableSerializer(serializers.ModelSerializer):
fields = ('title', 'age', 'tag') fields = ('title', 'age', 'tag')
class UniqueConstraintCustomMessageCodeSerializer(serializers.ModelSerializer):
class Meta:
model = UniqueConstraintCustomMessageCodeModel
fields = ('username', 'company_id', 'role')
class TestUniqueConstraintValidation(TestCase): class TestUniqueConstraintValidation(TestCase):
def setUp(self): def setUp(self):
self.instance = UniqueConstraintModel.objects.create( self.instance = UniqueConstraintModel.objects.create(
@ -778,6 +804,31 @@ class TestUniqueConstraintValidation(TestCase):
) )
assert serializer.is_valid() assert serializer.is_valid()
def test_unique_constraint_custom_message_code(self):
UniqueConstraintCustomMessageCodeModel.objects.create(username="Alice", company_id=1, role="member")
expected_code = "duplicate_username" if django_version[0] >= 5 else UniqueTogetherValidator.code
serializer = UniqueConstraintCustomMessageCodeSerializer(data={
"username": "Alice",
"company_id": 1,
"role": "admin",
})
assert not serializer.is_valid()
assert serializer.errors == {"non_field_errors": ["Username must be unique within a company."]}
assert serializer.errors["non_field_errors"][0].code == expected_code
def test_unique_constraint_default_message_code(self):
UniqueConstraintCustomMessageCodeModel.objects.create(username="Alice", company_id=1, role="member")
serializer = UniqueConstraintCustomMessageCodeSerializer(data={
"username": "John",
"company_id": 1,
"role": "member",
})
expected_message = UniqueTogetherValidator.message.format(field_names=', '.join(("company_id", "role")))
assert not serializer.is_valid()
assert serializer.errors == {"non_field_errors": [expected_message]}
assert serializer.errors["non_field_errors"][0].code == UniqueTogetherValidator.code
# Tests for `UniqueForDateValidator` # Tests for `UniqueForDateValidator`
# ---------------------------------- # ----------------------------------