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

8401

Формы — важный элемент любого веб-приложения, но, к сожалению, работать с ними достаточно сложно. Сначала нужно подтвердить данные на стороне клиента, затем — на сервере. И даже этого недостаточно, если разработчик приложения озабочен такими проблемами безопасности как 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

Тест на знание python

Какой будет результат выполнения этого кода?
Что выведет этот код?
Какой будет результат выполнения этого кода?
Какой будет результат выполнения кода — print(abc) ?
Какой будет результат выполнения этого кода?