Валидация полей формы. Создание пользовательского валидатора

Курс по Django: https://stepik.org/a/183363

Архив проекта: 46_sitewomen.zip

На прошлом занятии мы с вами создали форму для добавления новых статей на сайт. И знаем, что каждое поле проверяется на корректность (валидность) данных, прежде чем они попадут в БД. Так вот, классы полей формы имеют больше встроенных параметров, которыми можно задавать ограничения, чем классы моделей. Например, помимо max_length в классе CharField можно прописать и параметр min_length, задающий минимальное значение. Например, так:

title = forms.CharField(max_length=255, min_length=5, label="Заголовок", widget=forms.TextInput(attrs={'class': 'form-input'}))

В результате сформируется ограничение для min_length и на уровне HTML-тега и на уровне фреймворка Django, если переданная длина заголовка окажется меньше 5 символов.

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

    title = forms.CharField(max_length=255, min_length=5, label="Заголовок",
                            widget=forms.TextInput(attrs={'class': 'form-input'}),
                            error_messages={
                                'min_length': 'Слишком короткий заголовок',
                                'required': 'Без заголовка - никак',
                            })

Соответственно, у других классов свои параметры для задания ограничений. Все их можно посмотреть или на странице документации:

https://docs.djangoproject.com/en/4.2/ref/forms/fields/

Или перейти в сам класс поля, посмотреть в инициализаторе, а также в инициализаторе базового класса.

Дополнительные валидаторы

Каждый класс поля формы и модели имеет атрибут validators, позволяющий указывать список классов-валидаторов для предобработки переданных данных на стороне сервера. Набор стандартных таких классов доступен по ветке:

django.core.validators

Например, импортируем два валидатора:

from django.core.validators import MinLengthValidator, MaxLengthValidator

и пропишем их для поля slug следующим образом:

    slug = forms.SlugField(max_length=255, label="URL", validators=[
        MinLengthValidator(5),
        MaxLengthValidator(100),
    ])

Переходим на страницу добавления поста и введем слаг меньше 5 символов. При отправке данных увидим сообщение от валидатора MinLengthValidator.

Если стандартные сообщения нас не устраивают, то мы можем определить свои следующим образом:

    slug = forms.SlugField(max_length=255, label="URL", validators=[
        MinLengthValidator(5, message="Минимум 5 символов"),
        MaxLengthValidator(100, message="Максимум 100 символов"),
    ])

Конечно, похожего результата мы могли бы достичь, если бы прописали у класса Slug параметр min_length=5. Но на все случаи жизни параметры не определишь. Они охватывают лишь наиболее частые ситуации. Например, в классах модели нет параметра min_length, но мы совершенно спокойно можем указать валидатор MinLengthValidator. Например, в классе модели Women для поля slug указать такие же валидаторы:

    slug = models.SlugField(max_length=255, db_index=True, unique=True, validators=[
        MinLengthValidator(5),
        MaxLengthValidator(100),
    ])

Они будут использоваться при проверке перед записью данных в БД.

Создание пользовательских валидаторов

По аналогии с существующими классами валидации мы можем объявить свои собственные. Делается это предельно просто. Если посмотреть на имеющиеся классы, то можно заметить, они должны содержать магический метод __call__(), в котором выполняется проверка, если она не проходит, то генерируется исключение ValidationError с выдаваемым сообщением. Давайте это сделаем.

Наш валидатор будет проверять, чтобы в данных присутствовали только русские символы, цифры, дефис и пробел. Другие символы будем считать недопустимыми. Для этого определим валидатор следующим образом:

@deconstructible
class RussianValidator:
    ALLOWED_CHARS = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯабвгдеёжзийклмнопрстуфхцчшщбыъэюя0123456789- "
    code = 'russian'
 
    def __init__(self, message=None):
        self.message = message if message else "Должны присутствовать только русские символы, дефис и пробел."
 
    def __call__(self, value):
        if not (set(value) <= set(self.ALLOWED_CHARS)):
            raise ValidationError(self.message, code=self.code, params={"value": value})

Я здесь воспользовался декоратором deconstructible, так как он присутствует у всех классов-валидаторов. Затем, мы можем при создании экземпляра этого класса указать сообщение об ошибке, либо будет использоваться стандартное. И, самое главное, это магический метод __call__(). Напомню, что он вызывается, когда объект класса RussianValidator вызывается подобно функциям с круглыми скобками. Соответственно, в него передается значение поля, в котором этот валидатор будет определен. Внутри этого метода идет проверка на допустимые символы и если находится хотя бы один недопустимый, то генерируется исключение ValidationError с сообщением об ошибке.

Давайте подключим этот валидатор к полю title класса AddPostForm:

class AddPostForm(forms.Form):
    title = forms.CharField(max_length=255, min_length=5, label="Заголовок",
                            widget=forms.TextInput(attrs={'class': 'form-input'}),
                            validators=[
                                RussianValidator(),
                            ], 
                            error_messages={
                                'min_length': 'Слишком короткий заголовок',
                                'required': 'Без заголовка - никак',
                            })
    ...

Перейдем на страницу добавления поста и введем в заголовке латинские символы. При отправке формы сработает наш валидатор и выдаст сообщение об ошибке.

Вот так легко и просто можно создавать свои классы валидаторы. Но создание таких классов имеет смысл только в том случае, если предполагается его многократно использовать в текущем проекте. Если же нам требуется выполнить такую проверку только для одного поля title, то делается это гораздо проще. В самом классе AddPostForm можно объявить метод, который начинается с ключевого слова clean_ и далее имя поля для валидации. Например, для поля title, следует объявить метод с именем clean_title следующим образом:

    def clean_title(self):
        title = self.cleaned_data['title']
        ALLOWED_CHARS = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЪЭЮЯабвгдеёжзийклмнопрстуфхцчшщбыъэюя0123456789- "
        if not (set(title) <= set(ALLOWED_CHARS)):
            raise ValidationError("Должны быть только русские символы, дефис и пробел.")
 
        return title

Получаем в итоге тот же самый эффект.

В заключение отмечу, что механизм проверки, валидации данных работает следующим образом. Сначала данные формы отправляются на сервер, там проверяются сначала стандартными валидаторами. Если эта проверка прошла, то только после этого вызываются пользовательские валидаторы, прописанные в классе формы или подключенные через параметр validators.

Курс по Django: https://stepik.org/a/183363

Видео по теме