Skip to content

30. Esercizio pratico: versione 12

In questo capitolo realizzeremo un’applicazione web basata sull’architettura MVC (Modello-Vista-Controllore). L'applicazione potrà fornire le proprie risposte in tre formati: jSON, XML, HTML. C'è un notevole salto di complessità tra ciò che faremo ora e ciò che è stato fatto in precedenza. Riutilizzeremo la maggior parte dei concetti visti finora e illustreremo in dettaglio tutte le fasi che portano all'applicazione finale.

30.1. Architettura MVC

Implementeremo il modello di architettura denominato MVC (Modello – Vista – Controller) nel modo seguente:

L’elaborazione di una richiesta da parte di un cliente avverrà nel modo seguente:

  • 1 - richiesta

I URL richiesti avranno la forma http://machine:port/action/param1/param2/… Il [Contrôleur principal] utilizzerà un file di configurazione per "instradare" la richiesta verso il controller corretto. A tal fine, utilizzerà il campo [action] del URL. Il resto del URL [param1/param2/…] è costituito da parametri facoltativi che saranno trasmessi all’azione. Il valore C di MVC è in questo caso la stringa [Contrôleur principal, Contrôleur / Action]. Se nessun controller è in grado di elaborare l'azione richiesta, il server web risponderà che l'URL richiesta non è stata trovata.

  • 2 - elaborazione
  • l’azione selezionata [2a] può utilizzare i parametri parami che le sono stati trasmessi da [Contrôleur principal]. Questi possono provenire da due fonti:
      • dal percorso [/param1/param2/…] dell'URL,
      • dai parametri inseriti nel corpo della richiesta del cliente;
    • durante l'elaborazione della richiesta dell'utente, l'azione potrebbe richiedere il livello [métier] [2b]. Una volta elaborata la richiesta del client, questa può generare diverse risposte. Un esempio classico è:
      • una risposta di errore se la richiesta non è stata elaborata correttamente;
      • una risposta di conferma in caso contrario;
    • il [Contrôleur / Action] invierà la propria risposta [2c] al controller principale insieme a un codice di stato. Questi codici di stato rappresenteranno in modo univoco lo stato in cui si trova l’applicazione. Si tratterà di un codice di successo o di un codice di errore;
  • 3 - risposta
    • a seconda che il client abbia richiesto una risposta jSON, XML o HTML, il [Contrôleur principal] istanzierà [3a], il tipo di risposta appropriato, e chiederà a quest’ultimo di inviare la risposta al cliente. Il [Contrôleur principal] gli trasmetterà sia la risposta che il codice di stato forniti dal [Contrôleur / Action] che è stato eseguito;
    • se la risposta desiderata è di tipo jSON o XML, la risposta selezionata formatterà la risposta fornita da [Contrôleur / Action] e la invierà tramite [3c]. Il client in grado di utilizzare questa risposta può essere uno script Python da console o uno script JavaScript ospitato in una pagina HTML;
    • se la risposta desiderata è di tipo HTML, la risposta selezionata sceglierà una delle viste HTML o [Vuei] utilizzando il codice di stato che le è stato fornito. Si tratta della vista V di MVC. A ogni codice di stato corrisponde un'unica vista. Questa vista V visualizzerà la risposta del [Contrôleur / Action] che è stato eseguito. Essa impacchetta i dati di questa risposta utilizzando HTML, CSS e JavaScript. Questi dati vengono definiti il modello della vista. È la M di MVC. Il client è quindi, nella maggior parte dei casi, un browser;

Ora precisiamo il legame tra l’architettura web MVC e l’architettura a livelli. A seconda della definizione che si dà al modello, questi due concetti sono collegati o meno. Prendiamo un’applicazione web MVC a un unico livello:

Image

Nell’esempio sopra riportato, i [Contrôleur / Action] integrano ciascuno una parte dei livelli [métier] e [dao]. Nel livello [web] è presente un'architettura MVC, ma l'applicazione nel suo complesso non presenta un'architettura a livelli. In questo caso c'è un solo livello, il livello web, che svolge tutte le funzioni.

Ora, consideriamo un’architettura web multistrato:

Image

Il livello [web] può essere implementato senza seguire il modello MVC. Si ha quindi un’architettura multilivello, ma il livello web non implementa il modello MVC.

Ad esempio, nell’ambiente .NET il livello [web] quisopra può essere implementato con ASP.NET e MVC, ottenendo così un’architettura a livelli con un livello [web] di tipo MVC. Fatto ciò, è possibile sostituire questo livello ASP.NET MVC con un livello ASP.NET classico (WebForms), mantenendo il resto (business, DAO, Driver) invariato. Si ottiene così un’architettura a livelli con un livello [web] che non è più di tipo MVC.

In MVC, abbiamo affermato che il modello M era quello della vista V, c.a.d, ovvero l’insieme dei dati visualizzati dalla vista V. Viene fornita un’altra definizione del modello M di MVC:

Image

Molti autori ritengono che ciò che si trova a destra del livello [web] costituisca il modello M del MVC. Per evitare ambiguità, si può parlare:

  • del modello di dominio quando si indica tutto ciò che si trova a destra del livello [web];
  • del modello della vista quando si indicano i dati visualizzati da una vista V;

Di seguito, quando parleremo di modello, ci riferiremo sempre al modello della vista.

30.2. Architettura dell’applicazione client/server

L’applicazione web avrà la seguente architettura:

  • in [1] il server web avrà due tipi di client:
    • in [2], un client da console che scambierà dati jSON e XML con il server;
    • in [3], un browser che riceverà HTML dal server e lo visualizzerà;
  • il server web [1] mantiene i livelli [métier] e [dao] delle versioni precedenti;
  • il client web [2] verrà aggiornato per tenere conto delle nuove specifiche di servizio URL dell’applicazione web;
  • l’applicazione HTML visualizzata dal browser deve essere riscritta completamente;

Svilupperemo l’applicazione in più fasi:

  • svilupperemo la versione jSON del server. Testeremo i servizi del server uno dopo l’altro con un client Postman. Questo metodo ci permette di costruire la struttura portante del server web senza preoccuparci delle viste (=HTML) dell’applicazione;
  • dopo aver testato il server jSON con Postman, lo testeremo con un client da console;
  • poi passeremo alla versione XML del server. Abbiamo visto che il passaggio da jSON a XML è stato banale;
  • infine passeremo alla versione HTML del server. Realizzeremo un’architettura MVC e definiremo le viste da visualizzare. L’applicazione HTML verrà testata sia con il client Postman che con un browser tradizionale;

30.3. La struttura del codice del server

Image

  • in [1: il server web nella sua interezza;
  • in [2]: per il momento ignoreremo le cartelle [static, templates, tests_views] relative alla versione HTML del server. Al di fuori di questa cartella troveremo lo script principale [main] e la sua configurazione;
  • in [3], i controller del server web. Si tratterà di istanze di classi;
 
  • in [4], la risposta HTTP del server sarà gestita da classi;
  • in [5], conserviamo il file di log dei server precedenti;

Quando realizzeremo la versione HTML del server, entreranno in gioco altre cartelle:

 
  • in [6], gli elementi statici dell’applicazione HTML;
  • in [7], i modelli dell’applicazione HTML scomposti in viste [9] e in frammenti di vista [8];
  • in [9], le classi che implementano i modelli delle viste;

30.4. I URL del servizio dell’applicazione

Per realizzare il server web, procederemo nel modo seguente:

  • partendo dalle viste dell’applicazione HTML, definiremo le azioni che l’applicazione web dovrà implementare. In questo caso utilizzeremo le viste reali, ma potrebbero essere semplicemente delle viste su carta;
  • partendo da queste azioni, definiremo i URL di servizio dell’applicazione HTML;
  • implementeremo queste URL di servizio con un server che fornisce jSON. Ciò consente di definire la struttura di base del server web senza preoccuparsi delle pagine HTML da fornire. Testeremo questi servizi URL con Postman;
  • successivamente testeremo il nostro server jSON con un client da console;
  • una volta che il server jSON sarà stato convalidato, passeremo alla scrittura dell’applicazione HTML;

La prima schermata sarà quella di autenticazione:

Image

  • l’azione che porta a questa prima vista si chiamerà [init-session] [1];
  • facendo clic sul pulsante [Valider] si attiverà l’azione [authentifier-utilisateur] con due parametri inviati [2-3];

La vista del calcolo dell’imposta:

Image

  • in [1], l’azione [authentifier-utilisateur] che ha portato a questa vista;
  • in [2], il clic sul pulsante [Valider] avvia l'esecuzione dell'azione [calculer-impot] con tre parametri passati [2-5];
  • facendo clic sul link [6] si avvia l'azione [lister-simulations] senza parametri;
  • facendo clic sul link [7] si avvia l'azione [fin-session] senza parametri;

La terza schermata mostra le simulazioni effettuate dall’utente autenticato:

Image

  • in [3], l’azione [lister-simulations] che ha portato a questa vista;
  • in [2], un clic sul link [Supprimer] attiva l’azione [supprimer-simulation] con un parametro, ovvero il numero della simulazione da eliminare dall’elenco;
  • un clic sul link [3] attiva l’azione [afficher-calcul-impot] senza parametri, che visualizza nuovamente la schermata del calcolo dell’imposta;
  • un clic sul link [4] avvia l'azione [fin-session] senza parametri;

Con queste prime informazioni, possiamo definire le diverse azioni di servizio del server URL:

Azione
Ruolo
Contesto di esecuzione
/init-session
Serve a specificare il tipo (json, xml, html) delle risposte desiderate
Richiesta GET
Può essere inviata in qualsiasi momento
/autenticare-utente
Autorizza o meno un utente ad accedere
Richiesta POST.
La richiesta deve avere due parametri inviati via POST [user, password]
Può essere inviata solo se il tipo di sessione (json, xml, html) è noto
/calcolo-imposta
Esegue una simulazione del calcolo delle imposte
Richiesta POST.
La richiesta deve contenere tre parametri POST: [marié, enfants, salaire]
Può essere inviata solo se il tipo di sessione (json, xml, html) è noto e l'utente è autenticato
/elenco-simulazioni
Richiede la visualizzazione dell'elenco delle simulazioni effettuate dall'inizio della sessione
Richiesta GET.
Può essere inviata solo se il tipo di sessione (json, xml, html) è noto e l'utente è autenticato
/elimina-simulazione/numero
Elimina una simulazione dall'elenco delle simulazioni
Richiesta GET.
Può essere emessa solo se il tipo di sessione (json, xml, html) è noto e l'utente è autenticato
/visualizza-calcolo-imposta
Visualizza la pagina HTML relativa al calcolo delle imposte
Richiesta GET.
Può essere inviata solo se il tipo di sessione (json, xml, html) è noto e l'utente è autenticato
/fine-sessione
Termina la sessione di simulazioni.
Tecnicamente, la vecchia sessione web viene eliminata e ne viene creata una nuova
Può essere inviata solo se il tipo di sessione (json, xml, html) è noto e l'utente è autenticato

Questi diversi URL di servizio saranno utilizzati sia per il server HTML che per i server jSON o XML. Due URL saranno utilizzate solo per questi ultimi due server: si tratta delle URL della versione precedente del client/server web che riprendiamo qui:

Azione
Ruolo
Contesto di esecuzione
/get-admindata
Restituisce i dati fiscali necessari per il calcolo dell’imposta
Richiesta GET.
Viene utilizzata solo se il tipo di sessione è json o xml. L'utente deve essere autenticato
/calcolo-imposte
Esegue il calcolo dell’imposta su un elenco di contribuenti inviato tramite jSON
Richiesta GET.
Viene utilizzata solo se il tipo di sessione è json o xml. L'utente deve essere autenticato

Tutti i controller associati a queste azioni procederanno allo stesso modo:

  • verificheranno i propri parametri. Questi si trovano nell’oggetto:
    • [request.path] per i parametri presenti in URL nella forma [/action/param1/param2/…];
    • nell’oggetto [request.form] per quelli trasmessi in [x-www-form-urlencoded] nel corpo della richiesta;
    • nell’oggetto [request.data] per quelli trasmessi in jSON nel corpo della richiesta;
  • un controller è simile a una funzione o a un metodo che verifica la validità dei propri parametri. Per il controller, tuttavia, la questione è leggermente più complessa:
    • i parametri attesi potrebbero essere assenti;
    • i parametri recuperati dal controller sono stringhe di caratteri. Se il parametro atteso è un numero, il controller deve verificare che la stringa di caratteri del parametro corrisponda effettivamente a quella di un numero;
    • una volta verificato che i parametri attesi siano presenti e sintatticamente corretti, occorre verificare che siano validi nel contesto di esecuzione del momento. Questo contesto è presente nella sessione. L’esempio dell’autenticazione è un esempio di contesto di esecuzione. Alcune azioni devono essere elaborate solo dopo che il cliente è stato autenticato. Generalmente, una chiave nella sessione indica se tale autenticazione ha avuto luogo o meno;
    • una volta effettuate le verifiche precedenti, il controller secondario può procedere. Questo lavoro di verifica dei parametri è molto importante. Non è accettabile che un cliente ci invii qualsiasi cosa in qualsiasi momento del ciclo di vita dell’applicazione. Dobbiamo controllare totalmente il ciclo di vita di quest’ultima;
    • una volta completato il proprio lavoro, il controller secondario restituisce un dizionario con le chiavi [action, état, réponse] al controller principale che lo ha chiamato:
      • [action] è l’azione appena eseguita;
      • [état] è un numero a tre cifre che indica il risultato dell’elaborazione dell’azione:
    • [x00] segnalerà il successo dell’elaborazione;
    • [x01] segnalerà un fallimento dell’elaborazione;
  • [réponse] è il dizionario dei risultati nella forma {‘risposta’:oggetto}. L’oggetto avrà strutture diverse a seconda dell’azione elaborata;

Ora esamineremo i diversi controller o, in sostanza, le diverse azioni che questi controller gestiscono e che scandiscono il funzionamento dell’applicazione web.

30.5. Configurazione del server

Image

La configurazione del database [config_database] e quella dei livelli del server [config_layers] sono identiche a quelle delle versioni precedenti. Nel file [config] compaiono nuove informazioni:


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

    # fase 1 ------

    # cartella di questo file
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # percorso radice
    root_dir = "C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020"

    # dipendenze
    absolute_dependencies = [
        # cartelle del progetto
        # 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",
        # Costanti, intervalli
        f"{root_dir}/impots/v05/entities",
        # Logger, SendAdminMail
        f"{root_dir}/impots/http-servers/02/utilities",
        # script [config_database, config_layers]
        script_dir,
        # controllori
        f"{script_dir}/../controllers",
        # risposte HTTP
        f"{script_dir}/../responses",
        # modelli delle viste
        f"{script_dir}/../models_for_views",
    ]

    # impostazione del syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # dipendenze del server web

    # i controller
    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

    # risposte HTTP
    from HtmlResponse import HtmlResponse
    from JsonResponse import JsonResponse
    from XmlResponse import XmlResponse

    # i modelli delle viste
    from ModelForAuthentificationView import ModelForAuthentificationView
    from ModelForCalculImpotView import ModelForCalculImpotView
    from ModelForErreursView import ModelForErreursView
    from ModelForListeSimulationsView import ModelForListeSimulationsView

    # fase 2 ------
    # configurazione dell'applicazione
    config.update({
        # utenti autorizzati a utilizzare l'applicazione
        "users"[
            {
                "login""admin",
                "password""admin"
            }
        ],

        # file di log
        "logsFilename"f"{script_dir}/../data/logs/logs.txt",

        # configurazione del server SMTP
        "adminMail": {
            # server SMTP
            "smtp-server""localhost",
            # porta del server SMTP
            "smtp-port""25",
            # amministratore
            "from""guest@localhost.com",
            "to""guest@localhost.com",
            # oggetto dell'e-mail
            "subject""plantage du serveur de calcul d'impôts",
            # TLS impostato su True se il server SMTP richiede l'autenticazione, altrimenti su False
            "tls"False
        },

        # durata della pausa del thread in secondi
        "sleep_time"0,

        # azioni consentite e relativi controllori
        "controllers": {
            # inizializzazione di una sessione di calcolo
            "init-session": InitSessionController(),
            # autenticazione di un utente
            "authentifier-utilisateur": AuthentifierUtilisateurController(),
            # calcolo dell'imposta in modalità individuale
            "calculer-impot": CalculerImpotController(),
            # calcolo dell’imposta in modalità batch
            "calculer-impots": CalculerImpotsController(),
            # elenco delle simulazioni
            "lister-simulations": ListerSimulationsController(),
            # eliminazione di una simulazione
            "supprimer-simulation": SupprimerSimulationController(),
            # fine della sessione di calcolo
            "fin-session": FinSessionController(),
            # visualizzazione della schermata di calcolo dell'imposta
            "afficher-calcul-impot": AfficherCalculImpotController(),
            # recupero dei dati dall'amministrazione fiscale
            "get-admindata": GetAdminDataController(),
            # controller principale
            "main-controller": MainController()
        },

        # i diversi tipi di risposta (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

        # le viste HTML e i relativi modelli dipendono dallo stato restituito dal controller
        "views"[
            {
                # vista di autenticazione
                "états": [
                    # /init-session (operazione riuscita)
                    700,
                    # /autenticazione-utente fallita
                    201
                ],
                "view_name""views/vue-authentification.html",
                "model_for_view": ModelForAuthentificationView()
            },
            {
                # pagina di calcolo dell'imposta
                "états"[
                    # /autenticazione-utente riuscita
                    200,
                    # /calcolo-imposta riuscito
                    300,
                    # /calcolo-imposta fallito
                    301,
                    # /visualizza-calcolo-imposta
                    800
                ],
                "view_name""views/vue-calcul-impot.html",
                "model_for_view": ModelForCalculImpotView()
            },
            {
                # visualizzazione dell'elenco delle simulazioni
                "états"[
                    # /elenco-simulazioni
                    500,
                    # /eliminare-simulazione
                    600
                ],
                "view_name""views/vue-liste-simulations.html",
                "model_for_view": ModelForListeSimulationsView()
            }
        ],

        # visualizzazione degli errori imprevisti
        "view-erreurs": {
            "view_name""views/vue-erreurs.html",
            "model_for_view": ModelForErreursView()
        },

        # reindirizzamenti
        "redirections"[
            {
                "états": [
                    400,  # /fine-sessione riuscita
                ],
                # reindirizzamento a
                "to""/init-session/html",
            }
        ],
    }
    )

    # fase 3 ------
    # configurazione del database
    import config_database
    config["database"] = config_database.configure(config)

    # fase 4 ------
    # istanziazione dei livelli dell'applicazione
    import config_layers
    config['layers'] = config_layers.configure(config)

    # si esegue la configurazione
    return config
  • fino alla riga 41 si trovano elementi classici;
  • righe 43-66: alla riga 43 viene definito il Python Path del server. A questo punto è possibile importare le dipendenze del progetto:
    • righe 45-55: l'elenco dei controller;
    • righe 57-60: l'elenco delle risposte HTTP;
    • righe 62-66: l'elenco dei modelli di vista;
  • righe 68-189: la configurazione dell’applicazione con una serie di costanti;
    • righe 71-98: queste righe le conosciamo già dalle versioni precedenti;
    • righe 101-122: il dizionario dei controller:
      • le chiavi sono i nomi delle azioni;
      • i valori sono un'istanza del controller che deve gestire tale azione. Ogni controller viene istanziato in un unico esemplare (singleton). La stessa istanza verrà eseguita da diversi thread del server. Occorrerà quindi prestare attenzione ai dati condivisi che ogni controller potrebbe voler modificare;
    • righe 125-129: il dizionario delle tre possibili risposte HTTP:
      • le chiavi corrispondono al tipo di risposta richiesto dal cliente (jSON, xml, html);
      • i valori sono un'istanza della risposta HTTP. Ogni generatore di risposta viene istanziato in un unico esemplare (singleton). Lo stesso generatore verrà eseguito da diversi thread del server. Occorrerà quindi prestare attenzione ai dati condivisi che ogni generatore potrebbe voler modificare;
    • righe 132-186: configurazione delle viste HTML. Per il momento, queste righe vengono ignorate;
  • righe 191-202: abbiamo già incontrato queste righe nelle versioni precedenti;

30.6. Percorso di una richiesta del client all’interno del server

Image

Seguiremo il percorso di una richiesta del client in arrivo sul server fino alla risposta HTTP inviata in risposta. Segue il percorso del server MVC.

30.6.1. Lo script [main]

Image

Lo script [main] è identico sotto molti aspetti a quello delle versioni precedenti. Lo riportiamo comunque per intero per ripartire su basi solide:


# si attende un parametro 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()

# si configura l'applicazione
import config
config = config.configure({'sgbd': sgbd})

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

# invio di un'e-mail all'amministratore
def send_adminmail(config: dict, message: str):
    # invio di un'e-mail all'amministratore dell'applicazione
    config_mail = config["adminMail"]
    config_mail["logger"] = config['logger']
    SendAdminMail.send(config_mail, message)

# verifica del file di log
logger = None
erreur = False
message_erreur = None
try:
    # registratore
    logger = Logger(config["logsFilename"])
except BaseException as exception:
    # log della console
    print(f"L'erreur suivante s'est produite : {exception}")
    # si annota l'errore
    erreur = True
    message_erreur = f"{exception}"
# si memorizza il logger nella configurazione
config['logger'] = logger
# gestione dell'errore
if erreur:
    # invio di un'e-mail all'amministratore
    send_adminmail(config, message_erreur)
    # fine dell'applicazione
    sys.exit(1)

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

# recupero dei dati dell'amministrazione fiscale
erreur = False
try:
    # admindata sarà un dato a livello di applicazione in sola lettura
    config["admindata"] = config["layers"]["dao"].get_admindata().asdict()
    # log di esito positivo
    logger.write("[serveur] connexion à la base de données réussie\n")
except ImpôtsError as ex:
    # si registra l'errore
    erreur = True
    # log di errore
    log = f"L'erreur suivante s'est produite : {ex}"
    # console
    print(log)
    # file di log
    logger.write(f"{log}\n")
    # e-mail all'amministratore
    send_adminmail(config, log)

# il thread principale non ha più bisogno del logger
logger.close()

# se si è verificato un errore, ci si arresta
if erreur:
    sys.exit(2)

# applicazione Flask
app = Flask(__name__, template_folder="templates", static_folder="static")
# chiave segreta della sessione
app.secret_key = os.urandom(12).hex()

# il front controller
def front_controller() -> tuple:
    # si elabora la richiesta
    logger = None
    

@app.route('/', methods=['GET'])
def index() -> tuple:
    # reindirizzamento 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:
    # si esegue il controller associato all'azione
    return front_controller()

# autenticazione utente
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    # si esegue il controller associato all'azione
    return front_controller()

# calcolo-imposta
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
    # si esegue il controller associato all'azione
    return front_controller()

# elenco-simulazioni
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
    # si esegue il controller associato all'azione
    return front_controller()

# elimina-simulazione
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
    # viene eseguito il controller associato all'azione
    return front_controller()

# fine-sessione
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
    # si esegue il controller associato all'azione
    return front_controller()

# visualizza-calcolo-imposta
@app.route('/afficher-calcul-impot', methods=['GET'])
def afficher_calcul_impot() -> tuple:
    # si esegue il controller associato all'azione
    return front_controller()

# get-admindata
@app.route('/get-admindata/<int:numero>', methods=['GET'])
def get_admindata() -> tuple:
    # viene eseguito il controller associato all'azione
    return front_controller()

# solo main
if __name__ == '__main__':
    # si avvia il server
    app.config.update(ENV="development", DEBUG=True)
    app.run(threaded=True)
  • righe 1-92: tutte queste righe sono già state trattate e spiegate;
  • riga 92: il server gestirà una sessione. Abbiamo quindi bisogno di una chiave segreta. Per ogni utente inseriremo due informazioni nella sessione:
    • se l’utente si è autenticato correttamente;
    • ogni volta che effettuerà un calcolo delle imposte, i risultati di tale calcolo saranno inseriti in un elenco che chiameremo «elenco delle simulazioni dell’utente». Questo elenco sarà inserito nella sessione;
  • righe 100-151: l’elenco delle URL di servizio del server. Le funzioni associate fungono da filtro: tutte le URL non presenti in questo elenco saranno respinte dal server Flask con l’errore [404 NOT FOUND]. Una volta superato questo filtro, la richiesta viene sistematicamente inoltrata a un «Front Controller» implementato dalla funzione [front_controller] delle righe 94-98 che presenteremo tra poco;
  • righe 100-103: gestione del route [/]. Il punto di ingresso dell’applicazione web sarà la funzione URL alla riga 107. Inoltre, alla riga 103, reindirizziamo il client verso questa funzione URL:
  • la funzione [url_for] viene importata alla riga 18. Qui presenta due parametri:
      • il primo parametro è il nome di una delle funzioni di routing, in questo caso quella della riga 107. Si nota che questa funzione richiede un parametro [type_response], che indica il tipo di risposta (json, xml, html) richiesta dal cliente;
      • il secondo parametro riprende il nome del parametro della riga 107, [type_response], e gli assegna un valore. Se ci fossero altri parametri, si ripeterebbe l’operazione per ciascuno di essi;
      • restituisce il valore URL associato alla funzione indicata dai due parametri che gli sono stati forniti. In questo caso, il risultato sarà il valore URL della riga 106, dove il parametro viene sostituito dal suo valore [/init-session/html];
    • la funzione [redirect] è stata importata alla riga 18. Il suo ruolo è quello di inviare un'intestazione di reindirizzamento HTTP al client:
      • il primo parametro è l’URL verso cui il client deve essere reindirizzato;
      • il secondo parametro è il codice di stato della risposta HTTP inviata al cliente. Il codice [status.HTTP_302_FOUND] corrisponde a un reindirizzamento HTTP;

La funzione [front_controller] alle righe 94-98 esegue le prime elaborazioni della richiesta del cliente:


# il front controller
def front_controller() -> tuple:
    # si elabora la richiesta
    logger = None
    try:
        # registrazione nel log
        logger = Logger(config["logsFilename"])
        # la si memorizza in una configurazione associata al thread
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        # si registra la richiesta
        logger.write(f"[ front_controller] requête : {request}\n")
        # si interrompe il thread se richiesto
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            # la pausa è casuale, in modo che alcuni thread vengano interrotti e altri no
            aléa = randint(01)
            if aléa == 1:
                # registrazione prima della pausa
                logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                # pausa
                time.sleep(sleep_time)
        # la richiesta viene inoltrata al controller principale
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        # si registra il risultato inviato al cliente
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        # si è verificato un errore irreversibile?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # si invia un'e-mail all'amministratore dell'applicazione
            send_adminmail(config, log)
        # si determina il tipo di risposta desiderato
        if session.get('typeResponse'is None:
            # il tipo di sessione non è stato ancora stabilito - sarà jSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        # si costruisce la risposta da inviare
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        # si invia la risposta
        return response, status_code
    except BaseException as erreur:
        # si tratta di un errore imprevisto - si registra l'errore, se possibile
        if logger:
            logger.write(f"[ front_controller] {erreur}")
        # si prepara la risposta per il cliente
        résultat = {"réponse": {"erreurs"[f"{erreur}"]}}
        # si invia una risposta in jSON
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # si chiude il file di log se è stato aperto
        if logger:
            logger.close()
  • righe 1-57: conosciamo già questo codice. Si trattava, ad esempio, del codice della funzione denominata [main] nello script [main] della versione precedente. C’è solo una cosa da notare, ovvero il controller utilizzato alle righe 25-26:
  • riga 25: dalla configurazione viene recuperata l’istanza del controller associata al nome [main-controller]. Si tratta delle seguenti righe:

    # dipendenze del server web
    # i controller
    
    from MainController import MainController

     # azioni consentite e relativi controller
        "controllers": {
            ,
            # controller principale
            "main-controller": MainController()
        },
  • (continua)
    • alla riga 10 sopra, si noti che si recupera un'istanza di classe;
  • riga 26: si richiede al controller [MainController] di elaborare la richiesta;
  • righe 30-45: la risposta restituita dal controller [MainController] viene inviata al client. Torneremo su queste righe più avanti;

Il compito della funzione [front_controller] e successivamente della classe [MainController] è quello di eseguire le operazioni comuni a tutte le richieste:

Nello schema sopra riportato, ci troviamo ancora nella fase 1 dell’elaborazione della richiesta. Il controller principale [MainController] proseguirà con la fase 1.

30.6.2. Il controller principale [MainController]

Il controller principale [MainController] prosegue il lavoro avviato dalla funzione [front_controller]:

Tutti i controller implementano la seguente interfaccia [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
  • L'interfaccia [InterfaceController] definisce solo l'unico metodo [execute] alla riga 8. Questo metodo riceve tre parametri:
    • [request]: la richiesta del cliente;
    • [session]: la sessione del cliente;
    • [config]: la configurazione dell'applicazione;

Il metodo [execute] restituisce una tupla di due elementi:

  • il primo è il dizionario dei risultati nella forma {‘azione’: azione, ‘stato’: stato, ‘risposta’: risultati};
  • il secondo è il codice di stato HTTP da restituire al cliente;

Il controller principale [MainController] [1] implementa l’interfaccia [InterfaceController] nel modo seguente:


# importazione delle dipendenze

from flask_api import status
from werkzeug.local import LocalProxy

# controller dell'applicazione web
from InterfaceController import InterfaceController

class MainController(InterfaceController):
    def execute(self, request: LocalProxy, session: LocalProxy, config: dict) -> (dict, int):
        # recuperiamo gli elementi del percorso
        params = request.path.split('/')
        action = params[1]

        # errori
        erreur = False
        # il tipo di sessione deve essere noto prima di determinate azioni
        type_response = session.get('typeResponse')
        if type_response is None and action != "init-session":
            # si registra l'errore
            résultat = {"action": action, "état"101,
                        "réponse"["pas de session en cours. Commencer par action [init-session]"]}
            erreur = True
        # per alcune azioni è necessario essere autenticati
        user = session.get('user')
        if not erreur and user is None and action not in ["init-session""authentifier-utilisateur"]:
            # si registra l'errore
            résultat = {"action": action, "état"101,
                        "réponse"[f"action [{action}] demandée par utilisateur non authentifié"]}
            erreur = True
        # Ci sono errori?
        if erreur:
            # viene restituito un messaggio di errore
            return résultat, status.HTTP_400_BAD_REQUEST
        else:
            # si esegue il controller associato all'azione
            controller = config["controllers"][action]
            résultat, status_code = controller.execute(request, session, config)
            return résultat, status_code

Il controller [MainController] esegue le prime verifiche di validità della richiesta.

  • righe 11-13: il controller inizia recuperando l’azione richiesta dal client. Si ricorda che le URL di servizio hanno la forma [/action/param1/param2/…] e che questa URL si trova all’interno di [request.path];
  • righe 17-23: l’azione [init-session] serve a inizializzare il tipo di risposta (json, xml, html) richiesto dal client. Questa informazione viene inserita nella sessione associata alla chiave [typeRéponse]. Pertanto, se l’azione non è [init-session], la sessione deve contenere la chiave [typeRéponse]; in caso contrario, la richiesta è errata;
  • righe 21-22: la struttura del risultato restituito da ciascun controller, in questo caso un messaggio di errore:
    • [action]: è il nome dell’azione in corso. Ciò consentirà di ottenere il suo nome quando si registrerà il risultato della richiesta;
    • [état]: è un codice di stato a tre cifre:
        • [x00] indica esito positivo;
        • [x01] in caso di esito negativo;
  • [réponse]: è la risposta alla richiesta. La sua natura è specifica per ogni richiesta;
  • righe 24-30: l'azione [authentifier-utilisateur] serve ad autenticare l'utente. Se ha esito positivo, una chiave [user=True] viene inserita nella sessione dell'utente. Alcune azioni di servizio URL sono accessibili solo da un utente autenticato. È proprio questo che viene verificato qui;
  • riga 26: solo le azioni [init-session] e [authentifier-utilisateur] possono essere eseguite da un utente non ancora autenticato;
  • righe 28-29: il risultato da inviare in caso di errore;
  • righe 32-34: se si è verificato uno dei due errori precedenti, si invia la risposta di errore al client con lo stato HTTP 400 BAD REQUEST;
  • righe 35-39: se non si è verificato alcun errore, si passa il controllo al controller incaricato di gestire l’azione in corso. La sua istanza si trova nella configurazione dell’applicazione;

La classe [MainController] prosegue il lavoro della funzione [front_controller]: insieme, queste due entità raggruppano tutto ciò che può essere fattorizzato nell’elaborazione delle richieste, attendendo l’ultimo momento per passare la richiesta a un controller specifico. La suddivisione del codice tra la funzione [front_controller] e la classe [MainController] è del tutto soggettiva. In questo caso ho voluto mantenere quanto già realizzato nella versione precedente: la funzione [front_controller] esisteva già con il nome [main]. In pratica, si potrebbe:

  • inserire tutto nella funzione [front_controller] ed eliminare la classe [MainController];
  • inserire tutto nella classe [MainController] ed eliminare la funzione [front_controller]. È proprio questa la soluzione che sceglierei, poiché ha il pregio di alleggerire il codice dello script principale [main];

30.7. Elaborazione specifica per un'azione

Torniamo all’architettura MVC dell’applicazione:

Image

Siamo ancora alla fase 1 sopra descritta. Se non si sono verificati errori, avrà inizio la fase 2. La richiesta è stata inoltrata al controller specifico per l’azione richiesta dalla richiesta stessa. Supponiamo che tale azione sia [/init-session], definita dal percorso:


# init-session
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    # viene eseguito il controller associato all'azione
    return front_controller()

Questa azione è collegata a un controller nella configurazione [config]:


        # azioni consentite e relativi controller
        "controllers": {
            # inizializzazione di una sessione di calcolo
            "init-session": InitSessionController(),
            
        },

Il controller [InitSessionController] (riga 4) prende quindi il controllo. Il suo codice è il seguente:


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):
        # si recuperano gli elementi del percorso
        dummy, action, type_response = request.path.split('/')

        # All'inizio nessun errore
        erreur = False
        # verifica del tipo di risposta
        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"]}
        # se non ci sono errori
        if not erreur:
            # si inserisce il tipo di sessione nella sessione 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
  • riga 6: come gli altri controller, il controller [InitSessionController] implementa l’interfaccia [InterfaceController];
  • riga 10: il URL è di tipo [/init-session/type_response]. Si recuperano l’azione [init-session] e il tipo di risposta desiderato;
  • riga 15: il tipo di risposta desiderato può essere solo uno di quelli presenti nella configurazione delle risposte:

        # i diversi tipi di risposta (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },
  • in caso contrario, si prepara un messaggio di errore 701 (riga 17);
  • righe 20-25: caso in cui il tipo di risposta desiderato sia valido;
  • riga 22: il tipo di risposta desiderato viene memorizzato nella sessione. Infatti, sarà necessario ricordarlo per le richieste successive;
  • righe 23-24: si prepara una risposta di successo 700;
  • riga 25: la risposta di successo viene restituita al codice chiamante;
  • riga 27: se si è verificato un errore, la risposta di errore viene restituita al codice chiamante;

30.8. Elaborazione della risposta HTTP del server

Torniamo all’architettura MVC dell’applicazione:

Image

Abbiamo appena visto le fasi 1 e 2. Abbiamo riscontrato tre codici di stato:

  • 700: /init-session riuscita;
  • 701: /init-session non è andato a buon fine;
  • 101: richiesta non valida perché la sessione non è stata inizializzata o perché l’utente non è autenticato;

Esaminiamo come la risposta del server verrà inviata al client durante la fase 3 sopra descritta. Ciò avviene nella funzione [front_controller] dello script [main]:


# il front controller
def front_controller() -> tuple:
    # si elabora la richiesta
    logger = None
    try:
        # logger
        logger = Logger(config["logsFilename"])
        # la si memorizza in una configurazione associata al thread
        thread_config = {"logger": logger}
        thread_name = threading.current_thread().name
        config[thread_name] = {"config": thread_config}
        # si registra la richiesta
        logger.write(f"[ front_controller] requête : {request}\n")
        # si interrompe il thread se richiesto
        sleep_time = config["sleep_time"]
        if sleep_time != 0:
            # la pausa è casuale, in modo che alcuni thread vengano interrotti e altri no
            aléa = randint(01)
            if aléa == 1:
                # registrazione prima della pausa
                logger.write(f"[ front_controller] mis en pause du thread pendant {sleep_time} seconde(s)\n")
                # pausa
                time.sleep(sleep_time)
        # la richiesta viene inoltrata al controller principale
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        # si registra il risultato inviato al client
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        # si è verificato un errore irreversibile?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # si invia un'e-mail all'amministratore dell'applicazione
            send_adminmail(config, log)
        # si determina il tipo di risposta desiderato
        if session.get('typeResponse'is None:
            # il tipo di sessione non è ancora stato stabilito - sarà jSON
            type_response = 'json'
        else:
            type_response = session['typeResponse']
        # si costruisce la risposta da inviare
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        # si invia la risposta
        return response, status_code
    except BaseException as erreur:
        # si tratta di un errore imprevisto - si registra l'errore, se possibile
        if logger:
            logger.write(f"[ front_controller] {erreur}")
        # si prepara la risposta per il cliente
        résultat = {"réponse": {"erreurs"[f"{erreur}"]}}
        # si invia una risposta in formato jSON
        return json_response(résultat, status.HTTP_500_INTERNAL_SERVER_ERROR)
    finally:
        # si chiude il file di log, se era stato aperto
        if logger:
            logger.close()
  • siamo alla riga 26: il controller principale ha restituito la sua risposta di errore;
  • righe 27-29: indipendentemente dalla risposta del controller principale (successo o errore), tale risposta viene registrata nel file di log;
  • righe 30-33: come nelle versioni precedenti, se lo stato HTTP è [500 INTERNAL SERVER ERROR], viene inviata un’e-mail all’amministratore dell’applicazione con il log dell’errore;
  • righe 34-39: verrà inviata la risposta HTTP e il risultato restituito dal controller verrà inserito nel corpo di tale risposta. È necessario sapere in quale formato (json, xml, html) il client desidera ricevere questa risposta. Si cerca il tipo di risposta desiderato nella sessione. Se non è presente, si imposta arbitrariamente questo tipo su jSON;
  • righe 40-43: viene costruita la risposta HTTP;

Nel file di configurazione, ogni tipo di risposta (json, xml, html) è stato associato a un'istanza di classe:


        # i diversi tipi di risposta (json, xml, html)
        "responses": {
            "json": JsonResponse(),
            "html": HtmlResponse(),
            "xml": XmlResponse()
        },

Le classi di risposta si trovano nella cartella [responses] della struttura del server:

Image

Ogni classe di risposta implementa la seguente interfaccia [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
  • righe 8-11: l'interfaccia [InterfaceResponse] definisce un unico metodo [build_http_response] con i seguenti parametri:
    • [request, session, config]: sono i parametri ricevuti dal controller dell’azione;
    • [résultat, status_code]: sono i risultati generati dal gestore dell’azione;

Presenteremo ora la risposta jSON. Essa è generata dalla seguente classe [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):
        # Risultati: il dizionario dei risultati
        # status_code: il codice di stato della risposta HTTP

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

Conosciamo bene questo codice, che abbiamo incontrato numerose volte. Si tratta del codice della funzione [json_response] del modulo [myutils].

30.9. Primi test

Nel codice analizzato abbiamo riscontrato tre codici di stato:

  • 700: /init-session riuscita;
  • 701: /init-session non riuscita;
  • 101: richiesta non valida, perché la sessione non è stata inizializzata o perché l’utente non è autenticato;

Proveremo a ottenerli con una sessione jSON.

  • Avviamo il server web, il SGBD e il server di posta;
  • avviamo un client Postman;

Test 1

Mostriamo innanzitutto una richiesta non valida perché la sessione non è stata inizializzata:

Image

  • [1-2]: la richiesta [POST http://localhost:5000/authentifier-utilisateur] è un percorso valido:

# autenticazione utente
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    # si esegue il controller associato all'azione
    return front_controller()

ma viene accettata solo se la sessione è stata precedentemente inizializzata con l'azione [/init-session].

Eseguiamo la richiesta e vediamo il risultato inviato dal server:

Image

  • [1-2]: abbiamo ottenuto una risposta jSON. Quando il tipo di risposta non è stato ancora specificato dal client, il server utilizza jSON per rispondere;
  • [3-5]: il dizionario jSON della risposta;
    • [action]: l’azione che è stata eseguita;
    • [état]: il codice di stato della risposta. Un codice [x01] indica un errore;
    • [réponse]: è specifico per ogni azione. In questo caso contiene un messaggio di errore;

Ora inizializziamo una sessione con un tipo di risposta errato:

Image

  • [1-2] è un percorso corretto:

# inizializzazione della sessione
@app.route('/init-session/<string:type_response>', methods=['GET'])
def init_session(type_response: str) -> tuple:
    # si esegue il controller associato all'azione
    return front_controller()

La richiesta entrerà quindi nel tunnel di elaborazione delle richieste del server MVC. Tuttavia, dovrebbe essere rifiutata durante tale elaborazione poiché il tipo di sessione richiesto non è corretto.

La risposta è la seguente:

Image

  • in [4], un codice di errore [x01];
  • in [5], la spiegazione dell’errore;

Ora inizializziamo una sessione jSON:

Image

La risposta è la seguente:

Image

Ora inizializziamo una sessione XML. La risposta jSON verrà sostituita da una risposta XML generata dalla seguente classe [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):
        # risultati: il dizionario dei risultati
        # status_code: il codice di stato della risposta HTTP

        # risultato: il dizionario da convertire in stringa XML
        xml_string = xmltodict.unparse({"root": résultat})
        # si restituisce la risposta HTTP
        response = make_response(xml_string)
        response.headers['Content-Type'] = 'application/xml; charset=utf-8'
        return response, status_code

Si tratta di codice che conosciamo, quello della funzione [xml_response] del modulo condiviso [myutils].

Inizializziamo una sessione XML:

Image

Il risultato del server è quindi il seguente:

Image

Otteniamo la stessa risposta di jSON, ma questa volta la risposta è formattata come XML.

30.10. L’azione [authentifier-utilisateur]

L'azione [authentifier-utilisateur] consente di autenticare un utente che desidera utilizzare l'applicazione per il calcolo delle imposte. Il suo percorso è definito come segue nello script [main]:


# autenticazione utente
@app.route('/authentifier-utilisateur', methods=['POST'])
def authentifier_utilisateur() -> tuple:
    # si esegue il controller associato all'azione
    return front_controller()

Il server attende due parametri inviati tramite POST:

  • [user]: l’identificativo dell’utente;
  • [password]: la sua password;

L'elenco degli utenti autorizzati è definito nella configurazione [config]:


        # utenti autorizzati a utilizzare l'applicazione
        "users"[
            {
                "login""admin",
                "password""admin"
            }
        ],

In questo caso, abbiamo un elenco composto da un solo elemento.

L'azione [authentifier-utilisateur] viene gestita dal controller [AuthentifierUtilisateurController] seguente:


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):
        # si recuperano gli elementi del percorso
        dummy, action = request.path.split('/')

        # i parametri di POST
        post_params = request.form
        # codice di stato della risposta HTTP
        status_code = None
        # inizialmente nessun errore
        erreur = False
        erreurs = []
        # è necessario un POST con due parametri
        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:
            # si recuperano i parametri del POST
            # parametro [user]
            user = post_params.get("user")
            if user is None:
                erreur = True
                erreurs.append("paramètre [user] manquant")
            # parametro [password]
            password = post_params.get("password")
            if password is None:
                erreur = True
                erreurs.append("paramètre [password] manquant")
            # errore?
            if erreur:
                status_code = status.HTTP_400_BAD_REQUEST
        # errore?
        if not erreur:
            # si sta verificando la validità della coppia (utente, password)
            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
            # trovata?
            if not trouvé:
                # si registra l'errore
                erreur = True
                status_code = status.HTTP_401_UNAUTHORIZED
                erreurs.append(f"Echec de l'authentification")
            else:
                # si registra nella sessione che l'utente è stato trovato
                session["user"] = True
        # operazione completata
        if not erreur:
            # ritorno senza errori
            résultat = {"action": action, "état"200"réponse"f"Authentification réussie"}
            return résultat, status.HTTP_200_OK
        else:
            # ritorno con errore
            return {"action": action, "état"201"réponse": erreurs}, status_code

  • riga 14: si recuperano i parametri di POST;
  • riga 19: l’elenco degli errori rilevati nella richiesta;
  • righe 20-24: si verifica che siano stati effettivamente inviati due parametri;
  • righe 27-31: si verifica la presenza di un parametro [users];
  • righe 32-36: si verifica la presenza di un parametro [password];
  • righe 38-39: se i parametri inviati sono errati, si prepara una risposta HTTP 400 BAD REQUEST;
  • righe 40-58: si verifica che le credenziali [user, password] appartengano a un utente autorizzato a utilizzare l'applicazione;
  • righe 51-55: se l’utente (user, password) non è autorizzato a utilizzare l’applicazione, si prepara una risposta HTTP 401 UNAUTHORIZED;
  • righe 56-58: se è autorizzato, si registra nella sessione, con la chiave [user], che si è autenticato;

Si noti che se l’utente si fosse autenticato con le credenziali [identifiants1] e non riuscisse ad autenticarsi con le credenziali [identifiants2], rimarrebbe comunque autenticato con le credenziali [identifiants1].

Eseguiamo alcuni test con Postman:

  • avviamo il server web, il SGBD e il server di posta;
  • con il client Postman:
    • si avvia una sessione jSON;
    • quindi ci si autentica;

Ecco alcuni casi.

Caso 1: POST senza parametri inviati

Image

  • in [3-5], il POST non ha corpo;

Il risultato della richiesta è il seguente:

Image

  • in [2], si è ottenuta una risposta HTTP 400 BAD REQUEST;
  • da [5], si è ottenuto un codice di errore [201];

Caso 2: POST con credenziali errate

Image

  • in [6], le credenziali sono errate;

Il server invia la seguente risposta:

Image

  • in [2], la risposta HTTP 401 UNAUTHORIZED;
  • in [5], la risposta di errore;

Caso 2: POST con credenziali corrette

Image

  • in [6], le credenziali sono corrette;

La risposta del server è la seguente:

  • in [2], una risposta HTTP 200 OK; Image
  • in [5], risposta di esito positivo;

30.11. L’azione [calculer_impot]

L’azione [calculer_impot] consente di calcolare l’imposta di un contribuente. Il suo percorso è definito come segue nello script [main]:


# calcolo-imposta
@app.route('/calculer-impot', methods=['POST'])
def calculer_impot() -> tuple:
    # si esegue il controller associato all'azione
    return front_controller()

Il server attende tre parametri inviati via POST:

  • [marié]: sì / no;
  • [enfants]: numero di figli del contribuente;
  • [salaire]: stipendio annuo del contribuente;

Il controllore [CalculerImpotController] elabora l'azione [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):
        # si recuperano gli elementi del percorso
        dummy, action = request.path.split('/')

        # nessun errore iniziale
        erreur = False
        erreurs = []
        # i parametri di POST
        post_params = request.form
        # è necessario un POST con tre parametri
        if len(post_params) != 3:
            erreur = True
            erreurs.append(
                "méthode POST requise avec les paramètres postés [marié, enfants, salaire]")
        # si analizzano i parametri inviati
        if not erreur:
            # parametro associato
            marié = post_params.get("marié")
            if marié is None:
                erreurs.append("paramètre [marié] manquant")
            else:
                # il parametro è valido?
                marié = marié.lower()
                if marié != "oui" and marié != "non":
                    erreur = True
                    erreurs.append(f"valeur [{marié}] invalide pour le paramètre [marié (oui/non)]")
            # parametro [enfants]
            enfants = post_params.get("enfants")
            if enfants is None:
                erreur = True
                erreurs.append("paramètre [enfants] manquant")
            else:
                # il parametro è valido?
                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)]")
            # parametro stipendio
            salaire = post_params.get("salaire")
            if salaire is None:
                erreur = True
                erreurs.append("paramètre [salaire] manquant")
            else:
                # il parametro è valido?
                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)]")
        # errore?
        if erreur:
            status_code = status.HTTP_400_BAD_REQUEST
            résultat = {"action": action, "état"301"réponse": erreurs}
            # si restituisce il risultato
            return résultat, status_code

        # calcolo dell'imposta
        # si recuperano il livello [métier] e il dizionario [adminData]
        métier = config["layers"]["métier"]
        admin_data = config["admindata"]
        # calcolo dell'imposta
        taxpayer = TaxPayer().fromdict({'marié': marié, 'enfants': enfants, 'salaire': salaire})
        métier.calculate_tax(taxpayer, admin_data)
        # numero della simulazione
        id_simulation = session.get('id_simulation'0)
        id_simulation += 1
        session['id_simulation'] = id_simulation
        # si inserisce il risultato nella sessione sotto forma di dizionario di un TaxPayer
        simulation = taxpayer.fromdict({'id': id_simulation}).asdict()
        # si aggiunge il risultato all'elenco delle simulazioni già effettuate e si inserisce quest'ultimo nella sessione
        simulations = session.get("simulations"[])
        simulations.append(simulation)
        session["simulations"] = simulations
        # risultato
        résultat = {"action": action, "état"300"réponse": simulation}
        status_code = status.HTTP_200_OK

        # si restituisce il risultato
        return résultat, status_code
  • riga 13: si recupera il nome dell’azione in corso;
  • riga 17: si accumulano gli errori in un elenco;
  • riga 19: si recuperano i parametri inviati. Questi vengono inviati nella forma [x-www-form-urlencoded] ed è per questo che vengono recuperati in [request.form]. Se fossero stati inviati come jSON, li avremmo recuperati come [request.data];
  • righe 21-24: si verifica che siano stati effettivamente inviati tre parametri;
  • righe 27-36: verifica della presenza e della validità del parametro inviato [marié];
  • righe 37-48: verifica della presenza e della validità del parametro inviato [enfants];
  • righe 49-60: verifica della presenza e della validità del parametro inviato [salaire];
  • righe 62-66: se si è verificato un errore, viene inviata una risposta di errore 400 BAD REQUEST con un codice di stato [301];
  • righe 69-71: se non si è verificato alcun errore, ci si prepara a calcolare l'imposta. A tal fine,
    • riga 70: si recupera un riferimento dal livello [métier];
    • riga 71: si recuperano i dati dell’amministrazione fiscale dalla configurazione del server;
  • righe 72-74: viene calcolata l’imposta del contribuente;
  • righe 75-77: si conta il numero di calcoli dell’imposta effettuati dall’utente;
    • riga 76: si recupera nella sessione il numero dell'ultimo calcolo effettuato. Qui si chiama [simulation] il risultato di un calcolo;
    • riga 77: si incrementa il numero dell’ultima simulazione;
    • riga 78: si reinserisce questo numero nella sessione;
  • righe 79-84: per tenere traccia dei calcoli effettuati dall’utente, inseriremo nella sua sessione l’elenco delle simulazioni che ha effettuato;
  • riga 80: una simulazione sarà il dizionario di un oggetto TaxPayer, la cui proprietà [id] avrà come valore il numero della simulazione;
  • righe 82-84: la simulazione corrente viene aggiunta all’elenco delle simulazioni presenti nella sessione;
  • righe 86-87: si prepara una risposta HTTP di esito positivo;
  • riga 90: si restituisce il risultato;

Effettuiamo alcuni test: il server web, il SGBD, il server di posta e un client Postman sono avviati.

Caso 1: eseguire un calcolo delle imposte quando la sessione non è inizializzata

Image

La risposta è la seguente:

Image

Caso 2: eseguire un calcolo delle imposte senza essere autenticati

Per prima cosa si avvia una sessione jSON con [/init-session/json]. Quindi si effettua la stessa richiesta di prima. La risposta è quindi la seguente:

Image

Caso 3: eseguire un calcolo delle imposte con parametri mancanti

Si avvia una sessione jSON, si effettua l’autenticazione e poi si esegue la seguente richiesta:

Image

  • in [5] manca il parametro [marié];

La risposta è la seguente:

Caso 4: eseguire un calcolo delle imposte con parametri errati

Image

Image

La risposta del server è la seguente:

Image

Caso 4: effettuare un calcolo delle imposte con parametri corretti

Image

La risposta del server è la seguente:

Image

30.12. L'azione [lister-simulations]

L'azione [lister-simulations] consente a un utente di visualizzare l'elenco delle simulazioni effettuate dall'inizio della sessione. Il suo percorso è definito come segue nello script [main]:


# elenco-simulazioni
@app.route('/lister-simulations', methods=['GET'])
def lister_simulations() -> tuple:
    # si esegue il controller associato all'azione
    return front_controller()

Il server non richiede alcun parametro. L’azione [lister-simulations] viene elaborata dal controller [ListerSimulationsController] seguente:


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):
        # si recuperano gli elementi del percorso
        dummy, action = request.path.split('/')

        # si recupera l'elenco delle simulazioni nella sessione
        simulations = session.get("simulations"[])
        # si restituisce il risultato
        return {"action": action, "état"500,
                "réponse": simulations}, status.HTTP_200_OK
  • riga 13: l'elenco delle simulazioni viene prelevato dalla sessione;
  • righe 15-16: viene restituita una risposta di esito positivo;

Eseguiamo il seguente test con Postman:

  • si avvia una sessione jSON;
  • si effettua l'autenticazione;
  • si effettuano due calcoli delle imposte;
  • si richiede l'elenco delle simulazioni;

La richiesta è la seguente:

  • in [3] non ci sono parametri; Image

La risposta del server è la seguente:

Image

  • in [4], l'elenco delle simulazioni dell'utente;

30.13. L'azione [supprimer-simulation]

L’azione [supprimer-simulation] consente a un utente di eliminare una delle simulazioni dal proprio elenco di simulazioni. Il suo percorso è definito come segue nello script [main]:


# elimina-simulazione
@app.route('/supprimer-simulation/<int:numero>', methods=['GET'])
def supprimer_simulation(numero: int) -> tuple:
    # si esegue il controller associato all'azione
    return front_controller()

Il server richiede un unico parametro: il numero della simulazione da eliminare. L’azione [supprimer-simulation] viene gestita dal controller [SupprimerSimulationController] come segue:


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):
        # si recuperano gli elementi del percorso
        dummy, action, numéro = request.path.split('/')

        # il parametro [numéro] è un numero intero positivo o zero in base al suo percorso
        numéro = int(numéro)
        # la simulazione con id=numero deve essere presente nell'elenco delle simulazioni
        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"
            # viene restituito un errore
            return {"action": action, "état"601"réponse"[msg_erreur]}, status.HTTP_400_BAD_REQUEST
        # eliminazione della simulazione con ID = numero
        simulation = liste_simulations.pop(0)
        simulations.remove(simulation)
        # si reinseriscono le simulazioni nella sessione
        session["simulations"] = simulations
        # restituisce il risultato
        return {"action": action, "état"600"réponse": simulations}, status.HTTP_200_OK
  • riga 10: si recuperano i due elementi del percorso della richiesta. Vengono recuperati come stringhe di caratteri;
  • riga 13: il parametro [numéro] viene convertito in un numero intero. Sappiamo che ciò è possibile grazie alla firma del suo percorso,

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

Sappiamo inoltre che si tratta di un numero intero >=0. Non è infatti possibile avere un URL o un [/supprimer-simulation/-4]. Questi valori vengono rifiutati dal server Flask;

  • riga 15: si recupera l’elenco delle simulazioni dalla sessione;
  • riga 16: con la funzione [filter], si cerca la simulazione con id==numero. Si ottiene un oggetto [filter] che viene convertito nel tipo [list];
  • righe 17-20: se il filtro non ha restituito alcun risultato, significa che la simulazione da eliminare non esiste. Viene restituito un messaggio di errore che lo indica;
  • righe 21-23: si elimina la simulazione restituita dal filtro;
  • riga 25: si reinserisce il nuovo elenco di simulazioni nella sessione;
  • riga 27: si restituisce nella risposta il nuovo elenco di simulazioni;

Eseguiamo un test di successo e un test di fallimento. Effettuiamo delle simulazioni, quindi richiediamo l’elenco delle simulazioni:

Image

  • le simulazioni qui hanno i numeri 2 e 3;

Si richiede di eliminare la simulazione con il n. 3.

Image

La risposta è la seguente:

Ora ripetiamo la stessa operazione (eliminazione della simulazione con id=3). La risposta è quindi la seguente:

Image

Image

30.14. L'azione [fin-session]

L’azione [fin-session] consente a un utente di terminare la propria sessione di simulazioni. Il suo percorso è definito come segue nello script [main]:


# fine sessione
@app.route('/fin-session', methods=['GET'])
def fin_session() -> tuple:
    # si esegue il controller associato all'azione
    return front_controller()

Il server non richiede alcun parametro. L’azione viene elaborata dal controller [FinSessionController] seguente:


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):
        # si recuperano gli elementi del percorso
        dummy, action = request.path.split('/')

        # si eliminano tutte le chiavi della sessione corrente
        session.clear()
        # si restituisce il risultato
        return {"action": action, "état"400"réponse""session réinitialisée"}, status.HTTP_200_OK
  • riga 13: vengono eliminate tutte le chiavi della sessione. Ciò comporta l’eliminazione di:
    • [typeResponse]: il tipo delle risposte HTTP (json, xml, html);
    • [id_simulation]: il numero dell'ultima simulazione effettuata;
    • [simulations]: l'elenco delle simulazioni dell'utente;
    • [user]: indicatore che l'utente è stato autenticato;
  • viene restituita la risposta;

Ci si potrebbe chiedere come verrà restituita la risposta HTTP della riga 15, ora che il tipo di risposta non è più presente nella sessione. Per scoprirlo, occorre tornare alla funzione |front_controller| dello script principale [main] e modificarla come segue:


…        
         # on not# si rileva il tipo di risposta desiderato, se tale informazione è presente nella sessione
        type_response1 = session.get('typeResponse'None)
        # si inoltra la richiesta al controller principale
        main_controller = config['controllers']["main-controller"]
        résultat, status_code = main_controller.execute(request, session, config)
        # si registra il risultato inviato al cliente
        log = f"[front_controller] {résultat}\n"
        logger.write(log)
        # Si è verificato un errore irreversibile?
        if status_code == status.HTTP_500_INTERNAL_SERVER_ERROR:
            # si invia un'e-mail all'amministratore dell'applicazione
            send_adminmail(config, log)
        # si determina il tipo di risposta desiderato
        type_response2=session.get('typeResponse')
        if  type_response2 is None and type_response1 is None:
            # il tipo di sessione non è stato ancora stabilito - sarà jSON
            type_response = 'json'
        elif type_response2 is not None:
            # il tipo di risposta è noto e presente nella sessione
            type_response = type_response2
        else:
            type_response=type_response1
        # si sta costruendo la risposta da inviare
        response_builder = config["responses"][type_response]
        response, status_code = response_builder \
            .build_http_response(request, session, config, status_code, résultat)
        # si invia la risposta
        return response, status_code
  • riga 3: viene memorizzato il tipo di risposta attualmente presente nella sessione;
  • riga 6: viene eseguita l’azione. Se si tratta di:
    • [fin-session], la chiave [typeResponse] non è più presente nella sessione;
    • [init-session], la chiave [typeResponse] della sessione potrebbe aver cambiato valore;;
  • righe 14-20: occorre inviare la risposta HTTP. Dobbiamo sapere in quale forma:
    • righe 16-18: se il tipo di risposta non è definito né da [type_response1] della riga 3, né da [type_response2] della riga 15, allora il tipo di risposta non era definito né prima né dopo l’azione. Si utilizza quindi jSON (riga 18);
    • righe 19-21: se esiste [type_response2], ovvero il tipo nella sessione dopo l’azione, allora è questo il tipo da utilizzare;
    • righe 22-23: altrimenti si utilizza [type_response1], il tipo di risposta prima dell’azione (che è necessariamente [fin-session]);

30.15. L’azione [get-admindata]

Passiamo ora alle due URL riservate ai servizi jSON e XML:

Azione
Ruolo
Contesto di esecuzione
/get-admindata
Restituisce i dati fiscali necessari per il calcolo dell’imposta
Richiesta GET.
Viene utilizzata solo se il tipo di sessione è json o xml. L'utente deve essere autenticato
/calcolare-imposte
Esegue il calcolo dell’imposta su un elenco di contribuenti inviati tramite jSON
Richiesta GET.
Viene utilizzata solo se il tipo di sessione è json o xml. L'utente deve essere autenticato

La URL [/get-admindata] è definita nelle rotte dello script principale [main] nel modo seguente:


# get-admindata
@app.route('/get-admindata', methods=['GET'])
def get_admindata() -> tuple:
    # si esegue il controller associato all'azione
    return front_controller()

La route [/get-admindata] viene gestita dal seguente controller [GetAdminDataController]:


# importazione delle dipendenze

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):
        # si recuperano gli elementi del percorso
        dummy, action = request.path.split('/')
        # sono accettate solo le sessioni json e xml
        type_response = session.get('typeResponse')
        if type_response != 'json' and type_response != 'xml':
            # viene restituita una risposta di errore
            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:
            # viene restituita una risposta di successo
            return {"action": action, "état"1000"réponse": config["adminData"].asdict()}, status.HTTP_200_OK
  • righe 13-21: si verifica se ci si trova in una sessione json o xml;
  • riga 24: viene restituito il dizionario dei dati dell'amministrazione fiscale che, all'avvio del server, erano stati inseriti nella configurazione:

    # admindata sarà un dato a livello di applicazione in sola lettura
    config["admindata"] = config["layers"]["dao"].get_admindata()

Utilizziamo un client Postman e richiediamo URL [/get-admindata], dopo aver avviato una sessione jSON ed effettuato l’autenticazione:

Image

La risposta del server è la seguente:

Image

30.16. L'azione [calculer-impots]

L'azione [calculer-impots] calcola le imposte di un elenco di contribuenti presente nel corpo della richiesta sotto forma di stringa jSON. Conosciamo già questa azione: nella versione precedente si chiamava [calculate_tax_in_bulk_mode].

Il suo percorso è il seguente:


# calcolo dell'imposta in batch
@app.route('/calculer-impots', methods=['POST'])
def calculer_impots():
    # si esegue il controller associato all'azione
    return front_controller()

Questa azione viene gestita dal controller [CalculerImpotsController] seguente:


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):
        # si recuperano gli elementi del percorso
        dummy, action = request.path.split('/')

        # sono accettate solo le sessioni json e xml
        type_response = session.get('typeResponse')
        if type_response != 'json' and type_response != 'xml':
            # viene restituita una risposta di errore
            return {
                       "action": action,
                       "état"1501,
                       "réponse"["cette action n'est possible que pour les sessions json ou xml"]
                   }, status.HTTP_400_BAD_REQUEST

        # si recupera il corpo del POST - ci si aspetta un elenco di dizionari
        msg_erreur = None
        list_dict_taxpayers = None
        # il corpo jSON del POST
        request_text = request.data
        try:
            # che viene trasformato in un elenco di dizionari
            list_dict_taxpayers = json.loads(request_text)
        except BaseException as erreur:
            # si rileva l’errore
            msg_erreur = f"le corps du POST n'est pas une chaîne jSON valide : {erreur}"
        # si ha un elenco non vuoto?
        if not msg_erreur and (not isinstance(list_dict_taxpayers, list) or len(list_dict_taxpayers) == 0):
            # si rileva l'errore
            msg_erreur = "le corps du POST n'est pas une liste ou alors cette liste est vide"
        # si ha una lista di dizionari?
        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
            # errore?
            if erreur:
                msg_erreur = "le corps du POST doit être une liste de dictionnaires"
        # errore?
        if msg_erreur:
            # si invia una risposta di errore al cliente
            résultats = {"action": action, "état"1501"réponse"[msg_erreur]}
            return résultats, status.HTTP_400_BAD_REQUEST

        # si controllano i TaxPayers uno per uno
        # Inizialmente nessun errore
        list_erreurs = []
        for dict_taxpayer in list_dict_taxpayers:
            # si crea un TaxPayer a partire da dict_taxpayer
            msg_erreur = None
            try:
                # l'operazione successiva eliminerà i casi in cui i parametri non sono
                # delle proprietà della classe TaxPayer, nonché i casi in cui i loro valori
                # non sono corretti
                TaxPayer().fromdict(dict_taxpayer)
            except BaseException as erreur:
                msg_erreur = f"{erreur}"
            # alcune chiavi devono essere presenti nel dizionario
            if not msg_erreur:
                # le chiavi [marié, enfants, salaire] devono essere presenti nel dizionario
                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]"
            # Ci sono degli errori?
            if msg_erreur:
                # si rileva l'errore proprio nel TaxPayer
                dict_taxpayer['erreur'] = msg_erreur
                # si aggiunge il TaxPayer all'elenco degli errori
                list_erreurs.append(dict_taxpayer)

        # sono stati elaborati tutti i contribuenti - ci sono errori?
        if list_erreurs:
            # si invia una risposta di errore al cliente
            résultats = {"action": action, "état"1501"réponse": list_erreurs}
            return résultats, status.HTTP_400_BAD_REQUEST

        # nessun errore, si può procedere
        # recupero dei dati dall'amministrazione fiscale
        admindata = config["admindata"]
        métier = config["layers"]["métier"]
        try:
            # si elaborano i TaxPayer uno per uno
            list_taxpayers = []
            for dict_taxpayer in list_dict_taxpayers:
                # calcolo dell'imposta
                taxpayer = TaxPayer().fromdict(
                    {'marié': dict_taxpayer['marié']'enfants': dict_taxpayer['enfants'],
                     'stipendio': dict_taxpayer['salaire']})
                métier.calculate_tax(taxpayer, admindata)
                # si memorizza il risultato come dizionario
                list_taxpayers.append(taxpayer.asdict())
            # si aggiunge list_taxpayers alle simulazioni attuali assegnando a ciascuna simulazione un numero
            simulations = session.get("simulations"[])
            id_simulation = session.get("id_simulation"0)
            for simulation in list_taxpayers:
                # si assegna un numero a ciascuna simulazione
                id_simulation += 1
                simulation['id'] = id_simulation
                # si aggiunge alla lista attuale delle simulazioni
                simulations.append(simulation)
            # si avvia nuovamente la sessione
            session["simulations"] = simulations
            session["id_simulation"] = id_simulation
            # si invia la risposta al cliente
            return {"action": action, "état"1500"réponse": list_taxpayers}, status.HTTP_200_OK
        except ImpôtsError as erreur:
            # si invia una risposta di errore al cliente
            return {"action": action, "état"1501"réponse"[f"{erreur}"]}, status.HTTP_500_INTERNAL_SERVER_ERROR
  • righe 16-24: si verifica che si tratti effettivamente di una sessione json o xml
  • righe 26-120: questo codice ci è sostanzialmente noto. Si tratta di quello della funzione |index_controller| della versione 10 dell’applicazione, che è stato adattato per soddisfare le specifiche dell’interfaccia [InterfaceController] implementata;
  • righe 104-115: il codice aggiunto per tenere conto del nuovo ambiente di questo controller. Abbiamo appena effettuato i calcoli fiscali. Dobbiamo memorizzare i risultati nell’elenco delle simulazioni conservate nella sessione;
  • riga 105: si recupera l’elenco delle simulazioni in sessione;
  • riga 106: si recupera il numero dell’ultima simulazione effettuata;
  • righe 107-112: si scorre l'elenco dei dizionari dei risultati del calcolo dell'imposta; a ciascuno di essi viene assegnato un numero di simulazione [id] e ogni dizionario viene aggiunto all'elenco delle simulazioni;
  • righe 113-115: il nuovo elenco delle simulazioni e il numero dell’ultima simulazione effettuata vengono reinseriti nella sessione;

Eseguiamo il seguente test Postman, dopo aver inizializzato una sessione jSON ed effettuato l’autenticazione:

Image

Image

La risposta del server è la seguente:

Image

Se ora richiediamo l’elenco delle simulazioni:

Si noterà che nell’elenco dei risultati di [/calcul-impots] i contribuenti non presentano l’attributo [id], mentre nell’elenco delle simulazioni ogni simulazione è identificata da un numero.

Image