summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMark Gensler <mark.gensler@protonmail.com>2024-06-25 15:04:48 +0100
committerSarah Boyce <42296566+sarahboyce@users.noreply.github.com>2024-07-04 11:49:27 +0200
commit0602fc2124f3e1f1562d497607e0efd5474b5831 (patch)
tree99235eb697e3c70ae02dc24009a65956accd0546
parent03b908ffed00510b49207d7334d9e392dce5f046 (diff)
[5.0.x] Fixed #35560 -- Made Model.full_clean() ignore GeneratedFields for constraints.
Accessing generated field values on unsaved models caused a crash when validating CheckConstraints and UniqueConstraints with expressions. Backport of 1005c2abd1ef0c156f449641e38c33e473989d37 from main.
-rw-r--r--django/db/models/base.py2
-rw-r--r--docs/releases/5.0.7.txt5
-rw-r--r--tests/model_fields/models.py76
-rw-r--r--tests/model_fields/test_generatedfield.py45
4 files changed, 126 insertions, 2 deletions
diff --git a/django/db/models/base.py b/django/db/models/base.py
index 9cb1cfa8e6..876a96c5a7 100644
--- a/django/db/models/base.py
+++ b/django/db/models/base.py
@@ -1251,7 +1251,7 @@ class Model(AltersData, metaclass=ModelBase):
field_map = {
field.name: Value(getattr(self, field.attname), field)
for field in meta.local_concrete_fields
- if field.name not in exclude
+ if field.name not in exclude and not field.generated
}
if "pk" not in exclude:
field_map["pk"] = Value(self.pk, meta.pk)
diff --git a/docs/releases/5.0.7.txt b/docs/releases/5.0.7.txt
index a0e3acd640..0bee6d57ce 100644
--- a/docs/releases/5.0.7.txt
+++ b/docs/releases/5.0.7.txt
@@ -10,4 +10,7 @@ issues with severity "low", and several bugs in 5.0.6.
Bugfixes
========
-* ...
+* Fixed a bug in Django 5.0 that caused a crash of ``Model.full_clean()`` on
+ unsaved model instances with a ``GeneratedField`` and certain defined
+ :attr:`Meta.constraints <django.db.models.Options.constraints>`
+ (:ticket:`35560`).
diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py
index e34f3c8947..d6d80e6fd7 100644
--- a/tests/model_fields/models.py
+++ b/tests/model_fields/models.py
@@ -592,3 +592,79 @@ class GeneratedModelNullVirtual(models.Model):
class Meta:
required_db_features = {"supports_virtual_generated_columns"}
+
+
+class GeneratedModelBase(models.Model):
+ a = models.IntegerField()
+ a_squared = models.GeneratedField(
+ expression=F("a") * F("a"),
+ output_field=models.IntegerField(),
+ db_persist=True,
+ )
+
+ class Meta:
+ abstract = True
+
+
+class GeneratedModelVirtualBase(models.Model):
+ a = models.IntegerField()
+ a_squared = models.GeneratedField(
+ expression=F("a") * F("a"),
+ output_field=models.IntegerField(),
+ db_persist=False,
+ )
+
+ class Meta:
+ abstract = True
+
+
+class GeneratedModelCheckConstraint(GeneratedModelBase):
+ class Meta:
+ required_db_features = {
+ "supports_stored_generated_columns",
+ "supports_table_check_constraints",
+ }
+ constraints = [
+ models.CheckConstraint(
+ condition=models.Q(a__gt=0),
+ name="Generated model check constraint a > 0",
+ )
+ ]
+
+
+class GeneratedModelCheckConstraintVirtual(GeneratedModelVirtualBase):
+ class Meta:
+ required_db_features = {
+ "supports_virtual_generated_columns",
+ "supports_table_check_constraints",
+ }
+ constraints = [
+ models.CheckConstraint(
+ condition=models.Q(a__gt=0),
+ name="Generated model check constraint virtual a > 0",
+ )
+ ]
+
+
+class GeneratedModelUniqueConstraint(GeneratedModelBase):
+ class Meta:
+ required_db_features = {
+ "supports_stored_generated_columns",
+ "supports_table_check_constraints",
+ }
+ constraints = [
+ models.UniqueConstraint(F("a"), name="Generated model unique constraint a"),
+ ]
+
+
+class GeneratedModelUniqueConstraintVirtual(GeneratedModelVirtualBase):
+ class Meta:
+ required_db_features = {
+ "supports_virtual_generated_columns",
+ "supports_expression_indexes",
+ }
+ constraints = [
+ models.UniqueConstraint(
+ F("a"), name="Generated model unique constraint virtual a"
+ ),
+ ]
diff --git a/tests/model_fields/test_generatedfield.py b/tests/model_fields/test_generatedfield.py
index 2fbfe3c82a..c185e19d8b 100644
--- a/tests/model_fields/test_generatedfield.py
+++ b/tests/model_fields/test_generatedfield.py
@@ -2,6 +2,7 @@ import uuid
from decimal import Decimal
from django.apps import apps
+from django.core.exceptions import ValidationError
from django.db import IntegrityError, connection
from django.db.models import (
CharField,
@@ -18,6 +19,8 @@ from django.test.utils import isolate_apps
from .models import (
Foo,
GeneratedModel,
+ GeneratedModelCheckConstraint,
+ GeneratedModelCheckConstraintVirtual,
GeneratedModelFieldWithConverters,
GeneratedModelNull,
GeneratedModelNullVirtual,
@@ -25,6 +28,8 @@ from .models import (
GeneratedModelOutputFieldDbCollationVirtual,
GeneratedModelParams,
GeneratedModelParamsVirtual,
+ GeneratedModelUniqueConstraint,
+ GeneratedModelUniqueConstraintVirtual,
GeneratedModelVirtual,
)
@@ -186,6 +191,42 @@ class GeneratedFieldTestMixin:
m = self._refresh_if_needed(m)
self.assertEqual(m.field, 3)
+ @skipUnlessDBFeature("supports_table_check_constraints")
+ def test_full_clean_with_check_constraint(self):
+ model_name = self.check_constraint_model._meta.verbose_name.capitalize()
+
+ m = self.check_constraint_model(a=2)
+ m.full_clean()
+ m.save()
+ m = self._refresh_if_needed(m)
+ self.assertEqual(m.a_squared, 4)
+
+ m = self.check_constraint_model(a=-1)
+ with self.assertRaises(ValidationError) as cm:
+ m.full_clean()
+ self.assertEqual(
+ cm.exception.message_dict,
+ {"__all__": [f"Constraint “{model_name} a > 0” is violated."]},
+ )
+
+ @skipUnlessDBFeature("supports_expression_indexes")
+ def test_full_clean_with_unique_constraint_expression(self):
+ model_name = self.unique_constraint_model._meta.verbose_name.capitalize()
+
+ m = self.unique_constraint_model(a=2)
+ m.full_clean()
+ m.save()
+ m = self._refresh_if_needed(m)
+ self.assertEqual(m.a_squared, 4)
+
+ m = self.unique_constraint_model(a=2)
+ with self.assertRaises(ValidationError) as cm:
+ m.full_clean()
+ self.assertEqual(
+ cm.exception.message_dict,
+ {"__all__": [f"Constraint “{model_name} a” is violated."]},
+ )
+
def test_create(self):
m = self.base_model.objects.create(a=1, b=2)
m = self._refresh_if_needed(m)
@@ -305,6 +346,8 @@ class GeneratedFieldTestMixin:
class StoredGeneratedFieldTests(GeneratedFieldTestMixin, TestCase):
base_model = GeneratedModel
nullable_model = GeneratedModelNull
+ check_constraint_model = GeneratedModelCheckConstraint
+ unique_constraint_model = GeneratedModelUniqueConstraint
output_field_db_collation_model = GeneratedModelOutputFieldDbCollation
params_model = GeneratedModelParams
@@ -318,5 +361,7 @@ class StoredGeneratedFieldTests(GeneratedFieldTestMixin, TestCase):
class VirtualGeneratedFieldTests(GeneratedFieldTestMixin, TestCase):
base_model = GeneratedModelVirtual
nullable_model = GeneratedModelNullVirtual
+ check_constraint_model = GeneratedModelCheckConstraintVirtual
+ unique_constraint_model = GeneratedModelUniqueConstraintVirtual
output_field_db_collation_model = GeneratedModelOutputFieldDbCollationVirtual
params_model = GeneratedModelParamsVirtual