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

Flask SQLAlchemy Application CRUD avec WTForms QuerySelectField et QuerySelectMultipleField

WTForms QuerySelectField et QuerySelectMultipleField permettent de gérer facilement les données de la relation SQLAlchemy .

8 mars 2021 Mise à jour 8 mars 2021
post main image
https://unsplash.com/@helpdeskheroes

Pour une nouvelle application Flask utilisant WTForms et SQLAlchemy, j'avais de nombreuses relations entre les tables et je cherchais la manière la plus simple de gérer ces tables. Le choix le plus évident est d'utiliser les champs QuerySelectField et QuerySelectMultipleField présents dans le paquet wtforms-sqlalchemy. Comme je ne les ai jamais utilisés auparavant, j'ai créé une petite application pour jouer avec.

Ci-dessous, je vous montre le code (développement sur Ubuntu 20.04). Si vous voulez le voir en action, vous pouvez télécharger le Docker image en bas de ce billet.

Résumé de la demande

Il s'agit d'une application CRUD qui permet de faire la démonstration des fonctions QuerySelectField et QuerySelectMultipleField. Pour réduire le code, j'ai ajouté Bootstrap-Flask. La base de données est SQLite. Je n'utilise pas Flask-SQLAlchemy mais une implémentation propriétaire.

Il y a trois tables :

  • Ami
  • Ville
  • Hobby

Friend-City est une relation d'homme à homme :

Un ami ne peut vivre que dans une seule ville, et une ville peut avoir de nombreux amis.

Ami-Hobby est une relation many-to-many :

Un ami peut avoir de nombreux passe-temps, et un passe-temps peut être pratiqué par de nombreux amis.

Dans le formulaire Ami :

  • le champ "QuerySelectField" est utilisé pour sélectionner une seule ville
  • le QuerySelectMultipleField est utilisé pour sélectionner un ou plusieurs passe-temps

J'ai plus ou moins dupliqué le code pour les opérations de création, de modification et de suppression. Cela laisse quelques possibilités d'expérimentation. Je n'ai pas utilisé le champ query_factory dans le formulaire avec QuerySelectField et QuerySelectMultipleField. Je l'ai plutôt ajouté à la fonction d'affichage, comme

	form.city.query = app_db.session.query(City).order_by(City.name)

Créer virtual environment

Allez dans votre répertoire de développement, créez un virtual environment pour un nouveau répertoire, par exemple flaskquery, activez-le et entrez dans le répertoire :

cd <your-development-directory>
python3 -m venv flaskquery
source flaskquery/bin/activate
cd flaskquery
mkdir project
cd project
mkdir app

Le répertoire du projet est "project" et notre application se trouve dans le répertoire app.

Installer les paquets

Pour minimiser le code, j'utiliserai Bootstrap-Flask. La meilleure partie est le rendu du formulaire avec une seule déclaration. En outre, nous utilisons SQLAlchemy et pour la base de données, SQLite. Je n'utilise pas Flask-SQLAlchemy, je l'ai déjà expliqué dans un article précédent. Pour les migrations, nous utilisons Alembic, je ne peux pas m'en passer.

pip install flask
pip install flask-wtf
pip install bootstrap-flask
pip install sqlalchemy
pip install wtforms-sqlalchemy
pip install alembic

Répertoire du projet

Pour votre information, voici l'arborescence du répertoire du projet terminé.

.
├── 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

Commencez avec une application minimale

Dans le répertoire du projet, nous créons un fichier run.py avec le contenu suivant :

#  run.py

from app import factory

app = factory.create_app()

if __name__ == '__main__':
    app.run(host= '0.0.0.0')

Notez que nous utilisons un fichier factory.py au lieu d'un fichier __init__.py. Évitez le fichier __init__.py. Lorsque votre application se développe, vous risquez de vous heurter à des importations circulaires.

Nous mettons un config.py dans le répertoire du projet pour stocker nos variables de configuration :

# 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

Deux services, le logging et la base de données

Nous pouvons tout mettre dans le fichier factory.py mais cela va être salissant. Créons donc des fichiers séparés avec des classes pour la journalisation et la base de données.

# 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

Pour accéder à la base de données, nous créons une session 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)

Nous utilisons un fichier intermédiaire services.py dans lequel nous instancions les services.

# services.py

from .service_app_logging import AppLogging
from .service_app_db import AppDb

app_logging = AppLogging()
app_db = AppDb()

L'usine d'applications, première version

Nous pouvons maintenant créer la première version de notre fichier 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

Notez que j'ai mis un "@app.route" ici pour la page d'accueil.

Dans le répertoire templates nous avons mis deux fichiers, voici le template de base, voir aussi l'exemple dans le paquet 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>

Et le modèle de page d'accueil.

{# home.html #}

{% extends "base.html" %}

{% block content %}

    {{ welcome_message }}

{% endblock %}

Première exécution

Allez dans le répertoire des projets et tapez :

python3  run.py

Pointez votre navigateur sur 127.0.0.1:5000 et vous devriez voir le message "Hello world". Vous devriez également voir le menu Bootstrap en haut de la page. Consultez le code source de la page et vérifiez les fichiers d'amorçage. Dans le répertoire du projet, il devrait également y avoir notre fichier journal app.log.

Ajouter le modèle

Nous avons maintenant une application en cours d'exécution. Dans le répertoire app, nous créons un fichier model.py. Nous avons les Amis, les Villes et les Hobbies.

# 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,
    )

Problème de tri

Il serait bon que SQLAlchemy renvoie les résultats triés par nom. Nous pouvons l'utiliser dans des listes.

  • Liste d'amis : Afficher le nom de l'ami, le nom de la ville et le nom des hobbies
  • Liste des villes : indique le nom de la ville et les noms de tous les amis vivant dans une ville
  • Liste des hobbies : indiquer le nom du hobby et le nom de tous les amis qui ont ce hobby

Par exemple, pour un hobby, nous accédons aux amis en tant que hobby.friends. Le tri semble facile, il suffit d'ajouter une clause "order_by" dans la relation. Cependant, comme nous faisons référence à une classe, Ami, nous ne pouvons l'utiliser qu'avec les classes qui sont chargées auparavant.

Dans notre modèle ci-dessus, nous ne pouvons pas trier les hobbies dans la classe Ami, parce que la classe Hobby n'a pas été chargée avant la classe Ami. Mais nous pouvons trier les amis dans la classe Hobby parce que la classe Ami a été chargée avant la classe Hobby.

Pour contourner ce problème, nous pouvons faire l'une des deux choses suivantes :

    hobbies = relationship(
        'Hobby',
        secondary=friend_mtm_hobby_table,
         back_populates='friends',
        order_by='Hobby.name',
    )

ou :

    hobbies = relationship(
        'Hobby',
        secondary=friend_mtm_hobby_table,
         back_populates='friends',
        order_by=lambda: Hobby.name,
    )

Dans les deux cas, la résolution des noms est reportée jusqu'à la première utilisation.

Utilisez Alembic pour créer la base de données

Alembic est un excellent outil pour la migration des bases de données. Nous l'avons déjà installé, mais nous devons l'initialiser avant de pouvoir l'utiliser. Allez dans le répertoire du projet et tapez :

alembic init alembic

Cela créera un fichier alembic.ini et un répertoire d'alambic dans le répertoire du projet. Dans alembic.ini, changez la ligne :

sqlalchemy.url = driver://user:pass@localhost/dbname

en :

sqlalchemy.url = sqlite:///app.db

Et dans alembic/env.py, changez la ligne :

target_metadata =  None

en :

from app.model import Base
target_metadata = [Base.metadata]

Créez la première révision :

alembic revision -m "1e revision"

Exécutez la migration :

alembic upgrade head

Et le fichier de base de données app.db a été créé dans le répertoire du projet.

Utilisez le navigateur SQLite pour visualiser la base de données

Installez le navigateur SQLite :

sudo apt install sqlitebrowser

Vous pouvez lancer le navigateur SQLite en cliquant avec le bouton droit de la souris sur la base de données. Une seule table a été créée : alembic_version.

Pour créer les tables de notre base de données, nous utilisons l'autogénération :

alembic revision --autogenerate -m "create db"

Exécutez la migration :

alembic upgrade head

Fermez le navigateur SQLite et ouvrez-le à nouveau et observez que les tables ont été créées :

  • ami
  • ville
  • hobby
  • ami_mtm_hobby

Maintenant, ajoutez un ami en utilisant "Execute SQL" :

INSERT  INTO friend (name) VALUES ('John');

N'oubliez pas de cliquer ensuite sur "Ecrire les modifications" ! Cliquez ensuite sur "Parcourir les données" et vérifiez que l'enregistrement inséré s'y trouve.

Modifier le message de la page d'accueil

Je veux afficher un message sur la page d'accueil qui montre tous nos amis. Pour cela, nous changeons factory.py pour obtenir les amis et les passer au modèle :

# 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

Et nous réitérons nos amis dans le modèle de page d'accueil :

{# home.html #}

{% extends "base.html" %}

{% block content %}

	{{ welcome_message }}

	<ul>
	{% for friend in friends %}
		<li>
			{{ friend.name }}
		</li>
	{% endfor %}
	</ul>

{% endblock %}

Rafraîchir la page dans le navigateur. Maintenant, le nom de notre ami John devrait être affiché sur la page d'accueil.

Ajouter un Blueprint pour gérer les données

Pour manipuler les tables de la base de données, nous créons un Blueprint, manage_data. Dans ce Blueprint , nous ajoutons les méthodes suivantes pour chaque table (objet) :

  • liste
  • nouveau
  • éditer
  • supprimer

Nous créons un répertoire blueprints et dans ce répertoire un répertoire "manage_data". Dans ce répertoire, nous créons deux fichiers, views.py et forms.py. Nous n'utilisons pas le paramètre QuerySelectField / QuerySelectMultipleField query_factory dans les classes de formulaires mais nous les ajoutons dans les méthodes d'affichage.

# 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')

Comme indiqué précédemment, il y a beaucoup de répétitions dans views.py mais cela permet de changer facilement les choses. Notez que nous partageons les modèles entre Ami, Ville et Hobby.

Dans l'ami views , nous voulons sélectionner une ville et choisir un ou plusieurs hobbies. Ici, nous initialisons les requêtes pour les champs QuerySelectField et QuerySelectMultipleField. Selon la documentation, si l'un des éléments du formulaire soumis ne peut être trouvé dans la requête, cela entraînera une erreur de validation. Et c'est exactement ce que nous voulons.

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

Ajouter des modèles pour le Blueprint

Nous avons trois modèles communs pour les listes, les nouvelles et les modifications, les suppressions. La fonction render_form() de Bootstrap-Flask met le formulaire sur la page.

{# 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 %}

Les opérations de création et de modification ne nécessitent qu'un seul modèle.

{# item_new_edit.html #}

{% from 'bootstrap/form.html' import render_form %}

{% extends "base.html" %}

{% block content %}

	<h1>
		{{ title }}
	</h1>
	{{ render_form(form) }}

{% endblock %}

Et enfin le modèle de suppression.

{# 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 %}

Mise à jour du menu dans le modèle de base

Dans le modèle de base, nous ajoutons des éléments de navigation pour Amis, Villes et Loisirs :

{# 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>

Ajouter le Blueprint à factory.py

Nous ajoutons quelques lignes au fichier factory.py pour ajouter le Blueprint. La version finale devient :

# 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

Exécutez l'application terminée

Là encore, allez dans le répertoire du projet et tapez :

python3  run.py

Pointez votre navigateur sur 127.0.0.1:5000. Vous devriez maintenant voir l'application terminée. Il y a des éléments de menu pour Amis, Villes et Hobbies. En cliquant sur Amis, vous accédez à la liste des amis. Vous pouvez y ajouter, modifier et supprimer des amis. Il en va de même pour les villes et les loisirs.

Téléchargez Docker image et lancez

Si vous souhaitez exécuter cette application, vous pouvez télécharger le Docker image (tgz, 64 MB) :

queryselectfield_100.tgz

Le md5sum est :

b4f8116e6b8f30c4980a7ff96f0428a5

Pour charger l'image :

docker load < queryselectfield_100.tgz

Pour courir :

docker run --name queryselectfield -p 5000:5000 queryselectfield:1.00

Et ensuite, pointez votre navigateur sur 127.0.0.1:5000.

Résumé

Cet exemple présente de nombreuses limites, mais il montre la puissance du WTForms QuerySelectField et QuerySelectMultipleField. Et en incluant Bootstrap-Flask , nous pouvons créer un menu sans aucun effort et n'avons pas à rendre les formulaires nous-mêmes.

Bien sûr, ce n'est pas prêt pour la production mais vous pouvez l'affiner, ajouter des contrôles, plus de champs, etc. Le QuerySelectField est idéal pour les relations de un à plusieurs et le QuerySelectMultipleField est idéal pour les relations many-to-many . Ils offrent suffisamment de flexibilité pour construire votre application.

Liens / crédits

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/

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.