Flask SQLAlchemy CRUD-toepassing met WTForms QuerySelectField en QuerySelectMultipleField
WTForms QuerySelectField en QuerySelectMultipleField maken het gemakkelijk om SQLAlchemy relatiegegevens te beheren.
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:
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/
Lees meer
Alembic Bootstrap Flask SQLAlchemy WTForms
Recent
- Database UUID primaire sleutels van je webapplicatie verbergen
- Don't Repeat Yourself (DRY) met Jinja2
- SQLAlchemy, PostgreSQL, maximum aantal rijen per user
- Toon de waarden in SQLAlchemy dynamische filters
- Veilige gegevensoverdracht met Public Key versleuteling en pyNaCl
- rqlite: een alternatief voor SQLite met hoge beschikbaarheid en distributed
Meest bekeken
- Met behulp van Python's pyOpenSSL om SSL-certificaten die van een host zijn gedownload te controleren
- Gebruik van UUIDs in plaats van Integer Autoincrement Primary Keys met SQLAlchemy en MariaDb
- Maak verbinding met een dienst op een Docker host vanaf een Docker container
- PyInstaller en Cython gebruiken om een Python executable te maken
- SQLAlchemy: Gebruik van Cascade Deletes om verwante objecten te verwijderen
- Flask RESTful API verzoekparametervalidatie met Marshmallow-schema's