Spec-Zone.ru › Django 5.1

Валидация форм и полей

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

В общем, любой метод очистки может вызвать ValidationError, если возникла проблема с обрабатываемыми данными, передавая соответствующую информацию конструктору ValidationError. См. ниже лучшие рекомендации по вызову ValidationError. Если ValidationError не вызван, метод должен вернуть очищенные (нормализованные) данные как объект Python.

Большинство проверок можно выполнить с помощью валидаторов – вспомогательных элементов, которые можно повторно использовать. Валидаторы – функции (или вызываемые объекты), которые принимают один аргумент и вызывают ValidationError при некорректном вводе. Валидаторы выполняются после вызова методов to_python и validate поля.

Валидация формы разделена на несколько шагов, которые можно настроить или переопределить:

  • Метод to_python() поля Field является первым шагом каждой валидации. Он принудительно преобразует значение в правильный тип данных и вызывает ValidationError, если это невозможно. Этот метод принимает исходное значение из виджета и возвращает преобразованное значение. Например, поле FloatField преобразует данные в объект Python float или вызывает ValidationError.
  • Метод validate() поля Field обрабатывает валидацию, специфичную для поля, которая не подходит для валидатора. Он принимает значение, преобразованное в правильный тип данных, и вызывает ValidationError при возникновении ошибки. Этот метод ничего не возвращает и не должен изменять значение. Вы должны переопределить его, чтобы обработать логику валидации, которую нельзя или не нужно включать в валидатор.
  • Метод run_validators() поля Field выполняет все валидаторы поля и объединяет все ошибки в единую ValidationError. Вам не нужно переопределять этот метод.
  • Метод clean() подкласса Field отвечает за выполнение 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. Для любого поля, если метод Field.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.

Как и выше, рекомендуется передавать список экземпляров 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.

Валидаторы можно использовать для проверки значений внутри поля, давайте рассмотрим пример валидации полей Django 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])

Обычные случаи, такие как валидация по электронной почте или регулярному выражению, можно обрабатывать с помощью имеющихся классов валидаторов в Django. Например, validators.validate_slug является экземпляром RegexValidator, созданным с шаблоном в качестве первого аргумента: ^[-a-zA-Z0-9_]+$. См. раздел о создании валидаторов для получения списка доступных валидаторов и примера создания валидатора.

Очистка полей форм по умолчанию

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

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

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/5.1/ref/forms/validation/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API