Мега-Учебник Flask, Часть 3: Веб-формы (издание 2018)
- четверг, 11 января 2018 г. в 03:14:21
Эта статья является переводом третьей части нового издания учебника Мигеля Гринберга. Прежний перевод давно утратил свою актуальность.
В этом третьем выпуске серии Мега-Учебник Flask я расскажу о том, как работать с формами.
Для справки ниже приведен список статей этой серии.
Примечание 1: Если вы ищете старые версии данного курса, это здесь.
Примечание 2: Если вдруг Вы хотели бы выступить в поддержку моей(Мигеля) работы в этом блоге, или просто не имеете терпения дожидаться неделю статьи, я (Мигель Гринберг)предлагаю полную версию данного руководства упакованную электронную книгу или видео. Для получения более подробной информации посетите learn.miguelgrinberg.com.
В главе 2 я создал простой шаблон для домашней страницы приложения и использовал ложные сущности в качестве заполнителей для объектов, которых у меня еще нет, таких как пользователи или сообщения в блогах. В этой главе я расскажу об одном из многих явлений, которые у меня есть в этом приложении, в частности, как принимать входные данные от пользователей через веб-формы.
Веб-формы являются одним из самых основных строительных блоков в любом веб-приложении. Я буду использовать формы, чтобы пользователи могли отправлять сообщения в блоге, а также для входа в приложение.
Прежде чем продолжить читать эту главу, убедитесь, что у вас есть приложение microblog
. Работа проделанная в предыдущей главе должна позволить запустить его без каких-либо ошибок.
Ссылки GitHub для этой главы: Browse, Zip, Diff.
Чтобы обрабатывать веб-формы в этом приложении, я собираюсь использовать расширение Flask-WTF, которое представляет собой обертку пакета WTForms и прекрасно интегрирует его с Flask. Это первое расширение Flask, которое я вам представляю, но не последнее. Расширения являются очень важной частью экосистемы Flask, поскольку они обеспечивают решения проблем.
Расширения Flask — это обычные пакеты Python, которые устанавливаются вместе с pip
. Надо установить Flask-WTF в свою виртуальную среду:
(venv) $ pip install flask-wtf
Пока что приложение очень простое, и по этой причине мне не нужно было беспокоиться о его конфигурации. Но для любых приложений, кроме простейших, вы обнаружите, что Flask (и, возможно, также используемые расширения Flask) предлагают некоторую свободу в том, как делать что-то, и вам нужно принять некоторые решения, которые вы передаете в качестве списка переменных конфигурации.
Для указания параметров конфигурации существует несколько форматов. Самое основное решение — определить ваши переменные как ключи в app.config, который использует стиль словаря для работы с переменными. Например, вы можете сделать что-то вроде этого:
app = Flask(__name__)
app.config['SECRET_KEY'] = 'you-will-never-guess'
# ... add more variables here as needed
Хотя вышеприведенного синтаксиса достаточно для создания параметров конфигурации для Flask, мне нравится применять принцип разделения проблем, поэтому вместо того, чтобы ставить мою конфигурацию в том же месте, где я создаю свое приложение, я буду использовать несколько более сложную структуру, которая позволяет мне сохранить мою конфигурацию в отдельном файле.
Формат, который мне очень нравится, поскольку он расширяемый, заключается в использовании класса для хранения переменных конфигурации. Чтобы все было хорошо организовано, я собираюсь создать класс конфигурации в отдельном модуле Python. Ниже вы можете увидеть новый класс конфигурации для этого приложения, хранящийся в модуле config.py в каталоге верхнего уровня.
import os
class Config(object):
SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'
Довольно просто, не так ли? Параметры конфигурации определяются как переменные класса внутри класса Config
. Поскольку для приложения требуется больше элементов конфигурации, они могут быть добавлены в этот класс, а позже, если я обнаружу, что мне нужно иметь более одного набора конфигурации, я могу создать его подклассы. Но пока об этом не беспокойтесь.
Переменная конфигурации SECRET_KEY
, которую я добавил как единственный элемент конфигурации, является важной частью большинства приложений Flask. Flask и некоторые его расширения используют значение секретного ключа в качестве криптографического ключа, полезного для генерации подписей или токенов. Расширение Flask-WTF использует его для защиты веб-форм от противной атаки под названием Cross-Site Request Forgery или CSRF (произносится как «seasurf»). Как следует из его названия, секретный ключ должен быть секретным, поскольку сила токенов и подписей, генерируемых им, зависит от того, что никто за пределами круга доверенных лиц сопровождающих приложения не знает об этом.
Значение секретного ключа задается как выражение с двумя терминами, к которым присоединяется оператор OR
. Первый термин ищет значение переменной среды, также называемой SECRET_KEY. Второй термин, это просто жестко закодированная строка. Это шаблон, который вы увидите, что я часто повторяю для конфигурационных переменных. Идея в том, что значение, появляющееся из переменной среды, предпочтительнее, но если среда не определяет переменную, то вместо нее используется жестко закодированная строка. При разработке этого приложения требования к безопасности невелики, поэтому можно просто игнорировать этот параметр и позволить использовать жестко закодированную строку. Но когда это приложение развертывается на рабочем сервере, я буду устанавливать уникальное и трудно угадываемое значение, так что сервер будет иметь безопасный ключ, который никто не знает.
Теперь, когда у меня есть файл конфигурации, мне нужно, чтобы Flask прочитал его и применил. Это можно сделать сразу после создания экземпляра приложения Flask с использованием метода app.config.from_object()
(app\__init__.py
):
from flask import Flask
from config import Config
app = Flask(__name__)
app.config.from_object(Config)
from app import routes
То, как я импортирую класс Config, может показаться запутанным сначала, но если вы посмотрите, как класс Flask (в верхнем регистре «F») импортируется из flask пакета (нижний регистр «f»), вы заметите, что я делая то же самое с конфигурацией. Строковый «config» — это имя модуля python config.py, и, очевидно, тот, который имеет верхний регистр «C», является фактическим классом.
Как я упоминал выше, элементы конфигурации могут быть доступны со словарным синтаксисом из app.config. Здесь вы можете увидеть быстрый сеанс с интерпретатором Python, где я проверяю, каково значение секретного ключа:
>>> from microblog import app
>>> app.config['SECRET_KEY']
'you-will-never-guess'
Расширение Flask-WTF использует классы Python для представления веб-форм. Класс формы просто определяет поля формы как переменные класса.
Еще раз имея в виду разделение проблем, я собираюсь использовать новый app/forms.py модуль для хранения классов веб-форм. Для начала определим форму входа пользователя, в которой пользователю будет предложено ввести имя пользователя и пароль. Форма также будет включать флажок "Запомнить меня" и кнопку Отправить:
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, SubmitField
from wtforms.validators import DataRequired
class LoginForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
password = PasswordField('Password', validators=[DataRequired()])
remember_me = BooleanField('Remember Me')
submit = SubmitField('Sign In')
Большинство расширений Flask используют соглашение об именах flask_ для импорта верхнего уровня. В этом случае Flask-WTF меняет все свои символы под flask_wtf. Здесь базовый класс FlaskForm импортируется из верхней части app/forms.py
.
Четыре класса, которые представляют типы полей, которые я использую для этой формы, импортируются непосредственно из пакета WTForms, поскольку расширение Flask-WTF не предоставляет настраиваемые версии. Для каждого поля объект создается как переменная класса в классе LoginForm
. Каждому полю присваивается описание или метка в качестве первого аргумента.
Дополнительный аргумент validators
, который вы видите в некоторых полях, используется для привязки поведения проверки к полям. Валидатор DataRequired просто проверяет, что поле не отправлено пустым. Существует еще много доступных валидаторов, некоторые из которых будут использоваться в других формах.
Следующим шагом является добавление формы в шаблон HTML, чтобы ее можно было визуализировать на веб-странице. Поля, определенные в классе LoginForm, знают, как визуализировать себя как HTML, поэтому эта задача довольно проста. Ниже вы можете увидеть шаблон входа в систему, который я собираюсь хранить в файле app/templates/Login.html
:
{% extends "base.html" %}
{% block content %}
<h1>Sign In</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
Для этого шаблона я повторно использую еще один раз base.html
, как показано в главе 2, через инструкцию расширенного наследования шаблона. На самом деле я сделаю это со всеми шаблонами, чтобы обеспечить единообразную компоновку, которая включает верхнюю панель навигации по всем страницам приложения.
В этом шаблоне предполагается, что объект формы, созданный из класса LoginForm
, будет предоставлен в качестве аргумента, который можно увидеть как ссылку form
. Этот аргумент будет отправлен функцией просмотра входа, которую я до сих пор не написал.
Элемент HTML
<form>
используется в качестве контейнера для веб-формы. Атрибут action
формы используется для того, чтобы сообщить обозревателю URL-адрес, который должен использоваться при отправке информации, введенной пользователем в форму. Если для действия задана пустая строка, форма передается URL-адресу, находящемуся в данный момент в адресной строке, то есть URL-адресу, который визуализирует форму на странице. Атрибут method
указывает метод HTTP-запроса, который должен использоваться при отправке формы на сервер. По умолчанию он отправляется с запросом GET
, но почти во всех случаях использование POST
запрос, улучшающий взаимодействие с пользователем, поскольку запросы этого типа могут отправлять данные формы в тело запроса, в то время как запросы GET
добавляют поля формы к URL-адресу, загромождая адресную строку обозревателя.
Аргумент шаблона form.hidden_tag()
создает скрытое поле, включающее токен, который используется для защиты формы от CSRF атак. Все, что нужно сделать для того, чтобы защищенная форма включала это скрытое поле и иметь переменную SECRET_KEY
, определенную в конфигурации Flask. Если вы заботитесь о этих двух вещах, Flask-WTF делает остальное за вас.
Если вы уже писали HTML Web Forms в прошлом, возможно, вы сочли странным, что в этом шаблоне нет HTML-полей. Это происходит потому, что поля из объекта Form знают, как визуализировать себя как HTML. Все, что мне нужно было сделать, это включить {{ form.<field_name>.label }}
в месте где нужна метка поля и {{ form.<field_name>() }}
где нужно само поле. Для полей, которым требуются дополнительные атрибуты HTML, они могут быть переданы в качестве аргументов. Поля username и Password в этом шаблоне принимают size
размер в качестве аргумента, который будет добавлен в HTML-элемент <input>
в качестве атрибута. Таким образом можно также присоединять классы CSS или идентификаторы к полям формы.
Заключительный шаг перед тем, как вы увидите эту форму в браузере, — это код новой функции просмотра в приложении, которая отображает шаблон из предыдущего раздела.
Итак, давайте напишем новую функцию представления, сопоставленную с URL-адресом /login
, которая создаст форму, и передаст её в шаблон для рендеринга. Эта функция просмотра может находиться в модуле app/routes.py
дополняя содержимое:
from flask import render_template
from app import app
from app.forms import LoginForm
# ...
@app.route('/login')
def login():
form = LoginForm()
return render_template('login.html', title='Sign In', form=form)
Здесь я импортировал класс LoginForm
из forms.py, создал экземпляр объекта из него и отправлял его в шаблон. Синтаксис form = form
может выглядеть странно, но просто передает объект формы, созданный в строке выше (и показан справа), в шаблон с формой имени (показан слева). Это все, что требуется для отображения полей формы.
Чтобы упростить доступ к форме входа, базовый шаблон должен включать в себя ссылку на панель навигации:
<div>
Microblog:
<a href="/index">Home</a>
<a href="/login">Login</a>
</div>
На этом этапе можно запустить приложение и посмотреть веб-браузере что получилось. При запуске приложения введите http://localhost:5000/
в адресной строке браузера, а затем нажмите ссылку «Войти» (Sign In) в верхней панели навигации, чтобы увидеть новую форму входа. Довольно круто, не так ли?
Если вы попытаетесь нажать кнопку отправки (Sign In), браузер отобразит ошибку "Method Not Allowed" (Метод не разрешен). Это связано с тем, что функция входа в систему из предыдущего раздела выполняет половину задания. Может отображать форму на веб-странице, но у нее нет никакой логики для обработки данных, представленных пользователем. Это еще одна область, где Flask-WTF облегчает работу. Ниже приведена обновленная версия функции просмотра, которая принимает и проверяет данные, представленные пользователем:
from flask import render_template, flash, redirect
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
flash('Login requested for user {}, remember_me={}'.format(
form.username.data, form.remember_me.data))
return redirect('/index')
return render_template('login.html', title='Sign In', form=form)
Первой новинкой в этой версии является аргумент methods
в дизайнере маршрутов. Это сообщения для Flask, что эта функция просмотра принимает запросы GET
и POST
, переопределяя значение по умолчанию, которое должно принимать только запросы GET
. Протокол HTTP указывает, что GET
-запросы — это те, которые возвращают информацию клиенту (в этом случае веб-браузер). Все запросы в приложении до сих пор относятся к этому типу. Запросы POST
обычно используются, когда браузер передает данные формы на сервер (на самом деле запросы GET
также могут использоваться для этой цели, но это не рекомендуется). Ошибка «Method Not Allowed», которую браузер показал вам раньше, появляется, потому что браузер попытался отправить запрос POST
, и приложение не настроено на его принятие. Предоставляя аргумент methods
, вы сообщаете Flask о необходимости принимать методы запроса.
Метод form.validate_on_submit()
выполняет всю обработку формы. Когда браузер отправляет запрос GET
для получения веб-страницы с формой, этот метод возвращает False
, поэтому в этом случае функция пропускает оператор if и переходит к отображению шаблона в последней строке функции.
Когда браузер отправляет запрос POST
в результате нажатия пользователем кнопки submit, form.validate_on_submit()
собирает все данные, запускает все валидаторы, прикрепленные к полям, и если все в порядке, вернет True
, сообща что данные действительны и могут быть обработаны приложением. Но если хотя бы одно поле не подтвердит проверку, функция вернет False
, и это приведет к тому, что форма будет возвращена пользователю, например, в случае запроса GET
. Позже я собираюсь добавить сообщение об ошибке, когда проверка не удалась.
Когда form.validate_on_submit()
возвращает значение True
, функция входа в систему вызывает две новые функции, импортированные из Flask. Функция flash()
— полезный способ показать сообщение пользователю. Многие приложения используют эту технику, чтобы сообщить пользователю, было ли какое-либо действие успешным или нет. В этом случае я буду использовать этот механизм как временное решение, потому что у меня нет всей инфраструктуры, необходимой для регистрации пользователей в реальности. Лучшее, что я могу сделать сейчас, это показать сообщение, подтверждающее, что приложение получило учетные данные.
Вторая новая функция, используемая в функции входа в систему, — redirect()
. Эта функция указывает веб-браузеру клиента автоматически перейти на другую страницу, указанную в качестве аргумента. Эта функция просмотра использует ее для перенаправления пользователя на /index
страницу приложения.
Когда вы вызываете функцию flash()
, Flask сохраняет сообщение, но на веб-страницах не будут появляться магические сообщения. Шаблоны приложения должны отображать эти свернутые сообщения таким образом, который работает для макета сайта. Я собираюсь добавить эти сообщения в базовый шаблон, чтобы все шаблоны наследовали эту функциональность. Это обновленный базовый шаблон:
<html>
<head>
{% if title %}
<title>{{ title }} - microblog</title>
{% else %}
<title>microblog</title>
{% endif %}
</head>
<body>
<div>
Microblog:
<a href="/index">Home</a>
<a href="/login">Login</a>
</div>
<hr>
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul>
{% for message in messages %}
<li>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</body>
</html>
Здесь я использую конструкцию with
, чтобы назначить результат вызова get_flashed_messages()
переменной messages
, все в контексте шаблона. Функция get_flashed_messages()
поступает из Flask и возвращает список всех сообщений, которые были зарегистрированы в flash()
ранее. Условие, которое следует, проверяет, имеет ли сообщение некоторый контент, и в этом случае элемент <ul>
отображается с каждым сообщением в виде элемента списка <li>
. Этот стиль рендеринга выглядит не очень хорошо, но тема стилизации веб-приложения появится позже.
Интересным свойством этих flash-сообщений является то, что после их запроса один раз через функцию get_flashed_messages
они удаляются из списка сообщений, поэтому они появляются только один раз после вызова функции flash()
.
Время, чтобы попробовать приложение еще раз и проверить, как работает форма. Убедитесь, что вы пытаетесь отправить форму с полями имени пользователя или пароля пустым, чтобы увидеть, как валидатор DataRequired
останавливает процесс отправки.
Валидаторы, прикрепленные к полям формы, предотвращают передачу недопустимых данных в приложение. Способ, которым приложение отвечает на недопустимый ввод в поля формы, заключается в повторном отображении формы, чтобы позволить пользователю внести необходимые исправления.
Если вы пытались ввести недопустимые данные, я уверен, что вы заметили, что, хотя механизмы проверки работают хорошо, нет сообщений пользователю, что что-то не так с формой, пользователь просто получает форму обратно. Следующая задача состоит в том, чтобы улучшить взаимодействие с пользователем, добавив значимое сообщение об ошибке рядом с каждым полем, которое не удалось проверить.
Фактически, валидаторы форм уже создают эти описательные сообщения об ошибках, поэтому все, что отсутствует, — это некоторая дополнительная логика в шаблоне для их отображения.
Вот шаблон входа с добавленными сообщениями (Это поле обязательно.) проверки имени пользователя и пароля:
{% extends "base.html" %}
{% block content %}
<h1>Sign In</h1>
<form action="" method="post">
{{ form.hidden_tag() }}
<p>
{{ form.username.label }}<br>
{{ form.username(size=32) }}<br>
{% for error in form.username.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span style="color: red;">[{{ error }}]</span>
{% endfor %}
</p>
<p>{{ form.remember_me() }} {{ form.remember_me.label }}</p>
<p>{{ form.submit() }}</p>
</form>
{% endblock %}
Единственное изменение, которое я сделал, это добавил циклы сразу после поля username
и password
, которые отображают сообщения об ошибках, добавленные валидаторами в красном цвете. Как правило, все поля, имеющие прикрепленные проверяющие элементы, будут иметь сообщения об ошибках, добавляемые в form.<field_name>.errors
. Это будет список, потому что поля могут иметь несколько прилагающихся валидаторов и предоставлений сообщений об ошибках может быть более одного для отображения пользователю.
Если вы попытаетесь отправить форму с пустым именем пользователя или паролем, вы получите красное сообщение об ошибке.
Форма входа в систему теперь почти закончена, но перед закрытием этой главы я хотел бы обсудить правильный способ включения связей в шаблоны и перенаправления. До сих пор вы видели несколько примеров, в которых были определены связи. Например, это текущая панель навигации в базовом шаблоне:
<div>
Microblog:
<a href="/index">Home</a>
<a href="/login">Login</a>
</div>
Функция просмотра login
также определяет ссылку, которая передается функции redirect()
:
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# ...
return redirect('/index')
# ...
Одна из проблем с написанием ссылок непосредственно в шаблонах и исходных файлах заключается в том, что если в один прекрасный день вы решите реорганизовать свои ссылки, вам придется искать и заменять эти ссылки во всем своем приложении.
Чтобы лучше контролировать эти ссылки, Flask предоставляет функцию url_for()
, которая генерирует URL-адреса, используя внутреннее отображение URL-адресов для просмотра функций. Например, url_for('login')
возвращает /login
, а url_for('index')
возвращает '/index
. Аргументом для url_for()
является имя конечной точки, которое является именем функции view.
Вы можете спросить, почему лучше использовать имена функций вместо URL-адресов. Дело в том, что URL-адреса гораздо чаще меняются, чем имена функций, которые являются внутренними. Вторая причина заключается в том, что, как вы узнаете позже, некоторые URL-адреса имеют динамические компоненты, поэтому для генерации этих URL вручную потребуется объединение нескольких элементов, что является утомительным и подверженным ошибкам. url_for()
также может генерировать эти сложные URL-адреса.
Поэтому с этого момента я буду использовать url_for()
каждый раз, когда мне нужно создать URL приложения. В итоге панель навигации в базовом шаблоне становится такой:
<div>
Microblog:
<a href="{{ url_for('index') }}">Home</a>
<a href="{{ url_for('login') }}">Login</a>
</div>
И вот обновленная функция login()
:
from flask import render_template, flash, redirect, url_for
# ...
@app.route('/login', methods=['GET', 'POST'])
def login():
form = LoginForm()
if form.validate_on_submit():
# ...
return redirect(url_for('index'))
# ...