Создаем Flask-приложение с базой данных. Веб-приложение для хранения информации о прочитанных книгах на Flask
Изучаем взаимодействие Flask с SQLAlchemy и WTForms, создавая веб-приложение — лайт-версию сервиса LiveLib.ru — для хранения информации о прочитанных книгах. Реализуем CRUD, пагинацию, фильтры и экспорт данных. Любое более-менее серьезное веб-приложение использует базу данных для хранения полученной от фронтенда информации. Для упрощения взаимодействия Flask-приложений с базой чаще всего используют библиотеку SQLAlchemy, а для получения и валидации данных пользователя – формы WTForms. Это приложение для ведения списка прочитанных книг. Для каждой книги создается отдельная карточка с постером, именем автора, названием жанра, описанием сюжета, оценкой и примечаниями. Карточки можно редактировать и удалять. Весь код проекта находится здесь. Готовое приложение AvidReader Прежде всего создадим директорию для проекта, активируем виртуальное окружение и установим все необходимые зависимости с помощью менеджера pipenv: Структура готового приложения выглядит так: Приведенный ниже код отвечает за создание экземпляра Flask-приложения и объекта базы данных. Сохраните его в файле /reader/__init__.py: /reader/__init__.py Примечание: если вы планируете использовать другой тип базы данных – MySQL или PostgreSQL – URI должен выглядеть так: Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека питониста» Интересно, перейти к каналу Теперь нужно создать модель (таблицу) в базе данных для хранения информации о книгах. Для этого сохраните приведенный ниже код в файле /reader/models.py: /reader/models.py Значение cover по умолчанию равно Для запуска приложения создайте файл run.py: run.py Все готово для создания базы данных – мы сделаем это в интерактивной консоли Flask: Загляните в папку /reader – там появился файл базы, database. Примечание: после создания таблицы ее структуру нельзя просто так изменить (добавить новый столбец, к примеру). Выход – воспользоваться расширением Flask-Migrate, либо, если данных в базе совсем мало и потерять их не жаль, выполнить: Интерактивная консоль позволяет добавлять записи в базу по одной: Или по нескольку сразу: Если сейчас выполнить запрос к базе, можно увидеть, что все три записи благополучно добавлены: И первый, и второй способы добавления записей в базу, очевидно, занимают слишком много времени. Поэтому проще наполнить базу информацией из файла books.json: В результате в базу было добавлено 7 новых записей: После наполнения базы можно приступать к функциям представления и шаблонам для вывода карточек. Сначала займемся маршрутом для главной страницы. Сохраните этот код в файле /reader/routes.py: /reader/routes.py Вторая функция обеспечивает отправку изображений (обложек книг) из директории /reader/uploads. Использование этой функции необходимо потому, что по умолчанию Flask ищет изображения только в директории static (и вложенных в нее папках). Указание на пользовательскую папку для загрузки изображений нужно добавить в __init__.py: Также в __init__.py надо сделать импорт модели и маршрутов: Кроме того, для вывода записей нужны два шаблона – base.html и index.html, а также файл со стилями CSS. Поместите их, соответственно, в папки /reader/templates/ и /reader/static/css. Все готово – можно запускать приложение: Пока что приложение выглядит так: Пагинации и изображений пока нет Для вывода оценки книги используется простейший код в шаблоне, который печатает количество звездочек, соответствующее оценке в базе: Весь код, созданный на этом этапе – здесь. Чтобы просматривать карточки книг, нужно сделать новый шаблон book.html, поместить файл illustration.jpg в /reader/uploads/ и добавить необходимый маршрут в routes.py: routes.py Кроме того, нужно добавить необходимую динамическую ссылку в шаблон index.html: Карточка книги выглядит так: Позже кнопки «Изменить» и «Удалить» станут функциональными SQLAlchemy делает фильтрацию данных простейшим делом. К примеру, вот так можно обеспечить вывод карточек книг в соответствии с датой добавления: Так же просто можно отобрать книги по определенному автору или жанру. Сделаем выборку по жанру «триллер»: И по максимальной оценке 5: Вставьте эти функции в /readers/routes.py и добавьте в папку templates шаблоны для вывода триллеров и лучших фильмов. В шаблон base.html нужно добавить ссылки для кнопок в верхнем меню: Теперь можно посмотреть на выборку по триллерам: Фильтр по триллерам И по лучшим книгам: Книги с оценкой 5 Обратите внимание: шаблонизатору Jinja2 не требуются никакие дополнительные ухищрения для работы с объектом данных, созданным в результате фильтрации: загрузка изображений и перенаправление на карточку книги не вызывают никаких проблем: Кроме того, к атрибутам объекта данных можно применять фильтры Jinja2. Этот фильтр обеспечивает вывод даты добавления книги в формате день-месяц-год: По умолчанию же (без фильтра) дата будет выглядеть так: Последнее, что мы сделаем на этом этапе – постраничный вывод карточек. Реализовать пагинацию с помощью SQLAlchemy действительно просто. В начале файла /reader/routes.py необходимо добавить импорт /reader/routes.py В шаблон index.html нужно внести всего 2 дополнения – изменить index.html И добавить вывод номеров страниц в самом низу: index.html Все готово: Пагинация записей на главной странице В шаблоны best.html и thrillers.html тоже нужно добавить пагинацию. Для этого необходимо внести изменения сначала в их функции представления, а потом и в сами шаблоны. Функция для best.html выглядит так: /reader/routes.py А для thrillers.html – так: /reader/routes.py Дополнения в самих шаблонах аналогичны тем, что мы уже сделали в index.html – нужно изменить best.html и thrillers.html Весь код и тестовый контент, созданные на этом этапе, можно взять здесь. В следующей, заключительной части мы реализуем загрузку и автоматическое сжатие изображений, сделаем CRUD-операции и добавим возможность экспорта контента в json-формате.Обзор проекта
Что мы изучим в процессе работы
Первый этап
mkdir reader cd reader mkdir .venv pipenv shell pipenv install -r requirements.txt
| run.py | ---reader | database.db | forms.py | models.py | routes.py | __init__.py | +---static | ---css | style.css | +---templates | base.html | best.html | book.html | create.html | edit.html | index.html | thrillers.html | _formhelpers.html | ---uploads
Приступаем к работе
from flask import Flask from flask_sqlalchemy import SQLAlchemy import os app = Flask(__name__) basedir = os.path.abspath(os.path.dirname(__file__)) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(basedir, 'database.db') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SECRET_KEY'] = 'hard to guess' db = SQLAlchemy(app)
mysql://username:password@host:port/database_name postgresql://username:password@host:port/database_name
Модель базы данных
from reader import app, db from sqlalchemy.sql import func class Book(db.Model): id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(100), unique=True, nullable=False) author = db.Column(db.String(100), nullable=False) genre = db.Column(db.String(20), nullable=False) rating = db.Column(db.Integer) cover = db.Column(db.String(50), nullable=False, default='default.jpg') description = db.Column(db.Text) notes = db.Column(db.Text) created_at = db.Column(db.DateTime(timezone=True), server_default=func.now()) def __repr__(self): return f'<Book {self.title}>'
default.jpg
– это изображение надо заранее поместить в папку /reader/uploads/. Обратите внимание на один из атрибутов поля title, unique=True
: это означает, что название книги должно быть уникальным. Если не предотвратить ввод дубликата (мы сделаем это позже во время валидации формы), работа приложения будет прервана ошибкой IntegrityError UNIQUE constraint failed
.
from reader import app if __name__ == '__main__': app.run(host='127.0.0.1', port=8000, debug=True)
(.venv) D:reader>set FLASK_APP=run (.venv) D:reader>flask shell >>> from app import db >>> from reader import db >>> from reader.models import Book >>> db.create_all()
>>> db.drop_all() >>> db.create_all()
Заполнение базы тестовыми данными
>>> book1 = Book(title = 'Преступление и наказание', ... author = 'Федор Достоевский', ... genre = 'драма', ... rating = '4', ... description = 'История Родиона Раскольникова.', ... notes = 'Невежество - мать всех преступлений.') >>> db.session.add(book1) >>> db.session.commit()
>>> book2 = Book(title = 'Нос', ... author = 'Николай Гоголь', ... genre = 'фэнтези', ... rating = '5', ... description = 'История сбежавшего носа.', ... notes = 'Без носа человек - черт знает что: птица не птица, гражданин не гражданин, - просто возьми, да и вышвырни в окошко!') >>> >>> book3 = Book(title = 'Мастер и Маргарита', ... author = 'Михаил Булгаков', ... genre = 'фэнтези', ... rating = '5', ... description = 'История о Дьяволе, искуплении и коте Бегемоте.', ... notes = 'Вздор! Лет через триста это пройдет.') >>> db.session.add(book2) >>> db.session.add(book3) >>> db.session.commit()
>>> Book.query.all() [<Book Преступление и наказание>, <Book Нос>, <Book Мастер и Маргарита>]
>>> from reader.models import Book >>> from reader import db >>> import json >>> with open('books.json', encoding="utf8") as f: ... books_json = json.load(f) ... for book in books_json: ... book = Book(author=book['author'], description=book['description'], genr e=book['genre'], rating=book['rating'], title=book['title'], notes=book['notes'] ) ... db.session.add(book) ... db.session.commit()
>>> Book.query.all() [<Book Преступление и наказание>, <Book Нос>, <Book Мастер и Маргарита>, <Book М изери>, <Book Замок Броуди>, <Book Облачный атлас>, <Book Пассажир>, <Book Голов окружение>, <Book Террор>, <Book Мизерере>] >>> exit()
Основные маршруты и шаблоны
from reader import app from reader.models import Book @app.route('/') def index(): books = Book.query.all() return render_template('index.html', books=books) @app.route('/uploads/<filename>') def send_file(filename): return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
UPLOAD_FOLDER = 'uploads' app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
from reader import routes, models
python run.py
Оценка книги
{% set stars = book.rating | int %} {% for n in range(stars) %} <span class="fa fa-star checked" style="color:orange"></span> {% endfor %}
Второй этап
@app.route('/<int:book_id>/') def book(book_id): book = Book.query.get_or_404(book_id) return render_template('book.html', book=book)
<a href="{{ url_for('book', book_id=book.id)}}">Подробнее</a>
Фильтры и работа с объектами данных
books = Book.query.order_by(Book.created_at.desc()).all()
@app.route('/thrillers/') def thrillers(): books = Book.query.filter(Book.genre == 'триллер').all() return render_template('thrillers.html', books=books)
@app.route('/best/') def best(): books = Book.query.filter(Book.rating > 4).all() return render_template('best.html', books=books)
<a class="btn btn-info mr-2" href="{{ url_for('thrillers') }}" role="button">Триллеры</a> <a class="btn btn-info mr-2" href="{{ url_for('best') }}" role="button">Лучшие</a>
{{ url_for('send_file', filename=book.cover) }} {{ url_for('book', book_id=book.id)}}
{{ book.created_at.strftime('%d-%m-%Y') }}
2022-06-25 14:17:53
Пагинация
request
, а затем изменить функцию представления для index
таким образом:
@app.route('/') def index(): page = request.args.get('page', 1, type=int) books = Book.query.order_by(Book.created_at.desc()).paginate(page=page, per_page=4) return render_template('index.html', books=books)
books
на books.items
:
{% for book in books.items %}
{% for page_num in books.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %} {% if page_num %} {% if books.page == page_num %} <a class="btn btn-info mb-4" href="{{ url_for('index', page=page_num) }}">{{ page_num }}</a> {% else %} <a class="btn btn-outline-info mb-4" href="{{ url_for('index', page=page_num) }}">{{ page_num }}</a> {% endif %} {% else %} ... {% endif %} {% endfor %}
@app.route('/best/') def best(): page = request.args.get('page', 1, type=int) books = Book.query.filter(Book.rating > 4).paginate(page=page, per_page=4) return render_template('best.html', books=books)
@app.route('/thrillers/') def thrillers(): page = request.args.get('page', 1, type=int) books = Book.query.filter(Book.genre == 'триллер').paginate(page=page, per_page=4) return render_template('thrillers.html', books=books)
books
на books.items
и добавить блок вывода пагинации:
{% for page_num in books.iter_pages(left_edge=1, right_edge=1, left_current=1, right_current=2) %} {% if page_num %} {% if books.page == page_num %} <a class="btn btn-info mb-4" href="{{ url_for('thrillers', page=page_num) }}">{{ page_num }}</a> {% else %} <a class="btn btn-outline-info mb-4" href="{{ url_for('thrillers', page=page_num) }}">{{ page_num }}</a> {% endif %} {% else %} ... {% endif %} {% endfor %}
Материалы по теме
- 0 views
- 0 Comment