Python flask учебник: Лучшие книги по Flask для Python-разработчиков
Лучшие книги по Flask для Python-разработчиков
Flask выделяется на фоне других фреймворков, поскольку полностью отдает контроль над разработкой в руки программиста. Бывает, что разработчику нужно использовать базу данных или метод аутентификации пользователей, отличные от предложенных документацией фреймворка. В таких случаях приходится изрядно повозиться, но только не когда вы работаете со Flask.
Flask отличается большой гибкостью,
поскольку изначально проектировался
с расчетом на расширения. Фреймворк
поставляется в виде надежной основы с
базовым функционалом, необходимым в
любом приложении. Ну а дальше разработчик
волен дополнять фреймворк в соответствии
со своими нуждами (благо, в экосистеме
Flask достаточно интересных расширений).
В общем, Flask — отличный инструмент, и любому разработчику, создающему веб-приложения на Python, определенно стоит его изучить. Чтобы помочь вам сориентироваться в учебных пособиях, мы собрали список лучших книг по Flask.
Книги из этого списка подойдут для
Python-разработчиков начиная со среднего
уровня владения языком. Знание самого
Flask чаще всего не требуется, но в концепциях
Python (модули, пакеты, функции, декораторы)
и объектно-ориентированном программировании
читатель должен хорошо разбираться.
Также следует учесть, что при работе
над примерами вы будете многое делать
в командной строке, так что ею тоже нужно
уметь пользоваться. И, поскольку речь
идет о веб-разработке, вам точно пригодится
знание HTML, CSS и JavaScript хотя бы на базовом
уровне.
1. Flask Web Development
Автор: Miguel Grinberg. Год издания: 2018. Язык:
английский, русский.
Автор этой книги, Мигель Гринберг, описал собственный процесс разработки веб-приложений при помощи Flask.
В большинстве книг, посвященных
разработке программ, авторы в качестве
примеров приводят маленькие кусочки
кода, демонстрирующие различные свойства
описываемых технологий. При этом они
практически никогда не приводят
«связующий» код, благодаря которому
все отдельные функции собираются в
цельное приложение. Эту часть работы
авторы скромно оставляют читателям.
Мигель Гринберг подошел к делу иначе.
Все его примеры кода — части единого
приложения. В самом начале вы создаете
его в базовой форме и с каждой новой
главой расширяете. В итоге читатель
получает собственное приложение —
социальную сеть для блогов.
Нажав на кнопку, можно скачать второе издание книги. На русский язык переведено первое (2014 года), его можно скачать здесь.
2. Flask: веб-разработка капля
за каплей
Год издания: 2016. Язык: русский.
Документация Flask, переведенная на
русский. Собственно, здесь сказать
больше нечего. Как известно, когда ничто
другое не помогает, — читай документацию.
3. Flask By Example
Автор: Gareth Dwyer. Год издания: 2016. Язык:
английский.
Это практическое руководство по созданию полнофункциональных веб-приложений при помощи Flask. Автор проведет вас через создание трех разных проектов.
Для начала вы создадите приложение,
которое будет отображать заголовки
последних новостей, а также курс валют
и сведения о погоде.
Второй проект — создание приложения
с картой преступлений (бэкенд — база
данных MySQL). С его помощью пользователи
смогут вносить в базу информацию о
местах совершения преступлений, чтобы
в конечном итоге получить карту опасных
зон.
В последнем проекте вы будете
использовать Flask с более современными
технологиями, такими как Bootstrap и MongoDB.
Вы создадите приложение для вызова
официанта в ресторане.
Все эти проекты помогут вам хорошо освоить Flask, а также отточить навыки работы с базами данных, HTML, CSS и JavaScript. Вы будете работать со сторонними API, а также изучите вопросы безопасности приложений и научитесь защищаться от распространенных атак, таких как SQL injection и XSS.
4. Building Web Applications with Flask
Автор: Italo Maia. Год издания: 2015. Язык:
английский.
Данное учебное пособие предназначено для веб-разработчиков, пишущих на Python и желающих поближе познакомиться с разработкой приложений на Flask.
Читая эту книгу, вы научитесь:
- создавать одностраничные приложения
при помощи Flask; - использовать макросы, фильтры, теги
и контролирующие структуры для рендеринга
ответов на пользовательские запросы; - безопасно работать с формами;
- использовать базы данных NoSQL и SQL;
- генерировать простые, но мощные
REST-сервисы из ваших моделей данных; - использовать компоненты Flask для
создания поддерживаемых проектов; - пользоваться всей мощью расширений
для создания надежных правил авторизации
и системы разрешений.
5. Flask Framework Cookbook
Автор: Shalabh Aggarwal. Год издания: 2014. Язык:
английский.
Это второе, обновленное издание книги. В нем рассматривается Python 3 и последняя версия Flask (последняя на 2014 год, — прим. ред.), а также убрано упоминание устаревших библиотек. И, конечно, в новой редакции вы найдете новые рецепты применения технологий.
Читая эту книгу, вы откроете для себя
различные способы использования Flask
для создания и развертывания микросервисов,
а также для управления ими.
Книга начинается с обзора конфигураций,
которые могут быть использованы в
приложении на Flask. Затем автор представляет
читателю работу с шаблонами, ORM и слоями
представлений. Также вы научитесь
создавать интерфейсы и управлять ими,
затронете вопросы отладки и логирования
ошибок. Наконец, вы научитесь разным
техникам развертывания для таких
платформ как Apache, Tornado и Heroku.
К концу книги вы получите все знания,
необходимые для уверенного создания и
масштабирования приложений на Flask.
6. Learning Flask Framework
Авторы: Matt Copperwaite, Charles Leifer. Год издания:
2015. Язык: английский.
Эта книга предназначена для
Python-разработчиков, которые хотят
научиться создавать что-то, используемое
в вебе. Фреймворк Flask следует принципам
Python, а потому будет понятен любому, кто
владеет этим языком (впрочем, в нем
смогут разобраться даже те, кто не знаком
с Python).
Из этой книги вы узнаете, как:
- создавать веб-страницы и делать
ваше веб-приложение модульным и гибким,
используя шаблоны; - хранить и получать реляционные
данные, используя SQLAlchemy; - разрабатывать схему миграций при
помощи Alembic; - создавать RESTful API, используя
Flask-Restless; - имитировать запросы и сессии,
используя тестовый клиент Flask; - делать Ajax-запросы из шаблонов
Jinja2.
Веб-формы (издание 2018) / Хабр
blog.miguelgrinberg.com
Miguel Grinberg
<<< предыдущая следующая >>>
Эта статья является переводом третьей части нового издания учебника Мигеля Гринберга. Прежний перевод давно утратил свою актуальность.
В этом третьем выпуске серии Мега-Учебник Flask я расскажу о том, как работать с формами.
Для справки ниже приведен список статей этой серии.
Примечание 1: Если вы ищете старые версии данного курса, это здесь.
Примечание 2: Если вдруг Вы хотели бы выступить в поддержку моей(Мигеля) работы в этом блоге, или просто не имеете терпения дожидаться неделю статьи, я (Мигель Гринберг)предлагаю полную версию данного руководства упакованную электронную книгу или видео. Для получения более подробной информации посетите learn.miguelgrinberg.com.
Краткое повторение
В главе 2 я создал простой шаблон для домашней страницы приложения и использовал ложные сущности в качестве заполнителей для объектов, которых у меня еще нет, таких как пользователи или сообщения в блогах. В этой главе я расскажу об одном из многих явлений, которые у меня есть в этом приложении, в частности, как принимать входные данные от пользователей через веб-формы.
Веб-формы являются одним из самых основных строительных блоков в любом веб-приложении. Я буду использовать формы, чтобы пользователи могли отправлять сообщения в блоге, а также для входа в приложение.
Прежде чем продолжить читать эту главу, убедитесь, что у вас есть приложение microblog
. Работа проделанная в предыдущей главе должна позволить запустить его без каких-либо ошибок.
Ссылки GitHub для этой главы: Browse, Zip, Diff.
Введение в Flask-WTF
Чтобы обрабатывать веб-формы в этом приложении, я собираюсь использовать расширение 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
:
app/templates/login.html
: Шаблон формы входа в систему
( Код исправлен в связи с изменениями в оригинале статьи 2018-06-09. Спасибо magax ! )
{% extends "base.html" %}
{% block content %}
<h2>Sign In</h2>
<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, через инструкцию расширенного наследования шаблона. На самом деле я сделаю это со всеми шаблонами, чтобы обеспечить единообразную компоновку, которая включает верхнюю панель навигации по всем страницам приложения.
В этом шаблоне предполагается, что объект формы, созданный из класса LoginForm
, будет предоставлен в качестве аргумента, который можно увидеть как ссылку form
. Этот аргумент будет отправлен функцией просмотра входа, которую я до сих пор не написал.
Элемент HTML
<form>
используется в качестве контейнера для веб-формы. Атрибут action
формы используется для того, чтобы сообщить веб-браузеру URL-адрес, который должен использоваться при отправке информации, введенной пользователем в форму. Если для действия задана пустая строка, форма передается URL-адресу, находящемуся в данный момент в адресной строке, то есть URL-адресу, который визуализирует форму на странице. Атрибут method
указывает метод HTTP-запроса, который должен использоваться при отправке формы на сервер. По умолчанию он отправляется с запросом GET
, но почти во всех случаях использование POST
запрос, улучшающий взаимодействие с пользователем, поскольку запросы этого типа могут отправлять данные формы в тело запроса, в то время как запросы GET
добавляют поля формы к URL-адресу, загромождая адресную строку обозревателя.
Добавлено 2018-06-09 Спасибо за замечания magax !
Атрибутnovalidate
используется для указания веб-браузеру не применять проверку к полям в этой форме, что фактически оставляет эту задачу приложению Flask, запущенному на сервере. Использованиеnovalidate
является полностью необязательным, но для этой первой формы важно, чтобы вы установили его, потому что это позволит вам протестировать проверку на стороне сервера позже в этой главе
Аргумент шаблона 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 %}
<h2>Sign In</h2>
<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>[{{ error }}]</span>
{% endfor %}
</p>
<p>
{{ form.password.label }}<br>
{{ form.password(size=32) }}<br>
{% for error in form.password.errors %}
<span>[{{ 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'))
# ...
<<< предыдущая следующая >>>
База данных (издание 2018) / Хабр
blog.miguelgrinberg.com
Miguel Grinberg
<<< предыдущая следующая >>>
Эта статья является переводом четвертой части нового издания учебника Мигеля Гринберга. Прежний перевод давно утратил свою актуальность.
Это четвертый выпуск серии Flask Mega-Tutorial, в котором я расскажу вам, как работать с базами данных.
Для справки ниже приведен список статей этой серии.
Примечание 1: Если вы ищете старые версии данного курса, это здесь.
Примечание 2: Если вдруг Вы хотели бы выступить в поддержку моей(Мигеля) работы в этом блоге, или просто не имеете терпения дожидаться неделю статьи, я (Мигель Гринберг)предлагаю полную версию данного руководства упакованную электронную книгу или видео. Для получения более подробной информации посетите learn.miguelgrinberg.com.
Тема этой главы чрезвычайно важна. Для большинства приложений необходимо поддерживать постоянные данные, которые могут быть эффективно извлечены, и это именно то, для чего создаются базы данных.
Ссылки GitHub для этой главы: Browse, Zip, Diff.
Базы данных в Flask
Поскольку я уверен, что вы уже слышали, Flask не поддерживает базы данных изначально. Это одна из многих областей, в которых Flask намеренно не самодостаточен, и это здорово, потому что у вас есть свобода выбора базы данных, которая наилучшим образом подходит для вашего приложения, вместо того, чтобы быть вынужденным адаптироваться к одному.
В Python есть большой выбор для баз данных, многие из которых интегрируются с Flask приложением. Базы данных можно разделить на две большие группы: те, которые соответствуют реляционной модели, и те, которые этого не делают. Последняя группа часто называется NoSQL, что указывает на то, что они не реализуют популярный язык реляционных запросов SQL. Хотя в обеих группах есть отличные продукты для баз данных, я считаю, что реляционные базы данных лучше подходят для приложений, которые имеют структурированные данные, такие как списки пользователей, сообщения в блогах и т.д., В то время как базы данных NoSQL имеют тенденцию быть лучше для данных, которые имеют менее определенную структуру. Это приложение, как и большинство других, может быть реализовано с использованием любого типа базы данных, но по указанным выше причинам я собираюсь работать с реляционной базой данных.
В главе 3 я показал вам первое расширение Flask. В этой главе я собираюсь использовать еще два. Первым является Flask-SQLAlchemy, расширение, которое обеспечивает Flask-дружественную оболочку к популярному пакету SQLAlchemy, который является Object Relational Mapper или ORM. ORM позволяют приложениям управлять базой данных с использованием объектов высокого уровня, таких как классы, объекты и методы, а не таблицы и SQL. Задача ORM — перевести операции высокого уровня в команды базы данных.
Самое приятное в SQLAlchemy заключается в том, что это ORM не для одного, а для многих реляционных баз данных. SQLAlchemy поддерживает длинный список движков баз данных, включая популярные MySQL, PostgreSQL и SQLite. Это очень сильно, потому что вы можете сделать свою разработку с помощью простой базы данных SQLite, которая не требует сервера, а затем, когда придет время развертывать приложение на производственном сервере, вы можете выбрать более надежный сервер MySQL или PostgreSQL, не измененяя вашего приложения.
Чтобы установить Flask-SQLAlchemy в виртуальную среду, убедитесь, что вы активировали ее сначала, а затем запустите:
(venv) $ pip install flask-sqlalchemy
Миграция баз данных
Большинство учебных пособий по базе данных, которые я видел, охватывают создание и использование базы данных, но не позволяют адекватно решить проблему создания обновлений существующей базы данных по мере того, как приложение нуждается в изменении или увеличении. Это сложно, потому что реляционные базы данных сосредоточены вокруг структурированных данных, поэтому при изменении структуры данные, которые уже находятся в базе данных, необходимо перенести в модифицированную структуру.
Второе расширение, которое я собираюсь представить в этой главе, Flask-Migrate, который фактически созданный вашим покорным слугой. Это расширение является Flask-оберткой для Alembic, основой для миграции базы данных SQLAlchemy. Работа с миграциями баз данных добавляет немного работы вначале, но это небольшая цена, чтобы заплатить за надежный способ внесения изменений в вашу базу данных в будущем.
Процесс установки для Flask-Migrate аналогичен другим расширениям, которые вы видели:
(venv) $ pip install flask-migrate
Конфигурация Flask-SQLAlchemy
Во время разработки я собираюсь использовать базу данных SQLite. Базы данных SQLite являются наиболее удобным выбором для разработки небольших приложений, иногда даже не очень маленьких, так как каждая база данных хранится в одном файле на диске и нет необходимости запускать сервер баз данных, как MySQL и PostgreSQL.
У нас есть два новых элемента конфигурации для добавления в файл конфигурации:
config.py
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config(object):
# ...
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'app.db')
SQLALCHEMY_TRACK_MODIFICATIONS = False
Расширение Flask-SQLAlchemy принимает местоположение базы данных приложения из переменной конфигурации SQLALCHEMY_DATABASE_URI
. Как вы помните из главы 3, в целом рекомендуется установить конфигурацию из переменных среды и предоставить резервное значение, когда среда не определяет переменную. В этом случае я беру URL-адрес базы данных из переменной среды DATABASE_URL
, и если это не определено, я настраиваю базу данных с именем app.db, расположенную в основном каталоге приложения, которая хранится в переменной basedir
.
Параметр конфигурации SQLALCHEMY_TRACK_MODIFICATIONS
установлен в значение False, чтобы отключить функцию Flask-SQLAlchemy, которая мне не нужна, которая должна сигнализировать приложению каждый раз, когда в базе данных должно быть внесено изменение.
База данных будет представлена в приложении, как database instance. Механизм миграции базы данных также будет иметь экземпляр. Это объекты, которые необходимо создать после приложения, в файле app/__ init__.py
:
from flask import Flask
from config import Config
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
app = Flask(__name__)
app.config.from_object(Config)
db = SQLAlchemy(app)
migrate = Migrate(app, db)
from app import routes, models
Я внес три изменения в скрипт init. Во-первых, я добавил объект db
, который представляет базу данных. Затем я добавил еще один объект, который представляет механизм миграции. Надеюсь, вы увидите образец работы с расширениями Flask. Большинство расширений инициализируются как эти два. Наконец, я импортирую новый модуль под названием models
внизу. Этот модуль определит структуру базы данных.
Модели баз данных
Данные, которые будут храниться в базе данных, будут представлены набором классов, обычно называемых моделями баз данных. Уровень ORM в SQLAlchemy будет выполнять переводы, необходимые для сопоставления объектов, созданных из этих классов, в строки в соответствующих таблицах базы данных.
Начнем с создания модели, представляющей пользователей. Используя инструмент WWW SQL Designer, я сделал следующую диаграмму для представления данных, которые мы хотим использовать в таблице users:
Поле id
обычно используется во всех моделях и используется как первичный ключ. Каждому пользователю в базе данных будет присвоено уникальное значение идентификатора, сохраненное в этом поле. Первичные ключи в большинстве случаев автоматически назначаются базой данных, поэтому мне просто нужно указать поле id
, помеченное как первичный ключ.
Поля username
, email
и password_hash
определяются как строки (или VARCHAR
на жаргоне базы данных), а их максимальная длина указывается так, чтобы база данных могла оптимизировать использование пространства. Хотя поля username
и email
не требуют пояснений, поля password_hash
заслуживают внимания. Я хочу убедиться, что приложение, которое я создаю, использует лучшие рекомендации по безопасности, и по этой причине я не буду хранить пароли пользователей в базе данных. Проблема с хранением паролей заключается в том, что если база данных когда-либо становится скомпрометированной, злоумышленники будут иметь доступ к паролям, и это может быть разрушительным для пользователей. Вместо того, чтобы писать пароли напрямую, я собираюсь написать хэши паролей (password hashes), которые значительно улучшают безопасность. Это будет тема другой главы, так что не беспокойтесь об этом сейчас.
Итак, теперь, когда я знаю, что мне нужно для таблицы моих пользователей, я могу перевести это в код в новом модуле app/models.py:
from app import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
def __repr__(self):
return '<User {}>'.format(self.username)
Созданный выше класс User
наследует от db.Model
, базового класса для всех моделей из Flask-SQLAlchemy. Этот класс определяет несколько полей как переменные класса. Поля создаются как экземпляры класса db.Column
, который принимает тип поля в качестве аргумента, плюс другие необязательные аргументы, которые, например, позволяют мне указать, какие поля уникальны и индексированы, что важно для эффективного поиска базы данных ,
Метод __repr__
сообщает Python, как печатать объекты этого класса, что будет полезно для отладки. Вы можете увидеть метод __repr __()
в действии в сеансе интерпретатора Python ниже:
>>> from app.models import User
>>> u = User(username='susan', email='[email protected]')
>>> u
<User susan>
Создание миграции репозитория
Класс модели, созданный в предыдущем разделе, определяет исходную структуру (или схему) базы данных для этого приложения. Но по мере того, как приложение продолжает расти, потребуется изменить структуру, которая, скорее всего, добавит новые сущности, но иногда также может изменять или удалять элементы. Alembic (инфраструктура миграции, используемая Flask-Migrate) сделает эти изменения схемы таким образом, чтобы не требовалось воссоздавать базу данных с нуля.
Чтобы выполнить эту, казалось бы, сложную задачу, Alembic поддерживает репозиторий миграции, который является каталогом, в котором хранится его сценарии миграции. Каждый раз, когда в схему базы данных вносится изменение, в репозиторий добавляется сценарий миграции с подробными сведениями об изменении. Чтобы применить миграции к базе данных, эти сценарии миграции выполняются в той последовательности, в которой они были созданы.
Flask-Migrate выдает свои команды через flask
команду. Вы уже видели flask run
, который является подчиненной командой, которая является родной для Flask. Подкоманда flask db
добавляется Flask-Migrate для управления всем, что связано с миграцией базы данных. Итак, давайте создадим репозиторий миграции для microblog, запустивflask db init
:
(venv) $ flask db init
Creating directory /home/miguel/microblog/migrations ... done
Creating directory /home/miguel/microblog/migrations/versions ... done
Generating /home/miguel/microblog/migrations/alembic.ini ... done
Generating /home/miguel/microblog/migrations/env.py ... done
Generating /home/miguel/microblog/migrations/README ... done
Generating /home/miguel/microblog/migrations/script.py.mako ... done
Please edit configuration/connection/logging settings in
'/home/miguel/microblog/migrations/alembic.ini' before proceeding.
Помните, что flask
команда полагается на переменную среды FLASK_APP
, чтобы знать, где расположено приложение Flask. Для этого приложения вы хотите установить FLASK_APP=microblog.py
, как описано в главе 1.
После запуска этой команды вы найдете новый каталог migrations, в котором есть несколько файлов и подкаталог версий. Все эти файлы теперь должны рассматриваться как часть вашего проекта и, их необходимо добавить в систему управления версиями.
Первая миграция базы данных
При наличии репозитория миграции настало время создать первую миграцию базы данных, которая будет включать таблицу Users
, сопоставляемую с моделью пользовательской базы данных. Существует два способа создания миграции базы данных: вручную или автоматически. Для автоматического создания миграции Alembic сравнивает схему базы данных, определенную моделями баз данных, с фактической схемой базы данных, используемой в настоящее время в базе данных. Затем он заполняет сценарий переноса изменениями, необходимыми для того, чтобы схема базы данных соответствовала моделям приложений. В этом случае, поскольку нет предыдущей базы данных, автоматический перенос добавит всю User
модель в сценарий переноса. flask db migrate
Подкоманда переноса DB генерирует эти автоматические миграции:
(venv) $ flask db migrate -m "users table"
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'user'
INFO [alembic.autogenerate.compare] Detected added index 'ix_user_email' on '['email']'
INFO [alembic.autogenerate.compare] Detected added index 'ix_user_username' on '['username']'
Generating /home/miguel/microblog/migrations/versions/e517276bb1c2_users_table.py ... done
Вывод команды дает вам представление о том, что Alembic включен в миграцию. Первые две строки являются информационными и их обычно можно игнорировать. Затем он говорит, что нашел таблицу 'user'
и два индекса '['email']'
и ‘[‘username’]’. Затем он сообщает вам, где он написал сценарий миграции. Код e517276bb1c2 — это автоматически созданный уникальный код для миграции (он будет другим для вас). Комментарий, заданный с параметром -m
, является необязательным, он добавляет короткий описательный текст в перенос.
Сгенерированный сценарий миграции теперь является частью вашего проекта и должен быть включен в систему управления версиями. Вы можете просмотреть сценарий, если вам интересно посмотреть, как он выглядит. Вы обнаружите, что у него есть две функции: upgrade()
и downgrade()
. Функция upgrade()
применяет миграцию, а функция downgrade()
удаляет ее. Это позволяет Alembic переносить базу данных в любую точку истории, даже в более старые версии, используя путь понижения.
Команда flask db migrate
не вносит никаких изменений в базу данных, она просто создает сценарий миграции. Чтобы применить изменения в базе данных, необходимо использовать команду flask db upgrade
.
(venv) $ flask db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> e517276bb1c2, users table
Поскольку это приложение использует SQLite, команда Upgrade обнаружит, что база данных не существует и создаст ее (вы заметите, что файл с именем app.db
будет добавлен после завершения этой команды, то есть базы данных SQLite). При работе с серверами баз данных, такими как MySQL и PostgreSQL, перед запуском обновления необходимо создать базу данных на сервере базы данных.
Процесс обновления базы данных и откатка изменений Upgrade и Downgrade
На данный момент приложение находится в зачаточном состоянии, но это не помешает обсудить, что будет в стратегии миграции базы данных в будущем. Представьте, что у вас есть приложение на вашей машине разработки, а также есть копия, развернутая на производственный сервер, который находится в сети и используется.
Предположим, что для следующей версии вашего приложения вам нужно внести изменения в свои модели, например, нужно добавить новую таблицу. Без миграции вам нужно будет выяснить, как изменить схему вашей базы данных, как на локальном хосте, так и на вашем сервере, и это может быть большой проблемой.
Но с поддержкой миграции базы данных, после изменения моделей в приложении вы создаете новый сценарий миграции ( flask db migrate ), вы, вероятно, просмотрите его, чтобы убедиться, что автоматическое создание сделало правильные вещи, а затем примените изменения в базе данных разработки ( flask db upgrade ). Вы добавите сценарий миграции в систему управления версиями и зафиксируете его.
Когда вы будете готовы выпустить новую версию приложения на свой production сервер, все, что вам нужно сделать, это захватить обновленную версию приложения, которая будет включать в себя новый сценарий миграции и запустить flask db upgrade
. Alembic обнаружит, что база данных не обновлена до последней редакции, и выполнит все новые сценарии миграции, созданные после предыдущего выпуска.
Как я упоминал ранее, у вас также есть команда flask db downgrade
, которая отменяет последнюю миграции. Хотя вам вряд ли понадобится этот вариант в момент рабочей эксплуатации, Вы можете найти его очень полезным во время разработки. Возможно, вы сгенерировали сценарий миграции и применили его, только чтобы обнаружить, что внесенные изменения не совсем то, что вам нужно. В этом случае можно понизить рейтинг базы данных, удалить сценарий миграции, а затем создать новый, чтобы заменить его.
Связи базы данных
Реляционные базы данных хороши в хранении связей между элементами данных. Рассмотрим случай, когда пользователь пишет сообщение в блоге. Пользователь будет иметь запись в таблице пользователей, и сообщение будет иметь запись в таблице сообщений. Самый эффективный способ записать кто написал данное сообщение, — связать две записи.
После того, как установлена связь между пользователем и постом, есть два типа запросов, которые нам могут понадобиться. Самый тривиальный, когда у вас есть пост и нужно знать кто из пользователей его написал. Чуть более сложный вопрос является обратным этому. Если у вас есть пользователь, то вам может понадобиться получить все написанные им записи. Flask-SQLAlchemy поможет нам с обоими типами запросов.
Расширим нашу базу для хранения постов, чтобы мы могли увидеть связи в действии. Для этого мы вернемся к нашему инструменту дизайна БД и создадим таблицу записей:
Таблица Сообщений будет иметь необходимый идентификатор, текст сообщения и метку времени. Но в дополнение к этим ожидаемым полям я добавляю поле user_id
, которое связывает сообщение с его автором. Вы видели, что у всех пользователей есть первичный ключ id
, который уникален. Способ связать запись блога с пользователем, который ее создал, — добавить ссылку на идентификатор пользователя, и это именно то, что является полем user_id
. Это поле user_id
называется внешним ключом (англ. foreign key). На приведенной выше схеме базы данных внешние ключи отображаются как связь между полем и полем id
таблицы, на которую он ссылается. Такого рода отношения называется один ко многим, потому что «один» пользователь пишет «много» Сообщений.
Измененный app/models.py показан ниже:
from datetime import datetime
from app import db
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
posts = db.relationship('Post', backref='author', lazy='dynamic')
def __repr__(self):
return '<User {}>'.format(self.username)
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.String(140))
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
def __repr__(self):
return '<Post {}>'.format(self.body)
Новый класс Post
будет представлять записи в блогах, написанные пользователями. Поле timestamp
будет проиндексировано, что полезно, если вы хотите получить сообщения в хронологическом порядке. Я также добавил аргумент по умолчанию и передал функцию datetime.utcnow
. Когда вы передаете функцию по умолчанию, SQLAlchemy установит для поля значение вызова этой функции (обратите внимание, что я не включил ()
после utcnow
, поэтому я передаю эту функцию сам, а не результат ее вызова ). В общем, это позволит работать с датами и временем UTC в серверном приложении. Это гарантирует, что вы используете единые временные метки независимо от того, где находятся пользователи. Эти временные метки будут преобразованы в локальное время пользователя, когда они будут отображаться.
Поле user_id
было инициализировано как внешний ключ для user.id
, что означает, что оно ссылается на значение id
из таблицы users
. В этой ссылке user — это имя таблицы базы данных, которую Flask-SQLAlchemy автоматически устанавливает как имя класса модели, преобразованного в нижний регистр. Класс User имеет новое поле сообщений, которое инициализируется db.relationship
. Это не фактическое поле базы данных, а высокоуровневое представление о взаимоотношениях между users и posts, и по этой причине оно не находится в диаграмме базы данных. Для отношения «один ко многим» поле db.relationship обычно определяется на стороне «один» и используется как удобный способ получить доступ к «многим». Так, например, если у меня есть пользователь, хранящийся в u
, выражение u.posts
будет запускать запрос базы данных, который возвращает все записи, написанные этим пользователем. Первый аргумент db.relationship
указывает класс, который представляет сторону отношения «много». Аргумент backref
определяет имя поля, которое будет добавлено к объектам класса «много», который указывает на объект «один». Это добавит выражение post.author
, которое вернет автора сообщения. Аргумент lazy определяет, как будет выполняться запрос базы данных для связи, о чем я расскажу позже. Не беспокойтесь, если эти детали не имеют для вас смысла, я покажу примеры в конце этой статьи.
Поскольку у меня есть обновления для моделей приложений, необходимо создать новую миграцию базы данных:
(venv) $ flask db migrate -m "posts table"
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.autogenerate.compare] Detected added table 'post'
INFO [alembic.autogenerate.compare] Detected added index 'ix_post_timestamp' on '['timestamp']'
Generating /home/miguel/microblog/migrations/versions/780739b227a7_posts_table.py ... done
И миграция должна быть применена к базе данных:
(venv) $ flask db upgrade
INFO [alembic.runtime.migration] Context impl SQLiteImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade e517276bb1c2 -> 780739b227a7, posts table
Если проект хранится в системе управления версиями, не забудьте добавить в него новый сценарий миграции.
Время запуска
Я заставил вас пройти через долгий процесс создания базы данных, но я еще не показал вам, как все работает. Поскольку приложение еще не имеет логики базы данных, давайте поиграемся с базой данных в интерпретаторе Python, чтобы ознакомиться с ним. Итак, продолжайте и запустите Python. Перед запуском интерпретатора убедитесь, что ваша виртуальная среда активирована.
В командной строке Python давайте импортируем экземпляр базы данных и модели:
>>> from app import db
>>> from app.models import User, Post
Начните с создания нового пользователя:
>>> u = User(username='john', email='[email protected]')
>>> db.session.add(u)
>>> db.session.commit()
Изменения в базе данных выполняются в контексте сеанса, к которому можно получить доступ как db.session
. Множество изменений можно накапливать в сеансе, и как только все изменения были зарегистрированы, вы можете выпустить один файл db.session.commit()
, который записывает все изменения атомарно. Если в любое время во время работы над сеансом произошла ошибка, вызов db.session.rollback()
отменяет сеанс и удаляет любые изменения, сохраненные в нем. Важно помнить, что изменения записываются только в базу данных при вызове db.session.commit()
. Сеансы гарантируют, что база данных никогда не останется в несогласованном состоянии.
Давайте добавим другого пользователя:
>>> u = User(username='susan', email='[email protected]')
>>> db.session.add(u)
>>> db.session.commit()
База данных может ответить на запрос, возвращающий всех пользователей:
>>> users = User.query.all()
>>> users
[<User john>, <User susan>]
>>> for u in users:
... print(u.id, u.username)
...
1 john
2 susan
Все модели имеют атрибут запроса, который является точкой входа для запуска запросов к базе данных. Самый основной запрос — тот, который возвращает все элементы этого класса, который называется all()
. Обратите внимание, что при добавлении этих пользователей поля id
автоматически устанавливались в 1 и 2.
Вот еще один способ. Если вы знаете идентификатор пользователя, вы можете получить результат следующим образом:
>>> u = User.query.get(1)
>>> u
<User john>
Теперь добавим сообщение в блоге:
>>> u = User.query.get(1)
>>> p = Post(body='my first post!', author=u)
>>> db.session.add(p)
>>> db.session.commit()
Мне не нужно было устанавливать значение для поля timestamp
, потому что это поле имеет значение по умолчанию, которое вы можете увидеть в определении модели. А как насчет поля user_id? Напомню, что отношение db.relationship
, которое я создал в классе User
, добавляет атрибут posts
для пользователей, а также атрибут автора для сообщений. Я назначаю автора сообщению, используя виртуальное поле автора, вместо того, чтобы иметь дело с идентификаторами пользователей. SQLAlchemy отлично подходит в этом отношении, поскольку обеспечивает абстракцию высокого уровня над отношениями и внешними ключами.
В завершении, давайте рассмотрим еще несколько запросов к базе данных:
>>> # get all posts written by a user
>>> u = User.query.get(1)
>>> u
<User john>
>>> posts = u.posts.all()
>>> posts
[<Post my first post!>]
>>> # same, but with a user that has no posts
>>> u = User.query.get(2)
>>> u
<User susan>
>>> u.posts.all()
[]
>>> # print post author and body for all posts
>>> posts = Post.query.all()
>>> for p in posts:
... print(p.id, p.author.username, p.body)
...
1 john my first post!
# get all users in reverse alphabetical order
>>> User.query.order_by(User.username.desc()).all()
[<User susan>, <User john>]
Документация по Flask-SQLAlchemy-это лучшее место, чтобы узнать о многих вариантах, доступных для запроса к базе данных.
Для завершения этого раздела, давайте сотрем тест пользователей и сообщений, созданных выше, так что бы база данных стала чистой и готовой к следующей главе:
>>> users = User.query.all()
>>> for u in users:
... db.session.delete(u)
...
>>> posts = Post.query.all()
>>> for p in posts:
... db.session.delete(p)
...
>>> db.session.commit()
shell context или лекарство от геморроя
Помните, что вы делали в начале предыдущего раздела, сразу после запуска интерпретатора Python? Первое, что вы сделали, это запустили некоторые операции импорта:
>>> from app import db
>>> from app.models import User, Post
В то время как вы работаете над своим приложением, вам нужно будет очень часто тестировать функционал в оболочке Python, поэтому повторять вышеуказанный импорт каждый раз будет утомительно. Команда flask shell
— еще один очень полезный инструмент в командной оболочке команд. shell
— это второе «ядро», реализующее flask
, после запуска. Цель этой команды-запустить интерпретатор Python в контексте приложения. Что это значит? Рассмотрим следующий пример:
(venv) $ python
>>> app
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'app' is not defined
>>>
(venv) $ flask shell
>>> app
<Flask 'app'>
При регулярном сеансе интерпретатора имя app
неизвестно, если явно не импортировано, но при использовании flask shell
команда предварительно импортирует экземпляр приложения. Прикол flask shell
заключается не в том, что предварительно импортирует приложение, а в том, что вы можете настроить «shell context», который представляет собой список других имен для предварительного импорта.
Следующая функция в microblog.py создает контекст оболочки, который добавляет экземпляр и модели базы данных в сеанс оболочки:
from app import app, db
from app.models import User, Post
@app.shell_context_processor
def make_shell_context():
return {'db': db, 'User': User, 'Post': Post}
Декоратор app.shell_context_processor
регистрирует функцию как функцию контекста оболочки. Когда запускается команда flask shell
, она будет вызывать эту функцию и регистрировать элементы, возвращаемые ею в сеансе оболочки. Причина, по которой функция возвращает словарь, а не список, заключается в том, что для каждого элемента вы также должны указывать имя, под которым оно будет ссылаться в оболочке, которое задается индексами словаря.
После того, как вы добавите функцию обработчика flask shell
, вы можете работать с объектами базы данных, не импортируя их:
(venv) $ flask shell
>>> db
<SQLAlchemy engine=sqlite:////Users/migu7781/Documents/dev/flask/microblog2/app.db>
>>> User
<class 'app.models.User'>
>>> Post
<class 'app.models.Post'>
<<< предыдущая следующая >>>
Шаблоны (издание 2018) / Хабр
blog.miguelgrinberg.com
Miguel Grinberg
<<< предыдущая следующая >>>
Эта статья является переводом второй части нового издания учебника Мигеля Гринберга, выпуск которого автор планирует завершить в мае 2018.Прежний перевод давно утратил свою актуальность.
Я, со своей стороны, постараюсь не отставать с переводом.
В этом втором выпуске серии Мега-Учебник Flask я расскажу о том, как работать с шаблонами.
Для справки ниже приведен список статей этой серии.
Примечание 1: Если вы ищете старые версии данного курса, это здесь.
Примечание 2: Если вдруг Вы хотели бы выступить в поддержку моей(Мигеля) работы в этом блоге, или просто не имеете терпения дожидаться неделю статьи, я (Мигель Гринберг)предлагаю полную версию данного руководства упакованную электронную книгу или видео. Для получения более подробной информации посетите learn.miguelgrinberg.com.
Краткое повторение
После завершения главы 1 вы должны иметь полностью работающее, но простое веб-приложение, имеющее следующую структуру файлов:
microblog\
venv\
app\
__init__.py
routes.py
microblog.py
Для запуска приложения вы установили FLASK_APP=microblog.py
в сеансе терминала, а затем выполняете запуск flask. Он запустит веб-сервер с приложением, которое вы можете открыть, введя http://localhost:5000
/ URL в адресной строке вашего веб-браузера.
В этой главе вы продолжите работу над тем же приложением, и, в частности, вы узнаете, как создавать более сложные веб-страницы, которые имеют более навороченную структуру и множество динамических компонентов. Если что-либо о приложении или технологическом процессе разработки пока неясно, перед продолжением просмотрите главу 1.
Ссылки GitHub для этой главы: Browse, Zip, Diff.
Что такое шаблоны?
Я хочу, чтобы на домашней странице моего приложения для микроблогов был заголовок, приветствующий пользователя. На данный момент я проигнорирую тот факт, что приложение еще не имеет концепции пользователей, поскольку это произойдет позже. Вместо этого я собираюсь использовать вымышленного (mock) пользователя, которого я собираюсь реализовать как словарь Python, следующим образом:
user = {'username': 'Miguel'}
Создание mock-объектов — полезный метод, который позволяет вам сосредоточиться на одной части приложения, не беспокоясь о других частях системы, которые еще не существуют. Я хочу создать домашнюю страницу своего приложения, и я не хочу, чтобы отсутствие системы пользователя отвлекало меня. Поэтому я просто создаю объект пользователя.
Функция просмотра в приложении возвращает простую строку. Теперь я хочу расширить эту возвращаемую строку в полную HTML-страницу, возможно, что-то вроде этого:
from app import app
@app.route('/')
@app.route('/index')
def index():
user = {'username': 'miguel'}
return '''
<html>
<head>
<title>Home Page - Microblog</title>
</head>
<body>
<h2>Hello, ''' + user['username'] + '''!</h2>
</body>
</html>'''
Если Вы не знакомы с HTML, я рекомендую, чтобы Вы прочитали Markup HTML в Википедии для краткого введения.
Обновите функцию представления как показано выше и смотрите в Вашем браузере.
Я надеюсь, Вы согласитесь со мной, что решение, используемое выше, не достаточно хорошо. Подумайте, насколько сложным будет код в этом представлении, когда у меня появятся сообщения в блоге от пользователей, которые будут постоянно меняться. Приложение также будет иметь больше функций просмотра, которые будут связаны с другими URL-адресами, поэтому представьте, если в один прекрасный день я решу изменить макет этого приложения и придется обновлять HTML в каждом представлении. Это явно не вариант, который будет масштабироваться по мере роста приложения.
Если бы вы могли сохранить логику своего приложения отдельно от макета или презентации ваших веб-страниц, тогда все было бы намного лучше организовано, не так ли? Вы даже можете нанять веб-дизайнера для создания убойного сайта, когда вы закодируете логику приложения в Python.
Шаблоны помогают достичь этого разделения между презентацией и бизнес-логикой. В Flask шаблоны записываются как отдельные файлы, хранящиеся в папке templates
, которая находится внутри пакета приложения. Поэтому, убедившись, что вы находитесь в каталоге microblog
, создайте каталог, в котором будут храниться шаблоны:
(venv) $ mkdir app/templates
или так:
(venv) C:\microblog>mkdir app\templates
Ниже вы можете увидеть свой первый шаблон, который похож по функциональности на страницу HTML, возвращаемую функцией просмотра index()
выше. Запишите этот файл в app/templates/index.html
:
<html>
<head>
<title>{{ title }} - Microblog</title>
</head>
<body>
<h2>Hello, {{ user.username }}!</h2>
</body>
</html>
Это стандартная, очень простая HTML-страница. Единственная интересная вещь на этой странице состоит в том, что для динамического контента есть несколько заполнителей, заключенных в {{...}}
разделы. Эти заполнители представляют части страницы, которые являются переменными и будут определены только во время выполнения.
Теперь, когда презентация страницы была выгружена в шаблон HTML, функция просмотра может быть упрощена (файл \app\routes.py):
# -*- coding: utf-8 -*-
from flask import render_template
from app import app
@app.route('/')
@app.route('/index')
def index():
user = {'username': 'Miguel'}
return render_template('index.html', title='Home', user=user)
Это выглядит намного лучше, не так ли? Попробуйте эту новую версию приложения, чтобы посмотреть, как работает шаблон. Как только вы загрузите страницу в свой браузер, вы можете просмотреть исходный HTML-код и сравнить его с исходным шаблоном.
Операция, которая преобразует шаблон в полную HTML-страницу, называется рендерингом. Чтобы отобразить шаблон, мне пришлось импортировать функцию, которая поставляется с флаговой инфраструктурой под названием render_template()
. Эта функция принимает имя файла шаблона и переменную список аргументов шаблона и возвращает один и тот же шаблон, но при этом все заполнители в нем заменяются фактическими значениями.
Функция render_template()
вызывает механизм шаблонов Jinja2
, который поставляется в комплекте с Flask. Jinja2
заменяет блоки {{...}}
соответствующими значениями, заданными аргументами, указанными в вызове render_template()
.
Условные операторы
Вы видели, как Jinja2
заменяет заполнители фактическими значениями во время рендеринга, но это всего лишь одна из многих мощных операций, поддерживаемых Jinja2
в файлах шаблонов. Например, шаблоны также поддерживают управляющие операторы, заданные внутри блоков {% ...%}
. Следующая версия шаблона index.html
добавляет условное выражение:
<html>
<head>
{% if title %}
<title>{{ title }} - Microblog</title>
{% else %}
<title>Welcome to Microblog!</title>
{% endif %}
</head>
<body>
<h2>Hello, {{ user.username }}!</h2>
</body>
</html>
Теперь шаблон немного умнее. Если функция просмотра забывает передать значение переменной заполнитель заголовка, вместо того, чтобы показывать пустой заголовок, шаблон предоставит значение по умолчанию. Вы можете попробовать, как это условие работает, удалив аргумент title в вызове render_template()
функции view (файл \app\routes.py).
Циклы
Пользователь, вошедший в систему, вероятно, захочет увидеть последние сообщения от подключенных пользователей на домашней странице, поэтому теперь я собираюсь расширить приложение, для поддержки этого.
Еще раз, использую трюк с поддельным объектом, чтобы создать некоторых пользователей и некоторые сообщения, для демонстрации:
# -*- coding: utf-8 -*-
from flask import render_template
from app import app
@app.route('/')
@app.route('/index')
def index():
user = {'username': 'Эльдар Рязанов'}
posts = [
{
'author': {'username': 'John'},
'body': 'Beautiful day in Portland!'
},
{
'author': {'username': 'Susan'},
'body': 'The Avengers movie was so cool!'
},
{
'author': {'username': 'Ипполит'},
'body': 'Какая гадость эта ваша заливная рыба!!'
}
]
return render_template('index.html', title='Home', user=user, posts=posts)
Для представления пользовательских сообщений я использую список, где каждый элемент является словарем, имеющим поля Author
и Body
. Когда я доберусь до реализации пользователей и сообщений в блоге по-настоящему, я постараюсь сохранить эти имена полей, чтобы вся работа, которую сделал для проектирования и тестирования шаблона домашней страницы, используя эти временные объекты, продолжала действовать и на момент когда я представляю реальных пользователей и сообщения из базы данных.
На стороне шаблона я должен решить новую проблему. Список сообщений может иметь любое количество элементов. Сколько сообщений будут представлены на странице? Шаблон не может делать какие-либо предположения о том, сколько записей существует, поэтому он должен быть готов к отображению стольких сообщений, сколько отправляет представление общим способом.
Для этого типа проблемы, Jinja2 предлагает for
структуры управления (файл app/templates/index.html):
<html>
<head>
{% if title %}
<title>{{ title }} - Microblog</title>
{% else %}
<title>Welcome to Microblog</title>
{% endif %}
</head>
<body>
<h2>Hi, {{ user.username }}!</h2>
{% for post in posts %}
<div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
{% endfor %}
</body>
</html>
Просто, не так ли? Попробуйте эту новую версию приложения и не забудьте поиграть с добавлением большего количества контента в список сообщений, чтобы увидеть, как шаблон адаптируется и всегда отображает все сообщения, отправляемые функцией представления.
Прим.переводчика: Все не так просто с кодировками. Я попробовал заменить приветствие на Превед
и получил ошибку. Следите за кодировкой файла index.html. Он должен быть сохранен в utf-8.
Наследование шаблонов
В большинстве веб-приложений вверху страницы имеется панель навигации с несколькими часто используемыми ссылками, такими как ссылка для редактирования вашего профиля, логина, выхода из системы и т.д. Я могу легко добавить панель навигации в index.html
шаблон с некоторым количеством HTML, но по мере того, как приложение будет расти, мне понадобится эта же панель навигации на других страницах. Не хотелось бы поддерживать несколько копий навигационной панели во многих HTML-шаблонах, это хорошая практика, обходиться без дублирования кода, если это возможно.
Jinja2 имеет функцию наследования шаблона, которая специально решает эту проблему. По существу, вы можете перемещать части макета страницы, которые являются общими для всех шаблонов, к базовому шаблону, из которого выводятся все остальные шаблоны.
Итак, теперь я определяю базовый шаблон base.html
, который включает в себя простую навигационную панель, а также логику заголовка, которую я реализовал ранее. Вам необходимо написать следующий шаблон в файле app/templates/base.html
:
<html>
<head>
{% if title %}
<title>{{ title }} - Microblog</title>
{% else %}
<title>Welcome to Microblog</title>
{% endif %}
</head>
<body>
<div>Microblog: <a href="/index">Home</a></div>
<hr>
{% block content %}{% endblock %}
</body>
</html>
В этом шаблоне я использовал оператор управления блоком, чтобы определить место, где производные шаблоны могут вставляться. Блокам присваивается уникальное имя, которое производные шаблоны могут ссылаться, когда они предоставляют свой контент.
С базовым шаблоном я теперь могу упростить index.html
, наследуя его от base.html
:
{% extends "base.html" %}
{% block content %}
<h2>Hi, {{ user.username }}!</h2>
{% for post in posts %}
<div><p>{{ post.author.username }} says: <b>{{ post.body }}</b></p></div>
{% endfor %}
{% endblock %}
Поскольку шаблон base.html теперь будет заботиться об общей структуре страницы, я удалил все эти элементы из index.html и оставил только часть содержимого. Оператор extends
устанавливает ссылку наследования между двумя шаблонами, так что Jinja2 знает, что когда ему предлагается визуализировать index.html
, он должен встроить его в base.html
. Два шаблона имеют согласованные операторы block
с именами content
, и именно так Jinja2 знает, как объединить два шаблона в один. Теперь, если мне нужно создать дополнительные страницы для приложения, я могу создать их как производные шаблоны из одного и того же шаблона base.html
, и именно так я могу иметь все страницы приложения, которые будут одинаково выглядеть и выглядеть без дублирования.
<<< предыдущая следующая >>>
I18n и L10n (издание 2018) / Хабр
Miguel Grinberg
Туда Сюда
Это тринадцатая часть серии Мега-Учебник Flask, в которой я расскажу вам, как реализовать поддержку нескольких языков для вашего приложения. В рамках этой работы вы также узнаете о создании собственных расширений CLI для flask.
Для справки ниже приведен список статей этой серии.
Примечание 1: Если вы ищете старые версии данного курса, это здесь.
Примечание 2: Если вдруг Вы захотели бы выступить в поддержку моей(Мигеля) работы, или просто не имеете терпения дожидаться статьи неделю, я (Мигель Гринберг)предлагаю полную версию данного руководства(на английском языке) в виде электронной книги или видео. Для получения более подробной информации посетите learn.miguelgrinberg.com.
Эта глава посвящена интернационализации и локализации, сокращенно I18n и L10n. Чтобы сделать приложение доступным для людей, не владеющих английским языком, будет реализован процесс перевода, который, с помощью сервисов-переводчиков языка, позволит мне предложить пользователям язык-приложения на выбор.
Ссылки GitHub для этой главы: Browse, Zip, Diff.
Введение в Flask-Babel
Как вы, наверное, уже догадались, существует расширение Flask, которое упрощает работу с переводами. Расширение называется Flask-Babel и устанавливается с помощью pip:
(venv) $ pip install flask-babel
Flask-Babel инициализируется, как и большинство других расширений Flask:
app/__init__.py
: Инициализация Flask-Babel.
# ...
from flask_babel import Babel
app = Flask(__name__)
# ...
babel = Babel(app)
В качестве примера, я расскажу вам, как перевести приложение на испанский язык, поскольку я, случается, говорю на этом языке. Я мог бы также работать с переводчиками, которые знают другие языки и поддерживают их. Чтобы отслеживать список поддерживаемых языков, следует добавить переменную конфигурации:
config.py: Список поддерживаемых языков.
class Config(object):
# ...
LANGUAGES = ['en', 'es']
Я использую двухбуквенные коды языков для этого приложения, но если вам нужно быть более конкретным, можно добавить код страны. Например, вы можете использовать en-US
, en-GB
и en-CA
для поддержки английского с разными диалектами США, Великобритания или Канада.
Экземпляр Babel
предоставляет декоратор localeselector
. Декорированная функция вызывается для каждого запроса, чтобы выбрать перевод языка для использования:
app/__init__.py
: Выбор предпочтительного языка.
from flask import request
# ...
@babel.localeselector
def get_locale():
return request.accept_languages.best_match(app.config['LANGUAGES'])
Здесь я использую атрибут объекта Flask request
, называемый accept_languages
. Этот объект обеспечивает интерфейс высокого уровня для работы с заголовком Accept-Language, отправляемым клиентами с запросом. Этот заголовок указывает язык клиента и языковые предпочтения в виде средневзвешенного списка. Содержимое этого заголовка можно настроить на странице настроек браузера, при этом по умолчанию обычно импортируются из языковых настроек в операционной системе компьютера. Большинство людей даже не знают, что такая настройка существует, но это полезно, поскольку пользователи могут предоставить список предпочтительных языков, каждый из которых имеет вес. Если вам интересно, вот пример сложного заголовка Accept-Languages
:
Accept-Language: da, en-gb;q=0.8, en;q=0.7
Видим, что Датский (da
) является предпочтительным языком (значение веса по умолчанию 1,0), а затем Британский английский (en-gb
) с весом 0,8, и в качестве последнего варианта Общий Английский (en
) с весом 0,7.
Чтобы выбрать лучший язык, вам нужно сравнить список языков, запрашиваемых клиентом, с языками, которые поддерживает приложение, и, используя предоставленные клиентом веса, найти лучший язык. Возможно вам кажется эта логика слишком сложной, но все это инкапсулируется в метод best_match()
, который принимает список языков, предлагаемых приложением в качестве аргумента и возвращает лучший выбор.
Маркировка текстов для перевода в исходном коде Python
Рано обрадовались. Теперь о грустном. Обычный рабочий процесс при создании приложения на нескольких языках заключается в разметке всех текстов, требующие перевода в исходном коде. После того, как тексты будут помечены, Flask-Babel будет сканировать все файлы и извлекать эти тексты в отдельный файл перевода, используя инструмент gettext. К сожалению, это утомительная задача, которая должна быть выполнена для перевода.
Я собираюсь показать вам несколько примеров этой маркировки, но получить полный набор изменений вы можете из пакета для этой главы или репозитория GitHub.
Способ, которым тексты помечены для перевода, заключается в обертывании их в вызов функции, которая вызывается как соглашение _()
, просто подчеркивание. Простейшими случаями являются те, где литеральные строки появляются в исходном коде. Ниже приведен пример оператора flash()`:
from flask_babel import _
# ...
flash(_('Your post is now live!'))
Идея заключается в том, что функция _()
переносит текст на базовый язык (в данном случае английский). Она будет использовать лучший по ее мнению язык, выбранный функцией get_locale
, декорированной функцией localeselector
, чтобы найти правильный перевод для данного клиента. Затем функция _()
вернет переведенный текст, который в этом случае станет аргументом для flash()
.
К сожалению, не все случаи так просты. Рассмотрим этот другой вызов flash()
из приложения:
flash('User {} not found.'.format(username))
Этот текст имеет динамический компонент, который вставлен в середине статического текста. Функция _()
имеет синтаксис, поддерживающий этот тип текстов, но основанный на старом синтаксисе подстановки строк:
flash(_('User %(username)s not found.', username=username))
Есть еще более трудный случай. Некоторые строковые литералы назначаются вне запроса, как правило, когда приложение запускается, поэтому в то время, когда эти тексты оцениваются, нет способа узнать, какой язык использовать. Примером этого являются метки, связанные с полями формы. Единственное решение для обработки этих текстов — найти способ отложить оценку строки до ее использования, которая будет находиться под фактическим запросом. Flask-Babel предоставляет версию (lazy evaluation) отложенного вычисления _()
, которая называется lazy_gettext()
:
from flask_babel import lazy_gettext as _l
class LoginForm(FlaskForm):
username = StringField(_l('Username'), validators=[DataRequired()])
# ...
Здесь я импортирую альтернативную функцию перевода и переименовываю ее в _l ()
, так что она была схожа по названию с оригинальной _()
. Эта новая функция переносит текст в специальный объект, содержащий метод перевода, который состоится позже, в момент использования строки.
Расширение Flask-Login высвечивает сообщение при каждой переадресации пользователя на страницу входа. Это сообщение написано на английском языке и формируется в умолчаниях самого расширения. Чтобы убедиться, что это сообщение также переведено, я собираюсь переопределить сообщение по умолчанию и предоставить другой вариант декорированный функцией _l()
для отложенного вызова:
login = LoginManager(app)
login.login_view = 'login'
login.login_message = _l('Please log in to access this page.')
Разметка текстов для перевода в шаблонах
В предыдущем разделе вы видели, как разметить переводимые тексты в исходном коде модулей Python, но это только часть процесса, так как файлы шаблонов также содержат текст. Функция _()
также доступна в шаблонах, поэтому процесс сильно похож. Например, рассмотрим этот фрагмент HTML из 404.html:
<h2>File Not Found</h2>
Версия с поддержкой перевода:
<h2>{{ _('File Not Found') }}</h2>
Обратите внимание, что здесь, помимо обертывания текста с помощью _()
, необходимо добавить {{...}}
, чтобы заставить _()
вычислять вместо того, чтобы считаться литералом в шаблоне.
Для более сложных фраз, содержащих динамические компоненты, можно использовать аргументы:
<h2>{{ _('Hi, %(username)s!', username=current_user.username) }}</h2>
В файле _post.html
есть особенно сложный случай, который заставил меня разбираться:
{% set user_link %}
<a href="{{ url_for('user', username=post.author.username) }}">
{{ post.author.username }}
</a>
{% endset %}
{{ _('%(username)s said %(when)s',
username=user_link, when=moment(post.timestamp).fromNow()) }}
Проблема здесь заключается в том, что я хотел, чтобы имя пользователя было ссылкой, указывающей на страницу профиля пользователя, а не только именем, так что мне пришлось создать промежуточную переменную под названием user_link
с помощью set
и endset
директивы шаблонов, а затем передать это как аргумент функции перевода.
Как я уже упоминал выше, вы можете скачать версию приложения со всеми переводимыми текстами в исходном коде Python и шаблонах.
Извлечение текста для перевода
После того, как у вас есть приложение со всеми _()
и_l()
на своих местах, вы можете использовать команду pybabel
, чтобы извлечь их в файл a.pot, что означает portable object template. Это текстовый файл, содержащий все тексты, которые были помечены как нуждающиеся в переводе. Цель этого файла состоит в том, чтобы служить шаблоном для создания файлов перевода на любой другой язык.
Для процесса извлечения требуется небольшой файл конфигурации, который сообщает pybabel
, какие файлы следует сканировать для переводимых текстов. Ниже вы можете увидеть babel.cfg, который я создал для этого приложения:
babel.cfg: PyBabel configuration file.
[python: app/**.py]
[jinja2: app/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_
Первые две строки определяют имена файлов шаблонов Python и Jinja2 соответственно. Третья строка определяет два расширения, предоставляемые движком шаблонов Jinja2, которые помогают Flask-Babel правильно анализировать файлы шаблонов.
Чтобы извлечь все тексты в .pot файл, вы можете использовать следующую команду:
(venv) $ pybabel extract -F babel.cfg -k _l -o messages.pot .
Команда pybabel extract
считывает файл конфигурации, указанный в параметре -F
, а затем сканирует все файлы py и html в каталогах, соответствующих настроенным источникам, начиная с каталога, указанного в команде (текущий каталог или .
в этом случае.) По умолчанию, pybabel
будем искать _()
как текстовый маркер, но я также использовал lazy вариант, который я импортировал как _l()
, так что мне нужно сказать об этом инструменту поиска опцией -k
_l
. Параметр -o
указывает имя выходного файла.
Должен отметить, что messages.pot не является файлом, который должен быть включен в проект. Это файл, который можно легко регенерировать в любое время, просто выполнив команду выше снова. Таким образом, нет необходимости передавать этот файл в систему управления версиями.
Создание Language Catalog
Следующим шагом в процессе является создание перевода для каждого языка, который будет поддерживаться в дополнение к базовому, который в данном случае английский. Я сказал, что собираюсь начать с добавления испанского языка (код языка es
), так что команда, которая делает это:
(venv) $ pybabel init -i messages.pot -d app/translations -l es
creating catalog app/translations/es/LC_MESSAGES/messages.po based on messages.pot
Команда pybabel init
принимает файл messages.pot
в качестве входных данных и создает новый каталог для определенного языка, указанного в параметре -l
в каталог, указанный в параметре -d
. Я буду сохранять все переводы в директории app/translations, потому что там Flask-Babel будет искать файлы перевода по умолчанию. Команда создаст подкаталог es
внутри этого каталога для данных на испанском. В частности, там появится новый файл с названием app/translations/es/LC_MESSAGES/messages.po. То есть там, где переводы должны быть сделаны.
Если вы хотите поддержать другие языки, то повторите вышеуказанную команду с каждым из кодов языка. Таким образом, что бы каждый язык получил свой собственный репозитарий с файлом messages.po.
Этот messages.po-файл, созданный в каждом языковом репозитории, использует формат, который является стандартом де-факто для языковых переводов, Формат, используемый утилитой gettext. Вот несколько строк начала испанского messages.po:
# Spanish translations for PROJECT.
# Copyright (C) 2017 ORGANIZATION
# This file is distributed under the same license as the PROJECT project.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2017.
#
msgid ""
msgstr ""
"Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2017-09-29 23:23-0700\n"
"PO-Revision-Date: 2017-09-29 23:25-0700\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language: es\n"
"Language-Team: es <[email protected]>\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.5.1\n"
#: app/email.py:21
msgid "[Microblog] Reset Your Password"
msgstr ""
#: app/forms.py:12 app/forms.py:19 app/forms.py:50
msgid "Username"
msgstr ""
#: app/forms.py:13 app/forms.py:21 app/forms.py:43
msgid "Password"
msgstr ""
Если пропустить заголовок, то видно, что ниже приведен список строк, которые были извлечены из вызовов _()
и _l()
. Для каждого текста вы получаете ссылку на расположение текста в приложении. Затем строка msgid
содержит текст на базовом языке, а следующая строка msgstr
содержит пустую строку. Эти пустые строки должны быть отредактированы, чтобы иметь текст на целевом языке.
Есть много приложений, которые работают с переводом .po
-файлов. Если вы чувствуете себя комфортно при редактировании текстового файла, то этого достаточно, но если вы работаете с большим проектом, то может быть рекомендовано работать со специализированным редактором. Наиболее популярным приложением для перевода является poedit
с открытым исходным кодом, который доступен для всех основных операционных систем. Если вы знакомы с VIM
, то po.vim
плагин дает некоторые ключевые отображения, которые делают работу с этими файлами проще.
Ниже вы можете увидеть часть испанской версии messages.po после того, как я добавил перевод:
#: app/email.py:21
msgid "[Microblog] Reset Your Password"
msgstr "[Microblog] Nueva Contraseña"
#: app/forms.py:12 app/forms.py:19 app/forms.py:50
msgid "Username"
msgstr "Nombre de usuario"
#: app/forms.py:13 app/forms.py:21 app/forms.py:43
msgid "Password"
msgstr "Contraseña"
Пакет загрузки для этой главы также содержит этот файл, так что вам не придется беспокоиться об этой части приложения.
Файл messages.po -это своего рода файл-источник для переводов. Если вы хотите начать использовать эти переведенные тексты, то файл должен быть скомпилирован в формат, который эффективен для использования приложением во время выполнения. Чтобы собрать все переводы для приложения, вы можете использовать команду компиляции pybabel compile
следующим образом:
(venv) $ pybabel compile -d app/translations
compiling catalog app/translations/es/LC_MESSAGES/messages.po to
app/translations/es/LC_MESSAGES/messages.mo
Эта операция добавляет файл messages.mo рядом с messages.po в каждом языковом репозитории. Файл .mo — это файл, который Flask-Babel будет использовать для загрузки переводов в приложение.
После создания messages.mo для испанского или любых других языков, добавленных в проект, эти языки готовы к использованию в приложении. Если вы хотите увидеть, как выглядит приложение на испанском языке, Вы можете изменить конфигурацию языка в веб-браузере, чтобы испанский язык был предпочтительным языком. Для Chrome это расширенная часть в настройках:
Если вы предпочитаете не изменять настройки браузера, другой альтернативой является принудительное использование языка, заставляя функцию localeselector
всегда возвращать один и тот же. Для испанского это выглядит так:
app/__init__.py
: Выбор испанского ( директивно).
@babel.localeselector
def get_locale():
# return request.accept_languages.best_match(app.config['LANGUAGES'])
return 'es'
Запуск приложения в браузере, настроенном на испанский язык, или в случае принудительного присвоения значения es
функции localeselector
, заставит все тексты появляться на испанском языке в приложении.
Обновление переводов
Одна из распространенных ситуаций при работе с переводами заключается в том, что вы можете начать использовать файл перевода, даже если он неполный. Это совершенно нормально, можно компилировать неполные файлы messages.po. В этом случае будут использоваться po-файлы и любые доступные переводы, а отсутствующие будут использовать базовый язык. Затем можно продолжить работу над переводами и выполнить компиляцию для обновления messages.mo.
Другой распространенный случай возникает, если вы пропустили некоторые тексты при добавлении _()
обертки. В этом случае вы увидите, что те тексты, которые вы пропустили, останутся на английском языке, потому что Flask-Babel ничего о них не знает. В этом случае необходимо добавить _()
или _l()
обертки при обнаружении текстов, которые не имеют их, а затем выполнить процедуру обновления, которая включает в себя два шага:
(venv) $ pybabel extract -F babel.cfg -k _l -o messages.pot .
(venv) $ pybabel update -i messages.pot -d app/translations
Команда extract
идентична той, которую я описывал ранее, но теперь она будет генерировать новую версию messages.pot со всеми предыдущими текстами плюс что-нибудь новое, которое вы недавно обернули с помощью _()
или _l()
. Вызов обновления принимает новый файл messages.pot и объединяет его во все файлы messages.po, связанные с проектом. Это будет интеллектуальное слияние, в котором любые существующие тексты будут оставлены в покое, в то время как будут затронуты только записи, которые были добавлены или удалены в messages.pot.
После обновления messages.po вы можете продолжить и перевести все новые тесты, а затем скомпилировать сообщения еще раз, чтобы сделать их доступными для приложения.
Перевод дат и времени
Теперь у меня есть полный испанский перевод для всех текстов в коде Python и шаблонах. Но если вы запустите приложение на испанском языке и будете хорошим наблюдателем, то вы заметите, что есть еще несколько мест, которые остались на английском языке. Я имею в виду временные метки, созданные Flask-Moment и moment.js, которые, очевидно, не были включены в перевод, потому что ни один из текстов, созданных этими пакетами, не является частью исходного кода или шаблона приложения.
moment.js поддерживает локализацию и интернационализацию, поэтому все, что мне нужно сделать, это настроить правильный язык. Flask-Babel возвращает выбранный язык и локаль для такого случая с помощью функции get_locale()
, поэтому я собираюсь добавить локаль в объект g
, чтобы получить доступ к нему из базового шаблона:
app/routes.py
: Сохраняем выбранный язык в flask.g.
# ...
from flask import g
from flask_babel import get_locale
# ...
@app.before_request
def before_request():
# ...
g.locale = str(get_locale())
Функция get_locale()
из Flask-Babel возвращает объект, но я просто хочу иметь код языка, который может быть получен путем преобразования объекта в строку. Теперь, когда у меня есть g.locale
, я могу получить к нему доступ из базового шаблона, чтобы настроить moment.js с правильным языком:
app/templates/base.html: Устанавливаем языковой стандарт для moment.js.
...
{% block scripts %}
{{ super() }}
{{ moment.include_moment() }}
{{ moment.lang(g.locale) }}
{% endblock %}
И теперь все даты и время должны появляться на том же языке, что и текст. Ниже вы можете увидеть, как приложение выглядит на испанском языке:
На этом этапе все тексты, кроме тех, которые были предоставлены пользователем в сообщениях блога или описаниях профиля, должны быть переведены на другие языки.
Усовершенствования для командной строки
Вы, вероятно, согласитесь со мной, что команды pybabel
слгка длинны и их трудно запомнить. Я собираюсь использовать эту возможность, чтобы показать вам, как вы можете создавать пользовательские команды, интегрированные с командой flask. До сих пор вы видели использование flask run
, flask shell
, и несколько flask db
суб-команды в Flask-Migrate. На самом деле легко добавить специфичные для приложения команды в flask. Итак, теперь я собираюсь создать несколько простых команд, которые запускают команды pybabel
со всеми аргументами, которые специфичны для этого приложения. Команды, которые я собираюсь добавить:
flask translate init LANG
добавить новый языкflask translate update
обновить все языковые репозиторииflask translate compile
для компиляции всех языковых репозиториев
babel export
не будет командой, потому что генерация файла messages.pot всегда является предварительным условием для выполнения команд init
или update
. Поэтому реализация этих команд будет генерировать файл шаблона перевода как временный файл.
Flask полагается на Click для всех своих операций с командной строкой. Команды, такие как translate
, которые являются корнем для нескольких подкоманд, создаются с помощью декоратора app.cli.group()
. Я собираюсь поместить эти команды в новый модуль под названием app/cli.py:
app/cli.py: Перевести группу команд.
from app import app
@app.cli.group()
def translate():
"""Translation and localization commands."""
pass
Имя команды происходит от имени декорированной функции, а справочное сообщение поступает из docstring. Поскольку это родительская команда, которая существует только для обеспечения базы для подкоманд, самой функции ничего не нужно делать.
Update
-обновление и compile
-компиляцию легко реализовать, поскольку они не принимают никаких аргументов:
app/cli.py: Обновление и компиляция вложенных команд.
import os
# ...
@translate.command()
def update():
"""Update all languages."""
if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
raise RuntimeError('extract command failed')
if os.system('pybabel update -i messages.pot -d app/translations'):
raise RuntimeError('update command failed')
os.remove('messages.pot')
@translate.command()
def compile():
"""Compile all languages."""
if os.system('pybabel compile -d app/translations'):
raise RuntimeError('compile command failed')
Обратите внимание, что декоратор из этих функций является производным от родительской функции translate
. Это может показаться запутанным, так как translate()
— это функция, но это стандартный способ, которым Click создает группы команд. Так же, как и в функции translate()
, docstrings -строки документации для этих функций используются в качестве сообщения справки в выводе —help.
Возможно вы заметили, что во всех командах, которые я запускаю есть проверка возвращаемого значения на ноль. Это означает, что команда выполнена и не вернула никакой ошибки. Если в команде ошибка, то я поднимаю RuntimeError
, что приводит к остановке скрипта. Функция update()
объединяет шаги извлечения и обновления в одной команде, и если все прошло успешно, она удаляет файл messages.pot после завершения обновления, так как этот файл может быть легко регенерирован при необходимости еще раз.
Команда init
принимает новый код языка в качестве аргумента. Вот реализация:
app/cli.py: Init — sub-команда инициализации.
import click
@translate.command()
@click.argument('lang')
def init(lang):
"""Initialize a new language."""
if os.system('pybabel extract -F babel.cfg -k _l -o messages.pot .'):
raise RuntimeError('extract command failed')
if os.system(
'pybabel init -i messages.pot -d app/translations -l ' + lang):
raise RuntimeError('init command failed')
os.remove('messages.pot')
Эта команда использует декоратор @click.argument
для определения кода языка. Click передает значение, указанное в команде функции обработчика в качестве аргумента, а затем я включаю аргумент в команду init
.
Последним шагом для включения этих команд является их импорт, чтобы команды регистрировались. Я решил сделать это в файле microblog.py в каталоге верхнего уровня:
microblog.py: Регистрация команд командной строки.
from app import cli
Здесь единственное, что мне нужно сделать, это импортировать новый модуль cli.py, нет никакой необходимости делать что-либо с ним, так как импорт вызывает декораторы команды для запуска и регистрации команды.
В этот момент, запуск flask --help
передаст команду translate
в качестве опции. И flask translate --help
отобразит вывод трех суб-команд, которые я определил:
(venv) $ flask translate --help
Usage: flask translate [OPTIONS] COMMAND [ARGS]...
Translation and localization commands.
Options:
--help Show this message and exit.
Commands:
compile Compile all languages.
init Initialize a new language.
update Update all languages.
Так что теперь, рабочий процесс гораздо проще и нет необходимости помнить длинные и сложные команды. Чтобы добавить новый язык, используйте:
(venv) $ flask translate init <language-code>
Обновить все языки после внесения изменений в маркеры _()
и _l()
:
(venv) $ flask translate update
И компилировать все языки после обновления файлов перевода:
(venv) $ flask translate compile
Туда Сюда
Чуть-чуть косметики (издание 2018) / Хабр
Miguel Grinberg
Туда Сюда
Это одиннадцатая часть Мега-Учебника Flask, в которой я расскажу вам, как заменить базовые шаблоны HTML новыми, основанными на структуре пользовательского интерфейса Bootstrap.
Под спойлером приведен список всех статей этой серии 2018 года.
Примечание 1: Если вы ищете старые версии данного курса, это здесь.
Примечание 2: Если вдруг Вы захотели бы выступить в поддержку моей(Мигеля) работы, или просто не имеете терпения дожидаться статьи неделю, я (Мигель Гринберг)предлагаю полную версию данного руководства(на английском языке) в виде электронной книги или видео. Для получения более подробной информации посетите learn.miguelgrinberg.com.
Вы уже некоторое время упражняетесь с моим приложением Microblog, поэтому я уверен, что вы заметили, что я не слишком старался, чтобы страницы выглядели красиво, или, лучше сказать, что я не потратил на это вообще времени. Шаблоны, которые я собрал вместе, довольно простые, с абсолютно простым стилем. Мне было важно сосредоточиться на фактической логике приложения, не отвлекаясь на хорошо выглядящие HTML и CSS.
Я уже достаточно времени фокусировался на бэкэнд-части этого приложения. Поэтому в этой главе я делаю перерыв от этого славного занятия и займусь тем, чтобы сделать приложение немного более привлекательным и внешне профессиональным.
Эта глава будет несколько отличаться от предыдущих, потому что я не буду так подробно, как обычно работать с Python, который в конце концов является основной темой этой книги. Создание привлекательных веб-страниц — это обширная тема, которая в значительной степени вовсе не связанная с веб-разработкой на Python, но я расскажу о некоторых основных принципах и идеях. О том, как подойти к задаче. И в итоге у вас также будет приложение с переработанными внешним видом для углубленного изучения.
Ссылки GitHub для этой главы: Browse, Zip, Diff.
CSS-фреймворки
Хотя вы можете утверждать, что «кодить» тяжко, ваша боль ничто по сравнению с тем, что испытывают веб-дизайнеры, создающие шаблоны, которые должны иметь приятный и лаконичный вид для всего списка веб-браузеров. В последние годы стало получше! Но еще есть странные ошибки или причуды в некоторых браузерах, которые сильно усложняют задачу проектирования веб-страниц таким образом, что бы они отображались везде и всегда одинаково хорошо. Это еще сложнее, если вам также нужно выполнить настройки отображения с учетом ограничений экранов для планшетов и смартфонов.
Если вы, как и я, являетесь разработчиком, который просто хочет получить хороший вид веб-страниц, но не имеете времени или интереса, чтобы изучать глубины механизмов для достижения этой эффективности путем написания RAW HTML и CSS, то единственным практическим решением является использование CSS framework
для упрощения этой задачи. Вы потеряете некоторую творческую свободу, принимая этот путь, но с другой стороны, ваши веб-страницы будут прилично выглядеть во всех браузерах при минимуме приложенных усилий. framework CSS предоставляет коллекцию классов CSS высокого уровня с готовыми стилями для распространенных типов элементов пользовательского интерфейса. Большинство из этих фреймворков также предоставляют JavaScript дополнения для решений, которые не могут быть сделаны строго с HTML и CSS.
Знакомство с Bootstrap
Одной из самых популярных платформ CSS является Bootstrap, созданный Twitter. Если вы хотите увидеть своими глазами страницы, которые могут быть разработаны с этой платформой в документации есть несколько примеров.
Вот некоторые из преимуществ использования Bootstrap для стиля веб-страниц:
- Похожие отображения во всех основных веб-браузерах
- Обработка настольных, планшетных и телефонных размеров экрана
- Настраиваемые макеты
- Красиво стилизованные навигационные панели, формы, кнопки, оповещения, всплывающие окна и т.д.
Самый простой способ использования Bootstrap — просто импортировать файл Bootstrap.min.CSS
в базовый шаблон. Можно либо загрузить копию этого файла и добавить его в проект, либо импортировать непосредственно из CDN. После этого вы можете начать использовать классы CSS общего назначения, которые он предоставляет, в соответствии с документацией, что очень удобно. Можно импортировать файл Bootstrap.min.js
, содержащий JavaScript-код фреймворка, что позволяет использовать самые продвинутые функции.
К счастью, есть расширение Flask называется Flask-Bootstrap. Предоставляет готовый к использованию базовый шаблон, который включает в себя платформу Bootstrap. Давайте установим это расширение:
(venv) $ pip install flask-bootstrap
Использование Flask-Bootstrap
Flask-Bootstrap должен быть инициализирован, как и большинство других расширений Flask:
app/__init__.py: Инициализация Flask-Bootstrap.
# ...
from flask_bootstrap import Bootstrap
app = Flask(__name__)
# ...
bootstrap = Bootstrap(app)
При инициализации расширения шаблон bootstrap/base.html становится доступным в других шаблонах приложения по тегу extends
.
Как вы помните, я уже использую тег extends
с моим собственным базовым шаблоном, который позволяет мне формировать общие части страницы в одном месте. Шаблон base.html определяет панель навигации, которая включает несколько ссылок, а также экспортирует блок content
. Все остальные шаблоны в моем приложении наследуют элементы базового шаблона и предоставляют блоку content
основное содержимое страницы.
Итак, как же мне следует использовать базовый шаблон Bootstrap? Идея состоит в том, чтобы использовать трехуровневую иерархию вместо двух. Шаблон bootstrap/base.html предоставляет базовую структуру страницы на основе фреймворка Bootstrap. Этот шаблон экспортирует несколько блоков для производных шаблонов, таких как title
, navbar
и content
(см. Полный список блоков здесь). Я собираюсь изменить свой base.html-шаблон, чтобы использовать в нем решения из bootstrap/base.html и обеспечить реализации для заголовков(title
), навигационных блоков(navbar
) и блоков контента(content
). В свою очередь, base.html будет экспортировать собственный блок app_content
для своих производных шаблонов определяющих содержимое страницы.
Ниже видно, как выглядит base.html после модификации его в наследника базового шаблона Bootstrap. Обратите внимание, что эта структура не включает весь HTML для панели навигации, но вы можете найти полную реализацию на GitHub или загрузить код для этой главы.
app/templates/base.html: Переработанный базовый шаблон.
{% extends 'bootstrap/base.html' %}
{% block title %}
{% if title %}{{ title }} - Microblog{% else %}Welcome to Microblog{% endif %}
{% endblock %}
{% block navbar %}
<nav>
... здесь должна быть навигационная панель (см. полный код на GitHub) ...
</nav>
{% endblock %}
{% block content %}
<div>
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<div role="alert">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
{# содержимое приложения которое должно быть предоставлено в блоке app_content #}
{% block app_content %}{% endblock %}
</div>
{% endblock %}
Здесь видно, как я использую шаблон из bootstrap/base.html, за которым следуют три блока, которые реализуют заголовок страницы, панель навигации и содержимое страницы соответственно.
Блок title
определяет текст, который будет использоваться в качестве заголовка страницы, с тегами <title>
. Для этого блока я просто переместил логику, которая находилась внутри тега <title>
в исходном базовом шаблоне.
Блок navbar
является необязательным блоком, который обычно используется в качестве панели навигации. Для этого блока я адаптировал пример из документации на панель навигации Bootstrap, так что он включает в себя брендинг сайта, а затем ссылки Home и Explore. После чего я добавил ссылки профиля, входа или выхода из системы, выровненные с правой границей страницы. Как я уже упоминал выше, я опустил HTML в приведенном выше примере, но вы можете получить полный шаблон base.html из пакета загрузки для этой главы.
Наконец, в блоке content
я определяю контейнер верхнего уровня, и внутри него у меня есть логика, которая отображает свернутые сообщения, которые теперь будут отображаться в стиле оповещений Bootstrap. За ним следует новый блок app_content
, который определяется так, чтобы производные шаблоны могли определять свой собственный контент.
Исходная версия всех шаблонов страниц определяла их содержимое в блоке с именем content
. Как вы видели выше, блок с именем content
используется из Flask-Bootstrap, поэтому я переименовал свой content
-блок как app_content
. После чего, все мои шаблоны необходимо исправить, чтобы использовать app_content
в качестве своего content
-блока(содержимого). В качестве примера: так выглядит модифицированная версия шаблона 404.html:
app/templates/404.html: Переработанный шаблон ошибки 404.
{% extends "base.html" %}
{% block app_content %}
<h2>File Not Found</h2>
<p><a href="{{ url_for('index') }}">Back</a></p>
{% endblock %}
Bootstrap визуализация форм
Область, в которой Flask-Bootstrap делает фантастическую работу, — это использование форм. Вместо того, чтобы устанавливать поля формы одно за другим, Flask-Bootstrap пользуется макросом, который принимает объект формы Flask-WTF в качестве аргумента и отображает полную форму с использованием стилей Bootstrap.
Ниже вы можете увидеть переработанный шаблон register.html в качестве примера:
app/templates/register.html: Шаблон регистрации пользователя.
{% extends "base.html" %}
{% import 'bootstrap/wtf.html' as wtf %}
{% block app_content %}
<h2>Register</h2>
<div>
<div>
{{ wtf.quick_form(form) }}
</div>
</div>
{% endblock %}
Разве это не здорово? import
в верхней части примера работает аналогично импорту Python. Только в составе шаблона. Эта команда добавляет макрос wtf.quick_form()
, который в одной строке кода отображает полную форму, включая поддержку ошибок проверки отображения, и все в стиле Bootstrap.
Еще раз повторюсь, я не собираюсь показывать вам все изменения, которые я сделал для других форм в приложении. Но все эти изменения сделаны в том самом коде, который вы можете скачать или проверить на GitHub.
Визуализация сообщений в блогах
Логика представления, которая отображает отдельные сообщения в блоге, была абстрагирована в подшаблон под названием _post.html. Все, что мне нужно сделать с этим шаблоном, — это внести небольшие корректировки, чтобы он выглядел в стиле Bootstrap.
app/templates/_post.html: Переработанный суб-шаблон сообщений.
<table>
<tr>
<td>
<a href="{{ url_for('user', username=post.author.username) }}">
<img src="{{ post.author.avatar(70) }}" />
</a>
</td>
<td>
<a href="{{ url_for('user', username=post.author.username) }}">
{{ post.author.username }}
</a>
says:
<br>
{{ post.body }}
</td>
</tr>
</table>
Отображение ссылок на страницы ленты сообщений
Связывание страниц — это еще одна область, где Bootstrap обеспечивает прямую поддержку. Для этого я просто обратился еще раз к документации Bootstrap и адаптировал один из своих примеров. Вот как они выглядят на странице index.html:
app/templates/index.html: Переработанный вариант многостраничного отображения сообщений.
...
<nav aria-label="...">
<ul>
<li>
<a href="{{ prev_url or '#' }}">
<span aria-hidden="true">←</span> Newer posts
</a>
</li>
<li>
<a href="{{ next_url or '#' }}">
Older posts <span aria-hidden="true">→</span>
</a>
</li>
</ul>
</nav>
Обратите внимание, что в этой реализации вместо того, чтобы скрывать следующую или предыдущую ссылку, когда в этом направлении больше нет контента, я применяю disabled
состояние, которое сделает ссылку недоступной.
Я не буду показывать все здесь, но подобное изменение должно быть применено к user.html. Пакет загрузки для этой главы включает и эти изменения.
До и после
Чтобы обновить приложение, загрузите zip-файл для этой главы и аккуратненько сравните и обновите свои шаблоны.
Ниже приведены несколько снимков экрана до и после, чтобы продемонстрировать трансформацию. Имейте в виду, что эти изменение были достигнуты без изменений логики приложений!
P.S. От переводчика
На первом скрине приглашение имеет стандартную надпись ‘Please log in to access this page.’
Это умолчание модуля flask-login. Если вас эта надпись по каким то причинам не устраивает, то ее можно заменить собственной. Для этого требуется определить свойство login_message
экземпляра класса LoginManager
. Однако, следует помнить, что если вы добавляете кириллические символы в модуль, то необходимо, что бы первая строка устанавливала требуемую кодировку.
app/__init__.py: Доработанный вариант
.
# -*- coding: utf-8 -*-
…
login = LoginManager(app)
login.login_view = ‘login’
login.login_message = «Пожалуйста, войдите, чтобы открыть эту страницу.» # <--
я добавил эту строку
…
Туда Сюда
Обработка ошибок (издание 2018) / Хабр
blog.miguelgrinberg.com
Miguel Grinberg
<<< предыдущая следующая >>>
Эта статья является переводом седьмой части нового издания учебника Мигеля Гринберга, выпуск которого автор планирует завершить в мае 2018.Прежний перевод давно утратил свою актуальность.
Я, со своей стороны, постараюсь не отставать с переводом.
Это седьмая глава серии Flask Mega-Tutorial, в которой я расскажу вам, как выполнять обработку ошибок в приложении Flask.
Для справки ниже приведен список статей этой серии.
Примечание 1: Если вы ищете старые версии данного курса, это здесь.
Примечание 2: Если вдруг Вы хотели бы выступить в поддержку моей(Мигеля) работы в этом блоге, или просто не имеете терпения дожидаться неделю статьи, я (Мигель Гринберг)предлагаю полную версию данного руководства упакованную электронную книгу или видео. Для получения более подробной информации посетите learn.miguelgrinberg.com.
В этой главе я перехожу от кодирования новых функций для моего микроблогического приложения и вместо этого обсужу несколько стратегий борьбы с ошибками, которые неизменно появляются в любом программном проекте. Чтобы проиллюстрировать эту тему, я намеренно допустил ошибку в коде, который я добавил в главе 6. Прежде чем продолжить чтение, посмотрите, сможете ли вы его найти!
Ссылки GitHub для этой главы: Browse, Zip, Diff.
Что происходит, когда возникает ошибка в приложении Flask? Лучший способ узнать это — испытать это самому. Запустите приложение и убедитесь, что у вас зарегистрировано не менее двух пользователей. Войдите в систему как один из пользователей, откройте страницу профиля и нажмите ссылку «Изменить». В редакторе профиля попробуйте изменить имя пользователя на существующее имя другого пользователя, который уже зарегистрирован, и попытайтесь применить исправления! Это приведет к появлению страшной страницы «Internal Server Error» ( «Внутренняя ошибка сервера» ):
В сеансе терминала, на котором запущено приложение, вы видите трассировку стека ошибки. Трассировки стека чрезвычайно полезны при отладке ошибок, поскольку они показывают последовательность вызовов в этом стеке, вплоть до строки, вызвавшей ошибку:
(venv) $ flask run
* Serving Flask app "microblog"
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
[2017-09-14 22:40:02,027] ERROR in app: Exception on /edit_profile [POST]
Traceback (most recent call last):
File "/home/miguel/microblog/venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1182, in _execute_context
context)
File "/home/miguel/microblog/venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 470, in do_execute
cursor.execute(statement, parameters)
sqlite3.IntegrityError: UNIQUE constraint failed: user.username
Трассировка стека указывает, чем вызвана ошибка. Приложение позволяет пользователю изменять имя пользователя без проверки, что новое имя пользователя не совпадает с другим пользователем, уже находящимся в системе. Ошибка возникает из SQLAlchemy, которая пытается записать новое имя пользователя в базу данных, но база данных отвергает его, потому что столбец имени пользователя определен с unique = True.
Важно, что страница с ошибкой, представленная пользователю, не содержит много информации об ошибке, и это правильно. Я определенно не хочу, чтобы пользователи узнали, что авария была вызвана ошибкой базы данных или какой базой данных я пользуюсь, а также именами таблиц и полей в моей базе данных. Вся эта информация должна быть внутренней.
Есть несколько вещей, которые далеки от идеала. У меня есть страница с ошибкой, которая безобразна и не соответствует макету приложения. У меня также есть важные трассировки стека приложений, которые сбрасываются на терминале, и мне нужно постоянно следить за тем, чтобы я не пропустил никаких ошибок. И, конечно, у меня есть ошибка. Я собираюсь решить все эти проблемы, но сначала поговорим о режиме отладки Flask.
Режим отладки
То, как ошибки обрабатываются выше, отлично подходит для системы, которая работает на production сервере. Если есть ошибка, пользователь получает страницу с неопределенной ошибкой (хотя я собираюсь сделать эту страницу с ошибкой более приятной), а важные данные об ошибке — в выводе сервера или в файле журнала.
Но когда вы разрабатываете приложение, вы можете включить режим отладки, режим, в котором Flask выводит действительно хороший отладчик непосредственно в ваш браузер. Чтобы активировать режим отладки, остановите приложение, а затем установите следующую переменную среды:
(venv) $ export FLASK_DEBUG=1
Если вы работаете в ОС Microsoft Windows, не забудьте использовать set
вместо экспорта.
После того, как вы установили FLASK_DEBUG, перезапустите сервер. Строки на вашем терминале будут немного отличаться от того, что вы привыкли видеть:
(venv) microblog2 $ flask run
* Serving Flask app "microblog"
* Forcing debug mode on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 177-562-960
Теперь устроим приложению аварийный сбой еще раз, чтобы увидеть интерактивный отладчик в вашем браузере:
Отладчик позволяет развернуть каждый уровень стека и увидеть соответствующий исходный код. Вы также можете открыть Python для любого из фреймов и выполнить любые допустимые выражения Python, например, чтобы проверить значения переменных.
Крайне важно, чтобы вы никогда не запускали приложение Flask в режиме отладки на рабочем сервере. Отладчик позволяет удаленно выполнять код на сервере, поэтому он может стать неожиданным подарком злоумышленнику, который хочет проникнуть в ваше приложение или на ваш сервер. В качестве дополнительной меры безопасности отладчик, запущенный в браузере, закроется, и при первом использовании запросит PIN-код, который вы можете увидеть на выходе команды flask run
.
Поскольку я говорю о режиме отладки, следует упомянуть про вторую важную функцию, которая включена в режиме отладки — перезагрузка. Это очень полезная функция разработки, которая автоматически перезапускает приложение при изменении исходного файла. Если вы выполните flask run
в режиме отладки, можно продолжать работать в своем приложении и при каждом сохранении файла, приложение перезапустится, чтобы забрать новый код.
Пользовательские страницы ошибок
Flask предоставляет механизм приложения для создания собственных страниц ошибок, так что вашим пользователям не нужно видеть простые и скучные значения по умолчанию. В качестве примера давайте определим пользовательские страницы ошибок для ошибок HTTP 404 и 500, двух наиболее распространенных. Определение страниц для других ошибок работает одинаково.
Чтобы объявить пользовательский обработчик ошибок, используется декоратор @errorhandler
. Я собираюсь поместить обработчики ошибок в новый модуль app/errors.py.
from flask import render_template
from app import app, db
@app.errorhandler(404)
def not_found_error(error):
return render_template('404.html'), 404
@app.errorhandler(500)
def internal_error(error):
db.session.rollback()
return render_template('500.html'), 500
Функции ошибок работают аналогично функциям просмотра. Для этих двух ошибок я возвращаю содержимое их соответствующих шаблонов. Обратите внимание, что обе функции возвращают второе значение после шаблона, который является номером кода ошибки. Для всех функций представления, которые я создал до сих пор, мне не нужно было добавлять второе возвращаемое значение, потому что по умолчанию 200 (код состояния для успешного завершения) — это то, что я хотел. Сейчас это страницы с ошибками, поэтому я хочу, чтобы код состояния ответа это отражал.
Обработчик ошибок для 500-й ошибки может быть вызван после возникновения сбоя базы данных, которая на самом деле была вызвана умышленным случаем дубликата имени пользователя. Чтобы убедиться, что неудачные сеансы базы данных не мешают доступу к базе данных, вызванным шаблоном, я выдаю откат сеанса. Это сбрасывает сеанс в чистое состояние.
Вот шаблон для ошибки 404:
{% extends "base.html" %}
{% block content %}
<h2>File Not Found</h2>
<p><a href="{{ url_for('index') }}">Back</a></p>
{% endblock %}
И вот одна из ошибок 500:
{% extends "base.html" %}
{% block content %}
<h2>An unexpected error has occurred</h2>
<p>The administrator has been notified. Sorry for the inconvenience!</p>
<p><a href="{{ url_for('index') }}">Back</a></p>
{% endblock %}
Оба шаблона наследуют шаблон base.html
, так что страница с ошибками имеет тот же внешний вид, что и обычные страницы приложения.
Чтобы получить эти обработчики ошибок, зарегистрированные в Flask, мне нужно импортировать новый модуль app/errors.py
после создания экземпляра приложения:
# ...
from app import routes, models, errors
Если вы установили FLASK_DEBUG = 0
в сеансе терминала и затем снова вызвали ошибку повторного имени пользователя, вы увидите более приятную страницу с ошибкой.
Или так! Рекомендую придумать что то свое в качестве упражнения.
Отправка ошибок по электронной почте
Другая проблема с обработкой ошибок по умолчанию, предоставляемой Flask, заключается в том, что нет уведомлений! Трассировка стека ошибки печатается на терминале, а это означает, что вывод процесса сервера должен контролироваться на обнаружение ошибок. Когда вы запускаете приложение во время разработки, это нормально, но как только приложение будет развернуто на production сервере, никто не будет смотреть на результат, поэтому необходимо создать более надежное решение.
Я думаю, что очень важно, чтобы я активно реагировал на ошибки. Если в production версии приложения возникает ошибка, я хочу знать сразу. Таким образом, моим первым решением будет сконфигурировать Flask для отправки мне сообщения по email сразу после возникновения ошибки с трассировкой стека ошибки в сообщении электронной почты.
Первым шагом является добавление данных сервера электронной почты в файл конфигурации:
class Config(object):
# ...
MAIL_SERVER = os.environ.get('MAIL_SERVER')
MAIL_PORT = int(os.environ.get('MAIL_PORT') or 25)
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS') is not None
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
ADMINS = ['[email protected]']
Переменные конфигурации для электронной почты содержат сервер и порт, флаг для включения зашифрованных соединений и необязательное имя пользователя и пароль. Пять переменных конфигурации получены из их сопоставлений переменным среды. Если сервер электронной почты не установлен в среде, то я буду использовать это как знак того, что ошибки электронной почты должны быть отключены. Порт сервера электронной почты также можно указать в переменной среды, но если он не установлен, используется стандартный порт 25. Учетные данные почтового сервера по умолчанию не используются, но могут быть предоставлены при необходимости. Переменная конфигурации ADMINS
представляет собой список адресов электронной почты, которые будут получать отчеты об ошибках, поэтому ваш собственный адрес электронной почты должен быть в этом списке.
Flask использует пакет logging
Python для ведения своих журналов, а этот пакет уже имеет возможность отправлять журналы по электронной почте. Все, что мне нужно сделать, чтобы отправлять электронные сообщения, содержащие ошибки, — это добавить экземпляр SMTPHandler в объект журнала Flask, которым является app.logger
:
import logging
from logging.handlers import SMTPHandler
# ...
if not app.debug:
if app.config['MAIL_SERVER']:
auth = None
if app.config['MAIL_USERNAME'] or app.config['MAIL_PASSWORD']:
auth = (app.config['MAIL_USERNAME'], app.config['MAIL_PASSWORD'])
secure = None
if app.config['MAIL_USE_TLS']:
secure = ()
mail_handler = SMTPHandler(
mailhost=(app.config['MAIL_SERVER'], app.config['MAIL_PORT']),
fromaddr='no-reply@' + app.config['MAIL_SERVER'],
toaddrs=app.config['ADMINS'], subject='Microblog Failure',
credentials=auth, secure=secure)
mail_handler.setLevel(logging.ERROR)
app.logger.addHandler(mail_handler)
Как видно, я включил регистратор электронной почты только, когда приложение работает без режима отладки, что определено приложением в app.debug
как True
, а также когда сервер электронной почты существует в конфигурации.
Настройка почтового регистратора несколько утомительна из-за необходимости обрабатывать дополнительные параметры безопасности, которые присутствуют на многих серверах электронной почты. Но в сущности, вышеприведенный код создает экземпляр SMTPHandler
, устанавливает его уровень, чтобы он отправлял только сообщения об ошибках, а не предупреждения, информационные или отладочные сообщения и, наконец, прикреплял их к app.logger
из Flask.
Существует два подхода к проверке работоспособности этой функции. Самый простой способ — использовать SMTP-сервер отладки от Python. Это ложный почтовый сервер, который принимает сообщения электронной почты, но вместо их отправки выводит их на консоль. Чтобы запустить этот сервер, откройте второй сеанс терминала и запустите на нем следующую команду:
(venv) $ python -m smtpd -n -c DebuggingServer localhost:8025
Оставьте запущенный SMTP-сервер отладки и вернитесь к своему первому терминалу и установите export
MAIL_SERVER = localhost
и MAIL_PORT = 8025
(используйте set
вместо export
, если вы используете Microsoft Windows). Убедитесь, что для переменной FLASK_DEBUG
установлено значение 0
или не установлено вообще, так как приложение не будет отправлять электронные письма в режиме отладки.
Запустите приложение и вызовите ошибку SQLAlchemy еще раз, чтобы узнать, как сеанс терминала, на котором работает поддельный почтовый сервер, показывает электронное письмо с полным содержимым стека ошибки.
Второй метод тестирования для этой функции — настроить настоящий почтовый сервер. Ниже приведена конфигурация для использования почтового сервера для учетной записи Gmail:
export MAIL_SERVER=smtp.googlemail.com
export MAIL_PORT=587
export MAIL_USE_TLS=1
export MAIL_USERNAME=<your-gmail-username>
export MAIL_PASSWORD=<your-gmail-password>
Если вы используете Microsoft Windows, не забудьте использовать set
вместо export
в каждой из приведенной выше инструкции.
Функции безопасности вашей учетной записи Gmail могут препятствовать приложению отправлять электронную почту через нее, если вы явно не разрешаете «less secure apps» («менее безопасным приложениям») доступ к вашей учетной записи Gmail. Прочитать об этом можно здесь, и если вас беспокоит безопасность вашей учетной записи, можно создать вторичную учетную запись, которую настройте только для проверки электронной почты, или временно включите разрешение для менее безопасных приложений на время запуска этого теста, а затем вернитесь к умолчанию.
Запись лога в файл
Получение ошибок по электронной почте полезно, но иногда недостаточно. Есть некоторые случаи сбоя, которые не описываются исключением Python и не являются серьезной проблемой, но они все равно могут быть достаточно интересными для сохранения в целях отладки. По этой причине я также буду поддерживать логфайл для приложения.
Чтобы включить ведение журнала другого обработчика, на этот раз типа RotatingFileHandler
необходимо включить logger приложения аналогично обработчику электронной почты.
# ...
from logging.handlers import RotatingFileHandler
import os
# ...
if not app.debug:
# ...
if not os.path.exists('logs'):
os.mkdir('logs')
file_handler = RotatingFileHandler('logs/microblog.log', maxBytes=10240,
backupCount=10)
file_handler.setFormatter(logging.Formatter(
'%(asctime)s %(levelname)s: %(message)s [in %(pathname)s:%(lineno)d]'))
file_handler.setLevel(logging.INFO)
app.logger.addHandler(file_handler)
app.logger.setLevel(logging.INFO)
app.logger.info('Microblog startup')
Я пишу логфайл с именем microblog.log
в каталоге logs, который я создаю, если он еще не существует.
Класс RotatingFileHandler
удобен, потому что он переписывает журналы, гарантируя, что файлы журнала не будут слишком большими, если приложение работает в течение длительного времени. В этом случае я ограничиваю размер логфайла 10 КБ, и храню последние десять файлов журнала в качестве резервных копий.
Класс logging.Formatter
предоставляет настройку формата сообщений журнала. Поскольку эти сообщения отправляются в файл, я хочу, чтобы они содержали как можно больше информации. Поэтому я использую формат, который включает отметку времени, уровень ведения журнала,
сообщение, исходный файл и номер строки, откуда возникла запись в журнале.
Чтобы сделать регистрацию более полезной, я также понижаю уровень ведения журнала до категории INFO
, как в регистраторе приложений, так и в обработчике файлов. Если вы не знакомы с категориями ведения журнала, это DEBUG
, INFO
, WARNING
,ERROR
и CRITICAL
в порядке возрастания степени тяжести.
В качестве первого полезного использования логфайла сервер записывает строку в журнал каждый раз, когда он запускается. Когда приложение запускается на production сервере, эти записи журнала сообщают вам, когда сервер был перезапущен.
Исправление дубля имени пользователя
Я слишком долго использовал ошибку дублирования имени пользователя. Теперь, когда я показал вам, как подготовить приложение для обработки подобных ошибок, я могу наконец-то это исправить.
Если вы помните, RegistrationForm
уже выполняет проверку для имен пользователей, но требования формы редактирования немного отличаются. Во время регистрации мне нужно убедиться, что имя пользователя, введенное в форму, не существует в базе данных. В форме профиля редактирования я должен выполнить ту же проверку, но с одним исключением. Если пользователь оставляет исходное имя пользователя нетронутым, то проверка должна его разрешить, поскольку это имя пользователя уже назначено этому пользователю. Ниже вы можете увидеть, как я выполнил проверку имени пользователя для этой формы:
class EditProfileForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
about_me = TextAreaField('About me', validators=[Length(min=0, max=140)])
submit = SubmitField('Submit')
def __init__(self, original_username, *args, **kwargs):
super(EditProfileForm, self).__init__(*args, **kwargs)
self.original_username = original_username
def validate_username(self, username):
if username.data != self.original_username:
user = User.query.filter_by(username=self.username.data).first()
if user is not None:
raise ValidationError('Please use a different username.')
Реализация выполняется в специальном методе проверки, функция super в конструкторе класса, который принимает исходное имя пользователя в качестве аргумента. Это имя пользователя сохраняется как переменная экземпляра и проверяется в методе validate_username()
. Если имя пользователя, введенное в форму, совпадает с исходным именем пользователя, то нет причин проверять базу данных на наличие дубликатов.
Чтобы использовать этот новый метод проверки, мне нужно добавить исходный аргумент имени пользователя в функцию вида, где создается объект формы:
@app.route('/edit_profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
form = EditProfileForm(current_user.username)
# ...
Теперь ошибка исправлена, и дубликаты в форме профиля редактирования будут предотвращены в большинстве случаев. Это не идеальное решение, поскольку оно может не работать, когда два или несколько процессов одновременно обращаются к базе данных. В этой ситуации состояние гонки может привести к валидации, но спустя мгновение при попытке переименования база данных уже была изменена другим процессом и не может переименовать пользователя. Это несколько маловероятно, за исключением очень занятых приложений, у которых много серверных процессов, поэтому я пока не буду беспокоиться об этом.
На этом этапе вы можете попытаться воспроизвести ошибку еще раз, чтобы увидеть, как ее предотвращает метод проверки формы.
<<< предыдущая следующая >>>
P.S.
Работа над ошибками
От переводчика
Решил я проверить получение сообщений ошибки админу на почту. Для этого я испортил модуль routes.py
. Для этой самой «порчи», я закомментировал декоратор @app.route('/edit_profile', methods=['GET', 'POST'])
перед def edit_profile()
. В итоге получил ошибку и в файл лога все это вывалилось, а вот письмо не прилетело. Я использую Python 3.3. Возможно в более новых версиях этого и не случится. Но в Windows 7 с русской раскладкой это случилось.
При попытке отправить сообщение админу приложение получило ошибку кодировки при формировании сообщения. В окне консоли содержались такие строки:
Как видим ссылка указывает на директорию в стандартном питоне, а не в виртуальном окружении.
logging
в 3-й версии является стандартной библиотекой Python, поэтому вам не нужно устанавливать ее используя pip
.
Про стандартные модули
И модуль протоколирования, который вы можете найти в PyPI, устаревший, а не Python3-совместимый.
(Согласно файлу README его последняя версия была выпущена 02 марта 2005 года.)
Поэтому просто не пытайтесь установить logging.
Возьмите новый модуль в стандартной библиотеке как должное. Если вам принципиально использовать его в виртальной библиотеке.
После копии в venv\Lib logging
импортируется из виртуальной среды
Еще раз получаю ошибку
logging
теперь виртуальный. А вот smtplib
стандартный.
Не думаю, что надо тащить все библиотеки из стандартной среды в виртуальную.
Ошибка от этого не исчезнет.
Про стандартный модуль email
Проблема с кодировкой в сообщении решается использованием стандартного пакета email
для создания сообщения с указанием предпочитаемой кодировки.
Вот пример с просторов интернета для этого пакета :
# -*- coding: utf-8 -*-
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import quopri
def QuoHead(String):
s = quopri.encodestring(String.encode('UTF-8'), 1, 0)
return "=?utf-8?Q?" + s.decode('UTF-8') + "?="
FIOin = "Хрюша Степашкин"
emailout = "[email protected]"
emailin = "[email protected]"
msg = MIMEMultipart()
msg["Subject"] = QuoHead("Добрый день " + FIOin).replace('=\n', '')
msg["From"] = (QuoHead("Каркуша Федоровна") + " <" + emailout + ">").replace('=\n', '')
msg["To"] = (QuoHead(FIOin) + " <" + emailin + ">").replace('=\n', '')
m = """Добрый день.
Это тестовое письмо.
Пожалуйста, не отвечайте на него."""
text = MIMEText(m.encode('utf-8'), 'plain', 'UTF-8')
msg.attach(text)
print(msg.as_string())
Но, как это применить для отправки сообщений об ошибке?!
Может кто-то предложит в комментариях к статье.
В модуле flask-mail
эта ситуевина вроде как поправлена. Но тут используется logging
и smtplib
В итоге пока так. Поправил я строку в модуле smtplib.py
.
Добавил encode('utf-8')
И после перезапуска сервера при искусственной ошибке я, наконец-то, получил сообщение на почту.
<<< предыдущая следующая >>>
Quickstart — Flask Documentation (1.1.x)
Хотите начать? Эта страница дает хорошее представление о Flask. Это
предполагает, что у вас уже установлен Flask. Если вы этого не сделаете, перейдите к
Раздел установки.
Минимальное приложение
Минимальное приложение Flask выглядит примерно так:
из флакона импортного флакона app = Flask (__ имя__) @ app.route ('/') def hello_world (): верните "Hello, World!"
Так что же делал этот код?
Сначала мы импортировали
Flask
class.Пример этого
class будет нашим приложением WSGI.Затем мы создаем экземпляр этого класса. Первый аргумент — это имя
модуль или пакет приложения. Если вы используете один модуль (как
в этом примере) следует использовать__name__
, потому что в зависимости от того,
запущен как приложение или импортирован как модуль имя будет другим
('__main__'
по сравнению с фактическим именем импорта). Это нужно для того, чтобы
Flask знает, где искать шаблоны, статические файлы и т. Д.Для большего
информацию см. в документацииFlask
.Затем мы используем декоратор
route ()
, чтобы сообщить Flask, какой URL
должен вызвать нашу функцию.Функция получает имя, которое также используется для генерации URL-адресов для этого
конкретной функции и возвращает сообщение, которое мы хотим отобразить в
браузер пользователя.
Просто сохраните его как hello.py
или что-то подобное. Не звони
ваше приложение колба.py
, потому что это будет конфликтовать с Flask
сам.
Для запуска приложения вы можете использовать команду flask или
переключатель python -m
с Flask. Прежде чем вы сможете это сделать, вам нужно
чтобы сообщить вашему терминалу, с каким приложением работать, экспортируя
FLASK_APP
переменная среды:
$ экспорт FLASK_APP = hello.py $ flask run * Работает на http://127.0.0.1:5000/
Если вы работаете в Windows, синтаксис переменной среды зависит от командной строки.
переводчик.В командной строке:
C: \ path \ to \ app> установить FLASK_APP = hello.py
И в PowerShell:
PS C: \ путь \ к \ приложению> $ env: FLASK_APP = "hello.py"
В качестве альтернативы вы можете использовать python -m flask :
$ экспорт FLASK_APP = hello.py $ python -m запуск фляги * Работает на http://127.0.0.1:5000/
Это запускает очень простой встроенный сервер, который достаточно хорош для тестирования.
но, вероятно, не то, что вы хотите использовать в производстве. Варианты развертывания см.
Варианты развертывания.
Теперь перейдите на http://127.0.0.1:5000/, и вы должны увидеть свой привет
мир приветствия.
Внешний видимый сервер
Если вы запустите сервер, вы заметите, что сервер доступен только
со своего компьютера, а не с любого другого компьютера в сети. Это
по умолчанию, потому что в режиме отладки пользователь приложения может выполнять
произвольный код Python на вашем компьютере.
Если у вас отключен отладчик или вы доверяете пользователям в своей сети,
можно сделать сервер pub
.Установка
— Документация Flask (1.1.x)
Версия Python
Мы рекомендуем использовать последнюю версию Python 3. Flask поддерживает Python 3.5.
и новее, Python 2.7 и PyPy.
Зависимости
Эти дистрибутивы будут установлены автоматически при установке Flask.
Werkzeug реализует WSGI, стандартный интерфейс Python между
приложения и серверы.Jinja — это язык шаблонов, который отображает страницы вашего приложения.
служит.MarkupSafe поставляется с Jinja. Он избегает ненадежного ввода при рендеринге
шаблоны, чтобы избежать инъекционных атак.ItsDangerous надежно подписывает данные для обеспечения их целостности. Это используется
для защиты файла cookie сеанса Flask.Click — это платформа для написания приложений командной строки. Это обеспечивает
командаflask
и позволяет добавлять собственные команды управления.
Необязательные зависимости
Эти дистрибутивы не будут установлены автоматически.Flask обнаружит и
используйте их, если вы их устанавливаете.
Виртуальные среды
Используйте виртуальную среду для управления зависимостями вашего проекта, как в
разработка и производство.
Какую проблему решает виртуальная среда? Чем больше Python проектов вы
есть, тем более вероятно, что вам нужно работать с разными версиями
Библиотеки Python или даже сам Python. Новые версии библиотек за одного
проект может нарушить совместимость в другом проекте.
Виртуальные среды — это независимые группы библиотек Python, по одной для каждой.
проект. Пакеты, установленные для одного проекта, не повлияют на другие проекты или
пакеты операционной системы.
Python 3 поставляется в комплекте с модулем venv
для создания виртуальных
среды. Если вы используете современную версию Python, вы можете продолжить
к следующему разделу.
Если вы используете Python 2, см. Сначала установите virtualenv.
Создать среду
Создайте папку проекта и папку venv
в пределах:
$ mkdir myproject $ cd myproject $ python3 -m venv venv
В Windows:
Если вам нужно установить virtualenv, потому что вы используете Python 2, используйте
вместо этого введите следующую команду:
$ python2 -m virtualenv venv
В Windows:
> \ Python27 \ Scripts \ virtualenv.exe venv
.