🐍📚 Создаем аналог LiveLib.ru на Flask. Часть 2: CRUD, IntegrityError и валидация WTForms

В заключительной части: реализуем набор операций для создания, редактирования и удаления записей; обеспечиваем автоматическое сжатие загружаемых обложек до нужного размера с помощью Pillow.
🐍📚 Создаем аналог LiveLib.ru на Flask. Часть 2: CRUD, IntegrityError и валидация WTForms

Третий этап

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

Валидация форм WTForms

Для получения пользовательских данных со стороны фронтенда в Flask-приложениях обычно используют формы WTForms. Эти формы «из коробки» предоставляют отличные опции для валидации введенных данных. Расширение функциональности форм за счет макросов и дополнительных валидаторов тоже не вызывает никаких сложностей, как мы это увидим позже.

Для взаимодействия с приложением нам потребуются две формы – BookForm и UpdateBook. Первая отвечает за создание новой карточки, вторая – за редактирование существующей записи. Обложки книг можно загружать как во время создания карточки, так и после – в процессе редактирования. Обратите внимание на формат использования валидаторов:

  1. DataRequired – не даст отправить форму, если поле останется незаполненным.
  2. Length(min=5, max=100) – минимальная длина строки в поле – 5 символов, максимальная – 100.
  3. FileAllowed(['jpg', 'png']) – разрешает присоединять к форме только изображения .jpg и .png.
  4. NumberRange(min=1, max=5)]) – обеспечивает ввод оценки для книги в диапазоне от 1 до 5 включительно.

Кроме стандартных валидаторов, мы будем использовать один пользовательский:

/reader/forms.py
        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 предусматривает вывод ошибок:

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):

edit.html 
        {% 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:

routes.py
        from reader.forms import BookForm, UpdateBook
from PIL import Image

    

Фласку тоже потребуется импорт дополнительных модулей – flash, url_for и redirect. Pillow нужны модули os и secrets, а для обработки ошибок базы понадобится IntegrityError.

Функция для обработки и сохранения обложек книг выглядит так:

routes.py
        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

    

Функция создания новых записей выглядит так:

routes.py
        @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– она возникает, если в процессе редактирования существующей карточки пользователь вводит название книги, которое уже есть в базе:

routes.py
        @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)
    

Удалить карточку книги проще простого:

routes.py
        @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:

/reader/models.py 
        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:

/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, ее уверенно можно назвать самой понятной и гибкой, чем и объясняется ее популярность. Напоминаем, что финальная версия кода находится здесь.

Материалы по теме

Комментарии

ВАКАНСИИ

Добавить вакансию
Разработчик C++
Москва, по итогам собеседования

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ