36. Ejercicio de aplicación: version 16

36.1. Introducción
Los URL de nuestra aplicación tienen, por el momento, el formato [/action/param1/param2/…]. Nos gustaría poder anteponer un prefijo a estos URL. Por ejemplo, con el prefijo [/do], tendríamos URL con el formato [/do/action/param1/param2/…].
36.2. La nueva configuración de las rutas

- en [2], el cálculo de las rutas se modificará;
- en [1], el archivo [config] se modifica para reflejar este cambio;
La configuración [config] queda de la siguiente manera:
def configure(config: dict) -> dict:
# configuración de syspath
import syspath
config['syspath'] = syspath.configure(config)
# configuración de la aplicación
import parameters
config['parameters'] = parameters.configure(config)
# configuración de la base de datos
import database
config["database"] = database.configure(config)
# instanciación de las capas de la aplicación
import layers
config['layers'] = layers.configure(config)
# configuración MVC de la capa [web]
config['mvc'] = {}
# Configuración de los controladores de la capa [web]
import controllers
config['mvc'].update(controllers.configure(config))
# acciones ASV (Acción Mostrar vista)
import asv_actions
config['mvc'].update(asv_actions.configure(config))
# acciones ADS (Acción «Do Something»)
import ads_actions
config['mvc'].update(ads_actions.configure(config))
# configuración de respuestas HTTP
import responses
config['mvc'].update(responses.configure(config))
# configuración de rutas
import routes
routes.configure(config)
# se aplica la configuración
return config
- líneas 37-39: el módulo [routes] (línea 38) se encarga de configurar las rutas (línea 39);
El archivo de rutas sin token CSRF [configs/routes_without_csrftoken] evoluciona de la siguiente manera:
# dependencias
from flask import redirect, request, session, url_for
from flask_api import status
# configuración de la aplicación
config = {}
# el controlador principal
def front_controller() -> tuple:
# se reenvía la solicitud al controlador principal
main_controller = config['mvc']['controllers']['main-controller']
return main_controller.execute(request, session, config)
# raíz de la aplicación
def index() -> tuple:
# redirección a /init-session/html
return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)
# init-session
def init_session(type_response: str) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# autenticar-usuario
def authentifier_utilisateur() -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# calcular-impuesto
def calculer_impot() -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
…
Se han eliminado las rutas del archivo. Solo quedan las funciones asociadas a ellas.
Los archivos de rutas con el identificador CSRF y [configs/routes_with_csrftoken] corren la misma suerte:
# dependencias
from flask import redirect, request, session, url_for
from flask_api import status
from flask_wtf.csrf import generate_csrf
# configuración
config = {}
# el controlador frontal
def front_controller() -> tuple:
# se reenvía la solicitud al controlador principal
main_controller = config['mvc']['controllers']['main-controller']
return main_controller.execute(request, session, config)
# raíz de la aplicación
def index() -> tuple:
# redirección a /init-session/html
return redirect(url_for("init_session", type_response="html", csrf_token=generate_csrf()), status.HTTP_302_FOUND)
# init-session
def init_session(type_response: str, csrf_token: str) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
# autenticar-usuario
def authentifier_utilisateur(csrf_token: str) -> tuple:
# se ejecuta el controlador asociado a la acción
return front_controller()
…
# iniciar sesión sin token CSRF para clients, json y xml
def init_session_without_csrftoken(type_response: str) -> tuple:
# redirección a /init-session/type_response
return redirect(url_for("init_session", type_response=type_response, csrf_token=generate_csrf()),
status.HTTP_302_FOUND)
Las rutas se calculan mediante el siguiente módulo [configs/routes]:
from flask import Flask
def configure(config: dict):
# configuración de rutas
# aplicación Flask
app = Flask(__name__, template_folder="../flask/templates", static_folder="../flask/static")
config['app'] = app
# importación de rutas de la aplicación web
if config['parameters']['with_csrftoken']:
import routes_with_csrftoken as routes
else:
import routes_without_csrftoken as routes
# se inyecta la configuración en las rutas
routes.config = config
# el prefijo de URL de la aplicación
prefix_url = config["parameters"]["prefix_url"]
# token CSRF
with_csrftoken = config["parameters"]['with_csrftoken']
if with_csrftoken:
csrftoken_param = f"/<string:csrf_token>"
else:
csrftoken_param = ""
# las rutas de la aplicación Flask
# raíz de la aplicación
app.add_url_rule(f'{prefix_url}/', methods=['GET'],
view_func=routes.index)
# inicio de sesión
app.add_url_rule(f'{prefix_url}/init-session/<string:type_response>{csrftoken_param}', methods=['GET'],
view_func=routes.init_session)
# iniciar sesión sin token CSRF
if with_csrftoken:
app.add_url_rule(f'{prefix_url}/init-session-without-csrftoken/<string:type_response>',
methods=['GET'],
view_func=routes.init_session_without_csrftoken)
# autenticar-usuario
app.add_url_rule(f'{prefix_url}/authentifier-utilisateur{csrftoken_param}', methods=['POST'],
view_func=routes.authentifier_utilisateur)
# calcular-impuesto
app.add_url_rule(f'{prefix_url}/calculer-impot{csrftoken_param}', methods=['POST'],
view_func=routes.calculer_impot)
# cálculo del impuesto por lotes
app.add_url_rule(f'{prefix_url}/calculer-impots{csrftoken_param}', methods=['POST'],
view_func=routes.calculer_impots)
# listar-simulaciones
app.add_url_rule(f'{prefix_url}/lister-simulations{csrftoken_param}', methods=['GET'],
view_func=routes.lister_simulations)
# eliminar-simulación
app.add_url_rule(f'{prefix_url}/supprimer-simulation/<int:numero>{csrftoken_param}', methods=['GET'],
view_func=routes.supprimer_simulation)
# fin-sesión
app.add_url_rule(f'{prefix_url}/fin-session{csrftoken_param}', methods=['GET'],
view_func=routes.fin_session)
# mostrar-cálculo-impuestos
app.add_url_rule(f'{prefix_url}/afficher-calcul-impot{csrftoken_param}', methods=['GET'],
view_func=routes.afficher_calcul_impot)
# get-datos-de-administración
app.add_url_rule(f'{prefix_url}/get-admindata{csrftoken_param}', methods=['GET'],
view_func=routes.get_admindata)
# mostrar-vista-cálculo-impuestos
app.add_url_rule(f'{prefix_url}/afficher-vue-calcul-impot{csrftoken_param}', methods=['GET'],
view_func=routes.afficher_vue_calcul_impot)
# mostrar-vista-autenticación
app.add_url_rule(f'{prefix_url}/afficher-vue-authentification{csrftoken_param}', methods=['GET'],
view_func=routes.afficher_vue_authentification)
# mostrar-vista-lista-simulaciones
app.add_url_rule(f'{prefix_url}/afficher-vue-liste-simulations{csrftoken_param}', methods=['GET'],
view_func=routes.afficher_vue_liste_simulations)
# mostrar-vista-lista_errores
app.add_url_rule(f'{prefix_url}/afficher-vue-liste-erreurs{csrftoken_param}', methods=['GET'],
view_func=routes.afficher_vue_liste_erreurs)
# caso especial
if with_csrftoken:
# iniciar-sesión-sin-token-csrf para los clients, json y xml
app.add_url_rule(f'{prefix_url}/init-session-without-csrftoken', methods=['GET'],
view_func=routes.init_session_without_csrftoken)
- líneas 6-8: se crea la aplicación Flask y se incluye en la configuración;
- líneas 10-14: se importa el archivo de rutas adecuado para la situación. Recordemos que el archivo importado carece, de hecho, de rutas. Solo contiene las funciones asociadas a ellas;
- línea 17: las funciones asociadas a las rutas necesitan conocer la configuración de la aplicación;
- línea 20: se anota el prefijo de URL. Este puede estar vacío;
- líneas 22-27: las rutas con el token CSRF tienen un parámetro adicional respecto a las que no lo tienen. Para gestionar esta diferencia, se utiliza la variable [csrftoken_param]:
- contiene la cadena vacía si no hay ningún token CSRF en las rutas;
- contiene la cadena [/<string:csrf_token>] si hay un token CSRF;
- líneas 29-96: cada ruta de la aplicación está asociada a una función del archivo de rutas importado en las líneas 10-14;
36.3. Los nuevos controladores

En el version anterior, todos los controladores obtenían la acción que se estaba procesando de la siguiente manera:
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# se recuperan los elementos del path
params = request.path.split('/')
action = params[1]
Este código ya no funciona si hay un prefijo, por ejemplo, [/do/lister-simulations]. En este caso, la acción de la línea 3 sería [do] y, por lo tanto, sería incorrecta.
Transformamos este código de la siguiente manera:
def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
# se recuperan los elementos del path
prefix_url = config["parameters"]["prefix_url"]
params = request.path[len(prefix_url):].split('/')
action = params[1]
Hacemos esto en todos los controladores.
36.4. Los nuevos modelos

En el version anterior, cada clase de modelo generaba un modelo de la siguiente manera:
def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
# se encapsulan los datos de la página en la plantilla
modèle = {}
# estado de la aplicación
état = résultat["état"]
# la plantilla depende del estado
…
# token CSRF
modèle['csrf_token'] = super().get_csrftoken(config)
# acciones posibles desde la vista
modèle['actions_possibles'] = […]
# se devuelve la plantilla
return modèle
A partir de ahora, la plantilla tendrá una clave adicional, el prefijo de URL:
def get_model_for_view(self, request: Request, session: LocalProxy, config: dict, résultat: dict) -> dict:
# encapsulamos los datos de la página en el modelo
modèle = {}
# estado de la aplicación
état = résultat["état"]
# la plantilla depende del estado
…
# token CSRF
modèle['csrf_token'] = super().get_csrftoken(config)
# acciones posibles desde la vista
modèle['actions_possibles'] = […]
# prefijo de URL
modèle["prefix_url"] = config["parameters"]["prefix_url"]
# se devuelve la plantilla
return modèle
36.5. Los nuevos fragmentos

Todos los fragmentos que contengan URL deben modificarse.
El fragmento [v-authentification]
<!-- formulario HTML - se envían sus valores con la acción [authentifier-utilisateur] -->
<form method="post" action="{{modèle.prefix_url}}/authentifier-utilisateur{{modèle.csrf_token}}">
<!-- título -->
<div class="alert alert-primary" role="alert">
<h4>Veuillez vous authentifier</h4>
</div>
…
</form>
El fragmento [v-calcul-impot]
<!-- formulario HTML enviado -->
<form method="post" action="{{modèle.prefix_url}}/calculer-impot{{modèle.csrf_token}}">
<!-- mensaje en 12 columnas sobre fondo azul -->
…
</form>
El fragmento [v-liste-simulations]
…
{% if modèle.simulations is defined and modèle.simulations|length!=0 %}
…
<!-- tabla de simulaciones -->
<table class="table table-sm table-hover table-striped">
…
<tr>
<th scope="row">{{simulation.id}}</th>
<td>{{simulation.marié}}</td>
<td>{{simulation.enfants}}</td>
<td>{{simulation.salaire}}</td>
<td>{{simulation.impôt}}</td>
<td>{{simulation.surcôte}}</td>
<td>{{simulation.décôte}}</td>
<td>{{simulation.réduction}}</td>
<td>{{simulation.taux}}</td>
<td><a href="{{modèle.prefix_url}}/supprimer-simulation/{{simulation.id}}{{modèle.csrf_token}}">Supprimer</a></td>
</tr>
{% endfor %}
</tr>
</tbody>
</table>
{% endif %}
El fragmento [v-menu]
<!-- menú Bootstrap -->
<nav class="nav flex-column">
<!-- visualización de una lista de enlaces HTML -->
{% for optionMenu in modèle.optionsMenu %}
<a class="nav-link" href="{{modèle.prefix_url}}{{optionMenu.url}}{{modèle.csrf_token}}">{{optionMenu.text}}</a>
{% endfor %}
</nav>
36.6. La nueva respuesta HTML

En el anterior version, el código de la respuesta HTML terminaba con una redirección:
class HtmlResponse(InterfaceResponse):
def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
résultat: dict) -> (Response, int):
# la respuesta HTML depende del código de estado devuelto por el controlador
état = résultat["état"]
…
# ahora hay que generar el URL de redireccionamiento sin olvidar el token CSRF si se solicita
if config['parameters']['with_csrftoken']:
csrf_token = f"/{generate_csrf()}"
else:
csrf_token = ""
# respuesta de redireccionamiento
return redirect(f"{ads['to']}{csrf_token}"), status.HTTP_302_FOUND
Este código pasa a ser ahora el siguiente:
def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
résultat: dict) -> (Response, int):
…
# Ahora hay que generar el URL de redireccionamiento sin olvidar el token CSRF si se solicita
if config['parameters']['with_csrftoken']:
csrf_token = f"/{generate_csrf()}"
else:
csrf_token = ""
# respuesta de redireccionamiento
return redirect(f"{config['parameters']['prefix_url']}{ads['to']}{csrf_token}"), status.HTTP_302_FOUND
36.7. Pruebas

Pongamos el siguiente prefijo en el archivo de configuración [parameters]:
…
# token csrf
"with_csrftoken": True,
# bases gestionadas MySQL (mysql), PostgreSQL (pgres)
"databases": ["mysql", "pgres"],
# prefijo de los URL de la aplicación
# establecer la cadena como vacía si no se desea ningún prefijo o /prefijo en caso contrario
"prefix_url": "/do",
…
Ejecutemos la aplicación y solicitemos el URL [http://localhost:5000/do]; la respuesta es la siguiente:

- en [1], el prefijo de URL;
- en [2], el token CSRF;
La aplicación también se puede probar con las pruebas de consola de [http-clients/09]:

En los archivos de configuración [1] y [2], el prefijo de URL debe incluirse en el URL del servidor:
"server": {
# "urlServer": "http://127.0.0.1:5000",
"urlServer": "http://127.0.0.1:5000/do",
"user": {
"login": "admin",
"password": "admin"
},
"url_services": {
…
}
- línea 3: el URL del servidor incluye ahora el prefijo de URL;
Una vez realizada esta modificación, todas las pruebas de consola deberían funcionar.