diff options
author | Gagaro <gagaro42@gmail.com> | 2022-01-31 16:04:13 +0100 |
---|---|---|
committer | Mariusz Felisiak <felisiak.mariusz@gmail.com> | 2022-05-10 11:22:23 +0200 |
commit | 667105877e6723c6985399803a364848891513cc (patch) | |
tree | b6b3a9fe9f2c8767bc6f6a68f0580eef021b2b55 /django/contrib/postgres/constraints.py | |
parent | 441103a04d1d167dc870eaaf90e3fba974f67c93 (diff) | |
download | django-667105877e6723c6985399803a364848891513cc.tar.gz |
Fixed #30581 -- Added support for Meta.constraints validation.
Thanks Simon Charette, Keryn Knight, and Mariusz Felisiak for reviews.
Diffstat (limited to 'django/contrib/postgres/constraints.py')
-rw-r--r-- | django/contrib/postgres/constraints.py | 50 |
1 files changed, 47 insertions, 3 deletions
diff --git a/django/contrib/postgres/constraints.py b/django/contrib/postgres/constraints.py index fa62379f67..8b59704063 100644 --- a/django/contrib/postgres/constraints.py +++ b/django/contrib/postgres/constraints.py @@ -1,11 +1,13 @@ import warnings 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.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.lookups import PostgresOperatorLookup from django.db.models.sql import Query from django.utils.deprecation import RemovedInDjango50Warning @@ -32,6 +34,7 @@ class ExclusionConstraint(BaseConstraint): deferrable=None, include=None, opclasses=(), + violation_error_message=None, ): if index_type and index_type.lower() not in {"gist", "spgist"}: raise ValueError( @@ -78,7 +81,7 @@ class ExclusionConstraint(BaseConstraint): category=RemovedInDjango50Warning, stacklevel=2, ) - super().__init__(name=name) + super().__init__(name=name, violation_error_message=violation_error_message) def _get_expressions(self, schema_editor, query): expressions = [] @@ -197,3 +200,44 @@ class ExclusionConstraint(BaseConstraint): "" if not self.include else " include=%s" % repr(self.include), "" 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()) |