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

Flask SQLAlchemy Aplicación CRUD con WTForms QuerySelectField y QuerySelectMultipleField

WTForms QuerySelectField y QuerySelectMultipleField facilitan la gestión de los datos de la relación SQLAlchemy .

8 marzo 2021 Actualizado 8 marzo 2021
post main image
https://unsplash.com/@helpdeskheroes

Para una nueva aplicación Flask que utiliza WTForms y SQLAlchemy, tenía muchas relaciones entre tablas y buscaba la forma más sencilla de gestionar estas tablas. La opción más obvia es utilizar los QuerySelectField y QuerySelectMultipleField presentes en el paquete wtforms-sqlalchemy. Como no los he utilizado antes, he creado una pequeña aplicación para jugar.

A continuación os muestro el código (desarrollo sobre Ubuntu 20.04). Si quieres verlo en acción, puedes descargar el Docker image al final de este post.

Resumen de la aplicación

Esta es una aplicación CRUD que demuestra el QuerySelectField y el QuerySelectMultipleField. Para reducir el código he añadido Bootstrap-Flask. La base de datos es SQLite. No estoy utilizando Flask-SQLAlchemy sino una implementación propia.

Hay tres tablas:

  • Amigo
  • Ciudad
  • Afición

Amigo-Ciudad es una relación de muchos a uno:

Un amigo puede vivir en una sola ciudad, y una ciudad puede tener muchos amigos.

Amigo-Afición es una relación many-to-many :

Un amigo puede tener muchas aficiones, y una afición puede ser practicada por muchos amigos.

En el formulario Amigo

  • el QuerySelectField se utiliza para seleccionar una sola ciudad
  • el QuerySelectMultipleField se utiliza para seleccionar un cero o más aficiones

Más o menos he duplicado el código para las operaciones de Crear, Editar y Eliminar. Esto deja algunos espacios para la experimentación. No utilicé el campo query_factory en el formulario con QuerySelectField y QuerySelectMultipleField. En su lugar, añadí esto a la función de la vista, como

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

Crear virtual environment

Ve a tu directorio de desarrollo, crea un virtual environment para un nuevo directorio, por ejemplo flaskquery, actívalo y entra en el directorio:

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

El directorio del proyecto es 'project' y nuestra aplicación está en el directorio app.

Instalar los paquetes

Para minimizar el código voy a utilizar Bootstrap-Flask. Lo mejor es el renderizado del formulario con una sola sentencia. Además utilizamos SQLAlchemy y para la base de datos SQLite. No utilizo Flask-SQLAlchemy, lo expliqué en un post anterior. Para las migraciones utilizamos Alembic, no puedo vivir sin él.

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

Directorio del proyecto

Para su referencia, aquí está el volcado del árbol del directorio del proyecto para el proyecto completado.

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

Empezar con una aplicación mínima

En el directorio del proyecto creamos un archivo run.py con el siguiente contenido:

#  run.py

from app import factory

app = factory.create_app()

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

Observa que utilizamos un archivo factory.py en lugar de un archivo __init__.py. Evite el archivo __init__.py. Cuando tu aplicación crece puedes encontrarte con importaciones circulares.

Ponemos un config.py en el directorio del proyecto para almacenar nuestras variables de configuración:

# 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

Dos servicios, logging y base de datos

Podemos poner todo en el archivo factory.py pero eso será un lío. Así que vamos a crear archivos separados con clases para el registro y la base de datos.

# 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

Para acceder a la base de datos creamos un scoped_session SQLAlchemy .

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

Utilizamos un fichero intermedio services.py donde instanciamos los servicios.

# services.py

from .service_app_logging import AppLogging
from .service_app_db import AppDb

app_logging = AppLogging()
app_db = AppDb()

La fábrica de aplicaciones, primera versión

Ahora podemos crear la primera versión de nuestro fichero factory.py:

# factory.py

from flask import  Flask, request, g, url_for,  current_app,  render_template
from flask_wtf.csrf import  CSRFProtect
from flask_bootstrap import  Bootstrap

from .services import app_logging, app_db

def  create_app():

    app =  Flask(__name__)

     app.config.from_object('config.ConfigDevelopment')

    # services
    app.logger = app_logging.init_app(app)
    app_db.init_app(app)
    app.logger.debug('test debug message')

     Bootstrap(app)

    csrf =  CSRFProtect()
    csrf.init_app(app)

    @app.teardown_appcontext
    def teardown_db(response_or_exception):
        if hasattr(app_db, 'session'):
            app_db.session.remove()

    @app.route('/')
    def index():
        return  render_template(
            'home.html',
            welcome_message='Hello world',
        )

    return app

Fíjate que aquí he puesto un '@app.route' para la página de inicio.

En el directorio templates ponemos dos archivos, aquí está la plantilla base, ver también el ejemplo en el paquete 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>

Y la plantilla de la página de inicio.

{# home.html #}

{% extends "base.html" %}

{% block content %}

    {{ welcome_message }}

{% endblock %}

Primera ejecución

Vaya al directorio del proyecto y escriba:

python3  run.py

Dirija su navegador a 127.0.0.1:5000 y debería ver el mensaje 'Hello world'. También deberías ver el menú Bootstrap en la parte superior de la página. Visualiza el código fuente de la página y comprueba los archivos bootstrap. En el directorio del proyecto también debería estar nuestro archivo de registro app.log.

Añadir el modelo

Ahora tenemos una aplicación en funcionamiento. En el directorio de la aplicación creamos un archivo model.py. Tenemos Amigos, Ciudades y Aficiones.

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

Problema de ordenación

Estaría bien que SQLAlchemy devolviera resultados ordenados por nombre. Podemos usar esto en las listas.

  • Lista de amigos: Mostrar el nombre del amigo, el nombre de la ciudad y el nombre de las aficiones
  • Lista de ciudades: muestra el nombre de la ciudad y los nombres de todos los amigos que viven en una ciudad
  • Lista de pasatiempos: muestra el nombre del pasatiempo y los nombres de todos los amigos que tienen ese pasatiempo

Por ejemplo, con un hobby accedemos a los amigos como hobby.friends. La ordenación parece fácil, simplemente añadimos una cláusula 'order_by' en la relación. Sin embargo, debido a que nos referimos a una clase, Friend, sólo podemos utilizar esto con las clases que se cargan antes.

En nuestro modelo anterior no podemos ordenar los hobbies en la clase Friend, porque la clase Hobby no fue cargada antes que la clase Friend. Pero podemos ordenar los amigos en la clase Hobby porque la clase Friend fue cargada antes que la clase Hobby.

Para evitar esto podemos hacer una de estas dos cosas

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

o:

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

En ambos casos la resolución de nombres se pospone hasta el primer uso.

Usar Alembic para crear la base de datos

Alembic es una gran herramienta para las migraciones de bases de datos. Ya la hemos instalado, pero debemos inicializarla antes de poder utilizarla. Ve al directorio del proyecto y escribe:

alembic init alembic

Esto creará un archivo alembic.ini y un directorio alembic en el directorio del proyecto. En alembic.ini, cambia la línea

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

por:

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

Y en alembic/env.py cambia la línea

target_metadata =  None

a:

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

Crear la primera revisión:

alembic revision -m "1e revision"

Ejecuta la migración:

alembic upgrade head

Y el archivo de base de datos app.db fue creado en el directorio del proyecto.

Usa el navegador SQLite para ver la base de datos

Instale el navegador SQLite :

sudo apt install sqlitebrowser

Puede iniciar el navegador SQLite haciendo clic con el botón derecho del ratón en la base de datos. Sólo se ha creado una tabla: alembic_version.

Para crear las tablas de nuestra base de datos utilizamos el autogenerador:

alembic revision --autogenerate -m "create db"

Ejecuta la migración:

alembic upgrade head

Cerramos el navegador SQLite y lo volvemos a abrir y observamos que se han creado las tablas:

  • amigo
  • ciudad
  • hobby
  • amigo_mtm_hobby

Ahora añade un amigo utilizando 'Ejecutar SQL':

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

No olvide hacer clic en "Escribir cambios" después de esto. A continuación, haga clic en "Examinar datos" y compruebe que el registro insertado está ahí.

Cambiar el mensaje de la página de inicio

Quiero mostrar un mensaje en la página de inicio que muestre a todos nuestros amigos. Para ello cambiamos factory.py para obtener los amigos y pasarlos a la plantilla:

# 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

Y iteramos nuestros amigos en la plantilla de la página de inicio:

{# home.html #}

{% extends "base.html" %}

{% block content %}

	{{ welcome_message }}

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

{% endblock %}

Refrescamos la página en el brower. Ahora el nombre de nuestro amigo Juan debería aparecer en la página de inicio.

Añadir un Blueprint para gestionar los datos

Para manipular las tablas de la base de datos creamos un Blueprint, manage_data. En este Blueprint añadimos los siguientes métodos para cada tabla (objeto):

  • lista
  • nuevo
  • editar
  • borrar

Creamos un directorio blueprints y en este directorio un directorio 'manage_data'. En este directorio creamos dos ficheros, views.py y forms.py. No utilizamos el parámetro QuerySelectField / QuerySelectMultipleField query_factory en las clases de los formularios sino que los añadimos en los métodos de las vistas.

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

Como se ha dicho antes, hay mucha repetición en views.py pero eso hace que sea fácil cambiar las cosas. Observa que compartimos las plantillas entre Friend, City y Hobby.

En el amigo views queremos seleccionar una ciudad y seleccionar una o más aficiones. Aquí inicializamos las consultas para el QuerySelectField y QuerySelectMultipleField. Según la documentación, si alguno de los elementos del formulario enviado no se encuentra en la consulta, se producirá un error de validación. Y esto es exactamente lo que queremos.

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

Añadir plantillas para el Blueprint

Tenemos tres plantillas compartidas para lista, nuevo y editar, eliminar. La función render_form() de Bootstrap-Flask pone el formulario en la página.

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

La operación de nuevo y de edición sólo requiere una plantilla.

{# item_new_edit.html #}

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

{% extends "base.html" %}

{% block content %}

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

{% endblock %}

Y por último la plantilla de borrado.

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

Actualizar el menú en la plantilla base

En la plantilla base añadimos elementos de navegación para Amigos, Ciudades y Aficiones:

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

Añadimos el Blueprint a factory.py

Añadimos unas líneas al factory.py para añadir el Blueprint. La versión final queda así:

# 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

Ejecutar la aplicación terminada

De nuevo, ve al directorio del proyecto y escribe:

python3  run.py

Dirija su navegador a 127.0.0.1:5000. Ahora debería ver la aplicación terminada. Hay elementos de menú para Amigos, Ciudades y Aficiones. Al hacer clic en Amigos, se accede a la lista de amigos. Aquí puedes añadir, editar y eliminar amigos. Lo mismo ocurre con las ciudades y los pasatiempos.

Descargar Docker image y ejecutar

Si quieres ejecutar esta aplicación puedes descargar el archivo Docker image (tgz, 64 MB):

queryselectfield_100.tgz

La suma md5 es:

b4f8116e6b8f30c4980a7ff96f0428a5

Para cargar la imagen:

docker load < queryselectfield_100.tgz

Para ejecutar:

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

Y luego apunte su navegador a 127.0.0.1:5000.

Resumen

Este es un ejemplo con muchas limitaciones pero muestra la potencia del WTForms QuerySelectField y QuerySelectMultipleField. Y al incluir Bootstrap-Flask podemos crear un menú sin ningún esfuerzo y no tenemos que renderizar los formularios nosotros mismos.

Por supuesto, esto no está listo para la producción, pero se puede refinar, añadir controles, más campos, etc. El QuerySelectField es genial para las relaciones uno a muchos y el QuerySelectMultipleField es genial para las relaciones many-to-many . Ofrecen suficiente flexibilidad para construir su aplicación.

Enlaces / créditos

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/

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.