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.
- Scaricate il kit ed estraetelo (non lavorate dentro lo zip!).
- Avviatelo e guardate cosa fa nel browser.
- Leggete il codice insieme alla spiegazione di questa pagina: ogni riga importante Γ¨ commentata.
- 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
pipnon viene trovato, provate conpy -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 convertitoreintche 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 ilbind("0.0.0.0", β¦)dei socket). Un compagno puΓ² aprirehttp://IL-VOSTRO-IP:5000.port=5000: la porta, come la5050dei 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
templatesestaticvanno scritti esattamente così: Flask li cerca con questi nomi. Se rinominate la cartella, compare l'erroreTemplateNotFound.
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.titoloinvece dig["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 inrequest.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:
methods=["GET", "POST"]: senza, la route accetta solo GET e premendo "Salva" otterreste l'errore 405 Method Not Allowed.- 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). - 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.pytiene 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 confetche 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/utentiche 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
/pulisciche 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