Skip to content

30. Ejercicio práctico: version 12

En este capítulo vamos a escribir una aplicación web que siga la arquitectura MVC (Modelo-Vista-Controlador). La aplicación podrá entregar sus respuestas en tres formatos: jSON, XML, HTML. Hay un salto de complejidad entre lo que vamos a hacer ahora y lo que se ha hecho anteriormente. Vamos a reutilizar la mayoría de los conceptos vistos hasta ahora y vamos a detallar todos los pasos que conducen a la aplicación final.

30.1. Arquitectura MVC

Vamos a implementar el modelo de arquitectura denominado MVC (Modelo – Vista – Controlador) de la siguiente manera:

El procesamiento de una solicitud de un cliente se desarrollará de la siguiente manera:

  • 1 - solicitud

Las URL solicitadas tendrán el formato http://máquina:puerto/acción/param1/param2/… El [Contrôleur principal] utilizará un archivo de configuración para «enrutar» la solicitud hacia el controlador adecuado. Para ello, utilizará el campo [action] del URL. El resto del URL [param1/param2/…] está formado por parámetros opcionales que se transmitirán a la acción. La C de MVC es aquí la cadena [Contrôleur principal, Contrôleur / Action]. Si ningún controlador puede procesar la acción solicitada, el servidor web responderá que no se ha encontrado la acción solicitada.

  • 2 - Procesamiento
  • La acción seleccionada [2a] puede utilizar los parámetros parami que le ha transmitido [Contrôleur principal]. Estos pueden proceder de dos fuentes:
      • la ruta [/param1/param2/…] del URL,
      • de los parámetros enviados en el cuerpo de la solicitud del cliente;
    • en el procesamiento de la solicitud del usuario, la acción puede necesitar la capa [métier] [2b]. Una vez procesada la solicitud del cliente, esta puede generar diversas respuestas. Un ejemplo clásico es:
      • una respuesta de error si la solicitud no se ha podido procesar correctamente;
      • una respuesta de confirmación en caso contrario;
    • [Contrôleur / Action] devolverá su respuesta [2c] al controlador principal, junto con un código de estado. Estos códigos de estado representarán de forma única el estado en el que se encuentra la aplicación. Será un código de éxito o un código de error;
  • 3 - respuesta
    • dependiendo de si el cliente ha solicitado una respuesta jSON, XML o HTML, el [Contrôleur principal] instanciará [3a] con el tipo de respuesta adecuado y le pedirá que envíe la respuesta al cliente. El [Contrôleur principal] le transmitirá tanto la respuesta como el código de estado proporcionados por el [Contrôleur / Action] que se ha ejecutado;
    • si la respuesta deseada es de tipo jSON o XML, la respuesta seleccionada dará formato a la respuesta del [Contrôleur / Action] que se le ha proporcionado y la enviará a [3c]. El cliente capaz de procesar esta respuesta puede ser un script de consola de Python o un script Javascript alojado en una página HTML;
    • si la respuesta deseada es de tipo HTML, la respuesta seleccionada elegirá [3b] una de las vistas HTML [Vuei] utilizando el código de estado que se le ha proporcionado. Es la V de MVC. A cada código de estado le corresponde una única vista. Esta vista V mostrará la respuesta del [Contrôleur / Action] que se ha ejecutado. Presenta los datos de esta respuesta con HTML, CSS y Javascript. A estos datos se les denomina el modelo de la vista. Es la M de MVC. El cliente suele ser, en la mayoría de los casos, un navegador;

Ahora, precisemos la relación entre la arquitectura web MVC y la arquitectura por capas. Según la definición que se dé al modelo, estos dos conceptos están relacionados o no. Tomemos una aplicación web MVC de una sola capa:

Image

En el ejemplo anterior, cada uno de los [Contrôleur / Action] integra una parte de las capas [métier] y [dao]. En la capa [web] sí que hay una arquitectura MVC, pero el conjunto de la aplicación no tiene una arquitectura en capas. Aquí solo hay una capa, la capa web, que lo hace todo.

Ahora, consideremos una arquitectura web multicapa:

Image

La capa [web] puede implementarse sin seguir el modelo MVC. En ese caso, se trata efectivamente de una arquitectura multicapa, pero la capa web no implementa el modelo MVC.

Por ejemplo, en el mundo .NET, la capa [web]se puede implementar con ASP.NET MVC y entonces se obtiene una arquitectura en capas con una capa [web] de tipo MVC. Una vez hecho esto, se puede sustituir esta capa ASP.NET MVC por una capa ASP.NET clásica (WebForms) manteniendo el resto (negocio, DAO, controlador) tal cual. De este modo, se obtiene una arquitectura por capas con una capa [web] que ya no es de tipo MVC.

En MVC, dijimos que el modelo M era el de la vista V, c.a.d. El conjunto de datos mostrados por la vista V. Se da otra definición del modelo M de MVC:

Image

Muchos autores consideran que lo que se encuentra a la derecha de la capa [web] forma el modelo M del MVC. Para evitar ambigüedades, se puede hablar:

  • del modelo del dominio cuando nos referimos a todo lo que se encuentra a la derecha de la capa [web];
  • del modelo de la vista cuando nos referimos a los datos mostrados por una vista V;

En lo que sigue, cuando hablemos de modelo, siempre nos referiremos al modelo de la vista.

30.2. Arquitectura de la aplicación cliente/servidor

La aplicación web tendrá la siguiente arquitectura:

  • en [1], el servidor web tendrá dos tipos de clients:
    • en [2], un cliente de consola que intercambiará jSON y XML con el servidor;
    • en [3], un navegador que recibirá HTML del servidor y lo mostrará;
  • el servidor web [1] conserva las capas [métier] y [dao] de las versiones anteriores;
  • el cliente web [2] evolucionará para tener en cuenta las nuevas URL del servicio de la aplicación web;
  • la aplicación HTML que se muestra en el navegador debe escribirse por completo;

Desarrollaremos la aplicación en varias fases:

  • vamos a desarrollar el servidor version jSON. Probaremos las URL del servicio del servidor una tras otra con un cliente Postman. Este método nos permite construir la estructura del servidor web sin preocuparnos por las vistas (=HTML) de la aplicación;
  • tras probar el servidor jSON con Postman, lo probaremos con un cliente de consola;
  • luego pasaremos a las version y XML del servidor. Hemos visto que el paso de la jSON a la XML fue trivial;
  • por último, pasaremos a version HTML del servidor. Construiremos una arquitectura MVC y definiremos las vistas que se mostrarán. La aplicación HTML se probará tanto con el cliente Postman como con un navegador convencional;

30.3. El árbol de directorios del código del servidor

Image

  • en [1: el servidor web en su totalidad;
  • en [2]: por el momento ignoraremos las carpetas [static, templates, tests_views] que se refieren a version y HTML del servidor. Fuera de esta carpeta encontraremos el script principal [main] y su configuración;
  • en [3], los controladores del servidor web. Serán instancias de clases;
 
  • en [4], la respuesta HTTP del servidor será gestionada por clases;
  • en [5], conservamos el archivo de registros de los servidores anteriores;

Cuando construyamos el version HTML del servidor, intervendrán otras carpetas:

 
  • en [6], los elementos estáticos de la aplicación HTML;
  • en [7], las plantillas de la aplicación HTML descompuestas en vistas [9] y en fragmentos de vista [8];
  • en [9], las clases que implementan los modelos de las vistas;

30.4. Los URL del servicio de la aplicación

Para crear el servidor web, procederemos de la siguiente manera:

  • a partir de las vistas de la aplicación HTML, definiremos las acciones que debe implementar la aplicación web. Aquí utilizaremos las vistas reales, pero podrían ser simplemente vistas en papel;
  • a partir de estas acciones, definiremos los URL de servicio de la aplicación HTML;
  • vamos a implementar estos servicios con un servidor que sirva páginas. Esto permite definir la estructura del servidor web sin preocuparnos por las páginas que se van a servir. Probaremos estos servicios URL con Postman;
  • luego probaremos nuestro servidor jSON con un cliente de consola;
  • una vez que el servidor jSON haya sido validado, pasaremos a la escritura de la aplicación HTML;

La primera vista será la de autenticación:

Image

  • la acción que conduce a esta primera vista se llamará [init-session] [1];
  • Al hacer clic en el botón [Valider] se activará la acción [authentifier-utilisateur] con dos parámetros enviados [2-3];

La vista del cálculo del impuesto:

Image

  • en [1], la acción [authentifier-utilisateur] que ha llevado a esta vista;
  • en [2], al hacer clic en el botón [Valider] se activa la ejecución de la acción [calculer-impot] con tres parámetros enviados [2-5];
  • al hacer clic en el enlace [6] se activa la acción [lister-simulations] sin parámetros;
  • al hacer clic en el enlace [7] se activa la acción [fin-session] sin parámetros;

La tercera vista es la de las simulaciones realizadas por el usuario autenticado:

Image

  • en [3], la acción [lister-simulations] que ha llevado a esta vista;
  • en [2], al hacer clic en el enlace [Supprimer] se activa la acción [supprimer-simulation] con un parámetro, el número de la simulación que se va a eliminar de la lista;
  • al hacer clic en el enlace [3] se activa la acción [afficher-calcul-impot] sin parámetros, que vuelve a mostrar la vista del cálculo del impuesto;
  • al hacer clic en el enlace [4] se activa la acción [fin-session] sin parámetros;

Con esta información inicial, podemos definir las diferentes acciones de servicio del servidor URL:

Acción
Función
Contexto de ejecución
/init-session
Sirve para establecer el tipo (json, xml, html) de las respuestas deseadas
Solicitud GET
Se puede emitir en cualquier momento
/autenticar-usuario
Autoriza o no a un usuario a conectarse
Solicitud POST.
La solicitud debe tener dos parámetros enviados [user, password]
Solo se puede emitir si se conoce el tipo de sesión (json, xml, html)
/calcular-impuestos
Realiza una simulación del cálculo de impuestos
Consulta POST.
La consulta debe tener tres parámetros enviados [marié, enfants, salaire]
Solo se puede emitir si se conoce el tipo de sesión (json, xml, html) y el usuario está autenticado
/lister-simulations
Solicita ver la lista de simulaciones realizadas desde el inicio de la sesión
Solicitud GET.
Solo se puede emitir si se conoce el tipo de sesión (json, xml, html) y el usuario está autenticado
/eliminar-simulación/número
Elimina una simulación de la lista de simulaciones
Consulta GET.
Solo se puede emitir si se conoce el tipo de sesión (json, xml, html) y el usuario está autenticado
/mostrar-cálculo-impuestos
Muestra la página HTML del cálculo del impuesto
Consulta GET.
Solo se puede emitir si se conoce el tipo de sesión (json, xml, html) y el usuario está autenticado
/fin-session
Finaliza la sesión de simulaciones.
Técnicamente, se elimina la sesión web anterior y se crea una nueva
Solo se puede emitir si se conoce el tipo de sesión (json, xml, html) y el usuario está autenticado

Estos diferentes URL de servicio se utilizarán tanto para el servidor HTML como para los servidores jSON o XML. Dos URL se utilizarán únicamente para estos dos últimos servidores: se trata de las URL de la version anterior del cliente/servidor web que retomamos aquí:

Acción
Función
Contexto de ejecución
/get-admindata
Proporciona los datos fiscales que permiten el cálculo del impuesto
Consulta GET.
Solo se utiliza si el tipo de sesión es json o xml. El usuario debe estar autenticado
/calcular-impuestos
Realiza el cálculo del impuesto de una lista de contribuyentes enviada en jSON
Consulta GET.
Solo se utiliza si el tipo de sesión es json o xml. El usuario debe estar autenticado

Todos los controladores asociados a estas acciones procederán de la misma manera:

  • comprobarán sus parámetros. Estos se encuentran en el objeto:
    • [request.path] para los parámetros presentes en el URL en forma de [/action/param1/param2/…];
    • en el objeto [request.form] para los que se transmiten en [x-www-form-urlencoded] en el cuerpo de la solicitud;
    • en el objeto [request.data] para aquellos que se transmiten en jSON en el cuerpo de la solicitud;
  • un controlador es similar a una función o método que comprueba la validez de sus parámetros. Sin embargo, en el caso del controlador es un poco más complicado:
    • los parámetros esperados pueden estar ausentes;
    • los parámetros recuperados por el controlador son cadenas de caracteres. Si el parámetro esperado es un número, el controlador debe verificar que la cadena de caracteres del parámetro sea efectivamente la de un número;
    • una vez verificado que los parámetros esperados están presentes y son sintácticamente correctos, hay que comprobar que sean válidos en el contexto de ejecución actual. Este contexto está presente en la sesión. El ejemplo de la autenticación es un ejemplo de contexto de ejecución. Algunas acciones solo deben procesarse una vez que el cliente se ha autenticado. Por lo general, una clave en la sesión indica si esta autenticación se ha producido o no;
    • una vez realizadas las comprobaciones anteriores, el controlador secundario puede trabajar. Esta labor de verificación de los parámetros es muy importante. No se puede aceptar que un cliente nos envíe cualquier cosa en cualquier momento de la vida de la aplicación. Debemos controlar totalmente la vida de esta;
    • una vez realizado su trabajo, el controlador secundario devuelve un diccionario con las claves [action, état, réponse] al controlador principal que lo ha llamado:
      • [action] es la acción que se acaba de ejecutar;
      • [état] es un número de tres dígitos que indica el resultado del procesamiento de la acción:
    • [x00] indicará que el procesamiento se ha realizado con éxito;
    • [x01] indicará un fallo en el procesamiento;
  • [réponse] es el diccionario de resultados en formato {‘respuesta’:objeto}. El objeto tendrá estructuras diferentes según la acción procesada;

Ahora vamos a repasar los diferentes controladores o, lo que viene a ser lo mismo, las diferentes acciones que estos controladores procesan y que marcan el ritmo de la aplicación web.

30.5. Configuración del servidor

Image

La configuración de la base de datos [config_database], así como la de las capas del servidor [config_layers], son idénticas a las de las versiones anteriores. El archivo [config] incluye nueva información:


def configure(config: dict) -> dict:
    import os

    # paso 1 ------

    # carpeta de este archivo
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # ruta raíz
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # dependencias
    absolute_dependencies = [
        # carpetas del proyecto
        # BaseEntity, MyException
        f"{root_dir}/classes/02/entities",
        # InterfaceImpôtsDao, InterfaceImpôtsMétier, InterfaceImpôtsUi
        f"{root_dir}/impots/v04/interfaces",
        # AbstractImpôtsdao, ImpôtsConsole, ImpôtsMétier
        f"{root_dir}/impots/v04/services",
        # ImpotsDaoWithAdminDataInDatabase
        f"{root_dir}/impots/v05/services",
        # AdminData, ImpôtsError, TaxPayer
        f"{root_dir}/impots/v04/entities",
        # Constantes, tramos
        f"{root_dir}/impots/v05/entities",
        # Registrador, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
        # scripts [config_database, config_layers]
        script_dir,
        # controladores
        f"{script_dir}/../controllers",
        # respuestas HTTP
        f"{script_dir}/../responses",
        # plantillas de vistas
        f"{script_dir}/../models_for_views",
    ]

    # se establece la ruta del sistema
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # dependencias del servidor web

    # los controladores
    from AfficherCalculImpotController import AfficherCalculImpotController
    from AuthentifierUtilisateurController import AuthentifierUtilisateurController
    from CalculerImpotController import CalculerImpotController
    from CalculerImpotsController import CalculerImpotsController
    from FinSessionController import FinSessionController
    from GetAdminDataController import GetAdminDataController
    from InitSessionController import InitSessionController
    from ListerSimulationsController import ListerSimulationsController
    from MainController import MainController
    from SupprimerSimulationController import SupprimerSimulationController

    # las respuestas HTTP
    from HtmlResponse import HtmlResponse
    from JsonResponse import JsonResponse
    from XmlResponse import XmlResponse

    # las plantillas de las vistas
    from ModelForAuthentificationView import ModelForAuthentificationView
    from ModelForCalculImpotView import ModelForCalculImpotView
    from ModelForErreursView import ModelForErreursView
    from ModelForListeSimulationsView import ModelForListeSimulationsView

    # paso 2 ------
    # configuración de la aplicación
    config.update({
        # usuarios autorizados a utilizar la aplicación
        "users"[
            {
                "login""admin",
                "password""admin"
            }
        ],

        # archivo de registros
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",

        # config servidor SMTP
        "adminMail": {
            # servidor SMTP
            "smtp-server""localhost",
            # puerto del servidor SMTP
            "smtp-port""25",
            # administrador
            "from""guest@localhost.com",
            "to""guest@localhost.com",
            # asunto del correo
            "subject""plantage du serveur de calcul d'impôts",
            # tls en True si el servidor SMTP requiere autorización, en False en caso contrario
            "tls"False
        },

        # duración de la pausa del hilo en segundos
        "sleep_time"0,

        # acciones autorizadas y sus controladores
        "controllers": {
            # inicialización de una sesión de cálculo
            "init-session": InitSessionController(),
            # autenticación de un usuario
            "authentifier-utilisateur": AuthentifierUtilisateurController(),
            # cálculo del impuesto en modo individual
            "calculer-impot": CalculerImpotController(),
            # cálculo del impuesto en modo por lotes
            "calculer-impots": CalculerImpotsController(),
            # lista de simulaciones
            "lister-simulations": ListerSimulationsController(),
            # eliminación de una simulación
            "supprimer-simulation": SupprimerSimulationController(),
            # fin de la sesión de cálculo
            "fin-session": FinSessionController(),
            # visualización de la vista de cálculo del impuesto
            "afficher-calcul-impot": AfficherCalculImpotController(),
            # Obtención de datos de la administración tributaria
            "get-admindata": GetAdminDataController(),
            # controlador principal
            "main-controller": MainController()
        },

        # los diferentes tipos de respuesta (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

        # las vistas HTML y sus plantillas dependen del estado devuelto por el controlador
        "views"[
            {
                # vista de autenticación
                "états": [
                    # /init-session éxito
                    700,
                    # /autenticar-usuario fallido
                    201
                ],
                "view_name""views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                # vista del cálculo del impuesto
                "états"[
                    # /autenticar-usuario correcto
                    200,
                    # /calcular-impuesto correcto
                    300,
                    # /calcular-impuesto error
                    301,
                    # /mostrar-cálculo-impuesto
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                # vista de la lista de simulaciones
                "états"[
                    # /listar-simulaciones
                    500,
                    # /eliminar-simulación
                    600
                ],
                "view_name""views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],

        # vista de errores inesperados
        "view-erreurs": {
            "view_name""views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },

        # redireccionamientos
        "redirections"[
            {
                "états": [
                    400,  # /fin-de-sesión-correcta
                ],
                # redirección a
                "to""/init-session/html",
            }
        ],
    }
    )

    # paso 3 ------
    # configuración de la base de datos
    import config_database
    config["database"] = config_database.configure(config)

    # paso 4 ------
    # instanciación de las capas de la aplicación
    import config_layers
    config['layers'] = config_layers.configure(config)

    # se devuelve la configuración
    return config
  • hasta la línea 41, encontramos elementos habituales;
  • líneas 43-66: al llegar a la línea 43, se define el Python Path del servidor. A continuación, se pueden importar las dependencias del proyecto:
    • líneas 45-55: la lista de controladores;
    • líneas 57-60: la lista de respuestas HTTP;
    • líneas 62-66: la lista de plantillas de vista;
  • líneas 68-189: la configuración de la aplicación con una serie de constantes;
    • líneas 71-98: ya conocemos estas líneas, que aparecen en versiones anteriores;
    • líneas 101-122: el diccionario de controladores:
      • las claves son los nombres de las acciones;
      • los valores son una instancia del controlador que debe gestionar esa acción. Cada controlador solo se instancia en un único ejemplar (singleton). La misma instancia será ejecutada por diferentes subprocesos del servidor. Por lo tanto, habrá que prestar atención a los datos compartidos que cada controlador pudiera querer modificar;
    • líneas 125-129: el diccionario de las tres respuestas posibles HTTP:
      • las claves son el tipo de respuesta deseado por el cliente (jSON, xml, html);
      • los valores son una instancia de la respuesta HTTP. Cada generador de respuesta solo se instancia en un único ejemplar (singleton). El mismo generador será ejecutado por diferentes subprocesos del servidor. Por lo tanto, habrá que prestar atención a los datos compartidos que cada generador pudiera querer modificar;
    • líneas 132-186: configuración de las vistas HTML. Por el momento, ignoramos estas líneas;
  • líneas 191-202: ya hemos visto estas líneas en versiones anteriores;

30.6. Recorrido de una solicitud del cliente dentro del servidor

Image

Vamos a seguir el recorrido de una solicitud de cliente que llega al servidor hasta la respuesta HTTP enviada a continuación. Sigue el recorrido del servidor MVC.

30.6.1. El script [main]

El script [main] es idéntico en muchos aspectos al de las versiones anteriores. No obstante, lo incluimos íntegramente para partir de una base sólida:


# se espera un parámetro mysql o pgres
import sys

syntaxe = f"{sys.argv[0]} mysql / pgres"
erreur = len(sys.argv) != 2
if not erreur:
    sgbd = sys.argv[1].lower()
    erreur = sgbd != "mysql" and sgbd != "pgres"
if erreur:
    print(f"syntaxe : {syntaxe}")
    sys.exit()

# se configura la aplicación
import config
config = config.configure({'sgbd': sgbd})

# dependencias
from flask import request, Flask, session, url_for, redirect
from flask_api import status
from SendAdminMail import SendAdminMail
from myutils import json_response
from Logger import Logger
import threading
import time
from random import randint
from ImpôtsError import ImpôtsError
import os

# envío de un correo electrónico al administrador
def send_adminmail(config: dict, message: str):
    # se envía un correo electrónico al administrador de la aplicación
    config_mail = config["adminMail"]
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)

# comprobación del archivo de registros
logger = None
erreur = False
message_erreur = None
try:
    # registrador
    logger = Logger(config["logsFilename"])
except BaseException as exception:
    # registro de consola
    print(f"L'erreur suivante s'est produite : {exception}")
    # se anota el error
    erreur = True
    message_erreur = f"{exception}"
# se almacena el registrador en el config
config['logger'] = logger
# gestión del error
if erreur:
    # correo electrónico al administrador
    send_adminmail(config, message_erreur)
    # fin de la aplicación
    sys.exit(1)

# registro de inicio
log = "[serveur] démarrage du serveur"
logger.write(f"{log}\n")
print(log)

# Recuperación de datos de la administración tributaria
erreur = False
try:
    # admindata será un dato de ámbito de la aplicación de solo lectura
    config["admindata"] = config["layers"]["dao"].get_admindata().asdict()
    # registro de éxito
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    # se registra el error
    erreur = True
    # registro de error
    log = f"L'erreur suivante s'est produite : {ex}"
    # consola
    print(log)
    # archivo de registros
    logger.write(f"{log}\n")
    # correo al administrador
    send_adminmail(config, log)

# el hilo principal ya no necesita el registrador
logger.close()

# si se ha producido un error, se detiene
if erreur:
    sys.exit(2)

# aplicación Flask
app = Flask(__name__, template_folder="templates", static_folder="static")
# clave secreta de la sesión
app.secret_key = os.urandom(12).hex()

# el controlador frontal
def front_controller() -> tuple:
    # se procesa la solicitud
    logger = None
    

@app.route('/', methods=['GET'])
def index() -> tuple:
    # redirección a /init-session/html
    return redirect(url_for("init_session", type_response="html"), status.HTTP_302_FOUND)

# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

# autenticar-usuario
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

# calcular-impuesto
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

# listar-simulaciones
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

# eliminar-simulación
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

# fin-sesión
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

# mostrar-cálculo-impuestos
@app.route('/afficher-calcul-impot', methods=['GET'])
def afficher_calcul_impot() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

# get-admindata
@app.route('/get-admindata/<int:numero>', methods=['GET'])
def get_admindata() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

# solo manual
if __name__ == '__main__':
    # se inicia el servidor
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • líneas 1-92: todas estas líneas ya se han visto y explicado;
  • línea 92: el servidor gestionará una sesión. Por lo tanto, necesitamos una clave secreta. Para cada usuario, incluiremos dos datos en la sesión:
    • si el usuario se ha autenticado correctamente;
    • cada vez que realice un cálculo de impuestos, los resultados de dicho cálculo se colocarán en una lista que llamaremos la lista de simulaciones del usuario. Esta lista se colocará en la sesión;
  • líneas 100-151: la lista de URL de servicio del servidor. Las funciones asociadas sirven de filtro: todas las URL que no estén presentes en esta lista serán rechazadas por el servidor Flask con el error [404 NOT FOUND]. Una vez superado este filtrado, la solicitud se transmite sistemáticamente a un «Front Controller» implementado por la función de las líneas 94-98, que presentaremos en breve;
  • líneas 100-103: gestión de la ruta [/]. El punto de entrada de la aplicación web será la URL de la línea 107. Así, en la línea 103, redirigimos al cliente hacia esta URL:
  • la función [url_for] se importa en la línea 18. Aquí tiene dos parámetros:
      • el primer parámetro es el nombre de una de las funciones de enrutamiento, en este caso la de la línea 107. Vemos que esta función espera un parámetro [type_response], que es el tipo (json, xml, html) de respuesta deseado por el cliente;
      • el segundo parámetro toma el nombre del parámetro de la línea 107, [type_response], y le asigna un valor. Si hubiera otros parámetros, se repetiría la operación para cada uno de ellos;
      • devuelve el URL asociado a la función designada por los dos parámetros que se le han proporcionado. En este caso, dará como resultado el URL de la línea 106, donde el parámetro se sustituye por su valor [/init-session/html];
    • la función [redirect] se ha importado en la línea 18. Su función es enviar un encabezado de redireccionamiento HTTP al cliente:
      • el primer parámetro es el URL al que debe redirigirse el cliente;
      • el segundo parámetro es el código de estado de la respuesta HTTP enviada al cliente. El código [status.HTTP_302_FOUND] corresponde a una redirección HTTP;

La función [front_controller] de las líneas 94-98 realiza los primeros tratamientos de la solicitud del cliente:


# el controlador frontal
def front_controller() -> tuple:
    # se procesa la solicitud
    logger = None
    try:
        # registrador
        logger = Logger(config["logsFilename"])
        # se almacena en una config asociada al hilo
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        # se registra la solicitud
        logger.write(f"[ front_controller] requête : {request}\n")
        # se interrumpe el hilo si así se ha solicitado
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            # la pausa es aleatoria para que algunos subprocesos se interrumpan y otros no
            aléa = randint(01)
            if aléa == 1:
                # registro antes de la pausa
                logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                # pausa
                time.sleep(sleep_time)
        # se reenvía la solicitud al controlador principal
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        # se registra el resultado enviado al cliente
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        # ¿Se ha producido un error fatal?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # se envía un correo electrónico al administrador de la aplicación
            send_adminmail(config, log)
        # se determina el tipo deseado para la respuesta
        if session.get('typeResponse'is None:
            # el tipo de sesión aún no se ha establecido; será jSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        # se construye la respuesta que se va a enviar
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        # se envía la respuesta
        return response, status_code
    except BaseException as erreur:
        # se trata de un error inesperado: se registra el error si es posible
        if logger:
            logger.write(f"[ front_controller] {erreur}")
        # se prepara la respuesta para el cliente
        résultat = {"réponse": {"erreurs"[f"{erreur}"]}}
        # se envía una respuesta en jSON
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # se cierra el archivo de registros si se ha abierto
        if logger:
            logger.close()
  • líneas 1-57: conocemos este código. Era, por ejemplo, el código de la función denominada [main] en el script [main] de la anterior version. Solo hay que destacar una cosa, el controlador utilizado en las líneas 25-26:
  • línea 25: se recupera de la configuración la instancia del controlador asociada al nombre [main-controller]. Se trata de las siguientes líneas:

    # dependencias del servidor web
    # los controladores
    
    from MainController import MainController

    # acciones permitidas y sus controladores
        "controllers": {
            ,
            # controlador principal
            "main-controller": MainController()
        },
  • (continuación)
    • línea 10 anterior, se observará que se recupera una instancia de clase;
  • línea 26: se solicita al controlador [MainController] que procese la solicitud;
  • líneas 30-45: la respuesta devuelta por el controlador [MainController] se envía al cliente. Volveremos sobre estas líneas un poco más adelante;

La función [front_controller] y, a continuación, la clase [MainController] se encargan de realizar el trabajo común a todas las solicitudes:

En el esquema anterior, todavía nos encontramos en la fase 1 del procesamiento de la solicitud. El controlador principal [MainController] continuará con el paso 1.

30.6.2. El controlador principal [MainController]

El controlador principal [MainController] continúa el trabajo iniciado por la función [front_controller]:

Todos los controladores implementan la siguiente interfaz [InterfaceController] [2]:

Image


from abc import ABC, abstractmethod

from werkzeug.local import LocalProxy

class InterfaceController(ABC):

    @abstractmethod
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        pass
  • La interfaz [InterfaceController] solo define el único método [execute] de la línea 8. Este método recibe tres parámetros:
    • [request]: la solicitud del cliente;
    • [session]: la sesión del cliente;
    • [config]: la configuración de la aplicación;

El método [execute] devuelve una tupla de dos elementos:

  • el primero es el diccionario de resultados en forma de {‘acción’: acción, ‘estado’: estado, ‘respuesta’: resultados};
  • el segundo es el código de estado HTTP que se debe devolver al cliente;

El controlador principal [MainController] [1] implementa la interfaz [InterfaceController] de la siguiente manera:


# importación de dependencias

from flask_api import status
from werkzeug.local import LocalProxy

# controladores de la aplicación web
from InterfaceController import InterfaceController

class MainController(InterfaceController):
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # se recuperan los elementos de path
        params = request.path.split('/')
        action = params[1]

        # errores
        erreur = False
        # el tipo de sesión debe conocerse antes de realizar ciertas acciones
        type_response = session.get('typeResponse')
        if type_response is None and action != "init-session":
            # se registra el error
            résultat = {"action": action, "état"101,
                        "réponse"["pas de session en cours. Commencer par action [init-session]"]}
            erreur = True
        # para ciertas acciones es necesario estar autenticado
        user = session.get('user')
        if not erreur and user is None and action not in ["init-session""authentifier-utilisateur"]:
            # se registra el error
            résultat = {"action": action, "état"101,
                        "réponse"[f"action [{action}] demandée par utilisateur non authentifié"]}
            erreur = True
        # ¿hay errores?
        if erreur:
            # se devuelve un mensaje de error
            return résultat, status.HTTP_400_BAD_REQUEST
        else:
            # se ejecuta el controlador asociado a la acción
            controller = config["controllers"][action]
            résultat, status_code = controller.execute(request, session, config)
            return résultat, status_code

El controlador [MainController] realiza las primeras comprobaciones de la validez de la solicitud.

  • Líneas 11-13: el controlador comienza recuperando la acción solicitada por el cliente. Recordemos que los URL de servicio tienen la forma [/action/param1/param2/…] y que este URL se encuentra en [request.path];
  • líneas 17-23: la acción [init-session] sirve para inicializar el tipo de respuesta (json, xml, html) deseado por el cliente. Esta información se incluye en la sesión asociada a la clave [typeRéponse]. Por lo tanto, si la acción no es [init-session], la sesión debe contener la clave [typeRéponse]; de lo contrario, la solicitud es errónea;
  • líneas 21-22: la estructura del resultado devuelto por cada controlador, en este caso un resultado de error:
    • [action]: es el nombre de la acción en curso. Esto permitirá disponer de su nombre cuando se registre el resultado de la solicitud;
    • [état]: es un código de estado de tres dígitos:
        • [x00] para un resultado satisfactorio;
        • [x01] para un error;
  • [réponse]: es la respuesta a la solicitud. Su naturaleza es específica para cada solicitud;
  • líneas 24-30: la acción [authentifier-utilisateur] sirve para autenticar al usuario. Si tiene éxito, se introduce una clave [user=True] en la sesión del usuario. Algunas URL de servicio solo son accesibles para un usuario autenticado. Esto es lo que se comprueba aquí;
  • línea 26: solo las acciones [init-session] y [authentifier-utilisateur] pueden ser realizadas por un usuario que aún no se haya autenticado;
  • líneas 28-29: el resultado que se debe enviar en caso de error;
  • líneas 32-34: si se ha producido alguno de los dos errores anteriores, se envía la respuesta de error al cliente con el estado HTTP 400 BAD REQUEST;
  • líneas 35-39: si no se ha producido ningún error, se pasa el control al controlador encargado de procesar la acción en curso. Su instancia se encuentra en la configuración de la aplicación;

La clase [MainController] continúa el trabajo de la función [front_controller]: entre ambas, reúnen todo lo que se puede factorizar en el procesamiento de las solicitudes, esperando hasta el último momento para pasar la solicitud a un controlador específico. La distribución del código entre la función [front_controller] y la clase [MainController] es totalmente subjetiva. En este caso, he querido conservar lo ya logrado en la anterior version: la función [front_controller] ya existía con el nombre [main]. En la práctica, se podría:

  • ponerlo todo en la función [front_controller] y eliminar la clase [MainController];
  • ponerlo todo en la clase [MainController] y eliminar la función [front_controller]. Yo me decantaría más bien por esta solución, ya que tiene la ventaja de aligerar el código del script principal [main];

30.7. Procesamiento específico de una acción

Volvamos a la arquitectura MVC de la aplicación:

Image

Todavía nos encontramos en el paso 1 anterior. Si no se ha producido ningún error, comenzará el paso 2. La solicitud se ha enviado al controlador específico de la acción solicitada en la misma. Supongamos que dicha acción es [/init-session], definida por la ruta:


# inicialización de sesión
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

Esta acción está vinculada a un controlador en la configuración [config]:


        # acciones permitidas y sus controladores
        "controllers": {
            # inicialización de una sesión de cálculo
            "init-session": InitSessionController(),
            
        },

Por lo tanto, el controlador [InitSessionController] (línea 4) toma el control. Su código es el siguiente:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class InitSessionController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # se recuperan los elementos de path
        dummy, action, type_response = request.path.split('/')

        # al principio no hay error
        erreur = False
        # verificación del tipo de respuesta
        if type_response not in config['responses'].keys():
            erreur = True
            résultat = {"action": action, "état"701,
                        "réponse"[f"paramètre [type={type_response}] invalide"]}
        # si no hay error
        if not erreur:
            # se establece el tipo de sesión en la sesión de Flask
            session['typeResponse'] = type_response
            résultat = {"action": action, "état"700,
                        "réponse"[f"session démarrée avec le type de réponse {type_response}"]}
            return résultat, status.HTTP_200_OK
        else:
            return résultat, status.HTTP_400_BAD_REQUEST
  • línea 6: al igual que los demás controladores, el controlador [InitSessionController] implementa la interfaz [InterfaceController];
  • línea 10: el URL es de tipo [/init-session/type_response]. Se recupera la acción [init-session] y el tipo de respuesta deseado;
  • línea 15: el tipo de respuesta deseado solo puede ser uno de los que figuran en la configuración de respuestas:

        # los diferentes tipos de respuesta (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },
  • si no es así, se prepara una respuesta de error 701 (línea 17);
  • líneas 20-25: caso en el que el tipo de respuesta deseado es válido;
  • línea 22: se guarda el tipo de respuesta deseado. De hecho, habrá que recordarlo para las solicitudes siguientes;
  • líneas 23-24: se prepara una respuesta de éxito 700;
  • línea 25: la respuesta de éxito se devuelve al código llamante;
  • línea 27: si se ha producido un error, se devuelve la respuesta de error al código llamante;

30.8. Elaboración de la respuesta HTTP del servidor

Volvamos a la arquitectura MVC de la aplicación:

Image

Acabamos de ver los pasos 1 y 2. Nos hemos encontrado con tres códigos de estado:

  • 700: /init-session se ha realizado correctamente;
  • 701: /init-session ha fallado;
  • 101: solicitud no válida, ya sea porque la sesión no se ha inicializado o porque el usuario no está autenticado;

Veamos cómo se envía la respuesta del servidor al cliente en el paso 3 anterior. Esto ocurre en la función [front_controller] del script [main]:


# el controlador frontal
def front_controller() -> tuple:
    # se procesa la solicitud
    logger = None
    try:
        # registrador
        logger = Logger(config["logsFilename"])
        # se almacena en una config asociada al hilo
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        # se registra la solicitud
        logger.write(f"[ front_controller] requête : {request}\n")
        # se interrumpe el hilo si así se ha solicitado
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            # la pausa es aleatoria para que algunos subprocesos se interrumpan y otros no
            aléa = randint(01)
            if aléa == 1:
                # registro antes de la pausa
                logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                # pausa
                time.sleep(sleep_time)
        # se reenvía la solicitud al controlador principal
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        # se registra el resultado enviado al cliente
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        # ¿Se ha producido un error fatal?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # se envía un correo electrónico al administrador de la aplicación
            send_adminmail(config, log)
        # se determina el tipo deseado para la respuesta
        if session.get('typeResponse'is None:
            # el tipo de sesión aún no se ha establecido; será jSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        # se construye la respuesta que se va a enviar
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        # se envía la respuesta
        return response, status_code
    except BaseException as erreur:
        # se trata de un error inesperado: se registra el error si es posible
        if logger:
            logger.write(f"[ front_controller] {erreur}")
        # se prepara la respuesta para el cliente
        résultat = {"réponse": {"erreurs"[f"{erreur}"]}}
        # se envía una respuesta en jSON
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # se cierra el archivo de registros si se ha abierto
        if logger:
            logger.close()
  • Estamos en la línea 26: el controlador principal ha devuelto su respuesta de error;
  • líneas 27-29: sea cual sea la respuesta del controlador principal (éxito o fallo), esta respuesta se registra en el archivo de logs;
  • líneas 30-33: al igual que en versiones anteriores, si el estado HTTP es [500 INTERNAL SERVER ERROR], se envía un correo electrónico al administrador de la aplicación con el registro del error;
  • líneas 34-39: se enviará la respuesta HTTP y el resultado devuelto por el controlador se incluirá en el cuerpo de dicha respuesta. Necesitamos saber en qué formato (json, xml, html) quiere el cliente esta respuesta. Buscamos el tipo de respuesta deseado en la sesión. Si no está ahí, entonces fijamos arbitrariamente este tipo en jSON;
  • líneas 40-43: se construye la respuesta HTTP;

En el archivo de configuración, cada tipo de respuesta (json, xml, html) se ha asociado a una instancia de clase:


        # los diferentes tipos de respuesta (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

Las clases de respuesta se encuentran en la carpeta [responses] del árbol de directorios del servidor:

Image

Cada clase de respuesta implementa la siguiente interfaz [InterfaceResponse]:


from abc import ABC, abstractmethod

from flask.wrappers import Response
from werkzeug.local import LocalProxy

class InterfaceResponse(ABC):

    @abstractmethod
    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        pass
  • líneas 8-11: la interfaz [InterfaceResponse] define un único método [build_http_response] con los siguientes parámetros:
    • [request, session, config]: son los parámetros recibidos por el controlador de la acción;
    • [résultat, status_code]: son los resultados generados por el controlador de la acción;

Vamos a presentar la respuesta jSON. Es generada por la siguiente clase [JsonResponse]:


import json

from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy

from InterfaceResponse import InterfaceResponse

class JsonResponse(InterfaceResponse):

    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        # resultados: el diccionario de resultados
        # status_code: el código de estado de la respuesta HTTP

        # se devuelve la respuesta HTTP
        response = make_response(json.dumps(résultat, ensure_ascii=False))
        response.headers['Content-Type'] = 'application/json; charset=utf-8'
        return response, status_code

Conocemos este código, con el que nos hemos encontrado en numerosas ocasiones. Es el código de la función [json_response] del módulo [myutils].

30.9. Primeras pruebas

En el código analizado, hemos encontrado tres códigos de estado:

  • 700: /init-session se ha realizado correctamente;
  • 701: /init-session ha fallado;
  • 101: solicitud no válida, ya sea porque la sesión no se ha inicializado o porque el usuario no está autenticado;

Vamos a intentar obtenerlos con una sesión jSON.

  • iniciamos el servidor web, el SGBD y el servidor de correo;
  • iniciamos un cliente Postman;

Prueba 1

En primer lugar, mostramos una solicitud no válida porque la sesión no se ha inicializado:

Image

  • [1-2]: la solicitud [POST http://localhost:5000/authentifier-utilisateur] es una ruta válida:

# autenticar-usuario
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

pero solo se acepta si la sesión se ha iniciado previamente con la acción [/init-session].

Ejecutemos la consulta y veamos el resultado enviado por el servidor:

Image

  • [1-2]: se ha obtenido una respuesta jSON. Cuando el cliente aún no ha establecido el tipo de respuesta, el servidor utiliza jSON para responder;
  • [3-5]: el diccionario jSON de la respuesta;
    • [action]: la acción que se ha ejecutado;
    • [état]: el código de estado de la respuesta. Un código [x01] indica un error;
    • [réponse]: se adapta a cada acción. En este caso, contiene un mensaje de error;

Ahora iniciemos una sesión con un tipo de respuesta incorrecto:

Image

  • [1-2] es una ruta correcta:

# iniciar sesión
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

Por lo tanto, entrará en el túnel de procesamiento de solicitudes del servidor MVC. Sin embargo, debería ser rechazada durante este procesamiento porque el tipo de sesión solicitado es incorrecto.

La respuesta es la siguiente:

Image

  • en [4], un código de error [x01];
  • en [5], la explicación del error;

Ahora, inicialicemos una sesión jSON:

Image

La respuesta es la siguiente:

Image

Ahora, inicialicemos una sesión XML. La respuesta jSON será sustituida por una respuesta XML generada por la siguiente clase [XmlResponse]:


import xmltodict
from flask import make_response
from flask.wrappers import Response
from werkzeug.local import LocalProxy

from InterfaceResponse import InterfaceResponse
from Logger import Logger

class XmlResponse(InterfaceResponse):

    def build_http_response(self, request: LocalProxy, session: LocalProxy, config: dict, status_code: int,
                            résultat: dict) -> (Response, int):
        # resultados: el diccionario de resultados
        # status_code: el código de estado de la respuesta HTTP

        # resultado: el diccionario que se va a convertir en cadena XML
        xml_string = xmltodict.unparse({"root": résultat})
        # se devuelve la respuesta HTTP
        response = make_response(xml_string)
        response.headers['Content-Type'] = 'application/xml; charset=utf-8'
        return response, status_code

Se trata de código que conocemos, el de la función [xml_response] del módulo compartido [myutils].

Inicializamos una sesión XML:

Image

El resultado del servidor es entonces el siguiente:

Image

Obtenemos la misma respuesta que en jSON, pero esta vez la respuesta se presenta en XML.

30.10. La acción [authentifier-utilisateur]

La acción [authentifier-utilisateur] permite autenticar a un usuario que desee utilizar la aplicación de cálculo de impuestos. Su ruta se define de la siguiente manera en el script [main]:


# autenticar-usuario
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

El servidor espera dos parámetros enviados:

  • [user]: el identificador del usuario;
  • [password]: su contraseña;

La lista de usuarios autorizados se define en la configuración [config]:


        # usuarios autorizados a utilizar la aplicación
        "users"[
            {
                "login""admin",
                "password""admin"
            }
        ],

Aquí tenemos una lista con un solo elemento.

La acción [authentifier-utilisateur] es procesada por el controlador [AuthentifierUtilisateurController] siguiente:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController
from Logger import Logger

class AuthentifierUtilisateurController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # se recuperan los elementos de path
        dummy, action = request.path.split('/')

        # los parámetros de POST
        post_params = request.form
        # código de estado de la respuesta HTTP
        status_code = None
        # al principio no hay errores
        erreur = False
        erreurs = []
        # se necesita un POST con dos parámetros
        if len(post_params) != 2:
            erreur = True
            status_code = status.HTTP_400_BAD_REQUEST
            erreurs.append("méthode POST requise, paramètre [action] dans l'URL, paramètres postés [user, password]")
        if not erreur:
            # se recuperan los parámetros del POST
            # parámetro [user]
            user = post_params.get("user")
            if user is None:
                erreur = True
                erreurs.append("paramètre [user] manquant")
            # parámetro [password]
            password = post_params.get("password")
            if password is None:
                erreur = True
                erreurs.append("paramètre [password] manquant")
            # ¿error?
            if erreur:
                status_code = status.HTTP_400_BAD_REQUEST
        # ¿error?
        if not erreur:
            # se comprueba la validez del par (usuario, contraseña)
            users = config['users']
            i = 0
            nbusers = len(users)
            trouvé = False
            while not trouvé and i < nbusers:
                trouvé = user == users[i]["login"] and password == users[i]["password"]
                i += 1
            # ¿Encontrado?
            if not trouvé:
                # se registra el error
                erreur = True
                status_code = status.HTTP_401_UNAUTHORIZED
                erreurs.append(f"Echec de l'authentification")
            else:
                # se anota en la sesión que se ha encontrado el usuario
                session["user"] = True
        # ya está
        if not erreur:
            # retorno sin error
            résultat = {"action": action, "état"200"réponse"f"Authentification réussie"}
            return résultat, status.HTTP_200_OK
        else:
            # retorno con error
            return {"action": action, "état"201"réponse": erreurs}, status_code

  • línea 14: se recuperan los parámetros de POST;
  • línea 19: la lista de errores encontrados en la solicitud;
  • líneas 20-24: se comprueba que efectivamente hay dos parámetros enviados;
  • líneas 27-31: se comprueba si existe un parámetro [users];
  • líneas 32-36: se comprueba la presencia de un parámetro [password];
  • líneas 38-39: si los parámetros enviados son erróneos, se prepara una respuesta HTTP 400 BAD REQUEST;
  • líneas 40-58: se comprueba que las credenciales [user, password] correspondan a un usuario autorizado para utilizar la aplicación;
  • líneas 51-55: si el usuario (nombre de usuario, contraseña) no está autorizado a utilizar la aplicación, se prepara una respuesta HTTP 401 UNAUTHORIZED;
  • líneas 56-58: si está autorizado, se anota con la clave [user] en la sesión que se ha autenticado;

Cabe señalar que si el usuario se autenticó con las credenciales [identifiants1] y no consigue autenticarse con las credenciales [identifiants2], seguirá estando autenticado con las credenciales [identifiants1].

Realicemos pruebas con Postman:

  • iniciamos el servidor web, el SGBD y el servidor de correo;
  • con el cliente Postman:
    • iniciamos una sesión jSON;
    • luego nos autenticamos;

Estos son los diferentes casos.

Caso 1: POST sin parámetros enviados

Image

  • en [3-5], el POST no tiene cuerpo;

El resultado de la solicitud es el siguiente:

Image

  • en [2], se obtuvo una respuesta HTTP 400 BAD REQUEST;
  • al introducir [5], se obtuvo un código de error [201];

Caso 2: POST con identificadores erróneos

Image

  • en [6], los identificadores son incorrectos;

El servidor envía la siguiente respuesta:

Image

  • en [2], la respuesta HTTP 401 UNAUTHORIZED;
  • en [5], la respuesta de error;

Caso 2: POST con credenciales correctas

Image

  • en [6], las credenciales son correctas;

La respuesta del servidor es la siguiente:

  • en [2], una respuesta HTTP 200 OK; Image
  • en [5], la respuesta de éxito;

30.11. La acción [calculer_impot]

La acción [calculer_impot] permite calcular el impuesto de un contribuyente. Su ruta se define de la siguiente manera en el script [main]:


# calcular-impuestos
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

El servidor espera tres parámetros enviados:

  • [marié]: sí / no;
  • [enfants]: número de hijos del contribuyente;
  • [salaire]: salario anual del contribuyente;

El controlador [CalculerImpotController] procesa la acción [calculer_impot]:


import re

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController
from TaxPayer import TaxPayer

class CalculerImpotController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # se recuperan los elementos de path
        dummy, action = request.path.split('/')

        # sin errores al inicio
        erreur = False
        erreurs = []
        # los parámetros de POST
        post_params = request.form
        # se necesita un POST con tres parámetros
        if len(post_params) != 3:
            erreur = True
            erreurs.append(
                "méthode POST requise avec les paramètres postés [marié, enfants, salaire]")
        # se analizan los parámetros enviados
        if not erreur:
            # parámetro emparejado
            marié = post_params.get("marié")
            if marié is None:
                erreurs.append("paramètre [marié] manquant")
            else:
                # ¿Es válido el parámetro?
                marié = marié.lower()
                if marié != "oui" and marié != "non":
                    erreur = True
                    erreurs.append(f"valeur [{marié}] invalide pour le paramètre [marié (oui/non)]")
            # parámetro [enfants]
            enfants = post_params.get("enfants")
            if enfants is None:
                erreur = True
                erreurs.append("paramètre [enfants] manquant")
            else:
                # ¿Es válido el parámetro?
                enfants = enfants.strip()
                match = re.match(r"\d+", enfants)
                if not match:
                    erreur = True
                    erreurs.append(f"valeur [{enfants}] invalide pour le paramètre [enfants (entier>=0)]")
            # parámetro salario
            salaire = post_params.get("salaire")
            if salaire is None:
                erreur = True
                erreurs.append("paramètre [salaire] manquant")
            else:
                # ¿Es válido el parámetro?
                salaire = salaire.strip()
                match = re.match(r"\d+", salaire)
                if not match:
                    erreur = True
                    erreurs.append(f"valeur [{salaire}] invalide pour le paramètre [salaire (entier>=0)]")
        # ¿Error?
        if erreur:
            status_code = status.HTTP_400_BAD_REQUEST
            résultat = {"action": action, "état"301"réponse": erreurs}
            # se devuelve el resultado
            return résultat, status_code

        # cálculo del impuesto
        # se recupera la capa [métier] y el diccionario [adminData]
        métier = config["layers"]["métier"]
        admin_data = config["admindata"]
        # cálculo del impuesto
        taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
        métier.calculate_tax(taxpayer, admin_data)
        # n.º de la simulación
        id_simulation = session.get('id_simulation'0)
        id_simulation += 1
        session['id_simulation'] = id_simulation
        # se introduce el resultado en la sesión en forma de diccionario de un TaxPayer
        simulation = taxpayer.fromdict({'id': id_simulation}).asdict()
        # se añade el resultado a la lista de simulaciones ya realizadas y se guarda esta en la sesión
        simulations = session.get("simulations"[])
        simulations.append(simulation)
        session["simulations"] = simulations
        # resultado
        résultat = {"action": action, "état"300"réponse": simulation}
        status_code = status.HTTP_200_OK

        # se muestra el resultado
        return résultat, status_code
  • línea 13: se recupera el nombre de la acción en curso;
  • línea 17: se acumulan los errores en una lista;
  • línea 19: se recuperan los parámetros enviados. Estos se envían con el formato [x-www-form-urlencoded] y por eso se recuperan en [request.form]. Si se hubieran enviado como jSON, los habríamos recuperado como [request.data];
  • líneas 21-24: se comprueba que efectivamente hay tres parámetros enviados;
  • líneas 27-36: comprobación de la presencia y la validez del parámetro enviado [marié];
  • líneas 37-48: comprobación de la existencia y la validez del parámetro enviado [enfants];
  • líneas 49-60: comprobación de la presencia y la validez del parámetro enviado [salaire];
  • líneas 62-66: si se ha producido un error, se envía una respuesta de error 400 BAD REQUEST con un código de estado [301];
  • líneas 69-71: si no se ha producido ningún error, se prepara el cálculo del impuesto. Para ello,
    • línea 70: se recupera una referencia de la capa [métier];
    • línea 71: se recuperan los datos de la administración tributaria en la configuración del servidor;
  • líneas 72-74: se calcula el impuesto del contribuyente;
  • líneas 75-77: se cuenta el número de cálculos de impuestos realizados por el usuario;
    • línea 76: se recupera en la sesión el número del último cálculo realizado. Aquí se denomina [simulation] al resultado de un cálculo;
    • línea 77: se incrementa el número de la última simulación;
    • línea 78: se vuelve a introducir este número en la sesión;
  • líneas 79-84: para realizar un seguimiento de los cálculos realizados por el usuario, vamos a introducir en su sesión la lista de simulaciones que ha realizado;
  • línea 80: una simulación será el diccionario de un objeto TaxPayer cuya propiedad [id] tendrá como valor el número de la simulación;
  • líneas 82-84: la simulación actual se añade a la lista de simulaciones presente en la sesión;
  • líneas 86-87: se prepara una respuesta HTTP de éxito;
  • línea 90: se devuelve el resultado;

Hagamos algunas pruebas: se inician el servidor web, el SGBD, el servidor de correo y un cliente Postman.

Caso 1: realizar un cálculo de impuestos cuando la sesión no está inicializada

Image

La respuesta es la siguiente:

Image

Caso 2: realizar un cálculo de impuestos sin estar autenticado

En primer lugar, se inicia una sesión jSON con [/init-session/json]. A continuación, se realiza la misma consulta que anteriormente. La respuesta es entonces la siguiente:

Image

Caso 3: realizar un cálculo de impuestos con parámetros faltantes

Inicializamos una sesión jSON, nos autenticamos y luego realizamos la siguiente consulta:

Image

  • en [5], falta el parámetro [marié];

La respuesta es la siguiente:

Caso 4: realizar un cálculo de impuestos con parámetros erróneos

Image

Image

La respuesta del servidor es la siguiente:

Image

Caso 4: realizar un cálculo de impuestos con parámetros correctos

Image

La respuesta del servidor es la siguiente:

Image

30.12. La acción [lister-simulations]

La acción [lister-simulations] permite a un usuario conocer la lista de simulaciones que ha realizado desde el inicio de la sesión. Su ruta se define de la siguiente manera en el script [main]:


# listar-simulaciones
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

El servidor no espera ningún parámetro. La acción [lister-simulations] es procesada por el controlador [ListerSimulationsController] siguiente:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class ListerSimulationsController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # se recuperan los elementos de path
        dummy, action = request.path.split('/')

        # se recupera la lista de simulaciones de la sesión
        simulations = session.get("simulations"[])
        # se muestra el resultado
        return {"action": action, "état"500,
                "réponse": simulations}, status.HTTP_200_OK
  • línea 13: se toma la lista de simulaciones de la sesión;
  • líneas 15-16: se devuelve una respuesta de éxito;

Realicemos la siguiente prueba con Postman:

  • iniciamos una sesión jSON;
  • nos autenticamos;
  • realizamos dos cálculos de impuestos;
  • solicitamos la lista de simulaciones;

La solicitud es la siguiente:

  • en [3], no hay ningún parámetro; Image

La respuesta del servidor es la siguiente:

Image

  • en [4], la lista de simulaciones del usuario;

30.13. La acción [supprimer-simulation]

La acción [supprimer-simulation] permite a un usuario eliminar una de las simulaciones de su lista de simulaciones. Su ruta se define de la siguiente manera en el script [main]:


# eliminar-simulación
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

El servidor espera un único parámetro: el número de la simulación que se va a eliminar. La acción [supprimer-simulation] es procesada por el controlador [SupprimerSimulationController] siguiente:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class SupprimerSimulationController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # se recuperan los elementos de path
        dummy, action, numéro = request.path.split('/')

        # el parámetro [numéro] es un entero positivo o nulo según su ruta
        numéro = int(numéro)
        # la simulación id=número debe existir en la lista de simulaciones
        simulations = session.get("simulations"[])
        liste_simulations = list(filter(lambda simulation: simulation['id'] == numéro, simulations))
        if not liste_simulations:
            msg_erreur = f"la simulation n° [{numéro}] n'existe pas"
            # se devuelve el error
            return {"action": action, "état"601"réponse"[msg_erreur]}, status.HTTP_400_BAD_REQUEST
        # eliminación de la simulación id=número
        simulation = liste_simulations.pop(0)
        simulations.remove(simulation)
        # se vuelven a incluir las simulaciones en la sesión
        session["simulations"] = simulations
        # se muestra el resultado
        return {"action": action, "état"600"réponse": simulations}, status.HTTP_200_OK
  • línea 10: se recuperan los dos elementos de la ruta de la solicitud. Se recuperan como cadenas de caracteres;
  • línea 13: el parámetro [numéro] se transforma en un entero. Sabemos que esto es posible gracias a la firma de su ruta,

@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])

Además, sabemos que es un entero >=0. De hecho, no puede haber un URL ni un [/supprimer-simulation/-4]. El servidor Flask rechaza estos valores;

  • línea 15: recuperamos la lista de simulaciones de la sesión;
  • línea 16: con la función [filter], se busca la simulación que tenga id==número. Se obtiene un objeto [filter] que se convierte al tipo [list];
  • líneas 17-20: si el filtro no ha devuelto ningún resultado, significa que la simulación que se debe eliminar no existe. Se devuelve un mensaje de error que lo indica;
  • líneas 21-23: se elimina la simulación devuelta por el filtro;
  • línea 25: se vuelve a introducir la nueva lista de simulaciones en la sesión;
  • línea 27: se devuelve en la respuesta la nueva lista de simulaciones;

Realizamos una prueba de éxito y una prueba de fallo. Hacemos simulaciones y luego solicitamos la lista de simulaciones:

Image

  • las simulaciones tienen aquí los números 2 y 3;

Solicitamos eliminar la simulación con el n.º 3.

Image

La respuesta es la siguiente:

Ahora, repitamos la misma operación (eliminación de la simulación de id=3). La respuesta es entonces la siguiente:

Image

Image

30.14. La acción [fin-session]

La acción [fin-session] permite a un usuario finalizar su sesión de simulaciones. Su ruta se define de la siguiente manera en el script [main]:


# fin de sesión
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

El servidor no espera ningún parámetro. La acción es procesada por el controlador [FinSessionController] siguiente:


from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class FinSessionController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # se recuperan los elementos de path
        dummy, action = request.path.split('/')

        # se eliminan todas las claves de la sesión actual
        session.clear()
        # se devuelve el resultado
        return {"action": action, "état"400"réponse""session réinitialisée"}, status.HTTP_200_OK
  • línea 13: se eliminan todas las claves de la sesión. Esto elimina:
    • [typeResponse]: el tipo de respuestas HTTP (json, xml, html);
    • [id_simulation]: número de la última simulación realizada;
    • [simulations]: la lista de simulaciones del usuario;
    • [user]: indicador de que el usuario ha sido autenticado;
  • se devuelve la respuesta;

Cabe preguntarse cómo se devolverá la respuesta HTTP de la línea 15, ahora que el tipo de respuesta ya no está en la sesión. Para saberlo, hay que volver a la función |front_controller| del script principal [main] y modificarla de la siguiente manera:


…        
         # on not# se anota el tipo de respuesta deseado si esta información se encuentra en la sesión
        type_response1 = session.get('typeResponse'None)
        # se reenvía la solicitud al controlador principal
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        # se registra el resultado enviado al cliente
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        # ¿Se ha producido un error grave?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # se envía un correo electrónico al administrador de la aplicación
            send_adminmail(config, log)
        # se determina el tipo deseado para la respuesta
        type_response2=session.get('typeResponse')
        if  type_response2 is None and type_response1 is None:
            # el tipo de sesión aún no se ha establecido; será jSON
            type_response = 'json'
        elif type_response2 is not None:
            # se conoce el tipo de respuesta y está en la sesión
            type_response = type_response2
        else:
            type_response=type_response1
        # se construye la respuesta que se va a enviar
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        # se envía la respuesta
        return response, status_code
  • línea 3: se almacena el tipo de respuesta actualmente en la sesión;
  • línea 6: se ejecuta la acción. Si se trata de:
    • [fin-session], la clave [typeResponse] ya no se encuentra en la sesión;
    • [init-session], la clave [typeResponse] de la sesión puede haber cambiado de valor;;
  • líneas 14-20: se debe enviar la respuesta HTTP. Necesitamos saber en qué formato:
    • líneas 16-18: si el tipo de respuesta no está definido ni por [type_response1] de la línea 3, ni por [type_response2] de la línea 15, entonces el tipo de respuesta no estaba definido ni antes ni después de la acción. En ese caso, se utiliza jSON (línea 18);
    • líneas 19-21: si existe [type_response2], el tipo en la sesión después de la acción, entonces es este tipo el que hay que utilizar;
    • líneas 22-23: de lo contrario, es [type_response1], el tipo de respuesta antes de la acción (esta es necesariamente [fin-session]), el que hay que utilizar;

30.15. La acción [get-admindata]

Ahora abordamos las dos URL reservadas para los servicios jSON y XML:

Acción
Función
Contexto de ejecución
/get-admindata
Muestra los datos fiscales necesarios para calcular el impuesto
Consulta GET.
Solo se utiliza si el tipo de sesión es json o xml. El usuario debe estar autenticado
/calcular-impuestos
Realiza el cálculo del impuesto de una lista de contribuyentes registrados en jSON
Consulta GET.
Solo se utiliza si el tipo de sesión es json o xml. El usuario debe estar autenticado

URL [/get-admindata] se define en las rutas del script principal [main] de la siguiente manera:


# get-admindata
@app.route('/get-admindata', methods=['GET'])
def get_admindata() -> tuple:
    # se ejecuta el controlador asociado a la acción
    return front_controller()

La ruta [/get-admindata] es procesada por el controlador [GetAdminDataController] siguiente:


# importación de dependencias

from flask_api import status
from werkzeug.local import LocalProxy

from InterfaceController import InterfaceController

class GetAdminDataController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # se recuperan los elementos de path
        dummy, action = request.path.split('/')
        # solo se aceptan las sesiones json y xml
        type_response = session.get('typeResponse')
        if type_response != 'json' and type_response != 'xml':
            # se devuelve una respuesta de error
            return {
                       "action": action,
                       "état"1001,
                       "réponse"["cette action n'est possible que pour les sessions json ou xml"]
                   }, status.HTTP_400_BAD_REQUEST
        else:
            # se devuelve una respuesta de éxito
            return {"action": action, "état"1000"réponse": config["adminData"].asdict()}, status.HTTP_200_OK
  • líneas 13-21: se comprueba que se está en una sesión json o xml;
  • línea 24: se devuelve el diccionario de datos de la administración tributaria que, desde el inicio del servidor, se había colocado en la configuración:

    # admindata será un dato de ámbito de aplicación de solo lectura
    config["admindata"] = config["layers"]["dao"].get_admindata()

Utilicemos un cliente Postman y solicitemos URL [/get-admindata], tras iniciar una sesión jSON y habernos autenticado:

Image

La respuesta del servidor es la siguiente:

Image

30.16. La acción [calculer-impots]

La acción [calculer-impots] calcula los impuestos de una lista de contribuyentes que se encuentra en el cuerpo de la solicitud en forma de cadena jSON. Ya conocemos esta acción: se llamaba [calculate_tax_in_bulk_mode] en la anterior version.

Su ruta es la siguiente:


# cálculo del impuesto por lotes
@app.route('/calculer-impots', methods=['POST'])
def calculer_impots():
    # se ejecuta el controlador asociado a la acción
    return front_controller()

Esta acción es procesada por el controlador [CalculerImpotsController] siguiente:


import json

from flask_api import status
from werkzeug.local import LocalProxy

from ImpôtsError import ImpôtsError
from InterfaceController import InterfaceController
from TaxPayer import TaxPayer

class CalculerImpotsController(InterfaceController):

    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # se recuperan los elementos de path
        dummy, action = request.path.split('/')

        # solo se aceptan las sesiones json y xml
        type_response = session.get('typeResponse')
        if type_response != 'json' and type_response != 'xml':
            # se devuelve una respuesta de error
            return {
                       "action": action,
                       "état"1501,
                       "réponse"["cette action n'est possible que pour les sessions json ou xml"]
                   }, status.HTTP_400_BAD_REQUEST

        # se recupera el cuerpo de post - se espera una lista de diccionarios
        msg_erreur = None
        list_dict_taxpayers = None
        # el cuerpo jSON del POST
        request_text = request.data
        try:
            # que se transforma en una lista de diccionarios
            list_dict_taxpayers = json.loads(request_text)
        except BaseException as erreur:
            # se observa el error
            msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
        # ¿tenemos una lista no vacía?
        if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
            # se observa el error
            msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
        # ¿Tenemos una lista de diccionarios?
        if not msg_erreur:
            erreur = False
            i = 0
            while not erreur and i < len(list_dict_taxpayers):
                erreur = not isinstance(list_dict_taxpayers[i], dict)
                i += 1
            # ¿error?
            if erreur:
                msg_erreur = "le corps du POST doit être une liste de dictionnaires"
        # ¿Error?
        if msg_erreur:
            # se envía una respuesta de error al cliente
            résultats = {"action": action, "état"1501"réponse"[msg_erreur]}
            return résultats, status.HTTP_400_BAD_REQUEST

        # se comprueban los TaxPayers uno por uno
        # al principio no hay errores
        list_erreurs = []
        for dict_taxpayer in list_dict_taxpayers:
            # se crea un TaxPayer a partir de dict_taxpayer
            msg_erreur = None
            try:
                # la siguiente operación eliminará los casos en los que los parámetros no sean
                # propiedades de la clase TaxPayer, así como los casos en los que sus valores
                # son incorrectos
                TaxPayer().fromdict(dict_taxpayer)
            except BaseException as erreur:
                msg_erreur = f"{erreur}"
            # ciertas claves deben estar presentes en el diccionario
            if not msg_erreur:
                # las claves [marié, enfants, salaire] deben estar presentes en el diccionario
                keys = dict_taxpayer.keys()
                if 'marié' not in keys or 'enfants' not in keys or 'salaire' not in keys:
                    msg_erreur = "le dictionnaire doit inclure les clés [marié, enfants, salaire]"
            # ¿Hay errores?
            if msg_erreur:
                # se observa el error en el propio TaxPayer
                dict_taxpayer['erreur'] = msg_erreur
                # se añade el TaxPayer a la lista de errores
                list_erreurs.append(dict_taxpayer)

        # se han procesado todos los contribuyentes; ¿hay algún error?
        if list_erreurs:
            # se envía una respuesta de error al cliente
            résultats = {"action": action, "état"1501"réponse": list_erreurs}
            return résultats, status.HTTP_400_BAD_REQUEST

        # sin errores, se puede trabajar
        # Recuperación de datos de la administración tributaria
        admindata = config["admindata"]
        métier = config["layers"]["métier"]
        try:
            # se procesan los TaxPayer uno por uno
            list_taxpayers = []
            for dict_taxpayer in list_dict_taxpayers:
                # cálculo del impuesto
                taxpayer = TaxPayer().fromdict(
                    {'marié': dict_taxpayer['marié']'enfants': dict_taxpayer['enfants'],
                     'salaire': dict_taxpayer['salaire']})
                métier.calculate_tax(taxpayer, admindata)
                # se almacena el resultado como un diccionario
                list_taxpayers.append(taxpayer.asdict())
            # se añade list_taxpayers a las simulaciones actuales, asignando un número a cada simulación
            simulations = session.get("simulations"[])
            id_simulation = session.get("id_simulation"0)
            for simulation in list_taxpayers:
                # se asigna un número a cada simulación
                id_simulation += 1
                simulation['id'] = id_simulation
                # se añade a la lista actual de simulaciones
                simulations.append(simulation)
            # se reinicia la sesión
            session["simulations"] = simulations
            session["id_simulation"] = id_simulation
            # se envía la respuesta al cliente
            return {"action": action, "état"1500"réponse": list_taxpayers}, status.HTTP_200_OK
        except ImpôtsError as erreur:
            # se envía una respuesta de error al cliente
            return {"action": action, "état"1501"réponse"[f"{erreur}"]}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • líneas 16-24: se comprueba que se está efectivamente en una sesión json o xml
  • líneas 26-120: este código nos resulta familiar en general. Es el de la función |index_controller| de la version 10 de la aplicación, que se ha adaptado para cumplir con las especificaciones de la interfaz [InterfaceController] implementada;
  • líneas 104-115: el código añadido para tener en cuenta el nuevo entorno de este controlador. Acabamos de realizar cálculos de impuestos. Debemos guardar los resultados en la lista de simulaciones mantenidas en la sesión;
  • línea 105: se recupera la lista de simulaciones de la sesión;
  • línea 106: se recupera el n.º de la última simulación realizada;
  • líneas 107-112: se recorre la lista de diccionarios de los resultados del cálculo del impuesto; a cada uno de ellos se le asigna un número de simulación [id] y cada diccionario se añade a la lista de simulaciones;
  • líneas 113-115: la nueva lista de simulaciones, así como el número de la última simulación realizada, se vuelven a poner en sesión;

Realizamos la siguiente prueba de Postman, tras haber iniciado una sesión jSON y habernos autenticado:

Image

Image

La respuesta del servidor es la siguiente:

Image

Si ahora solicitamos la lista de simulaciones:

Observaremos que en la lista de resultados de [/calcul-impots], los contribuyentes no tienen el atributo [id], mientras que en la lista de simulaciones, cada simulación tiene un número que la identifica.

Image