angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

Flask SQLAlchemy CRUD приложение с WTForms QuerySelectField и QuerySelectMultipleField

WTForms QuerySelectField и QuerySelectMultipleField облегчают управление данными связи SQLAlchemy .

8 марта 2021 Обновленный 8 марта 2021
post main image
https://unsplash.com/@helpdeskheroes

Для нового приложения 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):

queryselect field_100.tgz

Мд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/

Оставить комментарий

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

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.