Третий этап
В заключительной части туториала мы рассмотрим основные аспекты работы с формами WTForms, разработаем функции CRUD и напишем функцию для экспорта информации из базы данных. Код для первого и второго этапов разработки есть здесь.
Валидация форм WTForms
Для получения пользовательских данных со стороны фронтенда в Flask-приложениях обычно используют формы WTForms. Эти формы «из коробки» предоставляют отличные опции для валидации введенных данных. Расширение функциональности форм за счет макросов и дополнительных валидаторов тоже не вызывает никаких сложностей, как мы это увидим позже.
Для взаимодействия с приложением нам потребуются две формы – BookForm и UpdateBook. Первая отвечает за создание новой карточки, вторая – за редактирование существующей записи. Обложки книг можно загружать как во время создания карточки, так и после – в процессе редактирования. Обратите внимание на формат использования валидаторов:
- DataRequired – не даст отправить форму, если поле останется незаполненным.
- Length(min=5, max=100) – минимальная длина строки в поле – 5 символов, максимальная – 100.
- FileAllowed(['jpg', 'png']) – разрешает присоединять к форме только изображения .jpg и .png.
- NumberRange(min=1, max=5)]) – обеспечивает ввод оценки для книги в диапазоне от 1 до 5 включительно.
Кроме стандартных валидаторов, мы будем использовать один пользовательский:
def validate_title(self, title):
title = Book.query.filter_by(title=title.data).first()
if title:
raise ValidationError('Такая книга уже есть в списке прочитанных.')
Этот валидатор предотвращает создание карточки с названием книги, которое уже есть в базе. Без него приложение остановится с ошибкой:

Эта ошибка связана с атрибутом unique=True
столбца title
и ее проще предотвратить, чем обрабатывать. Но обработку мы тоже рассмотрим ниже – она будет реализована в функции представления.
Шаблоны для форм
Формам для создания и редактирования записей нужны шаблоны. Здесь есть код для create.html и edit.html. Шаблон create.html предусматривает вывод ошибок:
{% if form.genre.errors %}
{% for error in form.genre.errors %}
<span class="text-danger">{{ error }}</span></br>
{% endfor %}
{% endif %}
Для ввода и обработки разных типов информации используются различные классы:
- class="form-control-label" – однострочное текстовое поле;
- class="form-control-textarea" – многострочное текстовое поле;
- class="form-control-number" – ввод цифр;
- class="form-control-file" – выбор файла.
В шаблоне edit.html используется предварительное заполнение формы, уже внесенной в карточку информацией, – чтобы пользователю было проще ее редактировать. Эту информацию отправляет в форму функция edit
, которую мы рассмотрим чуть позже. Для генерации шаблона нужен вспомогательный макрос – он находится в файле formhelpers.html. Этот файл необходимо поместить в папку /templates вместе с обычными шаблонами. В шаблоне для редактирования есть блок для вывода сообщений об ошибках базы данных, получаемых от Flask и SQLAlchemy (остальные ошибки валидации обрабатывает WTForms):
{% with messages = get_flashed_messages() %}
{% if messages %}
{% for message in messages %}
<span class="text-danger">{{ message }}</span>
{% endfor %}
{% endif %}
{% endwith %}
Примечание: для создания и редактирования записей при желании можно использовать один и тот же шаблон. Если вставить в шаблон edit.html код из create.html, он точно так же получит существующие данные из функции представления. Кроме того, часто для редактирования и создания записей используют один и тот же код в одном и том же файле. В данном случае мы используем рендеринг формы редактирования с помощью макроса из _formhelpers.html просто в образовательных целях.
Обработка данных из форм
Для создания, редактирования и удаления записей нам нужно написать соответствующие функции в файле routes.py. Кроме того, нужно сделать функцию для безопасной загрузки изображений. Начнем с импорта форм и модуля Pillow, который обеспечит автоматическое сжатие обложек и сохранение файлов в нужную папку uploads:
from reader.forms import BookForm, UpdateBook
from PIL import Image
Фласку тоже потребуется импорт дополнительных модулей – flash, url_for и redirect. Pillow нужны модули os и secrets, а для обработки ошибок базы понадобится IntegrityError.
Функция для обработки и сохранения обложек книг выглядит так:
def save_picture(cover):
random_hex = secrets.token_hex(8)
_, f_ext = os.path.splitext(cover.filename)
picture_fn = random_hex + f_ext
picture_path = os.path.join(app.root_path, app.config['UPLOAD_FOLDER'], picture_fn)
output_size = (220, 340)
i = Image.open(cover)
i.thumbnail(output_size)
i.save(picture_path)
return picture_fn
Функция создания новых записей выглядит так:
@app.route('/create/', methods=('GET', 'POST'))
def create():
form = BookForm()
if form.validate_on_submit():
if form.cover.data:
cover = save_picture(form.cover.data)
else:
cover ='default.jpg'
title = form.title.data
author = form.author.data
genre = form.genre.data
rating = int(form.rating.data)
description = form.description.data
notes = form.notes.data
book = Book(title=title,
author=author,
genre=genre,
rating=rating,
cover=cover,
description=description,
notes=notes)
db.session.add(book)
db.session.commit()
return redirect(url_for('index'))
return render_template('create.html', form=form)
Если во время создания новой записи обложка не будет загружена, в карточке будет отображаться изображение по умолчанию. Как видно по приведенному выше коду, в базе данных сохраняется только имя файла изображения, сами картинки загружаются в папку uploads.
Функция для редактирования отправляет в форму уже существующие в карточке данные, чтобы форма не была пустой, и обрабатывает ошибку IntegrityError
– она возникает, если в процессе редактирования существующей карточки пользователь вводит название книги, которое уже есть в базе:
@app.route('/<int:book_id>/edit/', methods=('GET', 'POST'))
def edit(book_id):
book = Book.query.get_or_404(book_id)
form = UpdateBook()
if form.validate_on_submit():
if form.cover.data:
cover = save_picture(form.cover.data)
else:
cover = book.cover
book.title = form.title.data
book.author = form.author.data
book.genre = form.genre.data
book.rating = int(form.rating.data)
book.description = form.description.data
book.notes = form.notes.data
try:
db.session.commit()
return redirect(url_for('index'))
except IntegrityError:
db.session.rollback()
flash('Произошла ошибка: такая книга уже есть в базе', 'error')
return render_template('edit.html', form=form)
Удалить карточку книги проще простого:
@app.post('/<int:book_id>/delete/')
def delete(book_id):
book = Book.query.get_or_404(book_id)
db.session.delete(book)
db.session.commit()
return redirect(url_for('index'))
Для подтверждения удаления карточки будет использоваться всплывающее окно – код для этого уже есть в шаблонах. Осталось добавить в base.html, index.html, book.html, best.html и thrillers.html ссылки на соответствующие операции по созданию, редактированию и удалению карточек:
{{ url_for('create') }}
{{ url_for('edit', book_id=book.id) }}
{{ url_for('delete', book_id=book.id) }}
Теперь карточки можно удалять:

Редактировать:

И создавать:

Экспорт данных из базы
Информацию из наполненной базы данных можно экспортировать самыми разными способами. Мы рассмотрим простой и практичный метод, который не требует установки никаких дополнительных модулей. Файл json, который мы использовали в первой части туториала, был создан именно таким способом.
Для реализации метода нужно внести небольшое дополнение в файл моделей /reader/models.py и написать функцию для /reader/routes.py. Сначала дополним класс Book
:
from dataclasses import dataclass
@dataclass
class Book(db.Model):
id: int
title: str
author: str
genre: str
cover: str
rating: int
description: str
notes: str
created_at: str
Эти данные будут экспортированы. Если не нужен ID записи, или время создания, или еще что-нибудь – соответствующие строки можно удалить.
Добавим импорт jsonify
и функцию экспорта в /reader/routes.py:
@app.route('/export/')
def data():
data = Book.query.all()
return jsonify(data)
В файл __init__.py добавим параметр app.config['JSON_AS_ASCII'] = False
– иначе jsonify вместо кириллицы экспортирует абракадабру.
Все готово: если перейти по адресу http://localhost:8000/export/, можно увидеть все содержимое базы в виде словаря:

На этом работа над приложением закончена. Очевидно, что функциональность SQLAlchemy значительно упростила процесс разработки. И хотя SQLAlchemy – не единственная библиотека, предоставляющая Flask-приложениям все преимущества ORM, ее уверенно можно назвать самой понятной и гибкой, чем и объясняется ее популярность. Напоминаем, что финальная версия кода находится здесь.
Комментарии
Бомбовый урок,спасибо! Мало таких тем по flask в русскоязычном сегменте
Супер урок! Обидно что нет комментариев
Спасибо:). Проекты на Джанго комментируют чаще - он популярнее у русскоязычной аудитории. Но Фласк, я считаю, тоже классный фреймворк, и для многих проектов подходит даже лучше, чем Джанго.