Fixed #30581 -- Added support for Meta.constraints validation.
Thanks Simon Charette, Keryn Knight, and Mariusz Felisiak for reviews.
This commit is contained in:
parent
441103a04d
commit
667105877e
|
@ -1,11 +1,13 @@
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from django.contrib.postgres.indexes import OpClass
|
from django.contrib.postgres.indexes import OpClass
|
||||||
from django.db import NotSupportedError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db import DEFAULT_DB_ALIAS, NotSupportedError
|
||||||
from django.db.backends.ddl_references import Expressions, Statement, Table
|
from django.db.backends.ddl_references import Expressions, Statement, Table
|
||||||
from django.db.models import BaseConstraint, Deferrable, F, Q
|
from django.db.models import BaseConstraint, Deferrable, F, Q
|
||||||
from django.db.models.expressions import ExpressionList
|
from django.db.models.expressions import Exists, ExpressionList
|
||||||
from django.db.models.indexes import IndexExpression
|
from django.db.models.indexes import IndexExpression
|
||||||
|
from django.db.models.lookups import PostgresOperatorLookup
|
||||||
from django.db.models.sql import Query
|
from django.db.models.sql import Query
|
||||||
from django.utils.deprecation import RemovedInDjango50Warning
|
from django.utils.deprecation import RemovedInDjango50Warning
|
||||||
|
|
||||||
|
@ -32,6 +34,7 @@ class ExclusionConstraint(BaseConstraint):
|
||||||
deferrable=None,
|
deferrable=None,
|
||||||
include=None,
|
include=None,
|
||||||
opclasses=(),
|
opclasses=(),
|
||||||
|
violation_error_message=None,
|
||||||
):
|
):
|
||||||
if index_type and index_type.lower() not in {"gist", "spgist"}:
|
if index_type and index_type.lower() not in {"gist", "spgist"}:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
@ -78,7 +81,7 @@ class ExclusionConstraint(BaseConstraint):
|
||||||
category=RemovedInDjango50Warning,
|
category=RemovedInDjango50Warning,
|
||||||
stacklevel=2,
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
super().__init__(name=name)
|
super().__init__(name=name, violation_error_message=violation_error_message)
|
||||||
|
|
||||||
def _get_expressions(self, schema_editor, query):
|
def _get_expressions(self, schema_editor, query):
|
||||||
expressions = []
|
expressions = []
|
||||||
|
@ -197,3 +200,44 @@ class ExclusionConstraint(BaseConstraint):
|
||||||
"" if not self.include else " include=%s" % repr(self.include),
|
"" if not self.include else " include=%s" % repr(self.include),
|
||||||
"" if not self.opclasses else " opclasses=%s" % repr(self.opclasses),
|
"" if not self.opclasses else " opclasses=%s" % repr(self.opclasses),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS):
|
||||||
|
queryset = model._default_manager.using(using)
|
||||||
|
replacement_map = instance._get_field_value_map(
|
||||||
|
meta=model._meta, exclude=exclude
|
||||||
|
)
|
||||||
|
lookups = []
|
||||||
|
for idx, (expression, operator) in enumerate(self.expressions):
|
||||||
|
if isinstance(expression, str):
|
||||||
|
expression = F(expression)
|
||||||
|
if isinstance(expression, F):
|
||||||
|
if exclude and expression.name in exclude:
|
||||||
|
return
|
||||||
|
rhs_expression = replacement_map.get(expression.name, expression)
|
||||||
|
else:
|
||||||
|
rhs_expression = expression.replace_references(replacement_map)
|
||||||
|
if exclude:
|
||||||
|
for expr in rhs_expression.flatten():
|
||||||
|
if isinstance(expr, F) and expr.name in exclude:
|
||||||
|
return
|
||||||
|
# Remove OpClass because it only has sense during the constraint
|
||||||
|
# creation.
|
||||||
|
if isinstance(expression, OpClass):
|
||||||
|
expression = expression.get_source_expressions()[0]
|
||||||
|
if isinstance(rhs_expression, OpClass):
|
||||||
|
rhs_expression = rhs_expression.get_source_expressions()[0]
|
||||||
|
lookup = PostgresOperatorLookup(lhs=expression, rhs=rhs_expression)
|
||||||
|
lookup.postgres_operator = operator
|
||||||
|
lookups.append(lookup)
|
||||||
|
queryset = queryset.filter(*lookups)
|
||||||
|
model_class_pk = instance._get_pk_val(model._meta)
|
||||||
|
if not instance._state.adding and model_class_pk is not None:
|
||||||
|
queryset = queryset.exclude(pk=model_class_pk)
|
||||||
|
if not self.condition:
|
||||||
|
if queryset.exists():
|
||||||
|
raise ValidationError(self.get_violation_error_message())
|
||||||
|
else:
|
||||||
|
if (self.condition & Exists(queryset.filter(self.condition))).check(
|
||||||
|
replacement_map, using=using
|
||||||
|
):
|
||||||
|
raise ValidationError(self.get_violation_error_message())
|
||||||
|
|
|
@ -28,6 +28,7 @@ from django.db.models import NOT_PROVIDED, ExpressionWrapper, IntegerField, Max,
|
||||||
from django.db.models.constants import LOOKUP_SEP
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
from django.db.models.constraints import CheckConstraint, UniqueConstraint
|
from django.db.models.constraints import CheckConstraint, UniqueConstraint
|
||||||
from django.db.models.deletion import CASCADE, Collector
|
from django.db.models.deletion import CASCADE, Collector
|
||||||
|
from django.db.models.expressions import RawSQL
|
||||||
from django.db.models.fields.related import (
|
from django.db.models.fields.related import (
|
||||||
ForeignObjectRel,
|
ForeignObjectRel,
|
||||||
OneToOneField,
|
OneToOneField,
|
||||||
|
@ -1189,6 +1190,16 @@ class Model(metaclass=ModelBase):
|
||||||
setattr(self, cachename, obj)
|
setattr(self, cachename, obj)
|
||||||
return getattr(self, cachename)
|
return getattr(self, cachename)
|
||||||
|
|
||||||
|
def _get_field_value_map(self, meta, exclude=None):
|
||||||
|
if exclude is None:
|
||||||
|
exclude = set()
|
||||||
|
meta = meta or self._meta
|
||||||
|
return {
|
||||||
|
field.name: Value(getattr(self, field.attname), field)
|
||||||
|
for field in meta.local_concrete_fields
|
||||||
|
if field.name not in exclude
|
||||||
|
}
|
||||||
|
|
||||||
def prepare_database_save(self, field):
|
def prepare_database_save(self, field):
|
||||||
if self.pk is None:
|
if self.pk is None:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
@ -1221,7 +1232,7 @@ class Model(metaclass=ModelBase):
|
||||||
if errors:
|
if errors:
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
|
|
||||||
def _get_unique_checks(self, exclude=None):
|
def _get_unique_checks(self, exclude=None, include_meta_constraints=False):
|
||||||
"""
|
"""
|
||||||
Return a list of checks to perform. Since validate_unique() could be
|
Return a list of checks to perform. Since validate_unique() could be
|
||||||
called from a ModelForm, some fields may have been excluded; we can't
|
called from a ModelForm, some fields may have been excluded; we can't
|
||||||
|
@ -1234,13 +1245,15 @@ class Model(metaclass=ModelBase):
|
||||||
unique_checks = []
|
unique_checks = []
|
||||||
|
|
||||||
unique_togethers = [(self.__class__, self._meta.unique_together)]
|
unique_togethers = [(self.__class__, self._meta.unique_together)]
|
||||||
constraints = [(self.__class__, self._meta.total_unique_constraints)]
|
constraints = []
|
||||||
|
if include_meta_constraints:
|
||||||
|
constraints = [(self.__class__, self._meta.total_unique_constraints)]
|
||||||
for parent_class in self._meta.get_parent_list():
|
for parent_class in self._meta.get_parent_list():
|
||||||
if parent_class._meta.unique_together:
|
if parent_class._meta.unique_together:
|
||||||
unique_togethers.append(
|
unique_togethers.append(
|
||||||
(parent_class, parent_class._meta.unique_together)
|
(parent_class, parent_class._meta.unique_together)
|
||||||
)
|
)
|
||||||
if parent_class._meta.total_unique_constraints:
|
if include_meta_constraints and parent_class._meta.total_unique_constraints:
|
||||||
constraints.append(
|
constraints.append(
|
||||||
(parent_class, parent_class._meta.total_unique_constraints)
|
(parent_class, parent_class._meta.total_unique_constraints)
|
||||||
)
|
)
|
||||||
|
@ -1251,10 +1264,11 @@ class Model(metaclass=ModelBase):
|
||||||
# Add the check if the field isn't excluded.
|
# Add the check if the field isn't excluded.
|
||||||
unique_checks.append((model_class, tuple(check)))
|
unique_checks.append((model_class, tuple(check)))
|
||||||
|
|
||||||
for model_class, model_constraints in constraints:
|
if include_meta_constraints:
|
||||||
for constraint in model_constraints:
|
for model_class, model_constraints in constraints:
|
||||||
if not any(name in exclude for name in constraint.fields):
|
for constraint in model_constraints:
|
||||||
unique_checks.append((model_class, constraint.fields))
|
if not any(name in exclude for name in constraint.fields):
|
||||||
|
unique_checks.append((model_class, constraint.fields))
|
||||||
|
|
||||||
# These are checks for the unique_for_<date/year/month>.
|
# These are checks for the unique_for_<date/year/month>.
|
||||||
date_checks = []
|
date_checks = []
|
||||||
|
@ -1410,10 +1424,35 @@ class Model(metaclass=ModelBase):
|
||||||
params=params,
|
params=params,
|
||||||
)
|
)
|
||||||
|
|
||||||
def full_clean(self, exclude=None, validate_unique=True):
|
def get_constraints(self):
|
||||||
|
constraints = [(self.__class__, self._meta.constraints)]
|
||||||
|
for parent_class in self._meta.get_parent_list():
|
||||||
|
if parent_class._meta.constraints:
|
||||||
|
constraints.append((parent_class, parent_class._meta.constraints))
|
||||||
|
return constraints
|
||||||
|
|
||||||
|
def validate_constraints(self, exclude=None):
|
||||||
|
constraints = self.get_constraints()
|
||||||
|
using = router.db_for_write(self.__class__, instance=self)
|
||||||
|
|
||||||
|
errors = {}
|
||||||
|
for model_class, model_constraints in constraints:
|
||||||
|
for constraint in model_constraints:
|
||||||
|
try:
|
||||||
|
constraint.validate(model_class, self, exclude=exclude, using=using)
|
||||||
|
except ValidationError as e:
|
||||||
|
if e.code == "unique" and len(constraint.fields) == 1:
|
||||||
|
errors.setdefault(constraint.fields[0], []).append(e)
|
||||||
|
else:
|
||||||
|
errors = e.update_error_dict(errors)
|
||||||
|
if errors:
|
||||||
|
raise ValidationError(errors)
|
||||||
|
|
||||||
|
def full_clean(self, exclude=None, validate_unique=True, validate_constraints=True):
|
||||||
"""
|
"""
|
||||||
Call clean_fields(), clean(), and validate_unique() on the model.
|
Call clean_fields(), clean(), validate_unique(), and
|
||||||
Raise a ValidationError for any errors that occur.
|
validate_constraints() on the model. Raise a ValidationError for any
|
||||||
|
errors that occur.
|
||||||
"""
|
"""
|
||||||
errors = {}
|
errors = {}
|
||||||
if exclude is None:
|
if exclude is None:
|
||||||
|
@ -1443,6 +1482,16 @@ class Model(metaclass=ModelBase):
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
errors = e.update_error_dict(errors)
|
errors = e.update_error_dict(errors)
|
||||||
|
|
||||||
|
# Run constraints checks, but only for fields that passed validation.
|
||||||
|
if validate_constraints:
|
||||||
|
for name in errors:
|
||||||
|
if name != NON_FIELD_ERRORS and name not in exclude:
|
||||||
|
exclude.add(name)
|
||||||
|
try:
|
||||||
|
self.validate_constraints(exclude=exclude)
|
||||||
|
except ValidationError as e:
|
||||||
|
errors = e.update_error_dict(errors)
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
raise ValidationError(errors)
|
raise ValidationError(errors)
|
||||||
|
|
||||||
|
@ -2339,8 +2388,28 @@ class Model(metaclass=ModelBase):
|
||||||
connection.features.supports_table_check_constraints
|
connection.features.supports_table_check_constraints
|
||||||
or "supports_table_check_constraints"
|
or "supports_table_check_constraints"
|
||||||
not in cls._meta.required_db_features
|
not in cls._meta.required_db_features
|
||||||
) and isinstance(constraint.check, Q):
|
):
|
||||||
references.update(cls._get_expr_references(constraint.check))
|
if isinstance(constraint.check, Q):
|
||||||
|
references.update(
|
||||||
|
cls._get_expr_references(constraint.check)
|
||||||
|
)
|
||||||
|
if any(
|
||||||
|
isinstance(expr, RawSQL)
|
||||||
|
for expr in constraint.check.flatten()
|
||||||
|
):
|
||||||
|
errors.append(
|
||||||
|
checks.Warning(
|
||||||
|
f"Check constraint {constraint.name!r} contains "
|
||||||
|
f"RawSQL() expression and won't be validated "
|
||||||
|
f"during the model full_clean().",
|
||||||
|
hint=(
|
||||||
|
"Silence this warning if you don't care about "
|
||||||
|
"it."
|
||||||
|
),
|
||||||
|
obj=cls,
|
||||||
|
id="models.W045",
|
||||||
|
),
|
||||||
|
)
|
||||||
for field_name, *lookups in references:
|
for field_name, *lookups in references:
|
||||||
# pk is an alias that won't be found by opts.get_field.
|
# pk is an alias that won't be found by opts.get_field.
|
||||||
if field_name != "pk":
|
if field_name != "pk":
|
||||||
|
|
|
@ -1,16 +1,25 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from django.db.models.expressions import ExpressionList, F
|
from django.core.exceptions import FieldError, ValidationError
|
||||||
|
from django.db import connections
|
||||||
|
from django.db.models.expressions import Exists, ExpressionList, F
|
||||||
from django.db.models.indexes import IndexExpression
|
from django.db.models.indexes import IndexExpression
|
||||||
|
from django.db.models.lookups import Exact
|
||||||
from django.db.models.query_utils import Q
|
from django.db.models.query_utils import Q
|
||||||
from django.db.models.sql.query import Query
|
from django.db.models.sql.query import Query
|
||||||
|
from django.db.utils import DEFAULT_DB_ALIAS
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
__all__ = ["BaseConstraint", "CheckConstraint", "Deferrable", "UniqueConstraint"]
|
__all__ = ["BaseConstraint", "CheckConstraint", "Deferrable", "UniqueConstraint"]
|
||||||
|
|
||||||
|
|
||||||
class BaseConstraint:
|
class BaseConstraint:
|
||||||
def __init__(self, name):
|
violation_error_message = _("Constraint “%(name)s” is violated.")
|
||||||
|
|
||||||
|
def __init__(self, name, violation_error_message=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
if violation_error_message is not None:
|
||||||
|
self.violation_error_message = violation_error_message
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def contains_expressions(self):
|
def contains_expressions(self):
|
||||||
|
@ -25,6 +34,12 @@ class BaseConstraint:
|
||||||
def remove_sql(self, model, schema_editor):
|
def remove_sql(self, model, schema_editor):
|
||||||
raise NotImplementedError("This method must be implemented by a subclass.")
|
raise NotImplementedError("This method must be implemented by a subclass.")
|
||||||
|
|
||||||
|
def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS):
|
||||||
|
raise NotImplementedError("This method must be implemented by a subclass.")
|
||||||
|
|
||||||
|
def get_violation_error_message(self):
|
||||||
|
return self.violation_error_message % {"name": self.name}
|
||||||
|
|
||||||
def deconstruct(self):
|
def deconstruct(self):
|
||||||
path = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
|
path = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
|
||||||
path = path.replace("django.db.models.constraints", "django.db.models")
|
path = path.replace("django.db.models.constraints", "django.db.models")
|
||||||
|
@ -36,13 +51,13 @@ class BaseConstraint:
|
||||||
|
|
||||||
|
|
||||||
class CheckConstraint(BaseConstraint):
|
class CheckConstraint(BaseConstraint):
|
||||||
def __init__(self, *, check, name):
|
def __init__(self, *, check, name, violation_error_message=None):
|
||||||
self.check = check
|
self.check = check
|
||||||
if not getattr(check, "conditional", False):
|
if not getattr(check, "conditional", False):
|
||||||
raise TypeError(
|
raise TypeError(
|
||||||
"CheckConstraint.check must be a Q instance or boolean expression."
|
"CheckConstraint.check must be a Q instance or boolean expression."
|
||||||
)
|
)
|
||||||
super().__init__(name)
|
super().__init__(name, violation_error_message=violation_error_message)
|
||||||
|
|
||||||
def _get_check_sql(self, model, schema_editor):
|
def _get_check_sql(self, model, schema_editor):
|
||||||
query = Query(model=model, alias_cols=False)
|
query = Query(model=model, alias_cols=False)
|
||||||
|
@ -62,6 +77,14 @@ class CheckConstraint(BaseConstraint):
|
||||||
def remove_sql(self, model, schema_editor):
|
def remove_sql(self, model, schema_editor):
|
||||||
return schema_editor._delete_check_sql(model, self.name)
|
return schema_editor._delete_check_sql(model, self.name)
|
||||||
|
|
||||||
|
def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS):
|
||||||
|
against = instance._get_field_value_map(meta=model._meta, exclude=exclude)
|
||||||
|
try:
|
||||||
|
if not Q(self.check).check(against, using=using):
|
||||||
|
raise ValidationError(self.get_violation_error_message())
|
||||||
|
except FieldError:
|
||||||
|
pass
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<%s: check=%s name=%s>" % (
|
return "<%s: check=%s name=%s>" % (
|
||||||
self.__class__.__qualname__,
|
self.__class__.__qualname__,
|
||||||
|
@ -99,6 +122,7 @@ class UniqueConstraint(BaseConstraint):
|
||||||
deferrable=None,
|
deferrable=None,
|
||||||
include=None,
|
include=None,
|
||||||
opclasses=(),
|
opclasses=(),
|
||||||
|
violation_error_message=None,
|
||||||
):
|
):
|
||||||
if not name:
|
if not name:
|
||||||
raise ValueError("A unique constraint must be named.")
|
raise ValueError("A unique constraint must be named.")
|
||||||
|
@ -148,7 +172,7 @@ class UniqueConstraint(BaseConstraint):
|
||||||
F(expression) if isinstance(expression, str) else expression
|
F(expression) if isinstance(expression, str) else expression
|
||||||
for expression in expressions
|
for expression in expressions
|
||||||
)
|
)
|
||||||
super().__init__(name)
|
super().__init__(name, violation_error_message=violation_error_message)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def contains_expressions(self):
|
def contains_expressions(self):
|
||||||
|
@ -265,3 +289,61 @@ class UniqueConstraint(BaseConstraint):
|
||||||
if self.opclasses:
|
if self.opclasses:
|
||||||
kwargs["opclasses"] = self.opclasses
|
kwargs["opclasses"] = self.opclasses
|
||||||
return path, self.expressions, kwargs
|
return path, self.expressions, kwargs
|
||||||
|
|
||||||
|
def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS):
|
||||||
|
queryset = model._default_manager.using(using)
|
||||||
|
if self.fields:
|
||||||
|
lookup_kwargs = {}
|
||||||
|
for field_name in self.fields:
|
||||||
|
if exclude and field_name in exclude:
|
||||||
|
return
|
||||||
|
field = model._meta.get_field(field_name)
|
||||||
|
lookup_value = getattr(instance, field.attname)
|
||||||
|
if lookup_value is None or (
|
||||||
|
lookup_value == ""
|
||||||
|
and connections[using].features.interprets_empty_strings_as_nulls
|
||||||
|
):
|
||||||
|
# A composite constraint containing NULL value cannot cause
|
||||||
|
# a violation since NULL != NULL in SQL.
|
||||||
|
return
|
||||||
|
lookup_kwargs[field.name] = lookup_value
|
||||||
|
queryset = queryset.filter(**lookup_kwargs)
|
||||||
|
else:
|
||||||
|
# Ignore constraints with excluded fields.
|
||||||
|
if exclude:
|
||||||
|
for expression in self.expressions:
|
||||||
|
for expr in expression.flatten():
|
||||||
|
if isinstance(expr, F) and expr.name in exclude:
|
||||||
|
return
|
||||||
|
replacement_map = instance._get_field_value_map(
|
||||||
|
meta=model._meta, exclude=exclude
|
||||||
|
)
|
||||||
|
expressions = [
|
||||||
|
Exact(expr, expr.replace_references(replacement_map))
|
||||||
|
for expr in self.expressions
|
||||||
|
]
|
||||||
|
queryset = queryset.filter(*expressions)
|
||||||
|
model_class_pk = instance._get_pk_val(model._meta)
|
||||||
|
if not instance._state.adding and model_class_pk is not None:
|
||||||
|
queryset = queryset.exclude(pk=model_class_pk)
|
||||||
|
if not self.condition:
|
||||||
|
if queryset.exists():
|
||||||
|
if self.expressions:
|
||||||
|
raise ValidationError(self.get_violation_error_message())
|
||||||
|
# When fields are defined, use the unique_error_message() for
|
||||||
|
# backward compatibility.
|
||||||
|
for model, constraints in instance.get_constraints():
|
||||||
|
for constraint in constraints:
|
||||||
|
if constraint is self:
|
||||||
|
raise ValidationError(
|
||||||
|
instance.unique_error_message(model, self.fields)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
against = instance._get_field_value_map(meta=model._meta, exclude=exclude)
|
||||||
|
try:
|
||||||
|
if (self.condition & Exists(queryset.filter(self.condition))).check(
|
||||||
|
against, using=using
|
||||||
|
):
|
||||||
|
raise ValidationError(self.get_violation_error_message())
|
||||||
|
except FieldError:
|
||||||
|
pass
|
||||||
|
|
|
@ -387,6 +387,18 @@ class BaseExpression:
|
||||||
)
|
)
|
||||||
return clone
|
return clone
|
||||||
|
|
||||||
|
def replace_references(self, references_map):
|
||||||
|
clone = self.copy()
|
||||||
|
clone.set_source_expressions(
|
||||||
|
[
|
||||||
|
references_map.get(expr.name, expr)
|
||||||
|
if isinstance(expr, F)
|
||||||
|
else expr.replace_references(references_map)
|
||||||
|
for expr in self.get_source_expressions()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return clone
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
return copy.copy(self)
|
return copy.copy(self)
|
||||||
|
|
||||||
|
|
|
@ -806,7 +806,8 @@ class BaseModelFormSet(BaseFormSet):
|
||||||
for form in valid_forms:
|
for form in valid_forms:
|
||||||
exclude = form._get_validation_exclusions()
|
exclude = form._get_validation_exclusions()
|
||||||
unique_checks, date_checks = form.instance._get_unique_checks(
|
unique_checks, date_checks = form.instance._get_unique_checks(
|
||||||
exclude=exclude
|
exclude=exclude,
|
||||||
|
include_meta_constraints=True,
|
||||||
)
|
)
|
||||||
all_unique_checks.update(unique_checks)
|
all_unique_checks.update(unique_checks)
|
||||||
all_date_checks.update(date_checks)
|
all_date_checks.update(date_checks)
|
||||||
|
|
|
@ -395,6 +395,8 @@ Models
|
||||||
* **models.W043**: ``<database>`` does not support indexes on expressions.
|
* **models.W043**: ``<database>`` does not support indexes on expressions.
|
||||||
* **models.W044**: ``<database>`` does not support unique constraints on
|
* **models.W044**: ``<database>`` does not support unique constraints on
|
||||||
expressions.
|
expressions.
|
||||||
|
* **models.W045**: Check constraint ``<constraint>`` contains ``RawSQL()``
|
||||||
|
expression and won't be validated during the model ``full_clean()``.
|
||||||
|
|
||||||
Security
|
Security
|
||||||
--------
|
--------
|
||||||
|
|
|
@ -12,7 +12,7 @@ PostgreSQL supports additional data integrity constraints available from the
|
||||||
``ExclusionConstraint``
|
``ExclusionConstraint``
|
||||||
=======================
|
=======================
|
||||||
|
|
||||||
.. class:: ExclusionConstraint(*, name, expressions, index_type=None, condition=None, deferrable=None, include=None, opclasses=())
|
.. class:: ExclusionConstraint(*, name, expressions, index_type=None, condition=None, deferrable=None, include=None, opclasses=(), violation_error_message=None)
|
||||||
|
|
||||||
Creates an exclusion constraint in the database. Internally, PostgreSQL
|
Creates an exclusion constraint in the database. Internally, PostgreSQL
|
||||||
implements exclusion constraints using indexes. The default index type is
|
implements exclusion constraints using indexes. The default index type is
|
||||||
|
@ -27,6 +27,14 @@ PostgreSQL supports additional data integrity constraints available from the
|
||||||
:exc:`~django.db.IntegrityError` is raised. Similarly, when update
|
:exc:`~django.db.IntegrityError` is raised. Similarly, when update
|
||||||
conflicts with an existing row.
|
conflicts with an existing row.
|
||||||
|
|
||||||
|
Exclusion constraints are checked during the :ref:`model validation
|
||||||
|
<validating-objects>`.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.1
|
||||||
|
|
||||||
|
In older versions, exclusion constraints were not checked during model
|
||||||
|
validation.
|
||||||
|
|
||||||
``name``
|
``name``
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
@ -165,6 +173,15 @@ creates an exclusion constraint on ``circle`` using ``circle_ops``.
|
||||||
:class:`OpClass() <django.contrib.postgres.indexes.OpClass>` in
|
:class:`OpClass() <django.contrib.postgres.indexes.OpClass>` in
|
||||||
:attr:`~ExclusionConstraint.expressions`.
|
:attr:`~ExclusionConstraint.expressions`.
|
||||||
|
|
||||||
|
``violation_error_message``
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
The error message used when ``ValidationError`` is raised during
|
||||||
|
:ref:`model validation <validating-objects>`. Defaults to
|
||||||
|
:attr:`.BaseConstraint.violation_error_message`.
|
||||||
|
|
||||||
Examples
|
Examples
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
|
|
@ -31,24 +31,21 @@ option.
|
||||||
|
|
||||||
.. admonition:: Validation of Constraints
|
.. admonition:: Validation of Constraints
|
||||||
|
|
||||||
In general constraints are **not** checked during ``full_clean()``, and do
|
Constraints are checked during the :ref:`model validation
|
||||||
not raise ``ValidationError``\s. Rather you'll get a database integrity
|
<validating-objects>`.
|
||||||
error on ``save()``. ``UniqueConstraint``\s without a
|
|
||||||
:attr:`~UniqueConstraint.condition` (i.e. non-partial unique constraints)
|
.. versionchanged:: 4.1
|
||||||
and :attr:`~UniqueConstraint.expressions` (i.e. non-functional unique
|
|
||||||
constraints) are different in this regard, in that they leverage the
|
In older versions, constraints were not checked during model validation.
|
||||||
existing ``validate_unique()`` logic, and thus enable two-stage validation.
|
|
||||||
In addition to ``IntegrityError`` on ``save()``, ``ValidationError`` is
|
|
||||||
also raised during model validation when the ``UniqueConstraint`` is
|
|
||||||
violated.
|
|
||||||
|
|
||||||
``BaseConstraint``
|
``BaseConstraint``
|
||||||
==================
|
==================
|
||||||
|
|
||||||
.. class:: BaseConstraint(name)
|
.. class:: BaseConstraint(name, violation_error_message=None)
|
||||||
|
|
||||||
Base class for all constraints. Subclasses must implement
|
Base class for all constraints. Subclasses must implement
|
||||||
``constraint_sql()``, ``create_sql()``, and ``remove_sql()`` methods.
|
``constraint_sql()``, ``create_sql()``, ``remove_sql()`` and
|
||||||
|
``validate()`` methods.
|
||||||
|
|
||||||
All constraints have the following parameters in common:
|
All constraints have the following parameters in common:
|
||||||
|
|
||||||
|
@ -60,10 +57,37 @@ All constraints have the following parameters in common:
|
||||||
The name of the constraint. You must always specify a unique name for the
|
The name of the constraint. You must always specify a unique name for the
|
||||||
constraint.
|
constraint.
|
||||||
|
|
||||||
|
``violation_error_message``
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
.. attribute:: BaseConstraint.violation_error_message
|
||||||
|
|
||||||
|
The error message used when ``ValidationError`` is raised during
|
||||||
|
:ref:`model validation <validating-objects>`. Defaults to
|
||||||
|
``"Constraint “%(name)s” is violated."``.
|
||||||
|
|
||||||
|
``validate()``
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
.. method:: BaseConstraint.validate(model, instance, exclude=None, using=DEFAULT_DB_ALIAS)
|
||||||
|
|
||||||
|
Validates that the constraint, defined on ``model``, is respected on the
|
||||||
|
``instance``. This will do a query on the database to ensure that the
|
||||||
|
constraint is respected. If fields in the ``exclude`` list are needed to
|
||||||
|
validate the constraint, the constraint is ignored.
|
||||||
|
|
||||||
|
Raise a ``ValidationError`` if the constraint is violated.
|
||||||
|
|
||||||
|
This method must be implemented by a subclass.
|
||||||
|
|
||||||
``CheckConstraint``
|
``CheckConstraint``
|
||||||
===================
|
===================
|
||||||
|
|
||||||
.. class:: CheckConstraint(*, check, name)
|
.. class:: CheckConstraint(*, check, name, violation_error_message=None)
|
||||||
|
|
||||||
Creates a check constraint in the database.
|
Creates a check constraint in the database.
|
||||||
|
|
||||||
|
@ -78,10 +102,14 @@ specifies the check you want the constraint to enforce.
|
||||||
For example, ``CheckConstraint(check=Q(age__gte=18), name='age_gte_18')``
|
For example, ``CheckConstraint(check=Q(age__gte=18), name='age_gte_18')``
|
||||||
ensures the age field is never less than 18.
|
ensures the age field is never less than 18.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.1
|
||||||
|
|
||||||
|
The ``violation_error_message`` argument was added.
|
||||||
|
|
||||||
``UniqueConstraint``
|
``UniqueConstraint``
|
||||||
====================
|
====================
|
||||||
|
|
||||||
.. class:: UniqueConstraint(*expressions, fields=(), name=None, condition=None, deferrable=None, include=None, opclasses=())
|
.. class:: UniqueConstraint(*expressions, fields=(), name=None, condition=None, deferrable=None, include=None, opclasses=(), violation_error_message=None)
|
||||||
|
|
||||||
Creates a unique constraint in the database.
|
Creates a unique constraint in the database.
|
||||||
|
|
||||||
|
@ -203,3 +231,21 @@ For example::
|
||||||
creates a unique index on ``username`` using ``varchar_pattern_ops``.
|
creates a unique index on ``username`` using ``varchar_pattern_ops``.
|
||||||
|
|
||||||
``opclasses`` are ignored for databases besides PostgreSQL.
|
``opclasses`` are ignored for databases besides PostgreSQL.
|
||||||
|
|
||||||
|
``violation_error_message``
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
.. attribute:: UniqueConstraint.violation_error_message
|
||||||
|
|
||||||
|
The error message used when ``ValidationError`` is raised during
|
||||||
|
:ref:`model validation <validating-objects>`. Defaults to
|
||||||
|
:attr:`.BaseConstraint.violation_error_message`.
|
||||||
|
|
||||||
|
This message is *not used* for :class:`UniqueConstraint`\s with
|
||||||
|
:attr:`~UniqueConstraint.fields` and without a
|
||||||
|
:attr:`~UniqueConstraint.condition`. Such :class:`~UniqueConstraint`\s show the
|
||||||
|
same message as constraints defined with
|
||||||
|
:attr:`.Field.unique` or in
|
||||||
|
:attr:`Meta.unique_together <django.db.models.Options.constraints>`.
|
||||||
|
|
|
@ -198,9 +198,10 @@ There are three steps involved in validating a model:
|
||||||
1. Validate the model fields - :meth:`Model.clean_fields()`
|
1. Validate the model fields - :meth:`Model.clean_fields()`
|
||||||
2. Validate the model as a whole - :meth:`Model.clean()`
|
2. Validate the model as a whole - :meth:`Model.clean()`
|
||||||
3. Validate the field uniqueness - :meth:`Model.validate_unique()`
|
3. Validate the field uniqueness - :meth:`Model.validate_unique()`
|
||||||
|
4. Validate the constraints - :meth:`Model.validate_constraints`
|
||||||
|
|
||||||
All three steps are performed when you call a model's
|
All four steps are performed when you call a model's :meth:`~Model.full_clean`
|
||||||
:meth:`~Model.full_clean()` method.
|
method.
|
||||||
|
|
||||||
When you use a :class:`~django.forms.ModelForm`, the call to
|
When you use a :class:`~django.forms.ModelForm`, the call to
|
||||||
:meth:`~django.forms.Form.is_valid()` will perform these validation steps for
|
:meth:`~django.forms.Form.is_valid()` will perform these validation steps for
|
||||||
|
@ -210,12 +211,18 @@ need to call a model's :meth:`~Model.full_clean()` method if you plan to handle
|
||||||
validation errors yourself, or if you have excluded fields from the
|
validation errors yourself, or if you have excluded fields from the
|
||||||
:class:`~django.forms.ModelForm` that require validation.
|
:class:`~django.forms.ModelForm` that require validation.
|
||||||
|
|
||||||
.. method:: Model.full_clean(exclude=None, validate_unique=True)
|
.. versionchanged:: 4.1
|
||||||
|
|
||||||
This method calls :meth:`Model.clean_fields()`, :meth:`Model.clean()`, and
|
In older versions, constraints were not checked during the model
|
||||||
:meth:`Model.validate_unique()` (if ``validate_unique`` is ``True``), in that
|
validation.
|
||||||
order and raises a :exc:`~django.core.exceptions.ValidationError` that has a
|
|
||||||
``message_dict`` attribute containing errors from all three stages.
|
.. method:: Model.full_clean(exclude=None, validate_unique=True, validate_constraints=True)
|
||||||
|
|
||||||
|
This method calls :meth:`Model.clean_fields()`, :meth:`Model.clean()`,
|
||||||
|
:meth:`Model.validate_unique()` (if ``validate_unique`` is ``True``), and
|
||||||
|
:meth:`Model.validate_constraints()` (if ``validate_constraints`` is ``True``)
|
||||||
|
in that order and raises a :exc:`~django.core.exceptions.ValidationError` that
|
||||||
|
has a ``message_dict`` attribute containing errors from all four stages.
|
||||||
|
|
||||||
The optional ``exclude`` argument can be used to provide a list of field names
|
The optional ``exclude`` argument can be used to provide a list of field names
|
||||||
that can be excluded from validation and cleaning.
|
that can be excluded from validation and cleaning.
|
||||||
|
@ -238,6 +245,10 @@ models. For example::
|
||||||
|
|
||||||
The first step ``full_clean()`` performs is to clean each individual field.
|
The first step ``full_clean()`` performs is to clean each individual field.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.1
|
||||||
|
|
||||||
|
The ``validate_constraints`` argument was added.
|
||||||
|
|
||||||
.. method:: Model.clean_fields(exclude=None)
|
.. method:: Model.clean_fields(exclude=None)
|
||||||
|
|
||||||
This method will validate all fields on your model. The optional ``exclude``
|
This method will validate all fields on your model. The optional ``exclude``
|
||||||
|
@ -306,7 +317,7 @@ pass a dictionary mapping field names to errors::
|
||||||
'pub_date': ValidationError(_('Invalid date.'), code='invalid'),
|
'pub_date': ValidationError(_('Invalid date.'), code='invalid'),
|
||||||
})
|
})
|
||||||
|
|
||||||
Finally, ``full_clean()`` will check any unique constraints on your model.
|
Then, ``full_clean()`` will check unique constraints on your model.
|
||||||
|
|
||||||
.. admonition:: How to raise field-specific validation errors if those fields don't appear in a ``ModelForm``
|
.. admonition:: How to raise field-specific validation errors if those fields don't appear in a ``ModelForm``
|
||||||
|
|
||||||
|
@ -339,16 +350,40 @@ Finally, ``full_clean()`` will check any unique constraints on your model.
|
||||||
|
|
||||||
.. method:: Model.validate_unique(exclude=None)
|
.. method:: Model.validate_unique(exclude=None)
|
||||||
|
|
||||||
This method is similar to :meth:`~Model.clean_fields`, but validates all
|
This method is similar to :meth:`~Model.clean_fields`, but validates
|
||||||
uniqueness constraints on your model instead of individual field values. The
|
uniqueness constraints defined via :attr:`.Field.unique`,
|
||||||
optional ``exclude`` argument allows you to provide a list of field names to
|
:attr:`.Field.unique_for_date`, :attr:`.Field.unique_for_month`,
|
||||||
exclude from validation. It will raise a
|
:attr:`.Field.unique_for_year`, or :attr:`Meta.unique_together
|
||||||
|
<django.db.models.Options.unique_together>` on your model instead of individual
|
||||||
|
field values. The optional ``exclude`` argument allows you to provide a list of
|
||||||
|
field names to exclude from validation. It will raise a
|
||||||
:exc:`~django.core.exceptions.ValidationError` if any fields fail validation.
|
:exc:`~django.core.exceptions.ValidationError` if any fields fail validation.
|
||||||
|
|
||||||
|
:class:`~django.db.models.UniqueConstraint`\s defined in the
|
||||||
|
:attr:`Meta.constraints <django.db.models.Options.constraints>` are validated
|
||||||
|
by :meth:`Model.validate_constraints`.
|
||||||
|
|
||||||
Note that if you provide an ``exclude`` argument to ``validate_unique()``, any
|
Note that if you provide an ``exclude`` argument to ``validate_unique()``, any
|
||||||
:attr:`~django.db.models.Options.unique_together` constraint involving one of
|
:attr:`~django.db.models.Options.unique_together` constraint involving one of
|
||||||
the fields you provided will not be checked.
|
the fields you provided will not be checked.
|
||||||
|
|
||||||
|
Finally, ``full_clean()`` will check any other constraints on your model.
|
||||||
|
|
||||||
|
.. versionchanged:: 4.1
|
||||||
|
|
||||||
|
In older versions, :class:`~django.db.models.UniqueConstraint`\s were
|
||||||
|
validated by ``validate_unique()``.
|
||||||
|
|
||||||
|
.. method:: Model.validate_constraints(exclude=None)
|
||||||
|
|
||||||
|
.. versionadded:: 4.1
|
||||||
|
|
||||||
|
This method validates all constraints defined in
|
||||||
|
:attr:`Meta.constraints <django.db.models.Options.constraints>`. The
|
||||||
|
optional ``exclude`` argument allows you to provide a list of field names to
|
||||||
|
exclude from validation. It will raise a
|
||||||
|
:exc:`~django.core.exceptions.ValidationError` if any constraints fail
|
||||||
|
validation.
|
||||||
|
|
||||||
Saving objects
|
Saving objects
|
||||||
==============
|
==============
|
||||||
|
|
|
@ -65,6 +65,15 @@ advantage of developments in the ORM's asynchronous support as it evolves.
|
||||||
|
|
||||||
See :ref:`async-queries` for details and limitations.
|
See :ref:`async-queries` for details and limitations.
|
||||||
|
|
||||||
|
Validation of Constraints
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
:class:`Check <django.db.models.CheckConstraint>`,
|
||||||
|
:class:`unique <django.db.models.UniqueConstraint>`, and :class:`exclusion
|
||||||
|
<django.contrib.postgres.constraints.ExclusionConstraint>` constraints defined
|
||||||
|
in the :attr:`Meta.constraints <django.db.models.Options.constraints>` option
|
||||||
|
are now checked during :ref:`model validation <validating-objects>`.
|
||||||
|
|
||||||
.. _csrf-cookie-masked-usage:
|
.. _csrf-cookie-masked-usage:
|
||||||
|
|
||||||
``CSRF_COOKIE_MASKED`` setting
|
``CSRF_COOKIE_MASKED`` setting
|
||||||
|
@ -551,6 +560,10 @@ Miscellaneous
|
||||||
* The undocumented ``django.contrib.auth.views.SuccessURLAllowedHostsMixin``
|
* The undocumented ``django.contrib.auth.views.SuccessURLAllowedHostsMixin``
|
||||||
mixin is replaced by ``RedirectURLMixin``.
|
mixin is replaced by ``RedirectURLMixin``.
|
||||||
|
|
||||||
|
* :class:`~django.db.models.BaseConstraint` subclasses must implement
|
||||||
|
:meth:`~django.db.models.BaseConstraint.validate` method to allow those
|
||||||
|
constraints to be used for validation.
|
||||||
|
|
||||||
.. _deprecated-features-4.1:
|
.. _deprecated-features-4.1:
|
||||||
|
|
||||||
Features deprecated in 4.1
|
Features deprecated in 4.1
|
||||||
|
|
|
@ -38,6 +38,10 @@ class UniqueConstraintProduct(models.Model):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ChildUniqueConstraintProduct(UniqueConstraintProduct):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UniqueConstraintConditionProduct(models.Model):
|
class UniqueConstraintConditionProduct(models.Model):
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
color = models.CharField(max_length=32, null=True)
|
color = models.CharField(max_length=32, null=True)
|
||||||
|
|
|
@ -10,6 +10,7 @@ from django.test import SimpleTestCase, TestCase, skipUnlessDBFeature
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
ChildModel,
|
ChildModel,
|
||||||
|
ChildUniqueConstraintProduct,
|
||||||
Product,
|
Product,
|
||||||
UniqueConstraintConditionProduct,
|
UniqueConstraintConditionProduct,
|
||||||
UniqueConstraintDeferrable,
|
UniqueConstraintDeferrable,
|
||||||
|
@ -46,6 +47,24 @@ class BaseConstraintTests(SimpleTestCase):
|
||||||
with self.assertRaisesMessage(NotImplementedError, msg):
|
with self.assertRaisesMessage(NotImplementedError, msg):
|
||||||
c.remove_sql(None, None)
|
c.remove_sql(None, None)
|
||||||
|
|
||||||
|
def test_validate(self):
|
||||||
|
c = BaseConstraint("name")
|
||||||
|
msg = "This method must be implemented by a subclass."
|
||||||
|
with self.assertRaisesMessage(NotImplementedError, msg):
|
||||||
|
c.validate(None, None)
|
||||||
|
|
||||||
|
def test_default_violation_error_message(self):
|
||||||
|
c = BaseConstraint("name")
|
||||||
|
self.assertEqual(
|
||||||
|
c.get_violation_error_message(), "Constraint “name” is violated."
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_custom_violation_error_message(self):
|
||||||
|
c = BaseConstraint(
|
||||||
|
"base_name", violation_error_message="custom %(name)s message"
|
||||||
|
)
|
||||||
|
self.assertEqual(c.get_violation_error_message(), "custom base_name message")
|
||||||
|
|
||||||
|
|
||||||
class CheckConstraintTests(TestCase):
|
class CheckConstraintTests(TestCase):
|
||||||
def test_eq(self):
|
def test_eq(self):
|
||||||
|
@ -122,16 +141,60 @@ class CheckConstraintTests(TestCase):
|
||||||
constraints = get_constraints(ChildModel._meta.db_table)
|
constraints = get_constraints(ChildModel._meta.db_table)
|
||||||
self.assertIn("constraints_childmodel_adult", constraints)
|
self.assertIn("constraints_childmodel_adult", constraints)
|
||||||
|
|
||||||
|
def test_validate(self):
|
||||||
|
check = models.Q(price__gt=models.F("discounted_price"))
|
||||||
|
constraint = models.CheckConstraint(check=check, name="price")
|
||||||
|
# Invalid product.
|
||||||
|
invalid_product = Product(price=10, discounted_price=42)
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
constraint.validate(Product, invalid_product)
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
constraint.validate(Product, invalid_product, exclude={"unit"})
|
||||||
|
# Fields used by the check constraint are excluded.
|
||||||
|
constraint.validate(Product, invalid_product, exclude={"price"})
|
||||||
|
constraint.validate(Product, invalid_product, exclude={"discounted_price"})
|
||||||
|
constraint.validate(
|
||||||
|
Product,
|
||||||
|
invalid_product,
|
||||||
|
exclude={"discounted_price", "price"},
|
||||||
|
)
|
||||||
|
# Valid product.
|
||||||
|
constraint.validate(Product, Product(price=10, discounted_price=5))
|
||||||
|
|
||||||
|
def test_validate_boolean_expressions(self):
|
||||||
|
constraint = models.CheckConstraint(
|
||||||
|
check=models.expressions.ExpressionWrapper(
|
||||||
|
models.Q(price__gt=500) | models.Q(price__lt=500),
|
||||||
|
output_field=models.BooleanField(),
|
||||||
|
),
|
||||||
|
name="price_neq_500_wrap",
|
||||||
|
)
|
||||||
|
msg = f"Constraint “{constraint.name}” is violated."
|
||||||
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
|
constraint.validate(Product, Product(price=500, discounted_price=5))
|
||||||
|
constraint.validate(Product, Product(price=501, discounted_price=5))
|
||||||
|
constraint.validate(Product, Product(price=499, discounted_price=5))
|
||||||
|
|
||||||
|
def test_validate_rawsql_expressions_noop(self):
|
||||||
|
constraint = models.CheckConstraint(
|
||||||
|
check=models.expressions.RawSQL(
|
||||||
|
"price < %s OR price > %s",
|
||||||
|
(500, 500),
|
||||||
|
output_field=models.BooleanField(),
|
||||||
|
),
|
||||||
|
name="price_neq_500_raw",
|
||||||
|
)
|
||||||
|
# RawSQL can not be checked and is always considered valid.
|
||||||
|
constraint.validate(Product, Product(price=500, discounted_price=5))
|
||||||
|
constraint.validate(Product, Product(price=501, discounted_price=5))
|
||||||
|
constraint.validate(Product, Product(price=499, discounted_price=5))
|
||||||
|
|
||||||
|
|
||||||
class UniqueConstraintTests(TestCase):
|
class UniqueConstraintTests(TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
cls.p1, cls.p2 = UniqueConstraintProduct.objects.bulk_create(
|
cls.p1 = UniqueConstraintProduct.objects.create(name="p1", color="red")
|
||||||
[
|
cls.p2 = UniqueConstraintProduct.objects.create(name="p2")
|
||||||
UniqueConstraintProduct(name="p1", color="red"),
|
|
||||||
UniqueConstraintProduct(name="p2"),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_eq(self):
|
def test_eq(self):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
@ -415,15 +478,135 @@ class UniqueConstraintTests(TestCase):
|
||||||
with self.assertRaisesMessage(ValidationError, msg):
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
UniqueConstraintProduct(
|
UniqueConstraintProduct(
|
||||||
name=self.p1.name, color=self.p1.color
|
name=self.p1.name, color=self.p1.color
|
||||||
).validate_unique()
|
).validate_constraints()
|
||||||
|
|
||||||
@skipUnlessDBFeature("supports_partial_indexes")
|
@skipUnlessDBFeature("supports_partial_indexes")
|
||||||
def test_model_validation_with_condition(self):
|
def test_model_validation_with_condition(self):
|
||||||
"""Partial unique constraints are ignored by Model.validate_unique()."""
|
"""
|
||||||
|
Partial unique constraints are not ignored by
|
||||||
|
Model.validate_constraints().
|
||||||
|
"""
|
||||||
obj1 = UniqueConstraintConditionProduct.objects.create(name="p1", color="red")
|
obj1 = UniqueConstraintConditionProduct.objects.create(name="p1", color="red")
|
||||||
obj2 = UniqueConstraintConditionProduct.objects.create(name="p2")
|
obj2 = UniqueConstraintConditionProduct.objects.create(name="p2")
|
||||||
UniqueConstraintConditionProduct(name=obj1.name, color="blue").validate_unique()
|
UniqueConstraintConditionProduct(
|
||||||
UniqueConstraintConditionProduct(name=obj2.name).validate_unique()
|
name=obj1.name, color="blue"
|
||||||
|
).validate_constraints()
|
||||||
|
msg = "Constraint “name_without_color_uniq” is violated."
|
||||||
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
|
UniqueConstraintConditionProduct(name=obj2.name).validate_constraints()
|
||||||
|
|
||||||
|
def test_validate(self):
|
||||||
|
constraint = UniqueConstraintProduct._meta.constraints[0]
|
||||||
|
msg = "Unique constraint product with this Name and Color already exists."
|
||||||
|
non_unique_product = UniqueConstraintProduct(
|
||||||
|
name=self.p1.name, color=self.p1.color
|
||||||
|
)
|
||||||
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
|
constraint.validate(UniqueConstraintProduct, non_unique_product)
|
||||||
|
# Null values are ignored.
|
||||||
|
constraint.validate(
|
||||||
|
UniqueConstraintProduct,
|
||||||
|
UniqueConstraintProduct(name=self.p2.name, color=None),
|
||||||
|
)
|
||||||
|
# Existing instances have their existing row excluded.
|
||||||
|
constraint.validate(UniqueConstraintProduct, self.p1)
|
||||||
|
# Unique fields are excluded.
|
||||||
|
constraint.validate(
|
||||||
|
UniqueConstraintProduct,
|
||||||
|
non_unique_product,
|
||||||
|
exclude={"name"},
|
||||||
|
)
|
||||||
|
constraint.validate(
|
||||||
|
UniqueConstraintProduct,
|
||||||
|
non_unique_product,
|
||||||
|
exclude={"color"},
|
||||||
|
)
|
||||||
|
constraint.validate(
|
||||||
|
UniqueConstraintProduct,
|
||||||
|
non_unique_product,
|
||||||
|
exclude={"name", "color"},
|
||||||
|
)
|
||||||
|
# Validation on a child instance.
|
||||||
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
|
constraint.validate(
|
||||||
|
UniqueConstraintProduct,
|
||||||
|
ChildUniqueConstraintProduct(name=self.p1.name, color=self.p1.color),
|
||||||
|
)
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_partial_indexes")
|
||||||
|
def test_validate_condition(self):
|
||||||
|
p1 = UniqueConstraintConditionProduct.objects.create(name="p1")
|
||||||
|
constraint = UniqueConstraintConditionProduct._meta.constraints[0]
|
||||||
|
msg = "Constraint “name_without_color_uniq” is violated."
|
||||||
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
|
constraint.validate(
|
||||||
|
UniqueConstraintConditionProduct,
|
||||||
|
UniqueConstraintConditionProduct(name=p1.name, color=None),
|
||||||
|
)
|
||||||
|
# Values not matching condition are ignored.
|
||||||
|
constraint.validate(
|
||||||
|
UniqueConstraintConditionProduct,
|
||||||
|
UniqueConstraintConditionProduct(name=p1.name, color="anything-but-none"),
|
||||||
|
)
|
||||||
|
# Existing instances have their existing row excluded.
|
||||||
|
constraint.validate(UniqueConstraintConditionProduct, p1)
|
||||||
|
# Unique field is excluded.
|
||||||
|
constraint.validate(
|
||||||
|
UniqueConstraintConditionProduct,
|
||||||
|
UniqueConstraintConditionProduct(name=p1.name, color=None),
|
||||||
|
exclude={"name"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_validate_expression(self):
|
||||||
|
constraint = models.UniqueConstraint(Lower("name"), name="name_lower_uniq")
|
||||||
|
msg = "Constraint “name_lower_uniq” is violated."
|
||||||
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
|
constraint.validate(
|
||||||
|
UniqueConstraintProduct,
|
||||||
|
UniqueConstraintProduct(name=self.p1.name.upper()),
|
||||||
|
)
|
||||||
|
constraint.validate(
|
||||||
|
UniqueConstraintProduct,
|
||||||
|
UniqueConstraintProduct(name="another-name"),
|
||||||
|
)
|
||||||
|
# Existing instances have their existing row excluded.
|
||||||
|
constraint.validate(UniqueConstraintProduct, self.p1)
|
||||||
|
# Unique field is excluded.
|
||||||
|
constraint.validate(
|
||||||
|
UniqueConstraintProduct,
|
||||||
|
UniqueConstraintProduct(name=self.p1.name.upper()),
|
||||||
|
exclude={"name"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_validate_expression_condition(self):
|
||||||
|
constraint = models.UniqueConstraint(
|
||||||
|
Lower("name"),
|
||||||
|
name="name_lower_without_color_uniq",
|
||||||
|
condition=models.Q(color__isnull=True),
|
||||||
|
)
|
||||||
|
non_unique_product = UniqueConstraintProduct(name=self.p2.name.upper())
|
||||||
|
msg = "Constraint “name_lower_without_color_uniq” is violated."
|
||||||
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
|
constraint.validate(UniqueConstraintProduct, non_unique_product)
|
||||||
|
# Values not matching condition are ignored.
|
||||||
|
constraint.validate(
|
||||||
|
UniqueConstraintProduct,
|
||||||
|
UniqueConstraintProduct(name=self.p1.name, color=self.p1.color),
|
||||||
|
)
|
||||||
|
# Existing instances have their existing row excluded.
|
||||||
|
constraint.validate(UniqueConstraintProduct, self.p2)
|
||||||
|
# Unique field is excluded.
|
||||||
|
constraint.validate(
|
||||||
|
UniqueConstraintProduct,
|
||||||
|
non_unique_product,
|
||||||
|
exclude={"name"},
|
||||||
|
)
|
||||||
|
# Field from a condition is excluded.
|
||||||
|
constraint.validate(
|
||||||
|
UniqueConstraintProduct,
|
||||||
|
non_unique_product,
|
||||||
|
exclude={"color"},
|
||||||
|
)
|
||||||
|
|
||||||
def test_name(self):
|
def test_name(self):
|
||||||
constraints = get_constraints(UniqueConstraintProduct._meta.db_table)
|
constraints = get_constraints(UniqueConstraintProduct._meta.db_table)
|
||||||
|
|
|
@ -2198,6 +2198,66 @@ class ConstraintsTests(TestCase):
|
||||||
]
|
]
|
||||||
self.assertCountEqual(errors, expected_errors)
|
self.assertCountEqual(errors, expected_errors)
|
||||||
|
|
||||||
|
def test_check_constraint_raw_sql_check(self):
|
||||||
|
class Model(models.Model):
|
||||||
|
class Meta:
|
||||||
|
required_db_features = {"supports_table_check_constraints"}
|
||||||
|
constraints = [
|
||||||
|
models.CheckConstraint(check=models.Q(id__gt=0), name="q_check"),
|
||||||
|
models.CheckConstraint(
|
||||||
|
check=models.ExpressionWrapper(
|
||||||
|
models.Q(price__gt=20),
|
||||||
|
output_field=models.BooleanField(),
|
||||||
|
),
|
||||||
|
name="expression_wrapper_check",
|
||||||
|
),
|
||||||
|
models.CheckConstraint(
|
||||||
|
check=models.expressions.RawSQL(
|
||||||
|
"id = 0",
|
||||||
|
params=(),
|
||||||
|
output_field=models.BooleanField(),
|
||||||
|
),
|
||||||
|
name="raw_sql_check",
|
||||||
|
),
|
||||||
|
models.CheckConstraint(
|
||||||
|
check=models.Q(
|
||||||
|
models.ExpressionWrapper(
|
||||||
|
models.Q(
|
||||||
|
models.expressions.RawSQL(
|
||||||
|
"id = 0",
|
||||||
|
params=(),
|
||||||
|
output_field=models.BooleanField(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
output_field=models.BooleanField(),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
name="nested_raw_sql_check",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
expected_warnings = (
|
||||||
|
[
|
||||||
|
Warning(
|
||||||
|
"Check constraint 'raw_sql_check' contains RawSQL() expression and "
|
||||||
|
"won't be validated during the model full_clean().",
|
||||||
|
hint="Silence this warning if you don't care about it.",
|
||||||
|
obj=Model,
|
||||||
|
id="models.W045",
|
||||||
|
),
|
||||||
|
Warning(
|
||||||
|
"Check constraint 'nested_raw_sql_check' contains RawSQL() "
|
||||||
|
"expression and won't be validated during the model full_clean().",
|
||||||
|
hint="Silence this warning if you don't care about it.",
|
||||||
|
obj=Model,
|
||||||
|
id="models.W045",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
if connection.features.supports_table_check_constraints
|
||||||
|
else []
|
||||||
|
)
|
||||||
|
self.assertEqual(Model.check(databases=self.databases), expected_warnings)
|
||||||
|
|
||||||
def test_unique_constraint_with_condition(self):
|
def test_unique_constraint_with_condition(self):
|
||||||
class Model(models.Model):
|
class Model(models.Model):
|
||||||
age = models.IntegerField()
|
age = models.IntegerField()
|
||||||
|
|
|
@ -2,6 +2,7 @@ import datetime
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from django.contrib.postgres.indexes import OpClass
|
from django.contrib.postgres.indexes import OpClass
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import IntegrityError, NotSupportedError, connection, transaction
|
from django.db import IntegrityError, NotSupportedError, connection, transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
CheckConstraint,
|
CheckConstraint,
|
||||||
|
@ -612,18 +613,26 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
|
||||||
timezone.datetime(2018, 6, 28),
|
timezone.datetime(2018, 6, 28),
|
||||||
timezone.datetime(2018, 6, 29),
|
timezone.datetime(2018, 6, 29),
|
||||||
]
|
]
|
||||||
HotelReservation.objects.create(
|
reservation = HotelReservation.objects.create(
|
||||||
datespan=DateRange(datetimes[0].date(), datetimes[1].date()),
|
datespan=DateRange(datetimes[0].date(), datetimes[1].date()),
|
||||||
start=datetimes[0],
|
start=datetimes[0],
|
||||||
end=datetimes[1],
|
end=datetimes[1],
|
||||||
room=room102,
|
room=room102,
|
||||||
)
|
)
|
||||||
|
constraint.validate(HotelReservation, reservation)
|
||||||
HotelReservation.objects.create(
|
HotelReservation.objects.create(
|
||||||
datespan=DateRange(datetimes[1].date(), datetimes[3].date()),
|
datespan=DateRange(datetimes[1].date(), datetimes[3].date()),
|
||||||
start=datetimes[1],
|
start=datetimes[1],
|
||||||
end=datetimes[3],
|
end=datetimes[3],
|
||||||
room=room102,
|
room=room102,
|
||||||
)
|
)
|
||||||
|
HotelReservation.objects.create(
|
||||||
|
datespan=DateRange(datetimes[3].date(), datetimes[4].date()),
|
||||||
|
start=datetimes[3],
|
||||||
|
end=datetimes[4],
|
||||||
|
room=room102,
|
||||||
|
cancelled=True,
|
||||||
|
)
|
||||||
# Overlap dates.
|
# Overlap dates.
|
||||||
with self.assertRaises(IntegrityError), transaction.atomic():
|
with self.assertRaises(IntegrityError), transaction.atomic():
|
||||||
reservation = HotelReservation(
|
reservation = HotelReservation(
|
||||||
|
@ -632,33 +641,58 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
|
||||||
end=datetimes[2],
|
end=datetimes[2],
|
||||||
room=room102,
|
room=room102,
|
||||||
)
|
)
|
||||||
|
msg = f"Constraint “{constraint.name}” is violated."
|
||||||
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
|
constraint.validate(HotelReservation, reservation)
|
||||||
reservation.save()
|
reservation.save()
|
||||||
# Valid range.
|
# Valid range.
|
||||||
HotelReservation.objects.bulk_create(
|
other_valid_reservations = [
|
||||||
[
|
# Other room.
|
||||||
# Other room.
|
HotelReservation(
|
||||||
HotelReservation(
|
datespan=(datetimes[1].date(), datetimes[2].date()),
|
||||||
datespan=(datetimes[1].date(), datetimes[2].date()),
|
start=datetimes[1],
|
||||||
start=datetimes[1],
|
end=datetimes[2],
|
||||||
end=datetimes[2],
|
room=room101,
|
||||||
room=room101,
|
),
|
||||||
),
|
# Cancelled reservation.
|
||||||
# Cancelled reservation.
|
HotelReservation(
|
||||||
HotelReservation(
|
datespan=(datetimes[1].date(), datetimes[1].date()),
|
||||||
datespan=(datetimes[1].date(), datetimes[1].date()),
|
start=datetimes[1],
|
||||||
start=datetimes[1],
|
end=datetimes[2],
|
||||||
end=datetimes[2],
|
room=room102,
|
||||||
room=room102,
|
cancelled=True,
|
||||||
cancelled=True,
|
),
|
||||||
),
|
# Other adjacent dates.
|
||||||
# Other adjacent dates.
|
HotelReservation(
|
||||||
HotelReservation(
|
datespan=(datetimes[3].date(), datetimes[4].date()),
|
||||||
datespan=(datetimes[3].date(), datetimes[4].date()),
|
start=datetimes[3],
|
||||||
start=datetimes[3],
|
end=datetimes[4],
|
||||||
end=datetimes[4],
|
room=room102,
|
||||||
room=room102,
|
),
|
||||||
),
|
]
|
||||||
]
|
for reservation in other_valid_reservations:
|
||||||
|
constraint.validate(HotelReservation, reservation)
|
||||||
|
HotelReservation.objects.bulk_create(other_valid_reservations)
|
||||||
|
# Excluded fields.
|
||||||
|
constraint.validate(
|
||||||
|
HotelReservation,
|
||||||
|
HotelReservation(
|
||||||
|
datespan=(datetimes[1].date(), datetimes[2].date()),
|
||||||
|
start=datetimes[1],
|
||||||
|
end=datetimes[2],
|
||||||
|
room=room102,
|
||||||
|
),
|
||||||
|
exclude={"room"},
|
||||||
|
)
|
||||||
|
constraint.validate(
|
||||||
|
HotelReservation,
|
||||||
|
HotelReservation(
|
||||||
|
datespan=(datetimes[1].date(), datetimes[2].date()),
|
||||||
|
start=datetimes[1],
|
||||||
|
end=datetimes[2],
|
||||||
|
room=room102,
|
||||||
|
),
|
||||||
|
exclude={"datespan", "start", "end", "room"},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ignore_warnings(category=RemovedInDjango50Warning)
|
@ignore_warnings(category=RemovedInDjango50Warning)
|
||||||
|
@ -731,6 +765,21 @@ class ExclusionConstraintTests(PostgreSQLTestCase):
|
||||||
constraint_name, self.get_constraints(RangesModel._meta.db_table)
|
constraint_name, self.get_constraints(RangesModel._meta.db_table)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_validate_range_adjacent(self):
|
||||||
|
constraint = ExclusionConstraint(
|
||||||
|
name="ints_adjacent",
|
||||||
|
expressions=[("ints", RangeOperators.ADJACENT_TO)],
|
||||||
|
violation_error_message="Custom error message.",
|
||||||
|
)
|
||||||
|
range_obj = RangesModel.objects.create(ints=(20, 50))
|
||||||
|
constraint.validate(RangesModel, range_obj)
|
||||||
|
msg = "Custom error message."
|
||||||
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
|
constraint.validate(RangesModel, RangesModel(ints=(10, 20)))
|
||||||
|
constraint.validate(RangesModel, RangesModel(ints=(10, 19)))
|
||||||
|
constraint.validate(RangesModel, RangesModel(ints=(51, 60)))
|
||||||
|
constraint.validate(RangesModel, RangesModel(ints=(10, 20)), exclude={"ints"})
|
||||||
|
|
||||||
def test_expressions_with_params(self):
|
def test_expressions_with_params(self):
|
||||||
constraint_name = "scene_left_equal"
|
constraint_name = "scene_left_equal"
|
||||||
self.assertNotIn(constraint_name, self.get_constraints(Scene._meta.db_table))
|
self.assertNotIn(constraint_name, self.get_constraints(Scene._meta.db_table))
|
||||||
|
|
|
@ -161,3 +161,59 @@ class UniqueFuncConstraintModel(models.Model):
|
||||||
constraints = [
|
constraints = [
|
||||||
models.UniqueConstraint(Lower("field"), name="func_lower_field_uq"),
|
models.UniqueConstraint(Lower("field"), name="func_lower_field_uq"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Product(models.Model):
|
||||||
|
price = models.IntegerField(null=True)
|
||||||
|
discounted_price = models.IntegerField(null=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
required_db_features = {
|
||||||
|
"supports_table_check_constraints",
|
||||||
|
}
|
||||||
|
constraints = [
|
||||||
|
models.CheckConstraint(
|
||||||
|
check=models.Q(price__gt=models.F("discounted_price")),
|
||||||
|
name="price_gt_discounted_price_validation",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ChildProduct(Product):
|
||||||
|
class Meta:
|
||||||
|
required_db_features = {
|
||||||
|
"supports_table_check_constraints",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueConstraintProduct(models.Model):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
color = models.CharField(max_length=32)
|
||||||
|
rank = models.IntegerField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["name", "color"], name="name_color_uniq_validation"
|
||||||
|
),
|
||||||
|
models.UniqueConstraint(fields=["rank"], name="rank_uniq_validation"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class ChildUniqueConstraintProduct(UniqueConstraintProduct):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UniqueConstraintConditionProduct(models.Model):
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
color = models.CharField(max_length=31, null=True, blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
required_db_features = {"supports_partial_indexes"}
|
||||||
|
constraints = [
|
||||||
|
models.UniqueConstraint(
|
||||||
|
fields=["name"],
|
||||||
|
name="name_without_color_uniq_validation",
|
||||||
|
condition=models.Q(color__isnull=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.test import TestCase, skipUnlessDBFeature
|
||||||
|
|
||||||
|
from .models import (
|
||||||
|
ChildProduct,
|
||||||
|
ChildUniqueConstraintProduct,
|
||||||
|
Product,
|
||||||
|
UniqueConstraintConditionProduct,
|
||||||
|
UniqueConstraintProduct,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PerformConstraintChecksTest(TestCase):
|
||||||
|
@skipUnlessDBFeature("supports_table_check_constraints")
|
||||||
|
def test_full_clean_with_check_constraints(self):
|
||||||
|
product = Product(price=10, discounted_price=15)
|
||||||
|
with self.assertRaises(ValidationError) as cm:
|
||||||
|
product.full_clean()
|
||||||
|
self.assertEqual(
|
||||||
|
cm.exception.message_dict,
|
||||||
|
{
|
||||||
|
"__all__": [
|
||||||
|
"Constraint “price_gt_discounted_price_validation” is violated."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_table_check_constraints")
|
||||||
|
def test_full_clean_with_check_constraints_on_child_model(self):
|
||||||
|
product = ChildProduct(price=10, discounted_price=15)
|
||||||
|
with self.assertRaises(ValidationError) as cm:
|
||||||
|
product.full_clean()
|
||||||
|
self.assertEqual(
|
||||||
|
cm.exception.message_dict,
|
||||||
|
{
|
||||||
|
"__all__": [
|
||||||
|
"Constraint “price_gt_discounted_price_validation” is violated."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_table_check_constraints")
|
||||||
|
def test_full_clean_with_check_constraints_disabled(self):
|
||||||
|
product = Product(price=10, discounted_price=15)
|
||||||
|
product.full_clean(validate_constraints=False)
|
||||||
|
|
||||||
|
def test_full_clean_with_unique_constraints(self):
|
||||||
|
UniqueConstraintProduct.objects.create(name="product", color="yellow", rank=1)
|
||||||
|
tests = [
|
||||||
|
UniqueConstraintProduct(name="product", color="yellow", rank=1),
|
||||||
|
# Child model.
|
||||||
|
ChildUniqueConstraintProduct(name="product", color="yellow", rank=1),
|
||||||
|
]
|
||||||
|
for product in tests:
|
||||||
|
with self.subTest(model=product.__class__.__name__):
|
||||||
|
with self.assertRaises(ValidationError) as cm:
|
||||||
|
product.full_clean()
|
||||||
|
self.assertEqual(
|
||||||
|
cm.exception.message_dict,
|
||||||
|
{
|
||||||
|
"__all__": [
|
||||||
|
"Unique constraint product with this Name and Color "
|
||||||
|
"already exists."
|
||||||
|
],
|
||||||
|
"rank": [
|
||||||
|
"Unique constraint product with this Rank already exists."
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_full_clean_with_unique_constraints_disabled(self):
|
||||||
|
UniqueConstraintProduct.objects.create(name="product", color="yellow", rank=1)
|
||||||
|
product = UniqueConstraintProduct(name="product", color="yellow", rank=1)
|
||||||
|
product.full_clean(validate_constraints=False)
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_partial_indexes")
|
||||||
|
def test_full_clean_with_partial_unique_constraints(self):
|
||||||
|
UniqueConstraintConditionProduct.objects.create(name="product")
|
||||||
|
product = UniqueConstraintConditionProduct(name="product")
|
||||||
|
with self.assertRaises(ValidationError) as cm:
|
||||||
|
product.full_clean()
|
||||||
|
self.assertEqual(
|
||||||
|
cm.exception.message_dict,
|
||||||
|
{
|
||||||
|
"__all__": [
|
||||||
|
"Constraint “name_without_color_uniq_validation” is violated."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("supports_partial_indexes")
|
||||||
|
def test_full_clean_with_partial_unique_constraints_disabled(self):
|
||||||
|
UniqueConstraintConditionProduct.objects.create(name="product")
|
||||||
|
product = UniqueConstraintConditionProduct(name="product")
|
||||||
|
product.full_clean(validate_constraints=False)
|
|
@ -146,10 +146,6 @@ class PerformUniqueChecksTest(TestCase):
|
||||||
mtv = ModelToValidate(number=10, name="Some Name")
|
mtv = ModelToValidate(number=10, name="Some Name")
|
||||||
mtv.full_clean()
|
mtv.full_clean()
|
||||||
|
|
||||||
def test_func_unique_check_not_performed(self):
|
|
||||||
with self.assertNumQueries(0):
|
|
||||||
UniqueFuncConstraintModel(field="some name").full_clean()
|
|
||||||
|
|
||||||
def test_unique_for_date(self):
|
def test_unique_for_date(self):
|
||||||
Post.objects.create(
|
Post.objects.create(
|
||||||
title="Django 1.0 is released",
|
title="Django 1.0 is released",
|
||||||
|
|
Loading…
Reference in New Issue