Формы являются ключевым компонентом любого веб-приложения, однако их реализация может быть достаточно сложной задачей. Сначала необходимо убедиться в корректности данных на клиентской стороне, а затем — на серверной. Более того, если разработчик берет во внимание угрозы безопасности такие, как CSRF, XSS, и SQL Injection, работы становится еще больше. Впрочем, на помощь приходит замечательная библиотека WTForms, которая берет на себя большую часть обработки. Однако, прежде чем воспользоваться WTForms, полезно понять, как управлять формами без использования библиотек и пакетов.

Работа с формами — сложный вариант

Начнем с создания шаблона login.html с нижеследующим кодом:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Login</title>
</head>
<body>

    {% if message %}
        <p>
            {{ message }}
        </p>
    {% endif %}

    <form action="" method="post">
        <p>
	    <label for="username">Username</label>
	    <input type="text" name="username">
	</p>
	<p>
	    <label for="password">Password</label>
	    <input type="password" name="password">
	</p>
	<p>
	    <input type="submit">
	</p>
    </form>

</body>
</html>

Теперь добавим этот код после функции представления books() в файле main2.py:

from flask import Flask, render_template, request
#...
@app.route('/login/', methods=['post', 'get'])
def login():
    message = ''
    if request.method == 'POST':
	username = request.form.get('username')  # получение данных формы
	password = request.form.get('password')

        if username == 'root' and password == 'pass':
            message = "Correct username and password"
        else:
            message = "Wrong username or password"

    return render_template('login.html', message=message)
#...

Следует отметить, что аргумент methods был передан декоратору route(). Обычно обработчики запросов выполняются только при запросах методом GET или HEAD. Однако это можно изменить, передав декоратору methods список доступных методов HTTP. С этого момента функция представления login будет вызываться, если запрос к /login/ идет методом GET, POST или HEAD. При попытке обратиться к /login/ с использованием другого метода появится ошибка 405 Method Not Allowed.

Ранее говорилось, что объект request предоставляет доступ к информации о текущем веб-запросе. Информация, поданная через форму, находится в атрибуте form объекта request. request.form реализован как неизменяемый тип словаря, известный как ImmutableMultiDict.

После всего этого необходимо запустить сервер и перейти на http://localhost:5000/login/. Здесь вы увидите форму.

форма во Flask

Так как текущий запрос к странице был сделан методом GET, код в блоке if внутри функции login() не выполняется.

При попытке отправить форму, оставив поля пустыми, страница будет следующего вида:

ошибка в форме во Flask

Теперь страница отправлена методом POST, и код в блоке if выполняется. Здесь приложение обрабатывает имя пользователя и пароль, устанавливая сообщение для message. Поскольку поля формы пусты, отображается сообщение об ошибке.

Если ввести правильные имя пользователя и пароль, а затем нажать Enter, появится сообщение: “Correct username and password”:

Заполненная форма во Flask

Такой код иллюстрирует, как можно работать с формами во Flask. Теперь рассмотрим библиотеку WTForms.

WTForms

WTForms — это продвинутая библиотека на Python, не связанная с конкретным фреймворком. Она умеет генерировать формы, проверять их и предварительно заполнять данные (что особенно полезно при редактировании), а также она обеспечивает защиту от CSRF. Установка WTForms осуществляется с использованием Flask-WTF.

Flask-WTF — расширение для Flask, которое интегрирует возможности WTForms. Оно предлагает дополнительные функции, такие как загрузка файлов, интеграция reCAPTCHA, интернационализация (i18n) и прочие. Чтобы установить Flask-WTF, используйте следующую команду:

(env) gvido@vm:~/flask_app$ pip install flask-wtf

Создание класса Form

Начнем с определения форм путем создания классов Python. Каждая форма должна наследоваться от FlaskForm из пакета flask_wtf. FlaskForm представляет собой обертку, содержащую полезные методы оригинального класса wtform.Form, который является базовым для создания форм. Поля формы описываются через переменные класса. Они задаются в виде объектов, соответствующих определенному типу поля. Пакет wtform предоставляет несколько классов для работы с полями, таких как: StringField, PasswordField, SelectField, TextAreaField, SubmitField и прочие.

Сначала создайте файл forms.py в директории flask_app и добавьте туда следующий код:

from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, TextAreaField
from wtforms.validators import DataRequired, Email

class ContactForm(FlaskForm):
    name = StringField("Name: ", validators=[DataRequired()])
    email = StringField("Email: ", validators=[Email()])
    message = TextAreaField("Message", validators=[DataRequired()])
    submit = SubmitField("Submit")

В этом коде определен класс формы ContactForm с четырьмя полями: name, email, message и submit. Эти переменные используются для отображения полей формы, а также для чтения и записи данных в них. Форма использует две StringField, одну TextAreaField и одну SubmitField. При создании объекта поля ему передаются определенные параметры. Первый параметр — строка, представляющая метку, которая будет показана в теге <label>, когда поле отрендерится. Второй параметр — это список валидаторов, которые передаются конструктору в виде аргументов-ключевых слов. Валидаторы представляют собой функции или классы, определяющие, соответствует ли введенное в поле значение требованиям. Для одного поля может быть установлено несколько валидаторов, разделенных запятыми (,). Модуль wtforms.validators предлагает базовые валидаторы, их можно также разрабатывать самостоятельно. В данном примере использованы валидаторы DataRequired и Email.

DataRequired: убеждается, что в поле введено хоть какое-то значение.

Email: проверяет, является ли введенная строка корректым email-адресом.

Данные, введенные в поля формы, примут валидацию только в случае соответствия всем требованиям валидаторов.

Примечание: это лишь введение в концепцию полей форм и валидаторов. Более подробный список доступных классов и функций можно найти здесь.

Установка SECRET_KEY

По умолчанию Flask-WTF защищает от CSFR-атак, используя внутренний токен, скрыто встраиваемый в форму. Этот токен помогает проверять подлинность запросов. Однако, чтобы CSFR-защита функционировала, нужно настроить секретный ключ, добавив его в файл main2.py:

#...
app.debug = True
app.config['SECRET_KEY'] = 'a really really really really long secret key'

manager = Manager(app)
#...

В этом фрагменте используется атрибут config объекта Flask, который действует как словарь, позволяя задавать параметры конфигурации для Flask и его расширений, а также кастомные настройки.

Секретный ключ должен представлять собой ссыльную строку — достаточно сложную для угадывания, и предпочтительно длинную. Помимо создания CSFR-токенов, SECRET_KEY задействуется в ряде других расширений Flask. Важно сохранить его в безопасном месте, предпочтительно — в переменной окружения, чтобы избежать его хранения в коде приложения. Об этом более подробно в последующих разделах.

Формы в консоли

Откройте оболочку Python через следующую команду:

(env)  gvido@vm:~/flask_app$  python main2.py  shell

Это действие запустит оболочку Python в контексте приложения.

Далее импортируем класс ContactForm и создадим экземпляр этой формы, подавая ей данные формы.

>>>
>>> from forms import ContactForm
>>> from werkzeug.datastructures import MultiDict
>>>
>>>
>>> form1 = ContactForm(MultiDict([('name', 'jerry'),('email', '[email protected]')]))
>>>

Обратите внимание на использование MultiDict для передачи данных, так как конструктор wtforms.Form ожидает аргументов именно этого типа. Если данные формы не заданы при создании формы, и она отправлена через запрос POST, wtforms.Form использует это для поиска данных в request.form. Не забывайте, что request.form возвращает объект типа ImmutableMultiDict, который аналогичен MultiDict, но неизменяем.

Форма может быть проверена с помощью метода validate(). Он возвращает True, если проверка прошла успешно, и False — если нет.

>>>
>>> form1.validate()
False
>>>

Форма не прошла валидацию, поскольку обязательное поле message при создании экземпляра не было заполнено. Вы можете получить доступ к ошибкам формы через атрибут errors:

>>>
>>> form1.errors
{'message': ['This field is required.'], 'csrf_token': ['The CSRF token is missing.']}
>>>

Важно заметить, что помимо ошибки в поле message, вывод содержит сообщение о недостающем CSRF-токене, так как данные формы не предполагают наличие POST-запроса с CSRF-токеном.

Можно отключить CSRF-защиту, установив параметр csrf_enabled=False при создании формы. Например:

>>> form3 = ContactForm(MultiDict([('name', 'spike'),('email', '[email protected]')]), csrf_enabled=False)
>>>
>>> form3.validate()
False
>>>
>>> form3.errors
{'message': ['This field is required.']}
>>>
>>>

Как и ожидалось, сообщение об ошибке отображается теперь только для поля message. Создадим новый объект формы, передав на этот раз информацию для всех полей:

>>>
>>> form4 = ContactForm(MultiDict([('name', 'jerry'), ('email', '[email protected]'), ('message', "hello tom")]),  csrf_enabled=False)
>>>
>>> form4.validate()
True
>>>
>>> form4.errors
{}
>>>

Форма успешно прошла валидацию.

Следующий шаг — рендеринг формы.

Рендеринг формы

Существует два основных подхода к рендерингу:

  1. Рисовать каждое поле отдельно.
  2. Рисовать все поля циклом.
Рендеринг полей один за одним

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

    {# отображаем метку поля #}
    {{ form.field_name.label() }}

    {# отображаем само поле #}
    {{ form.field_name() }}

    {# отображаем ошибки валидации, связанные с полем #}
    {% for error in form.field_name.errors %}
        {{ error }}
    {% endfor %}

Теперь можно протестировать этот метод в консоли:

>>>
>>> from forms import ContactForm
>>> from jinja2 import Template
>>>
>>> form = ContactForm()
>>>

Здесь создается экземпляр объекта формы без каких-либо данных запроса. Это применимо, когда форма отображается в первый раз через запрос GET.

>>>
>>>
>>> Template("{{ form.name.label() }}").render(form=form)
'<label for="name">Name: </label>'
>>>
>>> Template("{{ form.name() }}").render(form=form)
'<input id="name" name="name" type="text" value="">'
>>>
>>>
>>> Template("{{ form.email.label() }}").render(form=form)
'<label for="email">Email: </label>'
>>>
>>> Template("{{ form.email() }}").render(form=form)
'<input id="email" name="email" type="text" value="">'
>>>
>>>
>>> Template("{{ form.message.label() }}").render(form=form)
'<label for="message">Message</label>'
>>>
>>> Template("{{ form.message() }}").render(form=form)
'<textarea id="message" name="message"></textarea>'
>>>
>>>
>>> Template("{{ form.submit() }}").render(form=form)
'<input id="submit" name="submit" type="submit" value="Submit">'
>>>
>>>

Так как форма впервые отображается, у полей не будет ошибок валидации. Следующий код демонстрирует это:

>>>
>>>
>>> Template("{% for error in form.name.errors %}
            {{ error }}{% endfor %}").render(form=form)
''
>>>
>>>
>>> Template("{% for error in form.email.errors %}
            {{ error }}{% endfor %}").render(form=form)
''
>>>
>>>
>>> Template("{% for error in form.message.errors %}
            {{ error }}{% endfor %}").render(form=form)
''
>>>
>>>

Для отображения всех ошибок формы одновременно, вместо ошибок по отдельным полям, можно использовать form.errors. Это позволяет отображать все ошибки валидации в верхней части формы, как один общий список.

>>>
>>> Template("{% for error in form.errors %}
            {{ error }}{% endfor %}").render(form=form)
''
>>>

Рендеринг полей и меток может быть улучшен за счёт предоставления дополнительных значений атрибутов HTML, путем передачи аргументов ключ-значение. Рассмотрим пример:

>>>
>>> Template('{{ form.name(class="input", id="simple-input") }}').render(form=form)
'<input class="input" id="simple-input" name="name" type="text" value="">'
>>>
>>>
>>> Template('{{ form.name.label(class="lbl") }}').render(form=form)
'<label class="lbl" for="name">Name: </label>'
>>>
>>>

Предположим, форма была отправлена. Теперь можно попробовать отрендерить поля и посмотреть, как они будут отображены.

>>>
>>> from werkzeug.datastructures import MultiDict
>>>
>>> form = ContactForm(MultiDict([('name', 'spike'),('email', '[email protected]')]))
>>>
>>> form.validate()
False
>>>
>>>
>>> Template("{{ form.name() }}").render(form=form)
'<input id="name" name="name" type="text" value="spike">'
>>>
>>>
>>> Template("{{ form.email() }}").render(form=form)
'<input id="email" name="email" type="text" value="[email protected]">'
>>>
>>>
>>> Template("{{ form.message() }}").render(form=form)
'<textarea id="message" name="message"></textarea>'
>>>
>>>

Обратите внимание, что у атрибутов value в полях name и email есть значения, полученные от пользователя. Однако элемент <textarea> для поля message пуст, так как данные для него не были переданы. Чтобы увидеть ошибки валидации, связанные с полем message, можно использовать следующий подход:

>>>
>>> Template("{% for error in form.message.errors %}
            {{ error }}{% endfor %}").render(form=form)
'This field is required.'
>>>

Альтернативно, можно использовать form.errors для перебора всех ошибок валидации сразу:

>>>
>>> s  ="""\
... {% for field_name in form.errors %}
... {% for error in form.errors[field_name] %}
... <li>{{ field_name }}: {{ error }}<li>
... {% endfor %}
... {% endfor %}
... """
>>>
>>> Template(s).render(form=form)
'<li>csrf_token: The CSRF token is missing.</li>\n
<li>message: This field is required.</li>\n'
>>>
>>>

Нужно отметить, что ошибка отсутствия CSRF-токена возникла из-за попытки отправки без токена. Вы можете также отобразить поле CSRF подобно другим полям формы:

>>>
>>>  Template("{{ form.csrf_token() }}").render(form=form)
'<input id="csrf_token" name="csrf_token" type="hidden" value="IjZjOTBkOWM4ZmQ0MGMzZTY3NDc3ZTNiZDIxZTFjNDAzMGU1YzEwOTYi.DQlFlA.GQ-PrxsCJkQfoJ5k6i5YfZMzC7k">'
>>>

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

Рендеринг полей с помощью цикла

В коде ниже демонстрируется, как можно отрендерить поля формы с помощью цикла for.

>>>
>>> s = """\
...     <div>
... 	    {{ form.csrf_token }}
... 	</div>
... {% for field in form if field.name != 'csrf_token' %}
...    <div>
...        {{ field.label() }}
...        {{ field() }}
...        {% for error in field.errors %}
...            <div class="error">{{ error }}</div>
...        {% endfor %}
...    </div>
... {% endfor %}
... """
>>>
>>>
>>> print(Template(s).render(form=form))
    <div>
	<input id="csrf_token" name="csrf_token" type="hidden"  value="IjZjOTBkOWM4ZmQ0MGMzZTY3NDc3ZTNiZDIxZTFjNDAzMGU1YzEwOTYi.DQlFlA.GQ-PrxsCJkQfoJ5k6i5YfZMzC7k">

    </div>

    <div>
	<label for="name">Name: </label>
	<input id="name" name="name" type="text" value="spike">

    </div>

    <div>
	<label for="email">Email: </label>
	<input id="email" name="email" type="text"  value="[email protected]">

    </div>

    <div>
	<label for="message">Message</label>
	<textarea id="message" name="message"></textarea>

	    <div class="error">This field is required.</div>

    </div>

    <div>
	<label for="submit">Submit</label>
	<input id="submit" name="submit" type="submit"  value="Submit">

    </div>
>>>
>>>

Обратите внимание, что независимо от метода рендеринга необходимо вручную обернуть поля в тег <form>.

Теперь, узнав, как создавать, проверять и рендерить формы, можно применить эти навыки для создания реальных форм.

Сначала создайте HTML-шаблон contact.html с кодом следующим образом:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<form action="" method="post">

    {{ form.csrf_token() }}

    

    {% for field in form if field.name != "csrf_token" %}
	<p>{{ field.label() }}</p>
	<p>{{ field }}
	    

	    {% for error in field.errors %}
		{{ error }}
	    {% endfor %}
	</p>
    

    {% endfor %}

</form>

</body>
</html>

Единственный недостающий элемент — это функция представления, которую мы скоро создадим.

Работа с подтверждением формы

Откроем main2.py и добавим следующий код после функции представления login().

from flask import Flask, render_template, request, redirect,  url_for
from flask_script import Manager, Command, Shell
from forms import ContactForm
#...
@app.route('/contact/', methods=['get', 'post'])
def contact():
    form = ContactForm()
    if form.validate_on_submit():
	name = form.name.data
	email = form.email.data
	message = form.message.data
	print(name)
	print(email)
	print(message)
	# здесь логика базы данных
	print("\nData received. Now redirecting ...")
	return redirect(url_for('contact'))

    return render_template('contact.html', form=form)
#...

На седьмой строке создаётся объект формы. На восьмой строке проверяется результат метода validate_on_submit(), чтобы определить, будет ли выполняться код внутри условия if.

Почему выбран validate_on_submit(), а не validate(), как это было в консольном примере? Потому что validate() просто проверяет корректность данных формы без учёта метода запроса. Это значит, что если использовать validate(), то запрос GET к /contact/ запускает проверки формы и отобразит ошибки валидации. Процедура проверки должна запускаться только при отправке данных с помощью POST. В противном случае возвращается False. validate_on_submit() вызовет validate() внутри себя. Кроме того, важно отметить, что при создании экземпляра формы данные не передаются явно, так как WTForm автоматически считывает данные формы из request.form, когда форма отправляется посредством POST запроса.

Поля формы, определенные в классе, становятся атрибутами объекта формы. Чтобы получить данные определенного поля, используется атрибут data:

form.name.data  # доступ к данным в поле name.
form.email.data  # доступ к данным в поле email.

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

form.data  # доступ ко всем данным

Если отправить GET запрос к /contact/, метод validate_on_submit() вернёт False. Код внутри if будет пропущен, и пользователь получит пустую HTML-форму.

Когда форма отправляется с помощью POST-запроса, validate_on_submit() возвращает True, предполагая, что данные формы корректны. Внутри блока if, вызовы print() выведут введённые пользователем данные, а redirect() перенаправит пользователя обратно на /contact/. В противном случае, если validate_on_submit() возвращает False, внутри блока if исполнение будет пропущено, и пользователь увидит ошибки валидации на текущей странице.

Если сервер ещё не запущен, его нужно запустить и открыть http://localhost:5000/contact/. Вы увидите форму для ввода контактных данных:

контактная форма во Flask

Попробуйте нажать Submit, не заполнив поля. Тогда отобразятся ошибки валидации:

ошибка валидации формы во Flask

Попробуйте ввести корректные данные в поля Name и Message и неверный email, а затем снова отправьте форму.
ошибка валидации email во Flask

Обратите внимание, что поля сохраняют введенные ранее данные из предыдущего запроса.

Введите корректный email и нажмите Submit. Теперь данные будут переданы успешно, и в консоли вы увидите:

Spike
[email protected]
A Message

Data received. Now redirecting  ...

После печати полученных данных в консоли, функция представления перенаправит пользователя на /contact/. В результате отобразится чистая форма без ошибок валидации, как если бы пользователь впервые посетил страницу с GET-запросом.

Для улучшения пользовательского опыта лучше отобразить сообщение об успешном результате. Во Flask это можно сделать с помощью всплывающих сообщений.

Всплывающие сообщения

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

Для отображения сообщения используется функция flash() из библиотеки flask. Этот метод принимает текст сообщения и категорию (опционально) для его классификации: success, error, warning и так далее. Категория может быть использована в шаблоне для стилизации типа сообщения.

Снова откроем main2.py и добавим flash(“Message Received”, “success”) перед вызовом redirect() в функции contact():

from flask import Flask, render_template, request, redirect, url_for, flash
#...
	# здесь логика базы данных
	print("\nData received. Now redirecting ...")
	flash("Message Received", "success")
	return redirect(url_for('contact'))
    return render_template('contact.html', form=form)

Сообщение, переданное функции flash(), будет доступно только для следующего запроса, после чего оно удалится.

Это только настройка сообщения. Чтобы оно отобразилось, нужно изменить также и шаблон.

Для этого откройте файл contact.html и измените его следующим образом:

Jinja предоставляет функцию get_flashed_messages(), возвращающую список активных сообщений без категорий. Для получения сообщений с категориями, передайте with_category=True при вызове get_flashed_messages(). Когда with_categories установлено в True, get_flashed_messages() вернет список кортежей вида (category, message).

После изменений откройте http://localhost:5000/contact снова. Заполните форму и нажмите Submit. Вверху формы появится сообщение об успешной отправке.

Успешная отправка формы во Flask

Вопросы для самопроверки:

  1. Какие угрозы безопасности необходимо учитывать при работе с формами во Flask?
  2. Какая библиотека облегчает обработку форм во Flask и что она может делать?
  3. Какой тип данных используется для хранения данных формы в объекте request?
  4. Что такое SECRET_KEY в контексте Flask и какие функции он выполняет?
  5. Как можно проверить валидность формы в консоли Python и каковы возможные результаты этой проверки?
  6. Как использовать функцию flash() для отображения сообщений о выполнении действий на странице во Flask?