Ограничение на поле модели "вместе ноль или вообще нет"
Мне нужен способ создать валидатор или ограничение на уровне модели для оценки двух или более полей, которые могут быть null
/пустыми только в том случае, если все они null
/пустые одновременно.
Например, в следующей модели:
from django.db import models
class Example(models.Model):
A = models.CharField(max_length=16, blank=True)
B = models.DateField(null=True, blank=True)
C = models.FileField(upload_to='/', null=True)
Если я пытаюсь создать новый Example
с пустыми значениями B
или C
, то это должно вызвать ошибку ValidationError
; но если оба значения пусты, то все должно быть в порядке.
Вы можете использовать CheckConstraint
:
from django.db.models import CheckConstraint, Q
class Example(models.Model):
...
class Meta:
constraints = [
CheckConstraint(
check=(
(Q(B__isnull=True) & Q(C__isnull=True))
| Q(B__isnull=False) | Q(C__isnull=False)
),
name="b_c_null_check",
)
]
Ответ @Selcukis правильный, но я хотел получить более общий ответ. Взяв его ответ за основу, я сделал следующий класс, который наследует от CheckConstraint, но вы можете передать аргумент fields вместо аргумента check для создания ограничения:
from django.db.models import CheckConstraint, Q
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import gettext_lazy as _
class EmptyTogether(CheckConstraint):
def __init__(self, *, fields=None, check=None, name, violation_error_message=None):
if fields is not None and check is not None:
raise ImproperlyConfigured(
_("Creating an EmptyTogether constraint with 'fields' and 'check' attributes is prohibited")
)
if check is not None:
super().__init__(check=check,name=name,violation_error_message=violation_error_message)
return
if fields is None:
raise ImproperlyConfigured(
_("Creating an EmptyTogether constraint without the 'fields' attribute "
f"is prohibited; constraint {name} needs updating.")
)
if isinstance(fields,str) or not hasattr(fields, "__iter__"):
raise ImproperlyConfigured(
_("EmptyTogether constraint 'fields' attribute should be an iterable")
)
allNull = ~Q()
someNull = Q()
for field in fields:
q = Q(**{f'{field}__isnull':True})
if not isinstance(field,str):
if not (hasattr(field, "__iter__") and len(field)==2):
raise ImproperlyConfigured(
_("EmptyTogether constraint 'fields' should only contain non empty string values"
"or tuples (str,value)")
)
key,value = field[0],field[1]
if not isinstance(key,str):
raise ImproperlyConfigured(
_("EmptyTogether constraint 'fields' should only contain non empty string values"
"or tuples (str,value)")
)
q = Q(**{key:value})
allNull &= q
someNull |= q
check = (allNull) | ~(someNull)
super().__init__(check=check,name=name,violation_error_message=violation_error_message)
Я размещаю его здесь, чтобы если кто-то столкнется с этой проблемой, и ему понадобится проверить empty_together по слишком большому количеству полей во многих моделях, и ему нужен адаптируемый способ сделать это. Просто создайте utils.py в вашем проекте django и импортируйте его при необходимости
Применительно к расширенному примеру это будет что-то вроде:
from django.db import models
from PROYECT.utils import EmptyTogether
class Example(models.Model):
A = models.CharField(max_length=16, blank=True)
B = models.DateField(null=True, blank=True)
C = models.FileField(upload_to='/', null=True)
D = models.DecimalField(max_digits=6, decimal_places=2, default=0)
class Meta:
constraints = [
EmptyTogether(
fields=( ('A',''), 'B', ('D',0) ),
name="a_b_d_empty_check",
)
]
Наконец, я попытался сделать класс дуракоустойчивым, но если вы хотите пропустить все проверки, то можете оставить его в таком виде:
from django.db.models import CheckConstraint, Q
class EmptyTogether(CheckConstraint):
def __init__(self, *, fields=None, check=None, name, violation_error_message=None):
if check is not None:
super().__init__(check=check,name=name,violation_error_message=violation_error_message)
return
allNull = ~Q()
someNull = Q()
for field in fields:
q = Q(**{f'{field}__isnull':True})
if not isinstance(field,str):
key,value = field
q = Q(**{key:value})
allNull &= q
someNull |= q
check = (allNull) | ~(someNull)
super().__init__(check=check,name=name,violation_error_message=violation_error_message)