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:

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:

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:

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

- 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:

- 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:

- 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:

- 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

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

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]

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(0, 1)
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]:

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:

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:

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(0, 1)
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:

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:

- [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:

- [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:

- [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:

- in [4], un codice di errore [x01];
- in [5], la spiegazione dell’errore;
Ora inizializziamo una sessione jSON:

La risposta è la seguente:

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:

Il risultato del server è quindi il seguente:

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

- in [3-5], il POST non ha corpo;
Il risultato della richiesta è il seguente:

- 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

- in [6], le credenziali sono errate;
Il server invia la seguente risposta:

- in [2], la risposta HTTP 401 UNAUTHORIZED;
- in [5], la risposta di errore;
Caso 2: POST con credenziali corrette

- in [6], le credenziali sono corrette;
La risposta del server è la seguente:
- in [2], una risposta HTTP 200 OK;
- 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

La risposta è la seguente:

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:

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:

- in [5] manca il parametro [marié];
La risposta è la seguente:
Caso 4: eseguire un calcolo delle imposte con parametri errati


La risposta del server è la seguente:

Caso 4: effettuare un calcolo delle imposte con parametri corretti

La risposta del server è la seguente:

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;
La risposta del server è la seguente:

- 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:

- le simulazioni qui hanno i numeri 2 e 3;
Si richiede di eliminare la simulazione con il n. 3.

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


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:

La risposta del server è la seguente:

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:


La risposta del server è la seguente:

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.




