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:
parent
53e674d574
commit
1005c2abd1
@ -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)
|
||||||
|
@ -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`).
|
||||||
|
@ -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"
|
||||||
|
),
|
||||||
|
]
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user