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

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

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
Java Team Lead
Москва, по итогам собеседования
Senior Java Developer
Москва, по итогам собеседования

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