Проверка форм и полей формы¶
Проверка формы происходит при нормализации её данных. При возникновении необходимости вмешаться в этот процесс, есть много мест, где можно это сделать и которые влияют на разные этапы проверки. Во время обработки формы вызываются три типа методов для нормализации данных. Процесс проверки запускается при вызове метода is_valid() формы. Существуют ситуации, которые запускают нормализацию и проверку данных (обращение к свойству errors или прямой вызов метода full_clean()), но они возникают достаточно редко.
В общем случае, любой нормализующий метод может вызвать исключение ValidationError при наличии проблем с данными, передавая соответствующее сообщение об ошибке в конструктор исключения. Смотрите ниже примеры, как правильно вызывать ValidationError. Если проблем не выявлено, то метод должен возвращать нормализованное значение в виде объекта языка Python.
Большая часть проверок может быть выполнена с помощью validators, которые являются простыми в использовании вспомогательными объектами. Валидатор — это простая функция (или вызываемый объект, callable), которая принимает единственный аргумент и вызывает исключение ValidationError в случае проблем с полученным значением. Валидаторы запускаются после вызова методов поля: to_python и validate.
Проверка формы состоит из нескольких этапов, каждый из которых может быть настроен или переопределён:
Вызов метода поля
to_python()является первым этапом каждой проверки. Он приводит значение к соответствующему типу данных или вызывает исключениеValidationError, если это невозможно. Метод принимает сырое значение от виджета и возвращает нормализованное значение. Например, поле типаFloatFieldпреобразовывает данные в типfloatязыка Python или вызывает исключениеValidationError.Метод
validate()поля выполняет специфическую для поля проверку данных и приводит значение к правильному типу данных, или вызывает исключениеValidationErrorна любую ошибку. Этот метод не возвращает значение и не должен изменять проверяемые данные. Если вам надо обеспечить логику, которую невозможно или нежелательно выносить в валидатор, то вам следует переопределить этот метод.Метод поля
run_validators()запускает все валидаторы и аккумулирует все возникающие ошибки в одно исключениеValidationError. Вам не стоит переопределять этот метод.Метод
clean()поля отвечает за вызов методовto_python(),validate()иrun_validators()в правильном порядке и передачу их ошибок. Как только любой из этих методов вызовет исключениеValidationError, процесс проверки прекращается и ошибка передаётся выше. Этот метод возвращает проверенные данные, которые затем помещаются в словарьcleaned_dataформы.Для проверки значения поля используется метод
clean_<fieldname>(), где<fieldname>заменяется на имя поля. Этот метод выполняет проверку значения. Метод не принимает аргументы. Для получения значения поля обращайтесь к словарюself.cleaned_dataи помните, что там будет объект языка Python, а не строка, переданная формой (значение находится вcleaned_dataт.к. уже была выполнена проверка методомclean()поля).Например, если требуется проверить, что содержимое
CharFieldполя с именемserialnumberявляется уникальным, то методclean_serialnumber()будет правильным местом для такого функционала. Вам не нужно специальное поле (пусть будетCharField), но требуется хитрая проверка данных и, возможно, очистка/нормализация данных.Возвращаемое значение заменит текущее значение в
cleaned_data, поэтому это должно быть значение поля изcleaned_data(даже если метод не менял его) или новое проверенное значение.Метод
clean()потомка формы может выполнять любую проверку, которая нуждается в одновременном доступе к данным нескольких полей. Именно здесь вы можете проверять, что если полеAзаполнено, то полеBдолжно содержать правильный адрес электронной почты и так далее. Данные, которые возвращает этот метод, помещаются в свойствоcleaned_dataформы.Так как валидация полей выполняется перед вызовом
clean(), вы можете получить доступ к атрибуту формыerrors, который содержит уже полученные ошибки валидации.Следует отметить, что любая ошибка, вызванная методом
Form.clean()формы, не будет ассоциирована ни с каким полем. Такие ошибки привязываются к «особому» полю (__all__), доступ к которому можно получить через методnon_field_errors(). Если вам потребуется добавить ошибки к определённому полю формы, используйтеadd_error().Также следует отметить, что существует ряд соглашений, которым необходимо следовать при переопределении метода
clean()в вашем классеModelForm. (Обратитесь к документации на ModelForm для получения подробностей.)
Эти методы вызываются в порядке, указанном выше, по одному полю за раз. Для каждого поля формы (в порядке их определения в классе формы) вызывается сначала метод Field.clean(), затем вызывается метод clean_<fieldname>(). После того, как пара этих методов будет вызвана для каждого поля формы, наступает очередь метода Form.clean() формы. Он будет вызыван в любом случае, даже если предыдущие методы вызывали ошибку.
Примеры для каждого из этих методов показаны ниже.
Как упоминалось ранее, любой из этих методов может вызвать исключение ValidationError. Для любого поля, если его метод clean() вызвал исключение ValidationError, то следующий метод для этого поля не вызывается. Тем не менее, методы для остальных полей отрабатывают в штатном режиме.
Вызов ValidationError¶
Для удобной работы с ошибками валидации используйте следующие правила:
Передайте при создании код ошибки через аргумент
code:# Good ValidationError(_('Invalid value'), code='invalid') # Bad ValidationError(_('Invalid value'))
Переменные лучше передавать в аргументе
params, а в сообщении указать места для подстановки:# Good ValidationError( _('Invalid value: %(value)s'), params={'value': '42'}, ) # Bad ValidationError(_('Invalid value: %s') % value)
Используйте именованные параметры в сообщении. Это позволит использовать переменные в любом параметре при переопределении сообщения:
# Good ValidationError( _('Invalid value: %(value)s'), params={'value': '42'}, ) # Bad ValidationError( _('Invalid value: %s'), params=('42',), )
Оберните сообщения в
gettextдля последующего перевода:# Good ValidationError(_('Invalid value')) # Bad ValidationError('Invalid value')
Все вместе:
raise ValidationError(
_('Invalid value: %(value)s'),
code='invalid',
params={'value': '42'},
)
Соблюдать правила очень важно при создании переносимых форм, полей форм и моделей.
Не рекомендуется, но если вы в конце цепочки валидации(например, метод clean() формы) и никогда не будете переопределять сообщение, можно просто сделать:
ValidationError(_('Invalid value: %s') % value)
Методы Form.errors.as_data() и Form.errors.as_json() используют все возможности ValidationError (включая code и params).
Вызов нескольких ошибок¶
При обнаружении нескольких ошибок в процессе нормализации поля и при наличии желания отобразить их одновременно на форме, следует передать их в виде списка в конструктор исключения.
Рекомендуется использовать список объектов ValidationError с code и params, но можно использовать просто список строк:
# Good
raise ValidationError([
ValidationError(_('Error 1'), code='error1'),
ValidationError(_('Error 2'), code='error2'),
])
# Bad
raise ValidationError([
_('Error 1'),
_('Error 2'),
])
Использование проверки на практике¶
Выше мы рассмотрели как осуществляется проверка форм в целом. Так как временами бывает проще разобраться с функционалом, просмотрев его в действии, далее показан ряд небольших примеров, которые используют описанные возможности.
Использование валидаторов¶
Поля форм (и моделей) Django поддерживают использование простых функций и классов, которые известны как валидаторы. Это просто функция, которая принимает значение и ничего не возвращает, если значение верно, иначе вызывает ValidationError. Они могут быть переданы в конструктор поля через аргумент validators или определены в самом классе поля Field с помощью атрибута default_validators.
Простые валидаторы могут использоваться для проверки значений внутри полей. Давайте рассмотрим SlugField:
from django.core import validators
from django.forms import CharField
class SlugField(CharField):
default_validators = [validators.validate_slug]
Как можно увидеть SlugField — это обычное поле CharField, которое имеет валидатор, проверяющий вводимое значение на допустимые символы. Все это можно указать при определении поля:
slug = forms.SlugField()
аналогично:
slug = forms.CharField(validators=[validators.validate_slug])
Common cases such as validating against an email or a regular expression can be
handled using existing validator classes available in Django. For example,
validators.validate_slug is an instance of
a RegexValidator constructed with the first
argument being the pattern: ^[-a-zA-Z0-9_]+$. See the section on
writing validators to see a list of what is already
available and for an example of how to write a validator.
Встроенная проверка поля формы¶
Давайте сначала создадим собственное поле формы, которое проверяет, что переданные ему данные — это строка, содержащая адреса электронной почты, разделенные запятыми. Класс такого поля будет выглядеть следующим образом:
from django import forms
from django.core.validators import validate_email
class MultiEmailField(forms.Field):
def to_python(self, value):
"""Normalize data to a list of strings."""
# Return an empty list if no input was given.
if not value:
return []
return value.split(',')
def validate(self, value):
"""Check if value consists only of valid emails."""
# Use the parent's handling of required fields, etc.
super().validate(value)
for email in value:
validate_email(email)
Каждая форма, использующая такое поле, будет вызывать эти методы до выполнения всех остальных действий с данными поля. Такая проверка привязана к этому типу поля и не зависит от дальнейшего его использования.
Давайте создадим простую форму ContactForm, чтобы показать как можно использовать это поле:
class ContactForm(forms.Form):
subject = forms.CharField(max_length=100)
message = forms.CharField()
sender = forms.EmailField()
recipients = MultiEmailField()
cc_myself = forms.BooleanField(required=False)
Просто используем MultiEmailField как и любое другое поле. При вызове метода формы is_valid() происходит вызов метода MultiEmailField.clean(), который в свою очередь вызовет собственные методы to_python() и validate().
Проверка атрибута определённого поля¶
Продолжая работать над нашим примером, предположим, что на форме ContactForm поле электронной почты recipients всегда должно содержать адрес "fred@example.com". Эта проверка будет особенностью нашей формы, следовательно, нам не надо её помещать в класс MultiEmailField. Вместо этого мы напишем метод, который будет проверять поле recipients:
from django import forms
from django.core.exceptions import ValidationError
class ContactForm(forms.Form):
# Everything as before.
...
def clean_recipients(self):
data = self.cleaned_data['recipients']
if "fred@example.com" not in data:
raise ValidationError("You have forgotten about Fred!")
# Always return a value to use as the new cleaned data, even if
# this method didn't change it.
return data
Очистка и проверка полей, которые зависят друг от друга¶
Допустим, что мы добавили ещё одно требование для нашей формы: если поле cc_myself равно True, то поле subject должно содержать слово "help". Раз мы выполняем проверку нескольких полей, то метод формы clean() будет правильным местом для нашего кода. Обратите внимание, мы сейчас говорим о методе clean() формы, а раньше говорили о методе clean() поля. Важно понимать разницу между ними при реализации алгоритма проверки данных. Поля содержат один источник данных, а формы — это коллекции полей.
К моменту вызова метода формы clean() все clean() методы полей уже отработали. Таким образом, свойство формы self.cleaned_data будет заполнено данными, прошедшими проверку. Следовательно, надо принять во внимание возможность того, что данные некоторых полей не прошли начальную поверку.
Существует два способа сообщить об ошибках на этом этапе. Обычно ошибку отображают сверху формы. Для этого достаточно вызвать исключение ValidationError в методе формы clean(). Например:
from django import forms
from django.core.exceptions import ValidationError
class ContactForm(forms.Form):
# Everything as before.
...
def clean(self):
cleaned_data = super().clean()
cc_myself = cleaned_data.get("cc_myself")
subject = cleaned_data.get("subject")
if cc_myself and subject:
# Only do something if both fields are valid so far.
if "help" not in subject:
raise ValidationError(
"Did not send for 'help' in the subject despite "
"CC'ing yourself."
)
В этом коде, если возникает ошибка проверки, в верхней части формы (обычно) отображается сообщение об ошибке, описывающее проблему. Такие ошибки не относятся к полям и отображаются в шаблоне с помощью {{ form.non_field_errors }}.
Вызов super().clean() обеспечивает проверку данных в родительском классе. Если ваша форма наследуется от класса, который не возвращает словарь cleaned_data из метода clean() (это не обязательно), не записывайте в cleaned_data результат вызова super() и используйте вместо этого self.cleaned_data:
def clean(self):
super().clean()
cc_myself = self.cleaned_data.get("cc_myself")
...
Второй способ подразумевает назначение ошибки одному из полей. В нашем случае, давайте назначим сообщение об ошибке обоим полям («subject» и «cc_myself») при отображении формы. Использовать этот способ надо аккуратно, так как он может запутать пользователя. Мы лишь показываем возможные варианты, оставляя решение конкретной задачи вам и вашим дизайнерам. Наш новый код (заменяющий предыдущий пример) выглядит так:
from django import forms
class ContactForm(forms.Form):
# Everything as before.
...
def clean(self):
cleaned_data = super().clean()
cc_myself = cleaned_data.get("cc_myself")
subject = cleaned_data.get("subject")
if cc_myself and subject and "help" not in subject:
msg = "Must put 'help' in subject when cc'ing yourself."
self.add_error('cc_myself', msg)
self.add_error('subject', msg)
Вторым аргументом add_error() может быть просто строка, но лучше объект ValidationError. Подробности смотрите в Вызов ValidationError. Обратите внимание, add_error() автоматически убирает поле из cleaned_data.