Веб-формы

Объект запроса, с которым мы познакомились ранее, хранит всю информацию, отправленную клиентом вместе с запросом. В частности, свойство request.form обеспечивает доступ данным формы, отправленным в запросе POST.

Несмотря на то что возможностей объекта request вполне достаточно для обработки веб-форм, существует множество задач, которые быстро могут стать утомительными. Двумя примерами таких задач могут служить создание разметки HTML для форм и проверка данных, полученных с формой.

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

(venv) $ pip install flask-wtf

Защита от подделки межсайтовых запросов

По умолчанию Flask-WTF защищает все формы от атак, основанных на подделке межсайтовых запросов (Cross-Site Request Forgery, CSRF) . Суть атаки CSRF заключается в том, что злонамеренный веб-сайт отправляет запросы другому веб-сайту, на котором авторизована жертва.

Для реализации защиты от атак вида CSRF Flask-WTF требует от приложения настройки ключа шифрования. С помощью этого ключа Flask-WTF генерирует шифрованные блоки, которые используются для проверки аутентичности запросов, содержащие данные форм.

app = Flask(__name__)
app.config['SECRET_KEY'] = 'hard to guess string'

Словарь app.config – универсальное хранилище, используемое для хранения настроек фреймворком, расширениями или самим прило- жением. Добавлять настройки в объект app.config можно с помощью привычного синтаксиса обращения к словарям. Объект с настройками также имеет методы для импортирования настроек из файлов или из окружения. Для хранения универсального ключа шифрования фреймворк Flask и некоторые сторонние расширения используют параметр настройки SECRET_KEY. Как следует из имени, криптостойкость шифрования зависит от степени секретности значения этого параметра. Выбирайте для своих приложений разные секретные ключи и храните их в тайне от других

Классы форм

При использовании расширения Flask-WTF каждая веб-форма представлена классом, наследующим класс Form. Класс определяет список полей формы, каждое из которых представлено объектом. К каждому объекту поля может быть подключен один или более валидаторов (validators), где под валидаторами подразумеваются функции, осуществляющие допустимость данных, отправленных пользователем. В следующем примере показана простая веб-форма, состоящая из текстового поля и кнопки отправки.

from flask.ext.wtf import Form
from wtforms import StringField, SubmitField
from wtforms.validators import Required

class NameForm(Form):
    name = StringField('What is your name?', validators=[Required()])
    submit = SubmitField('Submit')

Поля в форме определяются как переменные класса, и каждой переменной класса присваивается объект поля того или иного типа. В предыдущем примере определяется форма NameForm, содержащая текстовое поле name и кнопку отправки формы submit. Класс StringField представляет элемент <input> с атрибутом type="text". Класс SubmitField представляет элемент <input> с атрибутом type="submit". Первый аргумент конструктора поля – текст для метки, которая будет отображаться рядом с полем в HTML-форме.

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

Перечислим все классы полей форм, поддерживаемые пакетом WTForms:

Рассмотрим список встроенных валидаторов из пакета WTForms:

Отображение форм в формат HTML

Поля форм в шаблонах принимают вид вызываемых методов, при вызове которых происходит отображение полей в разметку HTML. Если предположить, что функция представления передает экземпляр NameForm в шаблон в виде аргумента с именем form, шаблон может сгенерировать простую HTML-форму, как показано ниже:

<form method="POST">
    {{ form.name.label }} {{ form.name() }}
    {{ form.submit() }}
</form>

Обработка форм в функциях представления

Следуюшая функция представления index() отобразит форму и получит ее данные:

@app.route('/', methods=['GET', 'POST'])
def index():
    name = None
    form = NameForm()
    if form.validate_on_submit():
        name = form.name.data
        form.name.data = ''
    return render_template('index.html', form=form, name=name)

Аргумент methods, добавленный в декоратор app.route, сообщает фреймворку Flask, что он должен зарегистрировать функцию пред- ставления как обработчик запросов GET и POST. Когда аргумент methods отсутствует, функция представления регистрируется только для обработки запросов GET.

Добавление метода POST в список необходимо потому, что отправку форм удобнее производить методом POST1. Формы можно также отправлять методом GET, но GET-запросы не имеют тела, и данные передаются в виде параметров строки запроса в URL, которая видна в адресной строке браузера. По этой и некоторым другим причинам отправку форм предпочтительнее выполнять методом POST`.

Локальная переменная name будет хранить имя, введенное пользователем. Если пользователь оставит поле пустым, переменная будет инициализирована значением None. Функция представления создаст экземпляр класса NameForm, приводившегося выше, для представления формы. Метод validate_on_submit() формы вернет True, если она была отправлена пользователем и данные прошли проверку всеми валидаторами полей. В любых других случаях validate_on_submit() вернет False. Значение, возвращаемое этим методом, может служить основой для принятия решения о необходимости отображения или обработки формы.

При первом обращении пользователя к приложению сервер получит запрос GET без данных формы, поэтому validate_on_submit() вернет False. Тело инструкции if будет пропущено, и в ответ на запрос будет выполнено отображение шаблона, который получит объект формы и переменную name со значением None в виде аргументов. В результате пользователь увидит форму в окне браузера.

Когда пользователь заполнит и отправит форму, сервер получит запрос POST с данными. Метод validate_on_submit() вызовет валида- тор Required(), подключенный к полю name. Если поле непустое, оно будет принято валидатором и validate_on_submit() вернет True. После этого имя, введенное пользователем, станет доступно через атрибут data поля. В теле инструкции if это имя будет присвоено локальной переменной name, и затем поле формы будет очищено путем присваивания пустой строки атрибуту data. Вызов метода render_template() в последней строке отобразит шаблон, но на этот раз аргумент name будет содержать имя из формы, и приветствие получится персонализированным.

Переадресация и сеансы

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

Многие пользователи не понимают смысла предупреждения. По этой причине считается хорошей практикой никогда не оставлять POST-запрос последним.

Добиться этого можно, отвечая на POST-запросы кодом переадресации. Переадресация – это ответ специального типа, включающий адрес URL вместо строки с разметкой HTML. Когда браузер примет такой ответ, он выполнит запрос GET для перехода по указанному адресу URL и отобразит указанную страницу. Для отображения страницы может потребоваться на несколько миллисекунд больше из-за необходимости отправить второй запрос, но пользователь едва ли заметит такую задержку. После этого последним выполненным запросом будет GET-запрос, соответственно обновление страницы будет выполняться без всяких предупреждений со стороны браузера. Этот трюк известен как «Шаблон Post/Redirect/Get».

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

Приложения могут хранить данные между запросами в пределах сеанса пользователя (user session) – в хранилище, доступном для каждого подключившегося клиента. Понятие сеанса пользователя представляет переменную, связанную с контекстом запроса. Она называется session и действует подобно стандартному словарю Python.

from flask import Flask, render_template, session, redirect, url_for

@app.route('/', methods=['GET', 'POST'])
def index():
    form = NameForm()
    if form.validate_on_submit():
        session['name'] = form.name.data
        return redirect(url_for('index'))
    return render_template('index.html', form=form, name=session.get('name'))

В предыдущей версии приложения для хранения имени, введенного пользователем, применялась локальная переменная name. Теперь это имя хранится в сеансе пользователя в виде session['name'], благодаря чему оно запоминается между запросами.

Обработка запросов, имеющих в своем составе допустимые данные из формы, теперь завершается вызовом redirect(), вспомогательной функции, генерирующей HTTP-ответ переадресации.

Последнее изменение находится в вызове функции render_template(). Теперь значение для аргумента name извлекается непосредственно из сеанса: session.get('name'). Как и при работе с обычными словарями, применение метода get() помогает избежать исключения при обращении к несуществующему ключу, потому что для несуществующих ключей он возвращает значение по умолчанию None.

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

  1. Какое расширение используется для работы с формами в Flask?
  2. Перечислите основные классы полей форм.
  3. Как сгенерировать форму в шаблоне, изпользуя полученный от сервера параметр?
  4. Для чего используется аргумент methods?
  5. В какой переменной хранятся данные сеанса пользователя?

Задания

  1. Создайте форму со следующими полями:
    • Имя
    • Телефон
    • E-mail
  2. Добавьте для полей соответствующие валидаторы.
  3. Реализуйте обработку формы с сохранением ее данных в сеансе пользователя.

Предыдущий урок Следующий урок