Vai al contenuto

Periodo 3 Β· Introduzione a Flask

In questa lezione impari a

  • capire che cosa fa Flask al posto dei socket
  • creare route e pagine dinamiche con i template Jinja2
  • ricevere i dati di un form e controllarli
  • restituire dati in JSON e usarli da un altro programma

Avete già costruito un server con i socket: ora vediamo come Flask fa (dietro le quinte) le stesse cose, ma con il protocollo HTTP già pronto all'uso. È il primo passo verso quello che approfondirete in quinta.

marzo – maggio Β· ~8 settimane Β· 24 ore Β· 4 moduli + progetto

Come lavoriamo in questo periodo

Ogni modulo ha un kit da scaricare: un progetto giΓ  funzionante, con la grafica pronta. Non si parte mai dal foglio bianco.

  1. Scaricate il kit ed estraetelo (non lavorate dentro lo zip!).
  2. Avviatelo e guardate cosa fa nel browser.
  3. Leggete il codice insieme alla spiegazione di questa pagina: ogni riga importante Γ¨ commentata.
  4. Fate le missioni: piccole modifiche, dalla più facile (⭐) alla più impegnativa (⭐⭐⭐). Le ⭐ le fanno tutti, le ⭐⭐⭐ sono per chi vuole spingersi oltre.

In ogni zip trovate anche COME-AVVIARE.txt (i comandi e gli errori piΓΉ comuni) e MISSIONI.txt.

Prima di tutto: installare Flask

Serve una volta sola, nel terminale di Visual Studio Code:

pip install flask

Se il comando pip non viene trovato, provate con py -m pip install flask. Per controllare che sia installato: python -c "import flask" non deve dare errori.

3.1 Da socket a framework web

Durata indicativa: 2 settimane

Nel Periodo 2 avete scritto un server con socket: apre una porta, aspetta un client, riceve e manda byte. Flask fa esattamente questo sotto il cofano, ma vi risparmia tutta la parte "artigianale".

Socket "a mano" Flask
Aprire la porta socket.bind(), listen() app.run()
Leggere la richiesta byte grezzi da interpretare a mano giΓ  divisa in metodo, indirizzo, dati
Rispondere sendall() con byte return di una stringa
PiΓΉ client insieme bisogna gestirlo voi (thread) gestito automaticamente

Che cosa succede quando aprite una pagina

  BROWSER                                   SERVER FLASK (app.py)
  ───────                                   ─────────────────────
  scrivo 127.0.0.1:5000/saluto/Mario
        β”‚  richiesta HTTP:  GET /saluto/Mario
        └──────────────────────────────────►  cerca la route che corrisponde:
                                              @app.route("/saluto/<nome>")
                                              chiama  saluto(nome="Mario")
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€   return "<h1>Ciao Mario!</h1>"
        β”‚  risposta HTTP:  200 OK + l'HTML
  mostra la pagina "Ciao Mario!"

Una route Γ¨ un collegamento tra un indirizzo e una funzione Python. Quando il browser chiede quell'indirizzo, Flask esegue la funzione e manda indietro quello che la funzione restituisce con return.

Il codice del kit, pezzo per pezzo

Aprite app.py. Le prime righe creano l'applicazione:

from flask import Flask
app = Flask(__name__)

Poi c'Γ¨ la prima route, quella della pagina principale /:

@app.route("/")
def home():
    return "<h1>Ciao! Questo Γ¨ il mio primo server Flask</h1>"

La riga che comincia con @ si chiama decoratore: "attacca" l'indirizzo / alla funzione home. Quello che restituite puΓ² contenere HTML: il browser lo interpreta.

Le route con parametri

Una parte dell'indirizzo puΓ² essere variabile: si scrive tra < > e Flask la passa alla funzione con lo stesso nome.

@app.route("/saluto/<nome>")
def saluto(nome):
    return f"<h1>Ciao {nome}!</h1>"

/saluto/Mario β†’ nome vale "Mario"; /saluto/Giulia β†’ nome vale "Giulia".

Se vi servono numeri, aggiungete il tipo: <int:a> accetta solo interi e li converte giΓ  in int.

@app.route("/somma/<int:a>/<int:b>")
def somma(a, b):
    return f"{a} + {b} = <b>{a + b}</b>"

Provate /somma/3/ciao: Flask risponde 404 (pagina non trovata) senza nemmeno chiamare la funzione. È il convertitore int che controlla per voi.

L'avvio del server

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000, debug=True)
  • host="0.0.0.0": il server accetta collegamenti anche dagli altri PC del laboratorio (come il bind("0.0.0.0", …) dei socket). Un compagno puΓ² aprire http://IL-VOSTRO-IP:5000.
  • port=5000: la porta, come la 5050 dei socket.
  • debug=True: quando salvate il file il server si riavvia da solo, e gli errori compaiono nel browser con la riga sbagliata. Comodissimo in laboratorio, ma mai su un server pubblico.

Missioni

⭐ Missione 1 – Chi sono

Aggiungete la route /chi-sono che mostra in HTML il vostro nome, la classe e un vostro hobby (usate <h1>, <p> e <ul>).

⭐ Missione 2 – Il quadrato

Aggiungete /quadrato/<int:n>: per /quadrato/7 deve rispondere Il quadrato di 7 Γ¨ 49.

⭐⭐ Missione 3 – Pari o dispari

Aggiungete /pari/<int:n> che risponde "8 Γ¨ pari" oppure "7 Γ¨ dispari".

Aiutino

Dentro la funzione usate un normale if n % 2 == 0: con due return diversi.

⭐⭐ Missione 4 – La tabellina

Aggiungete /tabellina/<int:n> che mostra la tabellina di n da 1 a 10, una riga sotto l'altra.

Aiutino

Costruite una stringa con un ciclo for i in range(1, 11): aggiungendo ogni volta f"{n} x {i} = {n*i}<br>", poi restituitela con return.

⭐⭐⭐ Missione 5 – Tanti dadi

Aggiungete /dadi/<int:quanti> che lancia quanti dadi, mostra i numeri usciti e la loro somma. Se quanti Γ¨ piΓΉ di 10, rispondete "Troppi dadi!" con il codice di stato 400.

Aiutino

Per mandare un codice di stato diverso da 200 si restituiscono due valori: return "Troppi dadi!", 400. Controllate il codice nella scheda Rete (F12) del browser.

Domanda di confronto (orale): quante righe vi sono servite per un server che risponde a 5 indirizzi diversi? Quante ne servirebbero con i socket del Periodo 2?

3.2 Pagine vere con i template

Durata indicativa: 2 settimane

Scrivere l'HTML dentro le stringhe Python va bene per due righe, ma per una pagina vera diventa un disastro. La soluzione sono i template: file HTML normali, con dei "buchi" che Flask riempie con i dati. Il motore che lo fa si chiama Jinja2 ed Γ¨ giΓ  incluso in Flask.

Da qui in avanti lavoriamo su GameDB, il catalogo dei videogiochi della classe.

Le cartelle del progetto

flask4-2-template/
β”œβ”€β”€ app.py              ← il server: route e dati
β”œβ”€β”€ templates/          ← le pagine HTML (Flask le cerca QUI)
β”‚   β”œβ”€β”€ base.html       ← la struttura comune: testata, menu, piΓ¨ di pagina
β”‚   β”œβ”€β”€ index.html      ← l'elenco dei giochi
β”‚   β”œβ”€β”€ gioco.html      ← la scheda di un gioco
β”‚   └── 404.html        ← pagina "non trovato"
└── static/             ← file che non cambiano (Flask li cerca QUI)
    └── css/style.css   ← la grafica (giΓ  pronta)

I nomi templates e static vanno scritti esattamente così: Flask li cerca con questi nomi. Se rinominate la cartella, compare l'errore TemplateNotFound.

Dal Python al template: render_template

In app.py i giochi sono una lista di dizionari:

giochi = [
    {"id": 1, "titolo": "Minecraft", "anno": 2011, "genere": "sandbox", "voto": 9},
    {"id": 2, "titolo": "Rocket League", "anno": 2015, "genere": "sport", "voto": 8},
    # ...
]

@app.route("/")
def home():
    return render_template("index.html", giochi=giochi)

render_template("index.html", giochi=giochi) vuol dire: prendi il file templates/index.html, dagli la variabile giochi e restituisci l'HTML finito. Il nome a sinistra dell'uguale Γ¨ il nome che la variabile avrΓ  nel template.

Le tre "parole" di Jinja2

Si scrive Serve a Esempio
{{ ... }} stampare un valore {{ g.titolo }}
{% ... %} comandi: cicli, condizioni, blocchi {% for g in giochi %}
{# ... #} commenti (non arrivano al browser) {# da sistemare #}

Ecco il cuore di index.html, con un ciclo e una condizione:

{% for g in giochi %}
  <div class="scheda">
    <h3>{{ g.titolo }}</h3>
    <p class="meta">{{ g.anno }} Β· {{ g.genere }}</p>

    {% if g.voto >= 9 %}
      <span class="etichetta verde">Capolavoro Β· {{ g.voto }}</span>
    {% elif g.voto >= 7 %}
      <span class="etichetta">Bello Β· {{ g.voto }}</span>
    {% else %}
      <span class="etichetta rossa">Così così · {{ g.voto }}</span>
    {% endif %}
  </div>
{% endfor %}

È Python "travestito" da HTML: il for ripete il blocco per ogni gioco, l'if sceglie quale etichetta mostrare. Attenzione: a differenza di Python, qui i blocchi si chiudono con {% endfor %} e {% endif %}.

Nel template si scrive g.titolo invece di g["titolo"]: Jinja2 accetta entrambe le forme, ma il punto Γ¨ piΓΉ leggibile.

I filtri

Un filtro trasforma un valore e si scrive con la barra verticale |:

Filtro Risultato
{{ giochi | length }} quanti elementi ha la lista
{{ g.genere | capitalize }} prima lettera maiuscola
{{ g.titolo | upper }} tutto maiuscolo

L'ereditarietΓ : base.html

Tutte le pagine hanno la stessa testata e lo stesso piè di pagina. Invece di copiarli in ogni file, li scriviamo una volta in base.html e lasciamo dei blocchi da riempire:

<!-- base.html -->
<title>{% block titolo %}GameDB{% endblock %} Β· GameDB</title>
...
<main>
  {% block contenuto %}{% endblock %}
</main>

Ogni pagina "figlia" dice da chi eredita e riempie i blocchi:

<!-- gioco.html -->
{% extends "base.html" %}

{% block titolo %}{{ gioco.titolo }}{% endblock %}

{% block contenuto %}
  <h1>{{ gioco.titolo }}</h1>
  <p>Voto della classe: {{ gioco.voto }}/10</p>
{% endblock %}

Volete aggiungere una voce al menu? La scrivete in base.html e compare in tutte le pagine.

I link: url_for

Nei template non scriviamo gli indirizzi a mano, ma chiediamo a Flask di costruirli a partire dal nome della funzione:

<a href="{{ url_for('home') }}">Tutti i giochi</a>                        β†’ /
<a href="{{ url_for('scheda', id_gioco=g.id) }}">{{ g.titolo }}</a>      β†’ /gioco/3
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">

Se un giorno cambiate l'indirizzo nella @app.route, tutti i link si aggiornano da soli.

La pagina 404

@app.route("/gioco/<int:id_gioco>")
def scheda(id_gioco):
    gioco = cerca_gioco(id_gioco)
    if gioco is None:
        abort(404)
    return render_template("gioco.html", gioco=gioco)

@app.errorhandler(404)
def non_trovata(errore):
    return render_template("404.html"), 404

abort(404) interrompe la funzione; @app.errorhandler(404) decide cosa mostrare al posto della pagina grezza di Flask. Provate /gioco/99.

Missioni

⭐ Missione 1 – PiΓΉ giochi

Aggiungete alla lista due vostri giochi preferiti (con un id nuovo). Ricaricate: compaiono da soli, senza toccare l'HTML. È questa la forza dei template.

⭐ Missione 2 – Quanti anni ha?

Nella scheda di un gioco aggiungete la riga "Uscito 15 anni fa", calcolata dall'anno di uscita.

Aiutino

Nei {{ }} si possono fare i calcoli: {{ 2026 - gioco.anno }}. PiΓΉ elegante: in app.py importate datetime e passate al template l'anno corrente.

⭐⭐ Missione 3 – Filtrare per genere

Aggiungete la route /genere/<nome> che mostra solo i giochi di quel genere, riusando index.html. Poi nelle schede rendete cliccabile il genere.

Aiutino

Nella funzione costruite una nuova lista: scelti = [g for g in giochi if g["genere"] == nome] e passatela al template con lo stesso nome giochi=scelti. Il link: url_for('per_genere', nome=g.genere).

⭐⭐ Missione 4 – Le piattaforme

Aggiungete a ogni gioco la chiave "piattaforme" con una lista, per esempio ["PC", "Switch"], e mostratele nella scheda come etichette, una per piattaforma.

Aiutino

Un {% for p in gioco.piattaforme %}<span class="etichetta">{{ p }}</span>{% endfor %}.

⭐⭐⭐ Missione 5 – La classifica

Create la pagina /classifica (con il suo template e la voce nel menu) che mostra i giochi in una tabella, ordinati dal voto piΓΉ alto, con la posizione (1Β°, 2Β°, 3°…).

Aiutino

In Python: ordinati = sorted(giochi, key=lambda g: g["voto"], reverse=True). Nel template, dentro un for, la variabile loop.index vale 1, 2, 3… a ogni giro.

3.3 I form: l'utente manda i dati

Durata indicativa: 1-2 settimane

Finora il server manda dati al browser. Con un form succede il contrario: Γ¨ l'utente che manda dati al server. Ci sono due modi, e nel kit li trovate entrambi.

GET POST
Dove viaggiano i dati nell'indirizzo: /?q=mario nel corpo della richiesta, non si vedono
Si usa per cercare, filtrare (non cambia niente) salvare, modificare, cancellare
In Flask si leggono con request.args request.form

La ricerca: un form GET

<form method="get" action="{{ url_for('home') }}">
  <input name="q" value="{{ q }}" placeholder="Cerca per titolo...">
  <button>Cerca</button>
</form>

Cercando "mario", il browser apre /?q=mario. In Python:

@app.route("/")
def home():
    q = request.args.get("q", "")       # "" se nell'indirizzo non c'Γ¨ ?q=
    if q:
        risultati = [g for g in giochi if q.lower() in g["titolo"].lower()]
    else:
        risultati = giochi
    return render_template("index.html", giochi=risultati, q=q)

L'attributo name="q" del campo HTML e la parola tra virgolette in request.args.get("q") devono essere uguali. È l'errore più frequente: se non coincidono, il server non riceve niente.

Aggiungere un gioco: un form POST

La stessa route fa due cose diverse in base al metodo della richiesta:

  GET  /nuovo   β†’ l'utente apre la pagina    β†’ mostro il form vuoto
  POST /nuovo   β†’ l'utente preme "Salva"     β†’ controllo i dati
                                                 β”œβ”€ ci sono errori β†’ rimostro il form con gli errori
                                                 └─ tutto ok       β†’ salvo e vado alla scheda del gioco
@app.route("/nuovo", methods=["GET", "POST"])
def nuovo():
    errori = []
    if request.method == "POST":
        titolo = request.form.get("titolo", "").strip()
        voto = request.form.get("voto", "")
        # ... anno e genere

        if titolo == "":
            errori.append("Scrivi il titolo del gioco")
        if not voto.isdigit() or not (1 <= int(voto) <= 10):
            errori.append("Il voto deve essere un numero da 1 a 10")

        if not errori:
            # ... aggiungo il gioco alla lista
            return redirect(url_for("scheda", id_gioco=nuovo_id))

    return render_template("nuovo.html", errori=errori, generi=GENERI)

Tre cose da capire bene:

  1. methods=["GET", "POST"]: senza, la route accetta solo GET e premendo "Salva" otterreste l'errore 405 Method Not Allowed.
  2. I controlli si fanno nel server, sempre. Gli attributi HTML come min="1" aiutano l'utente, ma chiunque puΓ² mandare una richiesta POST senza passare dal form (lo farete con Thunder Client nel prossimo modulo).
  3. Dopo aver salvato si fa redirect, non si restituisce una pagina. Così, se l'utente preme F5, il browser non rimanda il form e il gioco non viene salvato due volte.

Missioni

⭐ Missione 1 – La piattaforma

Aggiungete al form un menu a tendina "Piattaforma" (PC, PlayStation, Xbox, Switch, Mobile), salvatela nel gioco e mostratela nella scheda.

⭐⭐ Missione 2 – Un anno credibile

Rifiutate gli anni prima del 1970 o successivi all'anno attuale, con un messaggio d'errore chiaro.

⭐⭐ Missione 3 – Niente doppioni

Se esiste giΓ  un gioco con lo stesso titolo (anche scritto con maiuscole diverse), mostrate l'errore "Questo gioco Γ¨ giΓ  nel catalogo".

Aiutino

any(g["titolo"].lower() == titolo.lower() for g in giochi) vale True se almeno un gioco ha quel titolo.

⭐⭐⭐ Missione 4 – Eliminare un gioco

Nella scheda aggiungete un pulsante "Elimina" che cancella il gioco e torna all'elenco. Usate un form POST verso /gioco/<id>/elimina (mai un semplice link: i link fanno GET, e GET non deve modificare i dati).

Aiutino

Nel template: <form method="post" action="{{ url_for('elimina', id_gioco=gioco.id) }}"><button class="pericolo">Elimina</button></form>. In Python: giochi.remove(gioco) e poi redirect(url_for("home")).

⭐⭐⭐ Missione 5 – Cerca anche per genere

Aggiungete al form di ricerca un menu a tendina con i generi (piΓΉ la voce "Tutti"): la ricerca deve funzionare per titolo, per genere o per entrambi.

3.4 Le API: dati per altri programmi

Durata indicativa: 1-2 settimane

Le pagine HTML sono fatte per le persone. Un'API invece restituisce dati "grezzi", in formato JSON, pensati per un altro programma: un'app per il telefono, un altro server, uno script Python. È la stessa cosa che avete fatto nel Periodo 1 con requests, ma stavolta l'API la scrivete voi.

  pagina HTML  (per una persona)          API JSON  (per un programma)
  GET /gioco/1                             GET /api/giochi/1
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                 {
  β”‚  Minecraft           β”‚                   "id": 1,
  β”‚  Anno: 2011          β”‚                   "titolo": "Minecraft",
  β”‚  Voto: 9/10          β”‚                   "anno": 2011,
  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                   "voto": 9
                                           }

Leggere: GET

@app.route("/api/giochi")
def api_elenco():
    return jsonify(giochi)

@app.route("/api/giochi/<int:id_gioco>")
def api_gioco(id_gioco):
    gioco = cerca_gioco(id_gioco)
    if gioco is None:
        return jsonify({"errore": "gioco non trovato"}), 404
    return jsonify(gioco)

jsonify() trasforma liste e dizionari Python in JSON e aggiunge l'intestazione Content-Type: application/json, così chi riceve sa come leggere la risposta. Notate che anche l'errore è in JSON, con il codice di stato giusto (404): un programma non legge le frasi, legge i codici.

Scrivere: POST

@app.route("/api/giochi", methods=["POST"])
def api_aggiungi():
    dati = request.get_json(silent=True)    # il JSON mandato dal client, o None
    if not dati or not dati.get("titolo"):
        return jsonify({"errore": "serve almeno il titolo"}), 400
    # ... creo il gioco e lo aggiungo alla lista
    return jsonify(gioco), 201              # 201 = Created
Codice Significato Quando lo usiamo
200 OK lettura riuscita
201 Created un elemento nuovo Γ¨ stato creato
400 Bad Request il client ha mandato dati sbagliati
404 Not Found l'elemento richiesto non esiste

Provare l'API

Il browser sa fare solo GET: per le altre richieste serve uno strumento come Thunder Client (estensione di VS Code) o Postman. Per una POST: metodo POST, indirizzo http://127.0.0.1:5000/api/giochi, scheda Body β†’ JSON:

{ "titolo": "Tetris", "anno": 1984, "genere": "puzzle", "voto": 10 }

Nel kit c'Γ¨ anche client.py: un programma Python che usa la vostra API con requests. Avviate il server in un terminale e client.py in un secondo terminale (nel terminale di VS Code c'Γ¨ il pulsante +).

risposta = requests.get("http://127.0.0.1:5000/api/giochi", timeout=5)
giochi = risposta.json()            # il JSON diventa una lista Python

Avete appena fatto parlare due programmi attraverso la rete: il client e il server. È lo stesso schema client-server dei socket, ma con HTTP e JSON al posto dei byte.

Missioni

⭐ Missione 1 – Provate tutto

Con Thunder Client provate: l'elenco, un gioco esistente, un gioco inesistente, una POST corretta e una POST senza titolo. Annotate per ognuna il codice di stato. Poi eseguite client.py.

⭐⭐ Missione 2 – Filtrare nell'API

Fate in modo che /api/giochi?genere=sport restituisca solo i giochi di quel genere (senza parametro, tutti come prima).

Aiutino

È lo stesso request.args.get(...) della ricerca del modulo 3.3.

⭐⭐ Missione 3 – Le statistiche

Create /api/statistiche che restituisce, per esempio, {"totale": 5, "media_voti": 7.8, "migliore": "Minecraft"}.

Aiutino

sum(g["voto"] for g in giochi) / len(giochi) per la media; max(giochi, key=lambda g: g["voto"]) per il migliore. Arrotondate con round(media, 1).

⭐⭐⭐ Missione 4 – Cancellare: DELETE

Aggiungete DELETE /api/giochi/<id>: se il gioco esiste lo elimina e risponde {"eliminato": id} con 200, altrimenti 404. Provatelo con Thunder Client.

⭐⭐⭐ Missione 5 – Un client interattivo

Modificate client.py: chiede all'utente titolo, anno e voto con input(), li manda all'API con una POST, e poi stampa la classifica dei giochi ordinata per voto. Fatelo girare contro il server di un compagno (cambiando l'indirizzo con il suo IP).

3.5 Progetto del periodo: la chat diventa web

Durata indicativa: 2 settimane

Progetto.

Obiettivo

Riprendere la chat del Periodo 2 (fatta con i socket) e trasformarla in un'applicazione web che si usa dal browser, anche dai PC dei compagni.

Come funziona il kit

  browser di Giulia                 server Flask                 browser di Marco
  ─────────────────                 ────────────                 ────────────────
  ogni 2 s: GET /api/messaggi ───►  restituisce la lista   ◄───  ogni 2 s: GET /api/messaggi
  "Invia":  POST /api/messaggi ──►  aggiunge alla lista
  • app.py tiene i messaggi in una lista e offre due API: leggere (GET) e scrivere (POST).
  • templates/chat.html Γ¨ la pagina.
  • static/js/chat.js Γ¨ il client: chiede i messaggi ogni 2 secondi con fetch e manda i nuovi con una POST. È commentato riga per riga.

Avviatelo, trovate il vostro IP con ipconfig e fatelo aprire ai compagni su http://VOSTRO-IP:5000.

Cosa dovete aggiungere (requisiti minimi)

  • Un limite di lunghezza dei messaggi (es. 200 caratteri), controllato nel server con risposta 400
  • Una route /api/utenti che restituisce l'elenco dei nomi che hanno scritto almeno un messaggio, senza ripetizioni
  • Una pagina HTML /statistiche (con un template che eredita lo stile) che mostra quanti messaggi ha scritto ognuno
  • Codice commentato nei punti chiave

Idee per chi vuole fare di piΓΉ

  • Un pulsante "πŸ˜€" che aggiunge emoji al messaggio
  • Il comando /pulisci che svuota la chat (solo se lo scrive un nome "admin")
  • Salvare i messaggi in un file JSON, cosΓ¬ non si perdono riavviando il server

Discussione finale

Siate pronti a rispondere: cosa cambia tra la versione con i socket e questa? Chi Γ¨ il client e chi il server? PerchΓ© qui il browser deve chiedere i messaggi ogni 2 secondi, mentre con i socket il server poteva mandarli lui?

Cosa consegnare: la cartella del progetto (senza __pycache__) + demo live in laboratorio + risposte alle domande.

academy-pc.it Β· materiale didattico β€” Modulo Introduzione a Flask, TPSIT quarta