Flask SQLAlchemy CRUD-Anwendung mit WTForms QuerySelectField und QuerySelectMultipleField
WTForms QuerySelectField und QuerySelectMultipleField erleichtern die Verwaltung von SQLAlchemy -Beziehungsdaten.
Für eine neue Flask -Anwendung, die WTForms und SQLAlchemy verwendet, hatte ich viele Beziehungen zwischen Tabellen und war auf der Suche nach der einfachsten Möglichkeit, diese Tabellen zu verwalten. Die naheliegendste Wahl ist die Verwendung von QuerySelectField und QuerySelectMultipleField aus dem Paket wtforms-sqlalchemy. Da ich sie noch nicht verwendet habe, habe ich eine kleine Anwendung erstellt, um damit zu spielen.
Unten zeige ich Ihnen den Code (Entwicklung auf Ubuntu 20.04). Wenn Sie es in Aktion sehen wollen, können Sie den Docker image am Ende dieses Beitrags herunterladen.
Zusammenfassung der Anwendung
Dies ist eine CRUD-Anwendung, die das QuerySelectField und QuerySelectMultipleField demonstriert. Um den Code zu reduzieren, habe ich Bootstrap-Flask hinzugefügt. Die Datenbank ist SQLite. Ich verwende nicht Flask-SQLAlchemy , sondern eine proprietäre Implementierung.
Es gibt drei Tabellen:
- Freund .
- Stadt
- Hobby
Freund-Stadt ist eine viele-zu-eins-Beziehung:
Ein Freund kann nur in einer Stadt leben, und eine Stadt kann viele Freunde haben.
Freund-Hobby ist eine many-to-many -Beziehung:
Ein Freund kann viele Hobbys haben, und ein Hobby kann von vielen Freunden ausgeübt werden.
Im Formular Freund:
- das QuerySelectField wird verwendet, um eine einzelne Stadt auszuwählen
- das QuerySelectMultipleField wird verwendet, um null oder mehr Hobbys auszuwählen
Ich habe den Code für die Operationen Erstellen, Bearbeiten und Löschen mehr oder weniger dupliziert. Dies lässt etwas Raum zum Experimentieren. Ich habe das Feld query_factory nicht im Formular mit QuerySelectField und QuerySelectMultipleField verwendet. Stattdessen fügte ich dies der Ansichtsfunktion hinzu, wie:
form.city.query = app_db.session.query(City).order_by(City.name)
virtual environment erstellen
Gehen Sie in Ihr Entwicklungsverzeichnis, erstellen Sie eine virtual environment für ein neues Verzeichnis, z. B. flaskquery, aktivieren Sie sie und geben Sie das Verzeichnis an:
cd <your-development-directory>
python3 -m venv flaskquery
source flaskquery/bin/activate
cd flaskquery
mkdir project
cd project
mkdir app
Das Projektverzeichnis ist "project" und unsere Anwendung befindet sich im Verzeichnis "app".
Pakete installieren
Um den Code zu minimieren, werde ich Bootstrap-Flask verwenden. Der beste Teil ist das Formular-Rendering mit nur einer einzigen Anweisung. Weiterhin verwenden wir SQLAlchemy und für die Datenbank SQLite. Ich verwende nicht Flask-SQLAlchemy, dies habe ich in einem früheren Beitrag erklärt. Für Migrationen verwenden wir Alembic, ich kann nicht ohne sie leben.
pip install flask
pip install flask-wtf
pip install bootstrap-flask
pip install sqlalchemy
pip install wtforms-sqlalchemy
pip install alembic
Projekt-Verzeichnis
Zur Veranschaulichung sehen Sie hier den Baumabbild des Projektverzeichnisses für das fertige Projekt.
.
├── 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
Wir beginnen mit einer minimalen App
Im Projektverzeichnis legen wir eine Datei run.py mit folgendem Inhalt an:
# run.py
from app import factory
app = factory.create_app()
if __name__ == '__main__':
app.run(host= '0.0.0.0')
Beachten Sie, dass wir eine Datei factory.py anstelle der Datei __init__.py verwenden. Vermeiden Sie die __init__.py. Wenn Ihre Anwendung wächst, können Sie auf zirkuläre Importe stoßen.
Wir legen eine config.py in das Projektverzeichnis, um unsere Konfigurationsvariablen zu speichern:
# 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
Zwei Dienste, Logging und Datenbank
Wir können alles in die Datei factory.py packen, aber das wird unübersichtlich. Also legen wir separate Dateien mit Klassen für Logging und Datenbank an.
# 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
Für den Zugriff auf die Datenbank erstellen wir eine 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)
Wir verwenden eine Zwischendatei services.py, in der wir die Dienste instanziieren.
# services.py
from .service_app_logging import AppLogging
from .service_app_db import AppDb
app_logging = AppLogging()
app_db = AppDb()
Die Anwendungsfabrik, erste Version
Nun können wir die erste Version unserer Datei factory.py erstellen:
# 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
Beachten Sie, dass ich hier eine "@app.route" für die Startseite eingefügt habe.
In das templates-Verzeichnis legen wir zwei Dateien, hier ist das Basis-Template, siehe auch das Beispiel im Paket 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>
Und das Homepage-Template.
{# home.html #}
{% extends "base.html" %}
{% block content %}
{{ welcome_message }}
{% endblock %}
Erster Durchlauf
Gehen Sie in das Projektverzeichnis und geben Sie ein:
python3 run.py
Richten Sie Ihren Browser auf 127.0.0.1:5000 und Sie sollten die Meldung "Hello world" sehen. Außerdem sollten Sie das Menü Bootstrap oben auf der Seite sehen. Sehen Sie sich den Quellcode der Seite an und überprüfen Sie die Bootstrap-Dateien. Im Projektverzeichnis sollte sich auch unsere Log-Datei app.log befinden.
Hinzufügen des Modells
Wir haben nun eine laufende Anwendung. Im app-Verzeichnis legen wir eine Datei model.py an. Wir haben Friends, Cities und 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,
)
Problem beim Sortieren
Es wäre schön, wenn SQLAlchemy nach Namen sortierte Ergebnisse liefert. Dies können wir in Listen verwenden.
- Freunde-Liste: Zeige den Namen des Freundes, den Namen der Stadt und den Namen der Hobbys
- Städte-Liste: zeigt den Städtenamen und die Namen aller Freunde, die in einer Stadt leben, an
- Hobbyliste: Zeige den Namen des Hobbys und die Namen aller Freunde, die dieses Hobby haben
Bei einem Hobby greifen wir z.B. auf die Freunde als hobby.friends zu. Das Sortieren sieht einfach aus, wir fügen einfach eine 'order_by'-Klausel in die Beziehung ein. Da wir uns jedoch auf eine Klasse, Friend, beziehen, können wir dies nur mit Klassen verwenden, die zuvor geladen wurden.
In unserem obigen Modell können wir die Hobbys in der Klasse Friend nicht sortieren, weil die Klasse Hobby nicht vor der Klasse Friend geladen wurde. Aber wir können die Freunde in der Hobby-Klasse sortieren, weil die Friend-Klasse vor der Hobby-Klasse geladen wurde.
Um dies zu umgehen, können wir eines von zwei Dingen tun:
hobbies = relationship(
'Hobby',
secondary=friend_mtm_hobby_table,
back_populates='friends',
order_by='Hobby.name',
)
oder:
hobbies = relationship(
'Hobby',
secondary=friend_mtm_hobby_table,
back_populates='friends',
order_by=lambda: Hobby.name,
)
In beiden Fällen wird die Namensauflösung bis zur ersten Verwendung aufgeschoben.
Verwenden Sie Alembic , um die Datenbank zu erstellen
Alembic ist ein großartiges Werkzeug für Datenbankmigrationen. Wir haben es bereits installiert, aber wir müssen es initialisieren, bevor wir es verwenden können. Gehen Sie in das Projektverzeichnis und geben Sie ein:
alembic init alembic
Dadurch wird eine alembic.ini-Datei und ein alembic-Verzeichnis im Projektverzeichnis erstellt. Ändern Sie in alembic.ini die Zeile:
sqlalchemy.url = driver://user:pass@localhost/dbname
zu:
sqlalchemy.url = sqlite:///app.db
Und in alembic/env.py ändern Sie die Zeile:
target_metadata = None
zu:
from app.model import Base
target_metadata = [Base.metadata]
Legen Sie die erste Revision an:
alembic revision -m "1e revision"
Führen Sie die Migration aus:
alembic upgrade head
Und die Datenbankdatei app.db wurde im Projektverzeichnis angelegt.
Verwenden Sie den Browser SQLite , um die Datenbank anzuzeigen
Installieren Sie den SQLite -Browser:
sudo apt install sqlitebrowser
Sie können den SQLite -Browser mit einem Rechtsklick auf die Datenbank starten. Es wurde nur eine Tabelle erstellt: alembic_version.
Um unsere Datenbanktabellen zu erstellen, verwenden wir Autogenerate:
alembic revision --autogenerate -m "create db"
Führen Sie die Migration aus:
alembic upgrade head
Schließen Sie den SQLite -Browser und öffnen Sie ihn erneut und beobachten Sie, dass die Tabellen erstellt wurden:
- Freund
- Stadt
- hobby
- freund_mtm_hobby
Fügen Sie nun einen Freund hinzu, indem Sie 'Execute SQL' verwenden:
INSERT INTO friend (name) VALUES ('John');
Vergessen Sie nicht, danach auf 'Änderungen schreiben' zu klicken! Klicken Sie dann auf 'Daten durchsuchen' und prüfen Sie, ob der eingefügte Datensatz vorhanden ist.
Ändern der Startseitenmeldung
Ich möchte auf der Startseite eine Nachricht anzeigen, die alle unsere Freunde anzeigt. Dazu ändern wir factory.py, um die Freunde zu erhalten und sie an das Template zu übergeben:
# 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
Und wir iterieren unsere Freunde im Home Page Template:
{# home.html #}
{% extends "base.html" %}
{% block content %}
{{ welcome_message }}
<ul>
{% for friend in friends %}
<li>
{{ friend.name }}
</li>
{% endfor %}
</ul>
{% endblock %}
Aktualisieren Sie die Seite im Brower. Jetzt sollte der Name unseres Freundes John auf der Startseite angezeigt werden.
Hinzufügen eines Blueprint zum Verwalten der Daten
Um die Datenbanktabellen zu manipulieren, erstellen wir eine Blueprint, manage_data. In dieser Blueprint fügen wir für jede Tabelle (Objekt) die folgenden Methoden hinzu:
- list
- neu
- bearbeiten
- löschen
Wir erstellen ein Verzeichnis blueprints und in diesem Verzeichnis ein Verzeichnis 'manage_data'. In diesem Verzeichnis erstellen wir zwei Dateien, views.py und forms.py. Wir verwenden die Parameter QuerySelectField / QuerySelectMultipleField query_factory nicht in den Formularklassen, sondern fügen sie in den View-Methoden hinzu.
# 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')
Wie bereits erwähnt, gibt es eine Menge Wiederholungen in views.py, aber das macht es einfach, Dinge zu ändern. Beachten Sie, dass wir die Vorlagen zwischen Friend, City und Hobby teilen.
Im Freund views wollen wir eine Stadt auswählen und ein oder mehrere Hobbys auswählen. Hier initialisieren wir die Abfragen für das QuerySelectField und QuerySelectMultipleField. Laut Dokumentation führt dies zu einem Validierungsfehler, wenn eines der Elemente im eingereichten Formular nicht in der Abfrage gefunden wird. Und das ist genau das, was wir wollen.
# 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)
Hinzufügen von Vorlagen für den Blueprint
Wir haben drei gemeinsame Vorlagen für Liste, Neu & Bearbeiten, Löschen. Die Funktion render_form() von Bootstrap-Flask bringt das Formular auf die Seite.
{# 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 %}
Der Neu- und Editiervorgang benötigt nur eine Vorlage.
{# item_new_edit.html #}
{% from 'bootstrap/form.html' import render_form %}
{% extends "base.html" %}
{% block content %}
<h1>
{{ title }}
</h1>
{{ render_form(form) }}
{% endblock %}
Und schließlich die Löschvorlage.
{# 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 %}
Aktualisieren des Menüs in der Basisvorlage
In der Basisvorlage fügen wir Navigationspunkte für Freunde, Städte und Hobbys hinzu:
{# 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>
Fügen Sie die Blueprint zur factory.py hinzu
Wir fügen ein paar Zeilen in die factory.py ein, um die Blueprint hinzuzufügen. Die endgültige Version wird:
# 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
Ausführen der fertigen Anwendung
Gehen Sie wieder in das Projektverzeichnis und geben Sie ein:
python3 run.py
Richten Sie Ihren Browser auf 127.0.0.1:5000. Nun sollten Sie die fertige Anwendung sehen. Es gibt Menüpunkte für Friends, Cities und Hobbies. Wenn Sie auf Freunde klicken, gelangen Sie in die Liste der Freunde. Hier können Sie Freunde hinzufügen, bearbeiten und löschen. Das Gleiche gilt für Städte und Hobbys.
Docker image herunterladen und ausführen
Wenn Sie diese Anwendung ausführen möchten, können Sie die Datei Docker image (tgz, 64 MB) herunterladen:
Die md5sum ist:
b4f8116e6b8f30c4980a7ff96f0428a5
Zum Laden des Images:
docker load < queryselectfield_100.tgz
Zum Ausführen:
docker run --name queryselectfield -p 5000:5000 queryselectfield:1.00
Und dann zeigen Sie mit Ihrem Browser auf 127.0.0.1:5000.
Zusammenfassung
Dies ist ein Beispiel mit vielen Einschränkungen, aber es zeigt die Leistungsfähigkeit von WTForms QuerySelectField und QuerySelectMultipleField. Und durch die Einbindung von Bootstrap-Flask können wir ohne Aufwand ein Menü erstellen und müssen die Formulare nicht selbst rendern.
Natürlich ist dies nicht produktionsreif, aber Sie können es verfeinern, Prüfungen hinzufügen, weitere Felder hinzufügen usw. Das QuerySelectField eignet sich hervorragend für eins-zu-viele-Beziehungen und das QuerySelectMultipleField eignet sich hervorragend für many-to-many -Beziehungen. Sie bieten genügend Flexibilität, um Ihre Anwendung zu erstellen.
Links / Impressum
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/
Mehr erfahren
Alembic Bootstrap Flask SQLAlchemy WTForms
Neueste
- Ausblenden der Primärschlüssel der Datenbank UUID Ihrer Webanwendung
- Don't Repeat Yourself (DRY) mit Jinja2
- SQLAlchemy, PostgreSQL, maximale Anzahl von Zeilen pro user
- Anzeige der Werte in den dynamischen Filtern SQLAlchemy
- Sichere Datenübertragung mit Public Key Verschlüsselung und pyNaCl
- rqlite: eine hochverfügbare und distverteilte SQLite -Alternative
Meistgesehen
- Verwendung von Pythons pyOpenSSL zur Überprüfung von SSL-Zertifikaten, die von einem Host heruntergeladen wurden
- Verwendung von UUIDs anstelle von Integer Autoincrement Primary Keys mit SQLAlchemy und MariaDb
- Verbindung zu einem Dienst auf einem Docker -Host von einem Docker -Container aus
- PyInstaller und Cython verwenden, um eine ausführbare Python-Datei zu erstellen
- SQLAlchemy: Verwendung von Cascade Deletes zum Löschen verwandter Objekte
- Flask RESTful API Validierung von Anfrageparametern mit Marshmallow-Schemas