1
0
mirror of https://github.com/django/django.git synced 2025-01-03 06:55:47 +00:00

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.
This commit is contained in:
Mark Gensler 2024-06-25 15:04:48 +01:00 committed by Sarah Boyce
parent 53e674d574
commit 1005c2abd1
4 changed files with 126 additions and 2 deletions

View File

@ -1340,7 +1340,7 @@ class Model(AltersData, metaclass=ModelBase):
field_map = { field_map = {
field.name: Value(getattr(self, field.attname), field) field.name: Value(getattr(self, field.attname), field)
for field in meta.local_concrete_fields 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: if "pk" not in exclude:
field_map["pk"] = Value(self.pk, meta.pk) field_map["pk"] = Value(self.pk, meta.pk)

View File

@ -10,4 +10,7 @@ issues with severity "low", and several bugs in 5.0.6.
Bugfixes 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`).

View File

@ -609,3 +609,79 @@ class GeneratedModelNullVirtual(models.Model):
class Meta: class Meta:
required_db_features = {"supports_virtual_generated_columns"} 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"
),
]

View File

@ -2,6 +2,7 @@ import uuid
from decimal import Decimal from decimal import Decimal
from django.apps import apps from django.apps import apps
from django.core.exceptions import ValidationError
from django.db import IntegrityError, connection from django.db import IntegrityError, connection
from django.db.models import ( from django.db.models import (
CharField, CharField,
@ -18,6 +19,8 @@ from django.test.utils import isolate_apps
from .models import ( from .models import (
Foo, Foo,
GeneratedModel, GeneratedModel,
GeneratedModelCheckConstraint,
GeneratedModelCheckConstraintVirtual,
GeneratedModelFieldWithConverters, GeneratedModelFieldWithConverters,
GeneratedModelNull, GeneratedModelNull,
GeneratedModelNullVirtual, GeneratedModelNullVirtual,
@ -25,6 +28,8 @@ from .models import (
GeneratedModelOutputFieldDbCollationVirtual, GeneratedModelOutputFieldDbCollationVirtual,
GeneratedModelParams, GeneratedModelParams,
GeneratedModelParamsVirtual, GeneratedModelParamsVirtual,
GeneratedModelUniqueConstraint,
GeneratedModelUniqueConstraintVirtual,
GeneratedModelVirtual, GeneratedModelVirtual,
) )
@ -186,6 +191,42 @@ class GeneratedFieldTestMixin:
m = self._refresh_if_needed(m) m = self._refresh_if_needed(m)
self.assertEqual(m.field, 3) 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): def test_create(self):
m = self.base_model.objects.create(a=1, b=2) m = self.base_model.objects.create(a=1, b=2)
m = self._refresh_if_needed(m) m = self._refresh_if_needed(m)
@ -305,6 +346,8 @@ class GeneratedFieldTestMixin:
class StoredGeneratedFieldTests(GeneratedFieldTestMixin, TestCase): class StoredGeneratedFieldTests(GeneratedFieldTestMixin, TestCase):
base_model = GeneratedModel base_model = GeneratedModel
nullable_model = GeneratedModelNull nullable_model = GeneratedModelNull
check_constraint_model = GeneratedModelCheckConstraint
unique_constraint_model = GeneratedModelUniqueConstraint
output_field_db_collation_model = GeneratedModelOutputFieldDbCollation output_field_db_collation_model = GeneratedModelOutputFieldDbCollation
params_model = GeneratedModelParams params_model = GeneratedModelParams
@ -318,5 +361,7 @@ class StoredGeneratedFieldTests(GeneratedFieldTestMixin, TestCase):
class VirtualGeneratedFieldTests(GeneratedFieldTestMixin, TestCase): class VirtualGeneratedFieldTests(GeneratedFieldTestMixin, TestCase):
base_model = GeneratedModelVirtual base_model = GeneratedModelVirtual
nullable_model = GeneratedModelNullVirtual nullable_model = GeneratedModelNullVirtual
check_constraint_model = GeneratedModelCheckConstraintVirtual
unique_constraint_model = GeneratedModelUniqueConstraintVirtual
output_field_db_collation_model = GeneratedModelOutputFieldDbCollationVirtual output_field_db_collation_model = GeneratedModelOutputFieldDbCollationVirtual
params_model = GeneratedModelParamsVirtual params_model = GeneratedModelParamsVirtual