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

Flask SQLAlchemy CRUD-Anwendung mit WTForms QuerySelectField und QuerySelectMultipleField

WTForms QuerySelectField und QuerySelectMultipleField erleichtern die Verwaltung von SQLAlchemy -Beziehungsdaten.

8 März 2021 Aktualisiert 8 März 2021
post main image
https://unsplash.com/@helpdeskheroes

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:

queryselectfield_100.tgz

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/

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.