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

Flask SQLAlchemy CRUD-toepassing met WTForms QuerySelectField en QuerySelectMultipleField

WTForms QuerySelectField en QuerySelectMultipleField maken het gemakkelijk om SQLAlchemy relatiegegevens te beheren.

8 maart 2021 Bijgewerkt 8 maart 2021
post main image
https://unsplash.com/@helpdeskheroes

Voor een nieuwe Flask applicatie die gebruik maakt van WTForms en SQLAlchemy, had ik veel relaties tussen tabellen en was ik op zoek naar de gemakkelijkste manier om deze tabellen te beheren. De meest voor de hand liggende keuze is om de QuerySelectField en QuerySelectMultipleField te gebruiken die aanwezig zijn in het wtforms-sqlalchemy pakket. Omdat ik ze nog niet eerder heb gebruikt, heb ik een kleine applicatie gemaakt om mee te spelen.

Hieronder laat ik je de code zien (ontwikkeling op Ubuntu 20.04). Als je het in actie wilt zien, kun je de Docker image onderaan dit bericht downloaden.

Samenvatting van de applicatie

Dit is een CRUD applicatie die de QuerySelectField en QuerySelectMultipleField demonstreert. Om de code te verkleinen heb ik Bootstrap-Flask toegevoegd. De database is SQLite. Ik gebruik niet Flask-SQLAlchemy maar een eigen implementatie.

Er zijn drie tabellen:

  • Vriend
  • Stad
  • Hobby

Vriend-Stad is een veel-op-één relatie:

Een vriend kan maar in één stad wonen, en een stad kan veel vrienden hebben.

Vriend-Hobby is een many-to-many relatie:

Een vriend kan vele hobby's hebben, en een hobby kan door vele vrienden worden beoefend.

In het formulier Vriend:

  • het QuerySelectField wordt gebruikt om een enkele stad te selecteren
  • het QuerySelectMultipleField wordt gebruikt om nul of meer hobby's te selecteren

Ik heb de code voor de Create, Edit en Delete operaties min of meer gedupliceerd. Dit laat wat ruimte voor experimenteren. Ik heb het query_factory veld niet gebruikt in het formulier met QuerySelectField en QuerySelectMultipleField. In plaats daarvan heb ik dit toegevoegd aan de view functie, zoals:

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

Maak virtual environment

Ga naar je ontwikkelmap, maak een virtual environment aan voor een nieuwe map, bijvoorbeeld flaskquery, activeer het en ga de map in:

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

De projectdirectory is 'project' en onze applicatie staat in de app-directory.

Installeer pakketten

Om de code te minimaliseren zal ik Bootstrap-Flask gebruiken. Het beste deel is de formulier rendering met slechts een enkel statement. Verder gebruiken we SQLAlchemy en voor de database SQLite. Ik gebruik geen Flask-SQLAlchemy, dit heb ik in een eerdere post uitgelegd. Voor migraties gebruiken we Alembic, ik kan niet zonder.

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

Project map

Ter referentie, hier is de tree dump van de project directory voor het voltooide project.

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

Begin met een minimale app

In de project directory maken we een run.py bestand aan met de volgende inhoud:

#  run.py

from app import factory

app = factory.create_app()

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

Merk op dat we een factory.py bestand gebruiken in plaats van een __init__.py bestand. Vermijd de __init__.py. Wanneer je applicatie groeit kan je in circulaire imports terecht komen.

We zetten een config.py in de project directory om onze configuratie variabelen op te slaan:

# 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

Twee diensten, logging en database

We kunnen alles in het factory.py bestand zetten maar dat wordt rommelig. Laten we dus aparte bestanden maken met klassen voor logging en database.

# 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

Om toegang te krijgen tot de database maken we een 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)

We gebruiken een tussenliggend bestand services.py waar we de services instantiëren.

# services.py

from .service_app_logging import AppLogging
from .service_app_db import AppDb

app_logging = AppLogging()
app_db = AppDb()

De applicatiefabriek, eerste versie

Nu kunnen we de eerste versie van ons factory.py bestand maken:

# 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

Merk op dat ik hier een '@app.route' zet voor de home page.

In de templates directory zetten we twee bestanden, hier is de basis template, zie ook het voorbeeld in het Bootstrap-Flask pakket.

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

En de home page template.

{# home.html #}

{% extends "base.html" %}

{% block content %}

    {{ welcome_message }}

{% endblock %}

Eerste run

Ga naar de project directory en type:

python3  run.py

Richt je browser op 127.0.0.1:5000 en je zou het bericht 'Hallo wereld' moeten zien. U zou ook het Bootstrap menu bovenaan de pagina moeten zien. Bekijk de broncode van de pagina en controleer de bootstrap-bestanden. In de projectdirectory zou ook ons logbestand app.log moeten staan.

Voeg het model toe

We hebben nu een draaiende applicatie. In de app directory maken we een model.py bestand aan. We hebben Friends, Cities en 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,
    )

Probleem sorteren

Het zou mooi zijn als SQLAlchemy gesorteerde op naam resultaten teruggeeft. We kunnen dit gebruiken in lijsten.

  • Vrienden lijst: Toon de naam van de vriend, de plaatsnaam en de naam van de hobby's
  • Steden lijst: toon de naam van de stad en de namen van alle vrienden die in een stad wonen
  • Hobby's lijst: toon de naam van de hobby en de namen van alle vrienden die deze hobby hebben

Bijvoorbeeld, met een hobby hebben we toegang tot de vrienden als hobby.friends. Sorteren lijkt eenvoudig, we voegen gewoon een 'order_by' clausule toe in de relatie. Echter, omdat we verwijzen naar een klasse, Vriend, kunnen we dit alleen gebruiken met klassen die al eerder zijn geladen.

In ons model hierboven kunnen we de hobbies in de Friend klasse niet sorteren, omdat de Hobby klasse niet voor de Friend klasse is geladen. Maar we kunnen wel de vrienden sorteren in de Hobby-klasse, omdat de Vriend-klasse voor de Hobby-klasse werd geladen.

Om dit te omzeilen kunnen we een van de twee volgende dingen doen:

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

of:

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

In beide gevallen wordt naamresolutie uitgesteld tot het eerste gebruik.

Gebruik Alembic om de database te maken

Alembic is een geweldig gereedschap voor database migraties. We hebben het al geïnstalleerd, maar we moeten het eerst initialiseren voordat we het kunnen gebruiken. Ga naar de project directory en type:

alembic init alembic

Dit maakt een alembic.ini bestand aan en een alembic directory in de project directory. In alembic.ini, verander de regel:

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

in:

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

En in alembic/env.py verander je de regel:

target_metadata =  None

naar:

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

Maak de eerste revisie:

alembic revision -m "1e revision"

Voer de migratie uit:

alembic upgrade head

En het database bestand app.db is aangemaakt in de project directory.

Gebruik de SQLite browser om de database te bekijken

Installeer de browser SQLite :

sudo apt install sqlitebrowser

U kunt de SQLite -browser starten door met de rechtermuisknop op de database te klikken. Er is slechts één tabel aangemaakt: alembic_version.

Om onze databasetabellen aan te maken gebruiken we autogenerate:

alembic revision --autogenerate -m "create db"

Voer de migratie uit:

alembic upgrade head

Sluit de SQLite browser en open deze opnieuw en zie dat de tabellen zijn aangemaakt:

  • friend
  • stad
  • hobby
  • vriend_mtm_hobby

Voeg nu een vriend toe met behulp van 'Uitvoeren SQL':

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

Vergeet niet om hierna op 'Wijzigingen schrijven' te klikken! Klik vervolgens op 'Browse Data' en controleer of het ingevoegde record er is.

Het bericht op de startpagina wijzigen

Ik wil op de home page een bericht tonen dat al onze vrienden laat zien. Hiervoor veranderen we factory.py om de vrienden op te halen en ze door te geven aan de template:

# 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

En we itereren onze vrienden in de home page template:

{# home.html #}

{% extends "base.html" %}

{% block content %}

	{{ welcome_message }}

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

{% endblock %}

Vernieuw de pagina in de brower. Nu moet de naam van onze vriend John worden weergegeven op de home page.

Voeg een Blueprint toe om de gegevens te beheren

Om de database tabellen te manipuleren maken we een Blueprint, manage_data. In deze Blueprint voegen we voor elke tabel (object) de volgende methoden toe:

  • list
  • nieuw
  • bewerken
  • delete

We maken een blueprints directory aan en in deze directory een 'manage_data' directory. In deze directory maken we twee bestanden aan, views.py en forms.py. We gebruiken de QuerySelectField / QuerySelectMultipleField query_factory parameter niet in de form classes maar voegen deze toe in de view methods.

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

Zoals eerder gezegd, is er veel herhaling in views.py maar dat maakt het gemakkelijk om dingen te veranderen. Merk op dat we de sjablonen delen tussen Vriend, Stad en Hobby.

In de vriend views willen we een stad selecteren en een of meer hobby's selecteren. Hier initialiseren we de queries voor het QuerySelectField en QuerySelectMultipleField. Volgens de documentatie zal het een validatiefout opleveren als een van de items in het ingediende formulier niet in de query kan worden gevonden. En dit is precies wat we willen.

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

Sjablonen toevoegen voor de Blueprint

We hebben drie gedeelde sjablonen voor lijst, nieuw & bewerken, verwijderen. De render_form() functie van Bootstrap-Flask zet het formulier op de pagina.

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

Voor nieuw en bewerken is slechts één sjabloon nodig.

{# item_new_edit.html #}

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

{% extends "base.html" %}

{% block content %}

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

{% endblock %}

En tenslotte de verwijder sjabloon.

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

Update het menu in de basis template

In het basis-sjabloon voegen we navigatie-items toe voor Vrienden, Steden en Hobby's:

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

Voeg de Blueprint toe aan factory.py

We voegen een paar regels toe aan factory.py om de Blueprint toe te voegen. De uiteindelijke versie wordt:

# 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

Start de afgewerkte applicatie

Ga weer naar de project directory en type:

python3  run.py

Richt je browser op 127.0.0.1:5000. Nu zou je de afgewerkte applicatie moeten zien. Er zijn menu items voor Vrienden, Steden en Hobby's. Als je op Vrienden klikt, kom je in de lijst met vrienden. Hier kunt u vrienden toevoegen, bewerken en verwijderen. Hetzelfde geldt voor steden en hobby's.

Docker image downloaden en uitvoeren

Als u deze toepassing wilt uitvoeren, kunt u de Docker image (tgz, 64 MB) downloaden:

queryselectfield_100.tgz

De md5sum is:

b4f8116e6b8f30c4980a7ff96f0428a5

Om het beeld te laden:

docker load < queryselectfield_100.tgz

Uitvoeren:

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

En dan uw browser te richten op 127.0.0.1:5000.

Samenvatting

Dit is een voorbeeld met veel beperkingen, maar het toont de kracht van de WTForms QuerySelectField en QuerySelectMultipleField. En door Bootstrap-Flask te gebruiken kunnen we zonder enige moeite een menu maken en hoeven we de formulieren niet zelf te renderen.

Natuurlijk is dit niet productieklaar, maar je kunt het verfijnen, controles toevoegen, meer velden, enz. De QuerySelectField is geweldig voor one-to-many relaties en QuerySelectMultipleField is geweldig voor many-to-many relaties. Ze bieden voldoende flexibiliteit om uw toepassing op te bouwen.

Links / credits

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/

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.