1
0
mirror of https://github.com/django/django.git synced 2024-12-22 17:16:24 +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.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)

View File

@ -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`).

View File

@ -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"
),
]

View File

@ -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