Это третья часть серии мега-туториалов Flask, в которой я собираюсь рассказать вам, как работать с веб-формами.

Оглавление
  • Глава 1: Привет, мир!

  • Глава 2: Шаблоны

  • Глава 3: Веб-формы (Эта статья)

  • Глава 4: База данных

  • Глава 5: Логины пользователей

  • Глава 6: Страница профиля и аватары

  • Глава 7: Обработка ошибок

  • Глава 8: Подписчики

  • Глава 9: Разбивка на страницы

  • Глава 10: Поддержка по электронной почте

  • Глава 11: Подтяжка лица

  • Глава 12: Даты и время

  • Глава 13: I18n и L10n

  • Глава 14: Ajax

  • Глава 15: Улучшенная структура приложения

  • Глава 16: Полнотекстовый поиск

  • Глава 17: Развертывание в Linux

  • Глава 18: Развертывание на Heroku

  • Глава 19: Развертывание в контейнерах Docker

  • Глава 20: Немного магии JavaScript

  • Глава 21: Уведомления пользователей

  • Глава 22: Фоновые задания

  • Глава 23: Интерфейсы прикладного программирования (API)

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

Веб-формы являются одним из самых основных строительных блоков в любом веб-приложении. Я буду использовать формы, позволяющие пользователям отправлять сообщения в блог, а также для входа в приложение.

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

Ссылки на GitHub для этой главы: BrowseZipDiff.

Введение в Flask-WTF

Для работы с веб-формами в этом приложении я собираюсь использовать расширение Flask-WTF, представляющее собой тонкую оболочку вокруг пакета WTForms, которое прекрасно интегрирует его с Flask. Это первое расширение 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 для хранения переменных конфигурации. Чтобы все было хорошо организовано, я собираюсь создать класс конфигурации в отдельном модуле Python. Ниже вы можете увидеть новый класс Config для этого приложения, хранящийся в модуле config.py в каталоге верхнего уровня.

config.py: Настройка секретного ключа

import os

class Config:
  SECRET_KEY = os.environ.get('SECRET_KEY') or 'you-will-never-guess'

Довольно просто, не так ли? Параметры конфигурации определяются как переменные класса внутри класса Config. Поскольку приложению требуется больше элементов конфигурации, их можно добавить в этот класс, а позже, если я обнаружу, что мне нужно иметь более одного набора конфигурации, я смогу создать его подклассы. Но пока не беспокойтесь об этом.

Переменная конфигурации SECRET_KEY, которую я добавил в качестве единственного элемента конфигурации, является важной частью большинства приложений Flask. Flask и некоторые из его расширений используют значение секретного ключа в качестве криптографического ключа, полезного для генерации подписей или токенов. Расширение Flask-WTF использует его для защиты веб-форм от вредоносной атаки, называемой подделкой межсайтовых запросов или CSRF (произносится "seasurf"). Как следует из названия, секретный ключ должен быть секретным, поскольку сила токенов и подписей, генерируемых с его помощью, зависит от того, что никто, кроме доверенных сопровождающих приложения, не знает об этом.

Значение секретного ключа задается как выражение с двумя объектами, объединенное оператором or. Первый объект ищет значение переменной окружения, также называемой SECRET_KEY. Второй объект - это просто жестко запрограммированная строка. Вы увидите, что я часто повторяю этот шаблон для переменных конфигурации. Идея заключается в том, что предпочтительным является значение, полученное из переменной окружения, но если среда не определяет переменную, то по умолчанию используется жестко заданная строка. При разработке этого приложения требования к безопасности низкие, поэтому вы можете просто проигнорировать этот параметр и позволить использовать жестко закодированную строку. Но когда это приложение будет развернуто на производственном сервере, я установлю уникальное и трудно угадываемое значение в среде, чтобы у сервера был безопасный ключ, который больше никто не знает.

Теперь, когда у меня есть конфигурационный файл, мне нужно сказать Flask, чтобы он прочитал его и применил. Это можно сделать сразу после создания экземпляра приложения Flask с помощью метода app.config.from_object():

app/__init__.py: Настройка Flask

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 для представления веб-форм. Класс Form просто определяет поля формы как переменные класса.

Еще раз помня о разделении задач, я собираюсь использовать новый модуль app/forms.py для хранения моих классов веб-форм. Для начала давайте определим форму входа пользователя, которая просит пользователя ввести имя пользователя и пароль. Форма также будет включать флажок "запомнить меня" и кнопку отправки.:

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_<name> об именовании для объектов импорта верхнего уровня. В этом случае Flask-WTF содержит все свои объекты под flask_wtf. Здесь в верхней части FlaskForm импортируется базовый класс app/forms.py.

Четыре класса, представляющие типы полей, которые я использую для этой формы, импортируются непосредственно из пакета WTForms, поскольку расширение Flask-WTF не предоставляет настраиваемых версий. Для каждого поля создается объект как переменная класса в классе LoginForm. Каждому полю присваивается описание или метка в качестве первого аргумента.

Необязательный аргумент validators, который вы видите в некоторых полях, используется для привязки поведения проверки к полям. Средство проверки DataRequired просто проверяет, что поле не отправлено пустым. Доступно еще много валидаторов, некоторые из которых будут использоваться в других формах.

Шаблоны форм

Следующим шагом будет добавление формы в HTML-шаблон, чтобы ее можно было отобразить на веб-странице. Хорошей новостью является то, что поля, определенные в классе LoginForm, знают, как отображать себя в формате HTML, поэтому эта задача довольно проста. Ниже вы можете увидеть шаблон входа в систему, который я собираюсь сохранить в файле app/templates/login.html:

app/templates/login.html: Шаблон формы для входа в систему

{% extends "base.html" %}

{% block content %}
  <h1>Sign In</h1>
  <form action="" method="post" novalidate>
    {{ 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, с помощью инструкции наследования шаблона extends . На самом деле я сделаю это со всеми шаблонами, чтобы обеспечить согласованный макет, который включает верхнюю панель навигации на всех страницах приложения.

Этот шаблон ожидает, что объект формы, созданный из класса LoginForm, будет указан в качестве аргумента, который, как вы можете видеть, указан как form. Этот аргумент будет отправлен функцией просмотра входа в систему, которую я еще не написал.

Элемент HTML <form> используется в качестве контейнера для веб-формы. Атрибут формы action используется для указания браузеру URL-адреса, который следует использовать при отправке информации, введенной пользователем в форму. Когда для действия задана пустая строка, форма отправляется по URL, который в данный момент находится в адресной строке, то есть по URL, с помощью которого форма отображалась на странице. Атрибут method определяет метод HTTP-запроса, который следует использовать при отправке формы на сервер. По умолчанию он отправляется с запросом GET, но почти во всех случаях использование запроса POST улучшает взаимодействие с пользователем, поскольку запросы этого типа могут отправлять данные формы в теле запроса, в то время как запросы GET добавляют поля формы к URL-адресу, загромождая адресную строку браузера. Атрибут novalidate используется для указания веб-браузеру не применять проверку к полям в этой форме, что фактически оставляет эту задачу приложению Flask, запущенному на сервере. Использование novalidate совершенно необязательно, но для этой первой формы важно установить ее, поскольку это позволит вам протестировать проверку на стороне сервера позже в этой главе.

Параметр шаблона form.hidden_tag()  генерирует скрытое поле, включающее токен, который используется для защиты формы от CSRF-атак. Все, что вам нужно сделать, чтобы защитить форму, - это включить это скрытое поле и задать SECRET_KEY переменную, определенную в конфигурации Flask. Если вы позаботитесь об этих двух вещах, Flask-WTF сделает все остальное за вас.

Если вы в прошлом писали веб-формы HTML, вам, возможно, показалось странным отсутствие полей HTML в этом шаблоне. Это потому, что поля из объекта form знают, как отображать себя в формате HTML. Все, что мне нужно было сделать, это включить {{ form.<field_name>.label }} там, где я хотел, метку поля, и {{ form.<field_name>() }} там, где я хотел поле. Для полей, которым требуются дополнительные атрибуты HTML, они могут быть переданы в качестве аргументов. Поля имени пользователя и пароля в этом шаблоне принимают size в качестве аргумента, который будет добавлен к элементу HTML <input> в качестве атрибута. Таким же образом вы также можете прикреплять классы CSS или идентификаторы к полям формы.

Представления форм

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

Итак, давайте напишем новую функцию просмотра, сопоставленную с URL-адресом /login, которая создает форму и передает ее шаблону для рендеринга. Эта функция просмотра также может быть включена в модуль app/routes.py вместе с предыдущим:

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 может показаться странным, но он просто передает объект form, созданный в строке выше (и показанный справа), в шаблон с именем form (показанный слева). Это все, что требуется для отображения полей формы.

Чтобы упростить доступ к форме входа, базовый шаблон может расширить элемент <div> в base.html, чтобы включить ссылку на него на панели навигации:

app/templates/base.html: Ссылка для входа в систему на панели навигации

<div>
  Microblog:
  <a href="/index">Home</a>
  <a href="/login">Login</a>
</div>

На этом этапе вы можете запустить приложение и просмотреть форму в своем веб-браузере. Когда приложение запущено, введите http://localhost:5000/ в адресной строке браузера, а затем нажмите на ссылку "Войти" в верхней панели навигации, чтобы увидеть новую форму входа. Довольно круто, не так ли?

Получение данных формы

Если вы попытаетесь нажать кнопку отправки, браузер отобразит ошибку "Метод не разрешен". Это связано с тем, что функция просмотра входа в систему из предыдущего раздела пока выполняет половину работы. Он может отображать форму на веб-странице, но пока не имеет логики для обработки данных, отправляемых пользователем. Это еще одна область, в которой Flask-WTF действительно упрощает работу. Здесь представлена обновленная версия функции просмотра, которая принимает и проверяет данные, предоставленные пользователем:

app/routes.py: Получение учетных данных для входа

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 в декораторе route. Он сообщает Flask, что эта функция просмотра принимает запросы GET и POST, переопределяя значение по умолчанию, которое должно принимать только запросы GET. Протокол HTTP гласит, что запросы GET - это те, которые возвращают информацию клиенту (в данном случае веб-браузеру). Все запросы в приложении на данный момент относятся к этому типу. Запросы POST обычно используются, когда браузер отправляет данные формы на сервер (на самом деле запросы GET также могут использоваться для этой цели, но это не рекомендуемая практика). Ошибка "Метод не разрешен", которую браузер показывал вам ранее, появляется из-за того, что браузер пытался отправить запрос POST, а приложение не было настроено на его прием. Предоставляя аргумент methods, вы указываете Flask, какие методы запроса должны быть приняты.

Метод form.validate_on_submit() выполняет всю работу по обработке форм. Когда браузер отправляет запрос GET на получение веб-страницы с формой, этот метод будет возвращать False, поэтому в этом случае функция пропускает оператор if и переходит непосредственно к отображению шаблона в последней строке функции.

Когда браузер отправляет запрос POST в результате нажатия пользователем кнопки отправки, form.validate_on_submit() собирается собрать все данные, запустить все средства проверки, прикрепленные к полям, и, если все в порядке, он вернет True, указывая, что данные действительны и могут быть обработаны приложением. Но если хотя бы одно поле не проходит проверку, функция вернет False, и это приведет к тому, что форма будет возвращена пользователю, как в случае с запросом GET. Позже я собираюсь добавить сообщение об ошибке при сбое проверки.

При возврате form.validate_on_submit() значения True функция просмотра входа в систему вызывает две новые функции, импортированные из Flask. Функция flash() - полезный способ показать сообщение пользователю. Многие приложения используют этот метод, чтобы сообщить пользователю, было ли какое-то действие успешным или нет. В данном случае я собираюсь использовать этот механизм как временное решение, потому что у меня пока нет всей инфраструктуры, необходимой для реального входа пользователей. Лучшее, что я могу сделать на данный момент, это показать сообщение, подтверждающее, что приложение получило учетные данные.

Вторая новая функция, используемая в функции просмотра входа в систему, - это redirect(). Эта функция инструктирует веб-браузер клиента автоматически переходить на другую страницу, указываемую в качестве аргумента. Эта функция просмотра использует его для перенаправления пользователя на страницу индекса приложения.

Когда вы вызываете функцию flash(), Flask сохраняет сообщение, но отображаемые сообщения волшебным образом не появляются на веб-страницах. Шаблоны приложения должны отображать эти флэш-сообщения таким образом, чтобы они соответствовали макету сайта. Я собираюсь добавить эти сообщения в базовый шаблон, чтобы все шаблоны унаследовали эту функциональность. Это обновленный базовый шаблон:

app/templates/base.html: Отображаемые сообщения в базовом шаблоне

<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(). Следующее условие проверяет, имеет ли messages какое-либо содержимое, и в этом случае элемент <ul> отображается с каждым сообщением в виде элемента списка <li>. Этот стиль рендеринга не очень подходит для сообщений о состоянии, но тема стилизации веб-приложения будет рассмотрена позже.

Интересное свойство этих отображаемых сообщений заключается в том, что после однократного запроса через функцию get_flashed_messages они удаляются из списка сообщений, поэтому появляются только один раз после вызова функции flash().

Сейчас самое время еще раз попробовать приложение и проверить, как работает форма. Обязательно попробуйте отправить форму с пустыми полями имени пользователя или пароля, чтобы увидеть, как валидатор DataRequired останавливает процесс отправки.

Улучшение проверки полей

Валидаторы, прикрепленные к полям формы, предотвращают прием неверных данных в приложение. Способ, которым приложение обрабатывает неверный ввод формы, заключается в повторном отображении формы, чтобы позволить пользователю внести необходимые исправления.

Если вы пытались отправить неверные данные, я уверен, вы заметили, что, хотя механизмы проверки работают хорошо, пользователю не дается никаких указаний на то, что с формой что-то не так, пользователь просто получает форму обратно. Следующая задача - улучшить взаимодействие с пользователем, добавив значимое сообщение об ошибке рядом с каждым полем, не прошедшим проверку.

Фактически, средства проверки форм уже генерируют эти описательные сообщения об ошибках, поэтому не хватает только некоторой дополнительной логики в шаблоне для их отображения.

Вот шаблон входа в систему с добавленными сообщениями о проверке полей в полях имени пользователя и пароля:

app/templates/login.html: Ошибки проверки в шаблоне формы входа в систему

{% extends "base.html" %}
{% block content %}
  <h1>Sign In</h1>
  <form action="" method="post" novalidate>
    {{ 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 %}

Единственное изменение, которое я внес, - это добавление циклов for сразу после полей имени пользователя и пароля, которые отображают сообщения об ошибках, добавленные валидаторами, красным цветом. Как правило, ко всем полям, к которым прикреплены средства проверки, будут добавлены сообщения об ошибках, возникающие в результате проверки, в разделе form.<field_name>.errors. Это будет список, потому что к полям может быть подключено несколько средств проверки, и более одного могут выдавать сообщения об ошибках для отображения пользователю.

Если вы попытаетесь отправить форму с пустым именем пользователя или паролем, вы получите красивое сообщение об ошибке, выделенное красным цветом.

Создание ссылок

Форма входа в систему на данный момент довольно полная, но прежде чем закрыть эту главу, я хочу обсудить правильный способ включения ссылок в шаблоны и перенаправления. До сих пор вы видели несколько примеров, в которых определены ссылки. Например, это текущая панель навигации в базовом шаблоне:

    <div>
        Microblog:
        <a href="/index">Home</a>
        <a href="/login">Login</a>
    </div>

Функция просмотра входа в систему также определяет ссылку, которая передается в функцию 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() является имя конечной точки, которое является именем функции просмотра.

Вы можете спросить, почему лучше использовать имена функций вместо URL-адресов. Дело в том, что URL-адреса с гораздо большей вероятностью изменятся, чем имена функций просмотра, которые являются полностью внутренними. Вторая причина заключается в том, что, как вы узнаете позже, некоторые URL-адреса содержат динамические компоненты, поэтому для создания этих URL-адресов вручную потребуется объединение нескольких элементов, что утомительно и чревато ошибками. Функция url_for() также способна генерировать эти сложные URL-адреса с гораздо более элегантным синтаксисом.

Итак, с этого момента я собираюсь использовать url_for() каждый раз, когда мне нужно сгенерировать URL-адрес приложения. Панель навигации в базовом шаблоне становится:

app/templates/base.html: Используйте функцию url_for() для ссылок

        <div>
            Microblog:
            <a href="{{ url_for('index') }}">Home</a>
            <a href="{{ url_for('login') }}">Login</a>
        </div>

А вот и обновленная login() функция просмотра:

app/routes.py: Используйте функцию url_for() для ссылок

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'))
  # ...

Комментарии (2)


  1. cdscds
    09.04.2024 04:07

    По тексту:
    "Это можно сделать сразу после создания экземпляра приложения Flask с помощью метода app.config.from_object():

    app/init.py: Настройка Flask" - неверное название файла.


    1. Alex_Mer5er Автор
      09.04.2024 04:07

      Спасибо за внимательность, исправил.