Проверка форм и полей формы

Проверка формы происходит при нормализации её данных. При возникновении необходимости вмешаться в этот процесс, есть много мест, где можно это сделать и которые влияют на разные этапы проверки. Во время обработки формы вызываются три типа методов для нормализации данных. Процесс проверки запускается при вызове метода is_valid() формы. Существуют ситуации, которые запускают нормализацию и проверку данных (обращение к свойству errors или прямой вызов метода full_clean()), но они возникают достаточно редко.

В общем случае, любой нормализующий метод может вызвать исключение 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, а не строка, переданная формой.

    Например, если требуется проверить, что содержимое CharField поля с именем serialnumber является уникальным, то метод clean_serialnumber() будет правильным местом для такого функционала. Вам не нужно специальное поле (пусть будет CharField), но требуется хитрая проверка данных и, возможно, очистка/нормализация данных.

    Подобно методу clean(), описанному выше, этот метод должен возвращать проверенные данные, независимо от того изменились они или нет.

  • Метод clean() потомка формы. Этот метод может выполнять любую проверку, которая нуждается в одновременном доступе к данным нескольких полей. Именно здесь вы можете проверять, что если поле A заполнено, то поле B должно содержать правильный адрес электронной почты и так далее. Данные, которые возвращает этот метод, помещаются в свойство cleaned_data формы. Не забудьте вернуть полный список данных, если вы переопределяете этот метод (по умолчанию метод Form.clean() просто возвращает содержимое свойства self.cleaned_data).

    Следует отметить, что любая ошибка, выданная методом clean() формы, не будет ассоциирована ни с каким полем. Такие ошибки привязываются к «особому» полю (__all__), доступ к которому можно получить через метод non_field_errors(). Если вам потребуется добавить ошибки к определённому полю формы, вам потребуется атрибут _errors формы, который описан далее.

    Также следует отметить, что существует ряд соглашений, которым необходимо следовать при переопределении метода clean() в вашем классе ModelForm. (Обратитесь к документации на ModelForm для получения подробностей.)

Эти методы вызываются в порядке, указанном выше, по одному полю за раз. Для каждого поля формы (в порядке их определения в классе формы) вызывается сначала метод clean() поля, затем вызывается метод clean_<fieldname>(). После того, как пара этих методов будет вызвана для каждого поля формы, наступает очередь метода clean() формы.

Примеры для каждого из этих методов показаны ниже.

Как упоминалось ранее, любой из этих методов может вызвать исключение ValidationError. Для любого поля, если его метод clean() вызвал исключение ValidationError, то следующий метод для этого поля не вызывается. Тем не менее, методы для остальных полей отрабатывают в штатном режиме.

Метод clean() формы вызывается в любом случае. Если этот метод вызывает исключение ValidationError, то атрибут cleaned_data формы будет содержать пустой словарь.

Предыдущий параграф говорит , что если вы переопределили у формы метод clean(), то вы должны пройтись по self.cleaned_data.items(), возможно проверив словарь в атрибуте _errors формы. В этом случае вы будете всегда знать, какое именно поле сгенерировало ошибку.

Наследование форм и изменение ошибок полей

Временами, в методе clean() формы, может потребоваться добавить сообщение об ошибке к определённому полю. Это не совсем обычная ситуация, правильнее было бы вызвать исключение ValidationError в методе``clean()`` формы, которое бы превратилось в ошибку самой формы и было бы доступно через метод формы non_field_errors().

Когда вам действительно потребуется добавить сообщение об ошибке к определённому полю, то следует добавить или дополнить значение словаря в атрибуте формы _errors. Ключом будет имя поля. Этот атрибут является экземпляром класса django.forms.util.ErrorDict. В любом случае, это просто словарь. Для каждого поля формы, в котором есть ошибка, в этом словаре будет храниться значение. Каждое значение этого словаря является экземпляром класса django.forms.util.ErrorList, т.е. списком, который знает как отображать себя в разных ситуациях. Таким образом, вы можете рассматривать атрибут _errors как словарь, связывающий имена полей со списками ошибок.

При добавлении ошибки для определённого поля, следует проверить наличие соответствующего ключа в атрибуте формы _errors. Если такого ключа нет, то создайте для него новую запись, содержащую пустой экземпляр ErrorList. В противном случае, вы можете добавить своё сообщение об ошибке в список ошибок поля и оно будет отображено на форме.

Пример внесения изменений в self._errors приведён далее.

What’s in a name?

Вас может заинтересовать, почему этот атрибут назван _errors, а не errors. По традиции, в Python с символа подчеркивания начинаются имена объектов, которые предназначены для внутреннего использования. В данном случае, наследуясь от класса Form, по существу, вы его реализуете заново. Следовательно, вам предоставляется право доступа к внутренним объектам класса Form.

Конечно, любой код вне вашей формы не должен обращаться к _errors напрямую. Содержимое этого атрибута доступно через свойство errors, которое заполняет _errors перед тем как вернуть его.

Другая причина чисто историческая: атрибут получил имя _errors с момента появления модуля форм и изменять его сейчас (особенно с момента, как имя errors начало использоваться свойством, из которого можно только получать данные) было бы неразумно по ряду причин. Вы можете выбрать понравившееся объяснение. Результат будет неизменен.

Использование проверки на практике

Выше мы рассмотрели как осуществляется проверка форм в целом. Так как временами бывает проще разобраться с функционалом, просмотрев его в действии, далее показан ряд небольших примеров, которые используют описанные возможности.

Использование валидаторов

Поля форм (и моделей) Django поддерживают использование простых функций и классов, которые известны как валидаторы. Они могут быть переданы в конструктор поля через аргумент validators или определены в самом классе формы с помощью атрибута default_validators.

Простые валидаторы могут использоваться для проверки значений внутри полей. Давайте рассмотрим EmailField:

class EmailField(CharField):
    default_error_messages = {
        'invalid': _(u'Enter a valid e-mail address.'),
    }
    default_validators = [validators.validate_email]

Как можно увидеть EmailField — это обычное поле CharField, которое имеет собственное сообщение об ошибке и валидатор для проверки введённого адреса электронной почты. Определение поля для ввода электронной почты:

email = forms.EmailField()

эквивалентно:

email = forms.CharField(validators=[validators.validate_email],
        error_messages={'invalid': _(u'Enter a valid e-mail address.')})

Встроенная проверка поля формы

Давайте сначала создадим собственное поле формы, которое проверяет, что переданные ему данные — это строка, содержащая адреса электронной почты, разделенные запятыми. Класс такого поля будет выглядеть следующим образом:

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(MultiEmailField, self).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:

class ContactForm(forms.Form):
    # Everything as before.
    ...

    def clean_recipients(self):
        data = self.cleaned_data['recipients']
        if "fred@example.com" not in data:
            raise forms.ValidationError("You have forgotten about Fred!")

        # Always return the cleaned data, whether you have changed it or
        # not.
        return data

Очистка и проверка полей, которые зависят друг от друга

Допустим, что мы добавили ещё одно требование для нашей формы: если отмечено поле cc_myself, то поле subject должно содержать слово "help". Раз мы выполняем проверку нескольких полей, то метод формы clean() будет правильным местом для нашего кода. Обратите внимание, мы сейчас говорим о методе clean() формы, а раньше говорили о методе clean() поля. Важно понимать разницу между ними при реализации алгоритма проверки данных. Поля содержат один источник данных, а формы — это коллекции полей.

К моменту вызова метода формы clean() все clean() методы полей уже отработали. Таким образом, свойство формы self.cleaned_data будет заполнено данными, прошедшими проверку. Следовательно, надо принять во внимание возможность того, что данные некоторых полей не прошли начальную поверку.

Существует два способа сообщить об ошибках на этом этапе. Обычно ошибку отображают сверху формы. Для этого достаточно вызвать исключение ValidationError в методе формы clean(). Например:

class ContactForm(forms.Form):
    # Everything as before.
    ...

    def clean(self):
        cleaned_data = super(ContactForm, self).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 forms.ValidationError("Did not send for 'help' in "
                        "the subject despite CC'ing yourself.")

        # Always return the full collection of cleaned data.
        return cleaned_data

В данном коде, при возникновении ошибки во время проверки данных, форма отобразит сообщение об ошибке сверху (обычное поведение), описывая проблему.

Следует отметить, что вызов super(ContactForm, self).clean() в приведенном коде обеспечивает выполнение дополнительной проверки средствами базового класса.

Второй способ подразумевает назначение ошибки одному из полей. В нашем случае, давайте назначим сообщение об ошибке обоим полям («subject» и «cc_myself») при отображении формы. Использовать этот способ надо аккуратно, так как он может запутать пользователя. Мы лишь показываем возможные варианты, оставляя решение конкретной задачи вам и вашим дизайнерам. Наш новый код (заменяющий предыдущий пример) выглядит так:

class ContactForm(forms.Form):
    # Everything as before.
    ...

    def clean(self):
        cleaned_data = super(ContactForm, self).clean()
        cc_myself = cleaned_data.get("cc_myself")
        subject = cleaned_data.get("subject")

        if cc_myself and subject and "help" not in subject:
            # We know these are not in self._errors now (see discussion
            # below).
            msg = u"Must put 'help' in subject when cc'ing yourself."
            self._errors["cc_myself"] = self.error_class([msg])
            self._errors["subject"] = self.error_class([msg])

            # These fields are no longer valid. Remove them from the
            # cleaned data.
            del cleaned_data["cc_myself"]
            del cleaned_data["subject"]

        # Always return the full collection of cleaned data.
        return cleaned_data

Как видно из кода, этот подход требует некоторых усилий, не требуя дополнительного дизайна, для создания удобных форм. Тем не менее, остановимся на деталях. Во-первых, мы отметили ранее, что может потребоваться проверка наличия ключа для поля в свойстве _errors формы. В нашем случае, поскольку мы знаем, что раз поля присутствуют в self.cleaned_data, значит они смогли пройти проверку на уровне полей. Следовательно, для них не будет соответствующих значений в свойстве формы _errors.

Во-вторых, как только мы решили, что связанные данные двух полей не прошли проверку, то следует убрать их значения из свойства cleaned_data.

На самом деле, в настоящее время Django полностью очищает свойство cleaned_data при возникновении любой ошибки на форме. Тем не менее, такое поведение может быть изменено в будущем, так что лучше явно делать необходимую очистку.