Формы — важный элемент любого веб-приложения, но, к сожалению, работать с ними достаточно сложно. Сначала нужно подтвердить данные на стороне клиента, затем — на сервере. И даже этого недостаточно, если разработчик приложения озабочен такими проблемами безопасности как 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/
. Откроется такая форма.
Запрос к странице был сделан с помощью метода GET, поэтому код внутри блока if
функции login()
пропущен.
Если попробовать отправить форму без ввода данных, страница будет выглядеть следующим образом:
В этот раз страница была отправлена методом POST, поэтому код внутри if оказался исполнен. Внутри этого блока приложение принимает имя пользователя и пароль и устанавливает сообщение для message
. Поскольку форма оказалась пустой, отобразилось сообщение об ошибке.
Если заполнить форму с корректными именем пользователям и паролем и нажать Enter, появится приветственное сообщение “Correct username and password”
:
Таким образом можно работать с формами во 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
{}
>>>
Проверка формы в этот раз прошла успешно.
Следующий шаг — рендеринг формы.
Рендеринг формы
Существует два варианта рендеринга:
- Один за одним.
- С помощью цикла
Рендеринг полей один за одним
Поскольку в шаблонах есть доступ к экземпляру формы, можно использовать имена полей, чтобы отрендерить имена, метки и ошибки:
{# выводим название поля #}
{{ 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/
. Появится следующая контактная форма:
Если попробовать нажать Submit, не вводя данных, появятся следующие сообщения об ошибках валидации:
Теперь можно ввести определенные данные в поля Name и Message и некорректные данные в поле Email, и попробовать отправить форму снова.
Нужно обратить внимание, что все поля содержат данные из прошлого запроса.
Теперь можно ввести корректный 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_categories
– True
, get_flashed_messages()
вернет список кортежей формы (category, message)
.
После этих изменений следует открыть https://localhost:5000/contact
снова. Заполнить форму и нажать Submit
. Сообщение об успешной отправке отобразится в верхней части формы.