Put out plainly I think this is bug in how we tried to be backward compatible.
When constraint validation support was added we tried to be as backward compatible as possible but I think we failed to account for the fact that passing validation_error_message
is a good way to opt-in into the new behaviour.
I’d encourage you to file a new bug report and even submit a PR to get this addressed. I think the following patch should get you started.
diff --git a/django/db/models/constraints.py b/django/db/models/constraints.py
index 56d547e6b0..b7c4186712 100644
--- a/django/db/models/constraints.py
+++ b/django/db/models/constraints.py
@@ -444,14 +444,17 @@ def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS):
self.get_violation_error_message(),
code=self.violation_error_code,
)
- # When fields are defined, use the unique_error_message() for
- # backward compatibility.
- for model, constraints in instance.get_constraints():
- for constraint in constraints:
- if constraint is self:
- raise ValidationError(
- instance.unique_error_message(model, self.fields),
- )
+ if self.violation_error_message == self.default_violation_error_message:
+ # When fields are defined, use the unique_error_message() as
+ # a default for backward compatibility.
+ validation_error_message = instance.unique_error_message(model, self.fields)
+ violation_error_code = None
+ else:
+ validation_error_message = self.get_violation_error_message()
+ violation_error_code = self.violation_error_code
+ raise ValidationError(
+ validation_error_message, code=violation_error_code,
+ )
else:
against = instance._get_field_value_map(meta=model._meta, exclude=exclude)
try:
diff --git a/tests/constraints/models.py b/tests/constraints/models.py
index 3ea5cf2323..5f83d2a6e2 100644
--- a/tests/constraints/models.py
+++ b/tests/constraints/models.py
@@ -31,13 +31,17 @@ class Meta:
class UniqueConstraintProduct(models.Model):
name = models.CharField(max_length=255)
color = models.CharField(max_length=32, null=True)
+ age = models.IntegerField(null=True)
class Meta:
constraints = [
models.UniqueConstraint(
fields=["name", "color"],
name="name_color_uniq",
- # Custom message and error code are ignored.
+ ),
+ models.UniqueConstraint(
+ fields=["color", "age"],
+ name="color_age_uniq",
violation_error_code="custom_code",
violation_error_message="Custom message",
)
diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py
index 55df5975de..ff8c516e5f 100644
--- a/tests/constraints/tests.py
+++ b/tests/constraints/tests.py
@@ -369,7 +369,7 @@ def test_validate_pk_field(self):
class UniqueConstraintTests(TestCase):
@classmethod
def setUpTestData(cls):
- cls.p1 = UniqueConstraintProduct.objects.create(name="p1", color="red")
+ cls.p1 = UniqueConstraintProduct.objects.create(name="p1", color="red", age=42)
cls.p2 = UniqueConstraintProduct.objects.create(name="p2")
def test_eq(self):
@@ -790,6 +790,12 @@ def test_model_validation(self):
UniqueConstraintProduct(
name=self.p1.name, color=self.p1.color
).validate_constraints()
+ msg = "Custom message"
+ with self.assertRaisesMessage(ValidationError, msg) as cm:
+ UniqueConstraintProduct(
+ color=self.p1.color, age=self.p1.age,
+ ).validate_constraints()
+ self.assertEqual(cm.exception.error_dict['__all__'][0].code, "custom_code")
@skipUnlessDBFeature("supports_partial_indexes")
def test_model_validation_with_condition(self):
@@ -827,6 +833,14 @@ class Meta:
NoCodeErrorConstraintModel(name="test").validate_constraints()
def test_validate(self):
+ constraint = UniqueConstraintProduct._meta.constraints[1]
+ msg = "Custom message"
+ non_unique_product = UniqueConstraintProduct(
+ color=self.p1.color, age=self.p1.age,
+ )
+ with self.assertRaisesMessage(ValidationError, msg) as cm:
+ constraint.validate(UniqueConstraintProduct, non_unique_product)
+ self.assertEqual(cm.exception.code, "custom_code")
constraint = UniqueConstraintProduct._meta.constraints[0]
msg = "Unique constraint product with this Name and Color already exists."
non_unique_product = UniqueConstraintProduct(