#11 Работа с формами во Flask

Формы — важный элемент любого веб-приложения, но, к сожалению, работать с ними достаточно сложно. Сначала нужно подтвердить данные на стороне клиента, затем — на сервере. И даже этого недостаточно, если разработчик приложения озабочен такими проблемами безопасности как 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(). По умолчанию обработчик запросов вызывается только в тех случаях, когда метод request.method — GET или HEAD. Это можно изменить, передав список разрешенных HTTP-методов аргументу-ключевому слову methods. С этого момента функция представления login будет вызываться только тогда, когда запрос к /login/ будет сделан с помощью методов GET, POST или HEAD. Если попробовать получить доступ к URL /login/ другим методом, появится ошибка HTTP 405 Method Not Allowed.

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

Дальше нужно запустить сервер и зайти на https://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 во Flask. Оно также предлагает дополнительные функции, такие как загрузка файлов, 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 и sumbit. Эти переменные будут использоваться, чтобы отрендерить поля формы, а также назначать и получать информацию из них. Эта форма создана с помощью двух StringField, TextAreaField и SumbitField. Каждый раз когда создается объект поля, определенные аргументы передаются его функции-конструктору. Первый аргумент — строка, содержащая метку, которая будет отображаться внутри тега <label> в тот момент, когда поле отрендерится. Второй опциональный аргумент — список валидаторов (элементов системы проверки), которые передаются конструктору в виде аргументов-ключевых слов. Валидаторы — это функции или классы, которые определяют, корректна ли введенная в поле информация. Для каждого поля можно использовать несколько валидаторов, разделив их запятыми (,). Модуль wtforms.validators предлагает базовые валидаторы, но их можно создавать самостоятельно. В этой форме используются два встроенных валидатора: DataRequired и Email.

DataRequired: он проверяет, ввел ли пользователь хоть какую-информацию в поле.

Email: проверяет, является ли введенный электронный адрес действующим.

Введенные данные не будут приняты до тех пор, пока валидатор не подтвердит соответствие данных.

Примечание: это лишь основа полей форм и валидаторов. Полный список доступен по ссылке https://wtforms.readthedocs.io.

Установка SECRET_KEY

По умолчанию Flask-WTF предотвращает любые варианты CSFR-атак. Это делается с помощью встраивания специального токена в скрытый элемент <input> внутри формы. Затем этот токен используется для проверки подлинности запроса. До того как Flask-WTF сможет сгенерировать csrf-токен, необходимо добавить секретный ключ. Установить его в файле main2.py необходимо следующим образом:

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

manager = Manager(app)
#...

Здесь используется атрибут config объекта Flask. Атрибут config работает как словарь и используется для размещения параметров настройки Flask и расширений Flask, но их можно добавлять и самостоятельно.

Секретный ключ должен быть строкой — такой, которую сложно разгадать и, желательно, длинной. SECRET_KEY используется не только для создания CSFR-токенов. Он применяется и в других расширениях 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', 'jerry@mail.com')]))
>>>

Стоит обратить внимание, что данные передаются в виде объекта MultiDict, потому что функция-конструктор класса wtforms.Form принимает аргумент типа MutiDict. Если данные формы не определены при создании экземпляра объекта формы, а форма отправлена с помощью запроса 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, вывод также содержит сообщение об ошибке о недостающем csfr-токене. Это из-за того что в данных формы нет запроса POST с csfr-токеном.

Отключить CSFR-защиту можно, передав csfr_enabled=False при создании экземпляра класса формы. Пример:

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

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

>>>
>>> form4 = ContactForm(MultiDict([('name', 'jerry'), ('email', 'jerry@mail.com'), ('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, чтобы получить доступ к ошибкам валидации, относящимся к форме. forms.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', 'spike@mail.com')]))
>>>
>>> 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="spike@mail.com">'
>>>
>>>
>>> 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'
>>>
>>>

Стоит обратить внимание, что ошибки csfr-токена нет, потому что запрос был отправлен без токена. Отрендерить поле csfr можно как и любое другое поле:

>>>
>>>  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="spike@mail.com">
	
    </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>, чтобы обернуть поля формы.

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

Вначале нужно создать шаблон 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)
#...

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

Почему используется validate_on_sumbit(), а не validate(), как это было в консоли?

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

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

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

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

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

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

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

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

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

Если попробовать нажать Submit, не вводя данных, появятся следующие сообщения об ошибках валидации:

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

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

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

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

Spike
spike@gmail.com
A Message

Data received. Now redirecting  ...

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


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

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

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

Для отображения сообщения используется функция flash() из пакета flask. Функция flash() принимает два аргумента: сообщение и категория (опционально). Категория указывает на тип сообщения: _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_categoriesTrue, get_flashed_messages() вернет список кортежей формы (category, message).

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

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