Flask SQLAlchemy CRUD приложение с WTForms QuerySelectField и QuerySelectMultipleField
WTForms QuerySelectField и QuerySelectMultipleField облегчают управление данными связи SQLAlchemy .
Для нового приложения Flask , использующего WTForms и SQLAlchemy, у меня было много связей между таблицами и я искал самый простой способ управления этими таблицами. Самый очевидный выбор - использовать QuerySelectField и QuerySelectMultipleField, присутствующие в пакете wtforms-sqlalchemy. Так как я не использовал их раньше, я создал небольшое приложение для игры.
Ниже я показываю код (разработка на Ubuntu 20.04). Если вы хотите увидеть его в действии, вы можете скачать Docker image внизу этой заметки.
Резюме приложения
Это приложение CRUD, которое демонстрирует QuerySelectField и QuerySelectMultipleField. Для уменьшения кода я добавил Bootstrap-Flask. База данных SQLite. Я использую не Flask-SQLAlchemy , а проприетарную реализацию.
Есть три таблицы:
- Друг
- Город
- Хобби
Город-друг - это многоликие отношения:
Друг может жить только в одном городе, а у города может быть много друзей.
Хобби друга - это отношения many-to-many :
У друга может быть много хобби, а хобби может практиковаться многими друзьями.
В форме "Друг":
- QuerySelectField используется для выбора одного города.
- поле QuerySelectMultipleField используется для выбора нулевого или более хобби.
Я более или менее дублировал код для операций Создать, Редактировать и Удалить. Это оставляет несколько комнат для экспериментов. Я не использовал поле query_factory в форме с QuerySelectField и QuerySelectMultipleField. Вместо этого я добавил это в функцию просмотра, например:
form.city.query = app_db.session.query(City).order_by(City.name)
Создать virtual environment
Перейдите в каталог разработки, создайте virtual environment для нового каталога, например, flaskquery, активируйте его и введите каталог:
cd <your-development-directory>
python3 -m venv flaskquery
source flaskquery/bin/activate
cd flaskquery
mkdir project
cd project
mkdir app
Каталог проекта - 'проект', а наше приложение находится в каталоге приложения.
Установить пакеты
Для минимизации кода я буду использовать Bootstrap-Flask. Самое лучшее - это рендеринг формы с одним оператором. Далее мы используем SQLAlchemy и для базы данных SQLite. Я не использую Flask-SQLAlchemy, я объяснял это в предыдущем посте. Для миграций мы используем Alembic, я не могу жить без него.
pip install flask
pip install flask-wtf
pip install bootstrap-flask
pip install sqlalchemy
pip install wtforms-sqlalchemy
pip install alembic
Каталог проекта
Для справки: дамп дерева каталога проекта для завершенного проекта.
.
├── alembic
│ ├── env.py
│ ├── README
│ ├── script.py.mako
│ └── versions
│ ├── 1c20e6a53339_create_db.py
│ └── d821ac509404_1e_revision.py
├── alembic.ini
├── app
│ ├── blueprints
│ │ └── manage_data
│ │ ├── forms.py
│ │ └── views.py
│ ├── factory.py
│ ├── factory.py_first_version
│ ├── model.py
│ ├── service_app_db.py
│ ├── service_app_logging.py
│ ├── services.py
│ └── templates
│ ├── base.html
│ ├── home.html
│ ├── item_delete.html
│ ├── item_new_edit.html
│ └── items_list.html
├── app.db
├── app.log
├── config.py
├── requirements.txt
└── run.py
Начните с минимального приложения
В директории проекта мы создаем файл run.py со следующим содержимым:
# run.py
from app import factory
app = factory.create_app()
if __name__ == '__main__':
app.run(host= '0.0.0.0')
Обратите внимание, что мы используем файл factory.py вместо файла __init__.py. Избегайте файла __init__.py. Когда ваше приложение вырастет, вы можете столкнуться с циклическим импортом.
Мы поместили config.py в каталог проекта для хранения наших конфигурационных переменных:
# config.py
import os
project_dir = os.path.abspath(os.path.dirname(__file__))
class Config(object):
DEBUG = False
TESTING = False
class ConfigDevelopment(Config):
DEBUG = True
SECRET_KEY = 'NO8py79NIOU7694rgLKJHIGo87tKUGT97g'
SQLALCHEMY_DB_URI = 'sqlite:///' + os.path.join(project_dir, 'app.db')
SQLALCHEMY_ENGINE_OPTIONS = {
'echo': True,
}
class ConfigTesting(Config):
TESTING = True
class ConfigProduction(Config):
pass
Два сервиса, лог и база данных
Мы можем поместить все в файл factory.py, но это будет грязно. Поэтому создадим отдельные файлы с классами для протоколирования и базы данных.
# service_app_logging.py
import logging
class AppLogging:
def __init__(self, app=None):
if app is not None:
self.init_app(app)
def init_app(self, app):
FORMAT = '%(asctime)s [%(levelname)-5.5s] [%(funcName)30s()] %(message)s'
logFormatter = logging.Formatter(FORMAT)
app.logger = logging.getLogger()
app.logger.setLevel(logging.DEBUG)
fileHandler = logging.FileHandler('app.log')
fileHandler.setFormatter(logFormatter)
app.logger.addHandler(fileHandler)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(logFormatter)
app.logger.addHandler(consoleHandler)
return app.logger
Для доступа к базе данных создадим SQLAlchemy scoped_session.
# service_app_db.py
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, scoped_session
class AppDb:
def __init__(self, app=None):
if app is not None:
self.init_app(app)
def init_app(self, app):
sqlalchemy_db_uri = app.config.get('SQLALCHEMY_DB_URI')
sqlalchemy_engine_options = app.config.get('SQLALCHEMY_ENGINE_OPTIONS')
engine = create_engine(
sqlalchemy_db_uri,
**sqlalchemy_engine_options
)
sqlalchemy_scoped_session = scoped_session(
sessionmaker(
bind=engine,
expire_on_commit=False
)
)
setattr(self, 'session', sqlalchemy_scoped_session)
Мы используем промежуточный файл services.py, где мы инстанцируем сервисы.
# services.py
from .service_app_logging import AppLogging
from .service_app_db import AppDb
app_logging = AppLogging()
app_db = AppDb()
Завод приложения, первая версия
Теперь мы можем создать первую версию нашего файла factory.py:
# factory.py
from flask import Flask, request, g, url_for, current_app, render_template
from flask_wtf.csrf import CSRFProtect
from flask_bootstrap import Bootstrap
from .services import app_logging, app_db
def create_app():
app = Flask(__name__)
app.config.from_object('config.ConfigDevelopment')
# services
app.logger = app_logging.init_app(app)
app_db.init_app(app)
app.logger.debug('test debug message')
Bootstrap(app)
csrf = CSRFProtect()
csrf.init_app(app)
@app.teardown_appcontext
def teardown_db(response_or_exception):
if hasattr(app_db, 'session'):
app_db.session.remove()
@app.route('/')
def index():
return render_template(
'home.html',
welcome_message='Hello world',
)
return app
Обратите внимание, что я поместил сюда '@app.route' для главной страницы.
В каталог шаблонов мы поместили два файла, вот базовый шаблон, смотрите также пример в пакете Bootstrap-Flask .
{% from 'bootstrap/nav.html' import render_nav_item %}
{% from 'bootstrap/utils.html' import render_messages %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>QuerySelectField and QuerySelectMultipleField</title>
{{ bootstrap.load_css() }}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
{{ render_nav_item('index', 'Home', use_li=True) }}
</ul>
</div>
</nav>
<main class="container">
{{ render_messages(container=False, dismissible=True) }}
{% block content %}{% endblock %}
</main>
{{ bootstrap.load_js() }}
</body>
</html>
И шаблон главной страницы.
{# home.html #}
{% extends "base.html" %}
{% block content %}
{{ welcome_message }}
{% endblock %}
Первый запуск
Перейдите в каталог проекта и введите тип:
python3 run.py
Направьте ваш браузер на 127.0.0.1:5000 и вы должны увидеть сообщение 'Hello world'. Вы также должны увидеть меню Bootstrap в верхней части страницы. Просмотрите исходный код страницы и проверьте файлы bootstrap. В каталоге проекта также должен быть наш лог-файл app.log.
Добавить модель
Теперь у нас есть запущенное приложение. В каталоге приложения мы создаем файл model.py. У нас есть "Друзья, города и увлечения".
# model.py
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, ForeignKey, Integer, String, Table
from sqlalchemy.orm import relationship
Base = declarative_base()
# many-to-many link table
friend_mtm_hobby_table = Table(
'friend_mtm_hobby',
Base.metadata,
Column('friend_id', Integer, ForeignKey('friend.id')),
Column('hobby_id', Integer, ForeignKey('hobby.id'))
)
class Friend(Base):
__tablename__ = 'friend'
id = Column(Integer, primary_key=True)
name = Column(String(100), server_default='')
city_id = Column(Integer, ForeignKey('city.id'))
city = relationship(
'City',
back_populates='friends',
)
hobbies = relationship(
'Hobby',
secondary=friend_mtm_hobby_table,
back_populates='friends',
order_by=lambda: Hobby.name,
)
class City(Base):
__tablename__ = 'city'
id = Column(Integer, primary_key=True)
name = Column(String(100), server_default='')
friends = relationship(
'Friend',
back_populates='city',
order_by=Friend.name,
)
class Hobby(Base):
__tablename__ = 'hobby'
id = Column(Integer, primary_key=True)
name = Column(String(100), server_default='')
friends = relationship(
'Friend',
secondary=friend_mtm_hobby_table,
back_populates='hobbies',
order_by=Friend.name,
)
Сортировочная проблема
Было бы неплохо, если бы SQLAlchemy вернул результаты сортировки по имени. Мы можем использовать это в списках.
- Список друзей: Показывать имя друга, название города и название увлечений.
- Список городов: показать название города и имена всех друзей, живущих в городе.
- Список увлечений: покажите название и имена всех друзей, у которых есть это увлечение.
Например, с хобби мы получаем доступ к друзьям как к хобби.друзьям. Сортировка выглядит легко, мы просто добавляем в отношения пункт 'order_by'. Однако, так как мы ссылаемся на класс "Друг", мы можем использовать его только с классами, которые были загружены ранее.
В нашей модели выше мы не можем сортировать хобби в классе "Друг", потому что класс "Хобби" не был загружен до класса "Друг". Но мы можем сортировать друзей в классе "Хобби", потому что класс "Друг" был загружен до класса "Хобби".
Чтобы обойти это, мы можем сделать одну из двух вещей:
hobbies = relationship(
'Hobby',
secondary=friend_mtm_hobby_table,
back_populates='friends',
order_by='Hobby.name',
)
или..:
hobbies = relationship(
'Hobby',
secondary=friend_mtm_hobby_table,
back_populates='friends',
order_by=lambda: Hobby.name,
)
В обоих случаях разрешение имен откладывается до первого использования.
Используйте Alembic для создания базы данных
Alembic - отличный инструмент для миграции баз данных. Мы уже установили его, но перед тем, как использовать, мы должны инициализировать его. Перейдите в каталог проекта и введите тип:
alembic init alembic
При этом будет создан файл alembic.ini и каталог alembic в каталоге проекта. В alembic.ini измените строку:
sqlalchemy.url = driver://user:pass@localhost/dbname
к:
sqlalchemy.url = sqlite:///app.db
А в alembic/env.py измените строку:
target_metadata = None
to:
from app.model import Base
target_metadata = [Base.metadata]
Создать первую ревизию:
alembic revision -m "1e revision"
Запустите миграцию:
alembic upgrade head
И в директории проекта был создан файл базы данных app.db.
Для просмотра базы данных используйте SQLite браузер.
Установите браузер SQLite :
sudo apt install sqlitebrowser
Вы можете запустить браузер SQLite , щелкнув правой кнопкой мыши на базе данных. Создана только одна таблица: alembic_version.
Для создания таблиц нашей базы данных мы используем автогенерацию:
alembic revision --autogenerate -m "create db"
Запустите миграцию:
alembic upgrade head
Закройте браузер SQLite , откройте его еще раз и убедитесь, что таблицы созданы:
- друг
- город
- хобби
- друг_mtm_hobby
Теперь добавьте друга, используя 'Execute SQL':
INSERT INTO friend (name) VALUES ('John');
Не забудьте после этого нажать 'Write changes' (Записать изменения)! Затем нажмите 'Browse Data' и убедитесь, что вставленная запись находится там.
Измените сообщение на главной странице
Я хочу показать сообщение на главной странице, которое покажет всем нашим друзьям. Для этого мы меняем factory.py, чтобы получить друзей и передать их в шаблон:
# factory.py
from flask import Flask, request, g, url_for, current_app, render_template
from flask_wtf.csrf import CSRFProtect
from flask_bootstrap import Bootstrap
from .services import app_logging, app_db
from .model import Friends
def create_app():
app = Flask(__name__)
app.config.from_object('config.ConfigDevelopment')
# services
app.logger = app_logging.init_app(app)
app_db.init_app(app)
app.logger.debug('test debug message')
Bootstrap(app)
csrf = CSRFProtect()
csrf.init_app(app)
@app.teardown_appcontext
def teardown_db(response_or_exception):
if hasattr(app_db, 'session'):
app_db.session.remove()
@app.route('/')
def index():
friends = app_db.session.query(Friend).order_by(Friend.name).all()
return render_template(
'home.html',
welcome_message='Hello world',
friends=friends,
)
return app
И мы итератируем наших друзей в шаблоне главной страницы:
{# home.html #}
{% extends "base.html" %}
{% block content %}
{{ welcome_message }}
<ul>
{% for friend in friends %}
<li>
{{ friend.name }}
</li>
{% endfor %}
</ul>
{% endblock %}
Обновите страницу в браузере. Теперь имя нашего друга John должно отображаться на главной странице.
Добавьте Blueprint для управления данными.
Для манипулирования таблицами базы данных мы создаем Blueprint, management_data. В этом Blueprint мы добавляем следующие методы для каждой таблицы (объекта):
- список
- новый сайт
- редактирование
- удалить
Мы создаем каталог blueprints и в этом каталоге каталог 'management_data'. В этой директории мы создаем два файла, views.py и form.py. Мы не используем параметр QuerySelectField / QuerySelectMultipleField query_factory в классах форм, а добавляем их в методы представления.
# forms.py
from flask_wtf import FlaskForm
from wtforms import IntegerField, StringField, SubmitField
from wtforms_sqlalchemy.fields import QuerySelectField, QuerySelectMultipleField
from wtforms.validators import InputRequired, Length
from app.services import app_db
from app.model import Friend, City, Hobby
# friend
class FriendNewEditFormMixin():
name = StringField('Name',
validators=[ InputRequired(), Length(min=2) ])
city = QuerySelectField('City',
get_label='name',
allow_blank=False,
blank_text='Select a city',
render_kw={'size': 1},
)
hobbies = QuerySelectMultipleField('Hobbies',
get_label='name',
allow_blank=False,
blank_text='Select one or more hobbies',
render_kw={'size': 10},
)
class FriendNewForm(FlaskForm, FriendNewEditFormMixin):
submit = SubmitField('Add')
class FriendEditForm(FlaskForm, FriendNewEditFormMixin):
submit = SubmitField('Update')
class FriendDeleteForm(FlaskForm):
submit = SubmitField('Confirm delete')
# city
class CityNewEditFormMixin():
name = StringField('Name',
validators=[ InputRequired(), Length(min=2) ])
class CityNewForm(FlaskForm, CityNewEditFormMixin):
submit = SubmitField('Add')
class CityEditForm(FlaskForm, CityNewEditFormMixin):
submit = SubmitField('Update')
class CityDeleteForm(FlaskForm):
submit = SubmitField('Confirm delete')
# hobby
class HobbyNewEditFormMixin():
name = StringField('Name',
validators=[ InputRequired(), Length(min=2) ])
class HobbyNewForm(FlaskForm, HobbyNewEditFormMixin):
submit = SubmitField('Add')
class HobbyEditForm(FlaskForm, HobbyNewEditFormMixin):
submit = SubmitField('Update')
class HobbyDeleteForm(FlaskForm):
submit = SubmitField('Confirm delete')
Как уже говорилось ранее, в views.py много повторений, но это позволяет легко что-либо изменить. Обратите внимание, что мы делим шаблоны между Другом, Городом и Хобби.
В друге views мы хотим выбрать город и выбрать одно или несколько увлечений. Здесь мы инициализируем запросы для QuerySelectField и QuerySelectMultipleField. В соответствии с документацией, если в запросе не удастся найти ни один из элементов в поданной форме, это приведет к ошибке проверки. И это именно то, что нам нужно.
# views.py
from flask import Flask, Blueprint, current_app, g, session, request, url_for, redirect, \
render_template, flash, abort
from app.services import app_db
from app.model import Friend, City, Hobby
from .forms import (
FriendNewForm, FriendEditForm, FriendDeleteForm,
CityNewForm, CityEditForm, CityDeleteForm,
HobbyNewForm, HobbyEditForm, HobbyDeleteForm,
)
manage_data_blueprint = Blueprint('manage_data', __name__)
@manage_data_blueprint.route('/friends/list', methods=['GET', 'POST'])
def friends_list():
friends = app_db.session.query(Friend).order_by(Friend.name).all()
thead_th_items = [
{
'col_title': '#',
},
{
'col_title': 'Name',
},
{
'col_title': 'City',
},
{
'col_title': 'Hobbies',
},
{
'col_title': 'Delete',
},
]
tbody_tr_items = []
for friend in friends:
city_name = '-'
if friend.city:
city_name = friend.city.name
hobby_names = '-'
if friend.hobbies:
hobby_names = ', '.join([x.name for x in friend.hobbies])
tbody_tr_items.append([
{
'col_value': friend.id,
},
{
'col_value': friend.name,
'url': url_for('manage_data.friend_edit', friend_id=friend.id),
},
{
'col_value': city_name,
},
{
'col_value': hobby_names,
},
{
'col_value': 'delete',
'url': url_for('manage_data.friend_delete', friend_id=friend.id),
}
])
return render_template(
'items_list.html',
title='Friends',
thead_th_items=thead_th_items,
tbody_tr_items=tbody_tr_items,
item_new_url=url_for('manage_data.friend_new'),
item_new_text='New friend',
)
@manage_data_blueprint.route('/cities/list', methods=['GET', 'POST'])
def cities_list():
cities = app_db.session.query(City).order_by(City.name).all()
thead_th_items = [
{
'col_title': '#',
},
{
'col_title': 'City',
},
{
'col_title': 'Friends',
},
{
'col_title': 'Delete',
},
]
tbody_tr_items = []
for city in cities:
friend_names = ''
if city.friends:
friend_names = ', '.join([x.name for x in city.friends])
tbody_tr_items.append([
{
'col_value': city.id,
},
{
'col_value': city.name,
'url': url_for('manage_data.city_edit', city_id=city.id),
},
{
'col_value': friend_names,
},
{
'col_value': 'delete',
'url': url_for('manage_data.city_delete', city_id=city.id),
}
])
return render_template(
'items_list.html',
title='Cities',
thead_th_items=thead_th_items,
tbody_tr_items=tbody_tr_items,
item_new_url=url_for('manage_data.city_new'),
item_new_text='New city',
)
@manage_data_blueprint.route('/hobbies/list', methods=['GET', 'POST'])
def hobbies_list():
hobbies = app_db.session.query(Hobby).order_by(Hobby.name).all()
thead_th_items = [
{
'col_title': '#',
},
{
'col_title': 'Name',
},
{
'col_title': 'Friends',
},
{
'col_title': 'Delete',
},
]
tbody_tr_items = []
for hobby in hobbies:
friend_names = ''
if hobby.friends:
friend_names = ', '.join([x.name for x in hobby.friends])
tbody_tr_items.append([
{
'col_value': hobby.id,
},
{
'col_value': hobby.name,
'url': url_for('manage_data.hobby_edit', hobby_id=hobby.id),
},
{
'col_value': friend_names,
},
{
'col_value': 'delete',
'url': url_for('manage_data.hobby_delete', hobby_id=hobby.id),
}
])
return render_template(
'items_list.html',
title='Hobbies',
thead_th_items=thead_th_items,
tbody_tr_items=tbody_tr_items,
item_new_url=url_for('manage_data.hobby_new'),
item_new_text='New hobby',
)
@manage_data_blueprint.route('/friend/new', methods=['GET', 'POST'])
def friend_new():
item = Friend()
form = FriendNewForm()
form.city.query = app_db.session.query(City).order_by(City.name)
form.hobbies.query = app_db.session.query(Hobby).order_by(Hobby.name)
if form.validate_on_submit():
form.populate_obj(item)
app_db.session.add(item)
app_db.session.commit()
flash('Friend added: ' + item.name, 'info')
return redirect(url_for('manage_data.friends_list'))
return render_template('item_new_edit.html', title='New friend', form=form)
@manage_data_blueprint.route('/friend/edit/<int:friend_id>', methods=['GET', 'POST'])
def friend_edit(friend_id):
item = app_db.session.query(Friend).filter(Friend.id == friend_id).first()
if item is None:
abort(403)
form = FriendEditForm(obj=item)
form.city.query = app_db.session.query(City).order_by(City.name)
form.hobbies.query = app_db.session.query(Hobby).order_by(Hobby.name)
if form.validate_on_submit():
form.populate_obj(item)
app_db.session.commit()
flash('Friend updated: ' + item.name, 'info')
return redirect(url_for('manage_data.friends_list'))
return render_template('item_new_edit.html', title='Edit friend', form=form)
@manage_data_blueprint.route('/friend/delete/<int:friend_id>', methods=['GET', 'POST'])
def friend_delete(friend_id):
item = app_db.session.query(Friend).filter(Friend.id == friend_id).first()
if item is None:
abort(403)
form = FriendDeleteForm(obj=item)
item_name = item.name
if form.validate_on_submit():
app_db.session.delete(item)
app_db.session.commit()
flash('Deleted friend: ' + item_name, 'info')
return redirect(url_for('manage_data.friends_list'))
return render_template('item_delete.html', title='Delete friend', item_name=item_name, form=form)
@manage_data_blueprint.route('/city/new', methods=['GET', 'POST'])
def city_new():
item = City()
form = CityNewForm()
if form.validate_on_submit():
form.populate_obj(item)
app_db.session.add(item)
app_db.session.commit()
flash('City added: ' + item.name, 'info')
return redirect(url_for('manage_data.cities_list'))
return render_template('item_new_edit.html', title='New city', form=form)
@manage_data_blueprint.route('/city/edit/<int:city_id>', methods=['GET', 'POST'])
def city_edit(city_id):
item = app_db.session.query(City).filter(City.id == city_id).first()
if item is None:
abort(403)
form = CityEditForm(obj=item)
if form.validate_on_submit():
form.populate_obj(item)
app_db.session.commit()
flash('City updated: ' + item.name, 'info')
return redirect(url_for('manage_data.cities_list'))
return render_template('item_new_edit.html', title='Edit city', form=form)
@manage_data_blueprint.route('/city/delete/<int:city_id>', methods=['GET', 'POST'])
def city_delete(city_id):
item = app_db.session.query(City).filter(City.id == city_id).first()
if item is None:
abort(403)
form = CityDeleteForm(obj=item)
item_name = item.name
if form.validate_on_submit():
app_db.session.delete(item)
app_db.session.commit()
flash('Deleted city: ' + item_name, 'info')
return redirect(url_for('manage_data.cities_list'))
return render_template('item_delete.html', title='Delete city', item_name=item_name, form=form)
@manage_data_blueprint.route('/hobby/new', methods=['GET', 'POST'])
def hobby_new():
item = Hobby()
form = HobbyNewForm()
if form.validate_on_submit():
form.populate_obj(item)
app_db.session.add(item)
app_db.session.commit()
flash('Hobby added: ' + item.name, 'info')
return redirect(url_for('manage_data.hobbies_list'))
return render_template('item_new_edit.html', title='New hobby', form=form)
@manage_data_blueprint.route('/hobby/edit/<int:hobby_id>', methods=['GET', 'POST'])
def hobby_edit(hobby_id):
item = app_db.session.query(Hobby).filter(Hobby.id == hobby_id).first()
if item is None:
abort(403)
form = HobbyEditForm(obj=item)
if form.validate_on_submit():
form.populate_obj(item)
app_db.session.commit()
flash('Hobby updated: ' + item.name, 'info')
return redirect(url_for('manage_data.hobbies_list'))
return render_template('item_new_edit.html', title='Edit hobby', form=form)
@manage_data_blueprint.route('/hobby/delete/<int:hobby_id>', methods=['GET', 'POST'])
def hobby_delete(hobby_id):
item = app_db.session.query(Hobby).filter(Hobby.id == hobby_id).first()
if item is None:
abort(403)
form = HobbyDeleteForm(obj=item)
item_name = item.name
if form.validate_on_submit():
app_db.session.delete(item)
app_db.session.commit()
flash('Deleted hobby: ' + item_name, 'info')
return redirect(url_for('manage_data.hobbies_list'))
return render_template('item_delete.html', title='Delete hobby', item_name=item_name, form=form)
Добавить шаблоны для Blueprint
У нас есть три общих шаблона для списка, новых и редактируемых, удаляемых. Функция render_form() Bootstrap-Flask помещает форму на страницу.
{# items_list.html #}
{% extends "base.html" %}
{% block content %}
<h1>
{{ title }}
</h1>
{% if tbody_tr_items %}
<table class="table">
<thead>
<tr>
{% for thead_th_item in thead_th_items %}
<th scope="col">
{{ thead_th_item.col_title }}
</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for tbody_tr_item in tbody_tr_items %}
<tr>
{% for tbody_td_item in tbody_tr_item %}
<td>
{% if tbody_td_item.url %}
<a href="{{ tbody_td_item.url }}">
{{ tbody_td_item.col_value }}
</a>
{% else %}
{% if tbody_td_item.col_value %}
{{ tbody_td_item.col_value }}
{% else %}
-
{% endif %}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>
No items found
</p>
{% endif %}
<a class="btn btn-outline-dark" href="{{ item_new_url }}" role="button">
{{ item_new_text }}
</a>
{% endblock %}
Операция создания и редактирования требует только одного шаблона.
{# item_new_edit.html #}
{% from 'bootstrap/form.html' import render_form %}
{% extends "base.html" %}
{% block content %}
<h1>
{{ title }}
</h1>
{{ render_form(form) }}
{% endblock %}
И, наконец, шаблон удаления.
{# item_delete.html #}
{% from 'bootstrap/form.html' import render_form %}
{% extends "base.html" %}
{% block content %}
<h1>
{{ title }}
</h1>
<p>
Confirm you want to delete: {{ item_name }}
</p>
{{ render_form(form) }}
{% endblock %}
Обновите меню в базовом шаблоне
В базовом шаблоне мы добавляем элементы навигации для друзей, городов и увлечений:
{# home.html #}
{% from 'bootstrap/nav.html' import render_nav_item %}
{% from 'bootstrap/utils.html' import render_messages %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>QuerySelectField and QuerySelectMultipleField</title>
{{ bootstrap.load_css() }}
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto">
{{ render_nav_item('index', 'Home', use_li=True) }}
{{ render_nav_item('manage_data.friends_list', 'Friends', use_li=True) }}
{{ render_nav_item('manage_data.cities_list', 'Cities', use_li=True) }}
{{ render_nav_item('manage_data.hobbies_list', 'Hobbies', use_li=True) }}
</ul>
</div>
</nav>
<main class="container">
{{ render_messages(container=False, dismissible=True) }}
{% block content %}{% endblock %}
</main>
{{ bootstrap.load_js() }}
</body>
</html>
Добавить Blueprint в factory.py.
Мы добавляем несколько строк в factory.py, чтобы добавить Blueprint. Окончательная версия становится:
# factory.py
from flask import Flask, request, g, url_for, current_app, render_template
from flask_wtf.csrf import CSRFProtect
from flask_bootstrap import Bootstrap
from .services import app_logging, app_db
from .model import Friend
def create_app():
app = Flask(__name__)
app.config.from_object('config.ConfigDevelopment')
# services
app.logger = app_logging.init_app(app)
app_db.init_app(app)
app.logger.debug('test debug message')
Bootstrap(app)
csrf = CSRFProtect()
csrf.init_app(app)
# blueprints
from .blueprints.manage_data.views import manage_data_blueprint
app.register_blueprint(manage_data_blueprint, url_prefix='/manage-data')
@app.teardown_appcontext
def teardown_db(response_or_exception):
if hasattr(app_db, 'session'):
app_db.session.remove()
@app.route('/')
def index():
friends = app_db.session.query(Friend).order_by(Friend.name).all()
return render_template(
'home.html',
welcome_message='Hello world',
friends=friends,
)
return app
Запустить готовое приложение
Опять же, перейдите в каталог проекта и напечатайте:
python3 run.py
Направьте ваш браузер на 127.0.0.1:5000. Теперь вы должны увидеть законченное приложение. Есть пункты меню "Друзья", "Города" и "Хобби". Нажав на кнопку Друзья, вы попадете в список друзей. Здесь Вы можете добавлять, редактировать и удалять друзей. То же самое касается городов и увлечений.
Скачать Docker image и запустить
Если вы хотите запустить это приложение, вы можете скачать Docker image (tgz, 64 MB):
Мд5сум:
b4f8116e6b8f30c4980a7ff96f0428a5
Чтобы загрузить изображение:
docker load < queryselectfield_100.tgz
Для запуска:
docker run --name queryselectfield -p 5000:5000 queryselectfield:1.00
А затем наведите ваш браузер на 127.0.0.1:5000.
Резюме
Это пример со многими ограничениями, но он показывает силу WTForms QuerySelectField и QuerySelectMultipleField. А включив Bootstrap-Flask , мы можем без особых усилий создать меню, и нам не придётся самим отрисовывать формы.
Конечно, это еще не готово к производству, но вы можете его доработать, добавить проверки, больше полей и т.д. Поле QuerySelectField отлично подходит для отношений "один-ко-многим", а поле QuerySelectMultipleField отлично подходит для отношений many-to-many . Они обеспечивают достаточную гибкость для построения вашего приложения.
Ссылки / кредиты
Alembic 1.5.5 documentation » Tutorial
https://alembic.sqlalchemy.org/en/latest/tutorial.html
Bootstrap-Flask
https://bootstrap-flask.readthedocs.io/en/stable/
Larger Applications (inluding the Circular Imports warning)
https://flask.palletsprojects.com/en/1.1.x/patterns/packages/
WTForms-SQLAlchemy
https://wtforms-sqlalchemy.readthedocs.io/en/stable/
Подробнее
Alembic Bootstrap Flask SQLAlchemy WTForms
Недавний
- Скрытие первичных ключей базы данных UUID вашего веб-приложения
- Don't Repeat Yourself (DRY) с Jinja2
- SQLAlchemy, PostgreSQL, максимальное количество строк для user
- Показать значения в динамических фильтрах SQLAlchemy
- Безопасная передача данных с помощью шифрования Public Key и pyNaCl
- rqlite: альтернатива dist с высокой степенью готовности и SQLite
Большинство просмотренных
- Используя Python pyOpenSSL для проверки SSL-сертификатов, загруженных с хоста
- Использование UUID вместо Integer Autoincrement Primary Keys с SQLAlchemy и MariaDb
- Подключение к службе на хосте Docker из контейнера Docker
- Использование PyInstaller и Cython для создания исполняемого файла Python
- SQLAlchemy: Использование Cascade Deletes для удаления связанных объектов
- Flask Удовлетворительный запрос API проверка параметров запроса с помощью схем Маршмэллоу