diff --git a/django/db/models/base.py b/django/db/models/base.py index 398ebdcf23..9f8c1c0cc0 100644 --- a/django/db/models/base.py +++ b/django/db/models/base.py @@ -1340,7 +1340,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 ` + (:ticket:`35560`). diff --git a/tests/model_fields/models.py b/tests/model_fields/models.py index 652c808b40..d9811ba164 100644 --- a/tests/model_fields/models.py +++ b/tests/model_fields/models.py @@ -609,3 +609,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