diff --git a/django/db/models/constraints.py b/django/db/models/constraints.py index 3859a60e2f..788e2b635b 100644 --- a/django/db/models/constraints.py +++ b/django/db/models/constraints.py @@ -690,11 +690,19 @@ class UniqueConstraint(BaseConstraint): queryset = queryset.exclude(pk=model_class_pk) if not self.condition: if queryset.exists(): - if self.fields: - # When fields are defined, use the unique_error_message() for - # backward compatibility. + if ( + self.fields + and 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 + ) raise ValidationError( - instance.unique_error_message(model, self.fields), + validation_error_message, + code=validation_error_message.code, ) raise ValidationError( self.get_violation_error_message(), diff --git a/docs/ref/models/constraints.txt b/docs/ref/models/constraints.txt index 7dfc3b7d28..c1f140c265 100644 --- a/docs/ref/models/constraints.txt +++ b/docs/ref/models/constraints.txt @@ -282,27 +282,47 @@ PostgreSQL 15+. .. attribute:: UniqueConstraint.violation_error_code -The error code used when ``ValidationError`` is raised during -:ref:`model validation `. Defaults to ``None``. +The error code used when a ``ValidationError`` is raised during +:ref:`model validation `. -This code is *not used* for :class:`UniqueConstraint`\s with -:attr:`~UniqueConstraint.fields` and without a -:attr:`~UniqueConstraint.condition`. Such :class:`~UniqueConstraint`\s have the -same error code as constraints defined with :attr:`.Field.unique` or in -:attr:`Meta.unique_together `. +Defaults to :attr:`.BaseConstraint.violation_error_code`, when either +:attr:`.UniqueConstraint.condition` is set or :attr:`.UniqueConstraint.fields` +is not set. + +If :attr:`.UniqueConstraint.fields` is set without a +:attr:`.UniqueConstraint.condition`, defaults to the :attr:`Meta.unique_together +` error code when there are multiple +fields, and to the :attr:`.Field.unique` error code when there is a single +field. + +.. versionchanged:: 5.2 + + In older versions, the provided + :attr:`.UniqueConstraint.violation_error_code` was not used when + :attr:`.UniqueConstraint.fields` was set without a + :attr:`.UniqueConstraint.condition`. ``violation_error_message`` --------------------------- .. attribute:: UniqueConstraint.violation_error_message -The error message used when ``ValidationError`` is raised during -:ref:`model validation `. Defaults to -:attr:`.BaseConstraint.violation_error_message`. +The error message used when a ``ValidationError`` is raised during +:ref:`model validation `. -This message is *not used* for :class:`UniqueConstraint`\s with -:attr:`~UniqueConstraint.fields` and without a -:attr:`~UniqueConstraint.condition`. Such :class:`~UniqueConstraint`\s show the -same message as constraints defined with -:attr:`.Field.unique` or in -:attr:`Meta.unique_together `. +Defaults to :attr:`.BaseConstraint.violation_error_message`, when either +:attr:`.UniqueConstraint.condition` is set or :attr:`.UniqueConstraint.fields` +is not set. + +If :attr:`.UniqueConstraint.fields` is set without a +:attr:`.UniqueConstraint.condition`, defaults to the :attr:`Meta.unique_together +` error message when there are +multiple fields, and to the :attr:`.Field.unique` error message when there is a +single field. + +.. versionchanged:: 5.2 + + In older versions, the provided + :attr:`.UniqueConstraint.violation_error_message` was not used when + :attr:`.UniqueConstraint.fields` was set without a + :attr:`.UniqueConstraint.condition`. diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt index 5c5020a3f4..eabc27c277 100644 --- a/docs/releases/5.2.txt +++ b/docs/releases/5.2.txt @@ -358,6 +358,11 @@ Miscellaneous * ``HttpRequest.accepted_types`` is now sorted by the client's preference, based on the request's ``Accept`` header. +* :attr:`.UniqueConstraint.violation_error_code` and + :attr:`.UniqueConstraint.violation_error_message` are now always used when + provided. Previously, these were ignored when :attr:`.UniqueConstraint.fields` + were set without a :attr:`.UniqueConstraint.condition`. + * The :func:`~django.template.context_processors.debug` context processor is no longer included in the default project template. diff --git a/tests/constraints/models.py b/tests/constraints/models.py index 829f671cdd..95a29ffa4d 100644 --- a/tests/constraints/models.py +++ b/tests/constraints/models.py @@ -72,15 +72,13 @@ class GeneratedFieldVirtualProduct(models.Model): 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. - violation_error_code="custom_code", - violation_error_message="Custom message", ) ] diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py index e1c431956f..9047710098 100644 --- a/tests/constraints/tests.py +++ b/tests/constraints/tests.py @@ -953,6 +953,41 @@ class UniqueConstraintTests(TestCase): ChildUniqueConstraintProduct(name=self.p1.name, color=self.p1.color), ) + def test_validate_unique_custom_code_and_message(self): + product = UniqueConstraintProduct.objects.create( + name="test", color="red", age=42 + ) + code = "custom_code" + message = "Custom message" + multiple_fields_constraint = models.UniqueConstraint( + fields=["color", "age"], + name="color_age_uniq", + violation_error_code=code, + violation_error_message=message, + ) + single_field_constraint = models.UniqueConstraint( + fields=["color"], + name="color_uniq", + violation_error_code=code, + violation_error_message=message, + ) + + with self.assertRaisesMessage(ValidationError, message) as cm: + multiple_fields_constraint.validate( + UniqueConstraintProduct, + UniqueConstraintProduct( + name="new-test", color=product.color, age=product.age + ), + ) + self.assertEqual(cm.exception.code, code) + + with self.assertRaisesMessage(ValidationError, message) as cm: + single_field_constraint.validate( + UniqueConstraintProduct, + UniqueConstraintProduct(name="new-test", color=product.color), + ) + self.assertEqual(cm.exception.code, code) + @skipUnlessDBFeature("supports_table_check_constraints") def test_validate_fields_unattached(self): Product.objects.create(price=42)