Сообщить об ошибке.

Валидация форм с помощью WTForms во Flask

Содержание:


Практическое применение модуля WTForm.

Ни один веб-сайт не обходится без HTML-форм, будь это страница обратной связи, страница авторизации или даже форма для комментариев. Когда нужно работать с данными HTML-формы, то код быстро становится очень трудным для чтения. Существуют библиотеки, призванные упростить управление этим процессом. Одним из них является WTForms.

Модуль WTForms определяет свои формы для использования в шаблонах как классы. Для этого рекомендуется разбить приложение на несколько модулей и добавить отдельный модуль для форм.

Примечание. Расширение Flask-WTF добавляет несколько небольших помощников, которые делают работу с формами и фреймворком Flask более быстрой и удобной.

Пример определения класса HTML-формы для приложения Flask.

Пример формы для типичной страницы регистрации:

# например forms.py
from wtforms import Form, BooleanField, StringField, PasswordField, validators

class RegistrationForm(Form):
    username = StringField('Имя пользователя', [validators.Length(min=4, max=25)])
    email = StringField('Email-адрес', [validators.Length(min=6, max=35)])
    password = PasswordField('Новый пароль', [
        validators.DataRequired(),
        validators.EqualTo('confirm', message='Пароли должны совпадать')
    ])
    confirm = PasswordField('Повторите пароль')
    accept_tos = BooleanField('Я принимаю TOS', [validators.DataRequired()])

Использование класса HTML-формы в функции-представлении Flask.

В функции-представлении, обработка формы, определенной выше, выглядит так:

# например views.py
@app.route('/register', methods=['GET', 'POST'])
def register():
    # создаем экземпляр класса формы
    form = RegistrationForm(request.form)
    # если HTTP-метод POST и данные формы валидны
    if request.method == 'POST' and form.validate():
        # используя схему `SQLAlchemy` создаем объект, 
        # для последующей записи в базу данных
        user = User(form.username.data, form.email.data,
                    form.password.data)
        db_session.add(user)
        flash('Спасибо за регистрацию')
        return redirect(url_for('login'))
    # если HTTP-метод GET, то просто отрисовываем форму
    return render_template('register.html', form=form)

Обратите внимание, что здесь представление использует модуль для работы с базой данных SQLAlchemy, но это, конечно, не является обязательным требованием. При необходимости код нужно скорректировать.

То, что нужно помнить:

  1. Создать экземпляр класса определенной вами формы из значения request.form, если данные отправляются с помощью метода HTTP POST, и request.args, если данные отправляются как GET.
  2. Для проверки/валидации данных формы нужно вызвать метод экземпляра формы .validate(), который вернет True, если данные валидны, и False в противном случае.
  3. Для доступа к отдельным значениям из формы необходимо использовать паттерн form.<NAME>.data, где <NAME> - имя поля формы.

Использование класса HTML-формы в шаблонах Jinja2.

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

Пример шаблона jinja2 _formhelpers.html с таким макросом:

{# _formhelpers.html #}
{% macro render_field(field) %}
  <dt>{{ field.label }}
  <dd>{{ field(**kwargs)|safe }}
  {% if field.errors %}
    <ul class=errors>
    {% for error in field.errors %}
      <li>{{ error }}</li>
    {% endfor %}
    </ul>
  {% endif %}
  </dd>
{% endmacro %}

Этот макрос принимает пару ключевых аргументов, которые передаются в функцию поля WTForm, которая генерирует поле. Ключевые аргументы будут вставлены в качестве атрибутов HTML. Так, например, можно вызвать render_field(form.username, class='username'), чтобы добавить атрибут class в HTML-элемент <input>. Обратите внимание, что WTForms возвращает стандартные строки Python, поэтому необходимо сообщить Jinja2, что эти данные уже экранированы с помощью фильтра {{ val | safe }}.

Пример шаблона register.html, который использует преимущества макроса, импортируемого из шаблона _formhelpers.html, созданного ранее:

{# register.html #}
{% from "_formhelpers.html" import render_field %}
<form method=post>
  <dl>
    {{ render_field(form.username) }}
    {{ render_field(form.email) }}
    {{ render_field(form.password) }}
    {{ render_field(form.confirm) }}
    {{ render_field(form.accept_tos) }}
  </dl>
  <p><input type=submit value=Register></p>
</form>

Краткое описание модуля WTForm.

Класс WTForm.Form

Класс Form содержит определения полей, делегирует проверку/валидацию, принимает ввод, объединяет ошибки и в целом служит связующим звеном, скрепляющим все вместе.

Чтобы определить форму, нужно создать подкласс Form и декларативно определить поля как атрибуты класса:

from wtforms import Form, StringField, validators

class MyForm(Form):
    first_name = StringField('First Name', validators=[validators.input_required()])
    last_name  = StringField('Last Name', validators=[validators.optional()])

Имена полей могут быть любыми допустимыми идентификаторами python со следующими ограничениями:

  1. Имена полей чувствительны к регистру.
  2. Имена полей не могут начинаться с символа подчеркивания '_'.
  3. Имена полей не могут начинаться c 'validate'.

Наследование форм.

Формы могут при необходимости подклассифицировать другие формы. Новая форма будет содержать все поля родительской формы, а также любые новые поля, определенные в подклассе. Повторное использование имени поля в подклассе приводит к тому, что новое определение переопределяет исходное.

class PastebinEdit(Form):
    language = SelectField('Programming Language', choices=PASTEBIN_LANGUAGES)
    code     = TextAreaField()

class PastebinEntry(PastebinEdit):
    name = StringField('User Name')

Метод validate_<fieldname> в качестве валидатора поля формы.

Чтобы обеспечить настраиваемую проверку/валидацию, для каждого поля формы можно определить метод с именем validate_<fieldname>, где fieldname - это имя поля:

class SignupForm(Form):
    age = IntegerField('Age')

    def validate_age(form, field):
        if field.data < 13:
            raise ValidationError("We're sorry, you must be 13 or older to register")

Атрибуты и методы экземпляра класса Form.

Атрибуты экземпляра класса Form:

  • Form.data: словарь, содержащий данные для каждого поля. Обратите внимание, что он генерируется каждый раз, когда к нему обращаются. При неоднократном обращении - это может быть дорогостоящей операцией. Обычно используется, для перебора всех значений полей формы. Если нужно получить доступ к данным для известных полей, то должны использовать form.<field>.data, а не прокси form.data[field].
  • Form.errors: словарь, содержащий список ошибок (после проверки методом Form.validate()) для каждого поля формы. Будет пустой, если форма не была проверена или ошибок не было.

Методы экземпляра класса Form:

  • Form.validate(): проверяет форму, вызвав функцию validate() для каждого поля. Возвращает True, если проверка прошла успешно. Если форма определяет метод `validate_ , то он добавляется как дополнительный валидатор для проверки поля.
  • Form.populate_obj(obj): заполняет атрибуты переданного объекта obj данными из полей формы.

    Примечание: Любой атрибут переданного obj с тем же именем, что и поле, будет переопределен. Используйте с осторожностью.

    Одним из распространенных способов использования:

    def edit_profile(request):
      user = User.objects.get(pk=request.session['userid'])
      form = EditProfileForm(request.POST, obj=user)
    
      if request.POST and form.validate():
          form.populate_obj(user)
          user.save()
          return redirect('/home')
      return render_to_response('edit_profile.html', form=form)
    

Определение полей формы

Поля формы отвечают за визуализацию и преобразование данных. Они делегируют полномочия валидаторам для проверки данных.

Поля декларативно определяются как атрибуты класса формы Form:

class MyForm(Form):
    name    = StringField('Full Name', [validators.required(), validators.length(max=10)])
    address = TextAreaField('Mailing Address', [validators.optional(), validators.length(max=200)])

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

Метка label и валидаторы могут быть переданы конструктору в качестве позиционных аргументов, в то время как все остальные аргументы должны передаваться в качестве ключевых аргументов. Некоторые поля (например, SelectField) также могут принимать дополнительные аргументы ключевых слов для конкретных полей. Обратитесь к справочнику по встроенным полям для получения информации о них.

Аргументы конструктора класса типа поля.

  • label: метка поля.
  • validators: последовательность встроенных валидаторов или вызываемых объектов, которые вызываются методом Form.validate() и по очереди применяются к значению поля.
  • filters: последовательность фильтров, которые запускаются для входных данных процессом.
  • description: описание поля, обычно используется для текста справки.
  • Id: - идентификатор для использования в поле. Разумное значение по умолчанию задается формой, и вам не нужно устанавливать его вручную.
  • default: значение по умолчанию для присвоения полю, если не предусмотрена форма или ввод объекта. Может быть вызываемым.
  • widget: если предоставляется, переопределяет виджет, используемый для визуализации поля.
  • render_kw: словарь, если предоставлен, то должен содержать ключевые слова (по умолчанию), которые будут переданы виджету widget во время рендеринга.

Встроенные типы полей для формы.

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

Во всех встроенных типах полей, аргумент field_arguments - это аргументы конструктора базового класса поля.

  • BooleanField(field_arguments, false_values=None): представляет . Устанавливает статус checked с помощью аргумента конструктора default. Любое значение default, например default="checked", делает отметку в html-элементе и устанавливает данные в значение True.

    Необязательный аргумент false_values​: последовательность строк, каждая из которых является строкой точного соответствия тому, что считается ложным значением. По умолчанию используется кортеж (False, 'false', '',)

  • DateField(field_arguments, format='%Y-%m-%d'): текстовое поле, в котором хранится значение, соответствующее формату datetime.date.

  • DateTimeField(field_arguments, format='%Y-%m-%d %H:%M:%S'): текстовое поле, в котором хранится значение, соответствующее формату datetime.datetime.

  • DecimalField(field_arguments, places=2, rounding=None, use_locale=False, number_format=None): текстовое поле, в котором отображаются и приводятся данные типа decimal.Decimal.

    Аргументы:

    • places: На сколько знаков после запятой нужно округлить значение для отображения в форме. Если None, то значение не округляется.
    • rounding: Как округлить значение, например decimal.ROUND_UP. Если значение не установлено, то используется значение из контекста текущего потока.
    • use_locale: Если True, то форматирование чисел будет на основе локали. Для форматирования чисел на основе локали требуется пакет babel.
    • `number_format – Необязательный числовой формат для языкового стандарта. Если этот параметр опущен, то для языкового стандарта используется десятичный формат.
  • FileField(field_arguments): отображает поле загрузки файла. По умолчанию, значением будет имя файла, отправленное в данных формы.

    Модуль WTForms не занимается обработки файлов. Расширение Flask-WTF может заменить значение имени файла объектом, представляющим загруженные данные.

    Пример использования:

    class UploadForm(Form):
      image        = FileField('Image File', [validators.regexp('^[^/\\]\.jpg$')])
      description  = TextAreaField('Image Description')
    
      def validate_image(form, field):
          if field.data:
              field.data = re.sub(r'[^a-z0-9_.-]', '_', field.data)
    
    def upload(request):
      form = UploadForm(request.POST)
      if form.image.data:
          image_data = request.FILES[form.image.name].read()
          open(os.path.join(UPLOAD_PATH, form.image.data), 'w').write(image_data)
    
  • MultipleFileField(field_arguments): то же самое, что и FileField, но позволяет выбирать несколько файлов.

  • FloatField(field_arguments): текстовое поле, за исключением того, что все вводимые данные приводятся к типу float. Ошибочный ввод игнорируется и не принимается в качестве значения. Для большинства применений DecimalField предпочтительнее FloatField, за исключением случаев, когда IEEE float абсолютно желателен вместо десятичного значения.

  • IntegerField(field_arguments): текстовое поле, за исключением всего ввода, приводится к целому числу. Ошибочный ввод игнорируется и не принимается в качестве значения.

  • RadioField(field_arguments, choices=[], coerce=unicode): подобно SelectField(), за исключением того, что отображает список переключателей.

    Итерация/цикл по полю приведет к созданию подполей (каждое из которых также содержит метку).

    {% for subfield in form.radio %}
      <tr>
          <td>{{ subfield }}</td>
          <td>{{ subfield.label }}</td>
      </tr>
    {% endfor %}
    

    Простой вывод поля без создания цикла, приведет к получению <ul> списка.

  • SelectField(field_arguments, choices=[], coerce=unicode, option_widget=None, validate_choice=True):

    Поля <select> принимают параметр choices, который представляет собой список пар (value, label). Это также может быть список только значений value, и в этом случае значение используется в качестве метки label. Значение может быть любого типа, но т.к. данные формы отправляются в браузер в виде строк, необходимо будет предоставить функцию coerce, которая преобразует строку обратно в ожидаемый тип.

    Пример поля SelectField() со статическими значениями:

    class PastebinEntry(Form):
      language = SelectField('Programming Language', choices=[('cpp', 'C++'), ('py', 'Python'), ('text', 'Plain Text')])
    

    Обратите внимание, что ключевое слово choices оценивается только один раз, поэтому, если надо создать динамический раскрывающийся список, то нужно будет назначить список вариантов для поля после создания экземпляра. Любые введенные варианты, которых нет в данном списке, приведут к сбою проверки в поле. НО можно пропустить проверку, передав аргумент validate_choice=False.

    Пример поля SelectField() со статическими значениями:

    class UserDetails(Form):
      group_id = SelectField('Group', coerce=int)
    
    def edit_user(request, id):
      user = User.query.get(id)
      form = UserDetails(request.POST, obj=user)
      form.group_id.choices = [(g.id, g.name) for g in Group.query.order_by('name')]
    

    Обратите внимание, что мы не передали варианты выбора конструктору SelectField, а создали список в функции-представлении. Кроме того, ключевое слово coerce для SelectField() говорит о том, что для приведения данных формы используется int().

  • SelectMultipleField(field_arguments, choices=[], coerce=unicode, option_widget=None): ни чем не отличается от обычного поля SelectField(), за исключением того, что оно может принимать и проверять несколько вариантов. Для этого необходимо указать HTML-атрибут size для поля <select> при рендеринге.

  • SubmitField(field_arguments): представляет <input type="submit">. Это позволяет проверить, была ли нажата данная кнопка отправки.

  • StringField(field_arguments): это поле является основой для большинства более сложных полей и представляет собой <input type="text">.

  • HiddenField(field_arguments): это строковое поле с виджетом HiddenInput. Оно будет отображаться как , но в противном случае принудительно преобразуется в строку.

    Скрытые поля похожи на любое другое поле в том смысле, что они могут принимать валидаторы и значения и быть доступны в объекте формы. Рассмотрите возможность проверки/валидации скрытых полей так же, как обычных.

  • PasswordField(field_arguments): поле ввода пароля <input type="password">, всегда преобразуется в строку. Кроме того, любое значение, принятое этим полем, не отображается обратно в браузер, как обычные поля.

  • TextAreaField(field_arguments): поле представляет собой текстовое поле <textarea> и может использоваться для многострочного ввода.


Встроенные валидаторы полей.

Поля формы, определенные в подклассе класса Form, могут проверяться встроенными валидаторами, определенными в wtform.validators. Встроенные валидаторы передаются списком, как аргумент типа поля формы.

from wtforms import Form, PasswordField
from wtforms.validators import InputRequired, EqualTo

class ChangePassword(Form):
    password = PasswordField('Новый пароль', [InputRequired(), 
                        EqualTo('confirm', message='Пароли должны совпадать')])
    confirm  = PasswordField('Повторите Пароль')

Список встроенных валидаторов, определяемых в wtforms.validators:

  • Email(message=None, granular_message=False, check_deliverability=False, allow_smtputf8=True, allow_empty_local=False):

    Проверяет адрес электронной почты. Требуется установка модуля email_validator.

    Аргументы:

    • message: сообщение об ошибке, которое должно появиться в случае ошибки проверки.
    • granular_messsage: сообщение об ошибке проверки из модуля email_validator (по умолчанию False).
    • check_deliverability: выполняет проверку разрешения доменных имен (по умолчанию False).
    • allow_smtputf8: вызывает ошибку проверки адресов, для которых требуется SMTPUTF8 (по умолчанию True).
    • allow_empty_local: разрешает пустую локальную часть (т. е. @example.com), например, для проверки псевдонимов (по умолчанию False).
  • EqualTo(fieldname, message=None): сравнивает значения двух полей.

    Этот валидатор можно использовать для облегчения одного из наиболее распространенных сценариев формы смены пароля:

    Аргументы:

    • fieldname: имя другого поля для сравнения.
    • message: – сообщение об ошибке, которое должно появиться в случае ошибки проверки. Можно интерполировать с помощью %(other_label)s и %(other_name)s, чтобы обеспечить более полезную ошибку.

    Пример:

    from wtforms import Form, PasswordField
    from wtforms.validators import InputRequired, EqualTo
    
    class ChangePassword(Form):
      password = PasswordField('Новый пароль', [InputRequired(), 
                          EqualTo('confirm', message='Пароли должны совпадать')])
      confirm  = PasswordField('Повторите Пароль')
    

    Здесь используется валидатор InputRequired(), чтобы предотвратить попытку валидатора EqualTo() проверить, не совпадают ли пароли, если пароли не были указаны вообще. Поскольку InputRequired() останавливает цепочку проверки, то EqualTo() не запускается в случае, если поле пароля остается пустым.

  • InputRequired(message=None): проверяет, что для поля были предоставлены данные. Другими словами, значение поля - не пустая строка. Этот валидатор также устанавливает флаг обязательного поля формы для заполнения.

  • IPAddress(ipv4=True, ipv6=False, message=None): проверяет IP-адрес. Аргумент Ipv4 - если True, принимать адреса IPv4 как действительные (по умолчанию True). Аргумент Ipv6 - если True, принимать IPv6-адреса как действительные (по умолчанию False)

  • Length(min=- 1, max=- 1, message=None): проверяет длину строки. Аргумент min - минимальная необходимая длина строки. Если не указан, минимальная длина проверяться не будет. Аргумент max - максимальная длина строки. Если не указан, максимальная длина проверяться не будет.

  • MacAddress(message=None): проверяет MAC-адрес. Аргумент message - сообщение об ошибке, которое будет выдано в случае ошибки проверки.

  • NumberRange(min=None, max=None, message=None): проверяет, что число имеет минимальное и/или максимальное значение включительно. Это будет работать с любым сопоставимым типом чисел, таким как числа с плавающей запятой и десятичные дроби, а не только с целыми числами.

  • Optional(strip_whitespace=True): разрешает пустой ввод (необязательное поле) и останавливает продолжение цепочки проверки. Если ввод пуст, также удаляются предыдущие ошибки из поля (например, ошибки обработки). Если аргумент strip_whitespace=True (по умолчанию), то также остановит цепочку проверки, если значение поля состоит только из пробелов.

  • Regexp(regex, flags=0, message=None): проверяет поле на соответствие регулярному выражению, предоставленному пользователем. Аргумент regex - cтрока регулярного выражения для использования. Также может быть скомпилированным шаблоном регулярного выражения. Аргумент flags - используемые флаги регулярного выражения, например re.IGNORECASE. Игнорируется, если регулярное выражение не является строкой.

  • URL(require_tld=True, message=None): простая проверка URL на основе регулярного выражения. Вероятно потребуется его проверка на доступность другими способами.

  • UUID(message=None): проверяет UUID.

  • AnyOf(values, message=None, values_formatter=None): сравнивает входящие данные с последовательностью допустимых входных данных. Аргумент values ​​- последовательность допустимых входных данных. Аргумент values_formatter - функция, используемая для форматирования списка значений в сообщении об ошибке message.

  • NoneOf(values, message=None, values_formatter=None): сравнивает входящие данные с последовательностью неверных входных данных. Аргумент values ​​- последовательность допустимых входных данных. Аргумент values_formatter - функция, используемая для форматирования списка значений в сообщении об ошибке message.

Создание собственного валидатора.

Выше было показано использование встроенного в класс Form валидатора (как метода с определенным именем) для проверки одного поля. Встроенные валидаторы хороши, но их сложно использовать повторно.

Так как тип поля формы принимает аргумент validators в качестве последовательности вызываемых объектов, то это может быть простая функция Python, которая возвращает значение bool. НО лучше использовать фабричную функцию, которая возвращает вызываемый объект:

def length(min=-1, max=-1, message=None):
    if not message:
        message = f'Must be between {min} and {max} characters long.'

    def _length(form, field):
        l = field.data and len(field.data) or 0
        if l < min or max != -1 and l > max:
            raise ValidationError(message)

    return _length

# использование валидатора `length`
class MyForm(Form):
    name = StringField('Name', [InputRequired(), length(max=50)])