In questa lezione impari a
- passare dati da Python a un template con render_template
- usare variabili, condizioni, cicli e filtri di Jinja2
- costruire tutte le pagine da una base comune con extends e block
- riusare un pezzo di pagina con include
- creare un filtro e variabili disponibili in tutti i template
Un template Γ¨ un file HTML con dei "buchi" che Flask riempie con i dati prima di mandarlo al browser. Il motore che fa questo lavoro si chiama Jinja2 ed Γ¨ incluso in Flask.
Il kit della lezione
Rispetto al kit 1 trovate: le etichette colorate, le date scritte in italiano ("5 novembre 2026"), i filtri per categoria in cima all'elenco (/categoria/evento), il testo degli annunci accorciato nelle schede, l'anno nel piè di pagina.
Dal Python al browser
app.py templates/annuncio.html browser
ββββββ βββββββββββββββββββββββ βββββββ
render_template( <h1>{{ annuncio.titolo }}</h1> βββΊ <h1>Uscita didattica</h1>
"annuncio.html", <p>{{ annuncio.testo }}</p> <p>Visita al Museo...</p>
annuncio=a)
return render_template("annuncio.html", annuncio=a)
- il primo argomento Γ¨ il file dentro
templates/; - gli altri sono le variabili che il template potrΓ usare. Il nome a sinistra dell'uguale Γ¨ il nome dentro il template; quello a destra Γ¨ il valore in Python.
Il browser riceve solo l'HTML finito: non vede mai le parentesi graffe nΓ© il codice Python. Provate: tasto destro β Visualizza sorgente pagina.
Le tre sintassi di Jinja2
| Si scrive | Serve a | Esempio |
|---|---|---|
{{ ... }} |
stampare un valore | {{ annuncio.titolo }} |
{% ... %} |
comandi: condizioni, cicli, blocchi | {% for a in annunci %} |
{# ... #} |
commenti, non arrivano al browser | {# da sistemare #} |
Per leggere i campi di un dizionario si puΓ² scrivere annuncio["titolo"] oppure, piΓΉ comodo, annuncio.titolo.
Le condizioni
{% if not annunci %}
<p class="vuoto">Nessun annuncio.</p>
{% else %}
<p class="meta">{{ annunci | length }} annunci</p>
{% endif %}
Si possono usare elif, gli operatori ==, !=, <, >=, e and, or, not. A differenza di Python, il blocco si chiude sempre con {% endif %}: le rientranze non contano.
I cicli
<div class="griglia">
{% for a in annunci %}
{% include "_scheda.html" %}
{% endfor %}
</div>
Dentro un ciclo esiste la variabile speciale loop:
| Variabile | Valore |
|---|---|
loop.index |
1, 2, 3β¦ (numero del giro) |
loop.index0 |
0, 1, 2β¦ |
loop.first / loop.last |
True al primo / all'ultimo giro |
loop.length |
quanti giri in tutto |
Per ciclare su un dizionario si usa .items(), come in Python. Nel kit è così che si disegnano i filtri per categoria:
{% for nome, colore in categorie.items() %}
<a class="etichetta {{ colore }}" href="{{ url_for('categoria', nome=nome) }}">{{ nome }}</a>
{% endfor %}
I filtri
Un filtro trasforma un valore; si scrive dopo una barra verticale | e si possono mettere in fila.
| Filtro | Esempio | Risultato |
|---|---|---|
length |
{{ annunci | length }} |
3 |
upper, lower |
{{ "ciao" | upper }} |
CIAO |
capitalize |
{{ "evento" | capitalize }} |
Evento |
truncate(n) |
{{ a.testo | truncate(60) }} |
il testo tagliato a 60 caratteri, con ... |
default(x) |
{{ a.autore | default("anonimo") }} |
anonimo se manca |
round(n) |
{{ 7.456 | round(1) }} |
7.5 |
Un filtro fatto da noi
Le date nel kit sono scritte come "2026-11-05" (il formato che useremo anche nel database), ma sulla pagina compaiono come "5 novembre 2026". Il merito Γ¨ di un filtro personalizzato, definito in app.py:
MESI = ["gennaio", "febbraio", "marzo", ...]
@app.template_filter("data_it")
def data_it(testo):
d = date.fromisoformat(testo) # "2026-11-05" β oggetto data
return f"{d.day} {MESI[d.month - 1]} {d.year}"
E nel template: {{ a.data | data_it }}. Il decoratore @app.template_filter("data_it") registra la funzione come filtro con quel nome.
Variabili per tutti i template
Le categorie servono in tante pagine (elenco, scheda, menuβ¦). Invece di passarle ogni volta a render_template, le diamo a tutti i template una volta sola:
CATEGORIE = {"avviso": "gialla", "compito": "rossa", "evento": "verde"}
@app.context_processor
def variabili_comuni():
return {"categorie": CATEGORIE, "anno": date.today().year}
Il dizionario restituito viene "aggiunto" a ogni template. Ecco perchΓ© in base.html potete scrivere {{ anno }} senza averlo mai passato.
CATEGORIEcollega ogni categoria al nome di una classe CSS (gialla,rossa,verde) già definita instyle.css. Così il colore dell'etichetta si decide in un posto solo:class="etichetta {{ categorie[a.categoria] }}".
L'ereditarietΓ dei template
Tutte le pagine hanno la stessa testata, lo stesso menu e lo stesso piè di pagina. Li scriviamo una volta in base.html e lasciamo dei blocchi da riempire:
<!-- base.html -->
<title>{% block titolo %}Home{% endblock %} Β· Bacheca della classe</title>
...
<main>
{% block contenuto %}{% endblock %}
</main>
<footer class="piede">Bacheca della classe Β· TPSIT 5Βͺ Β· {{ anno }}</footer>
Ogni pagina "figlia" dichiara da chi eredita e riempie i blocchi:
<!-- annuncio.html -->
{% extends "base.html" %}
{% block titolo %}{{ annuncio.titolo }}{% endblock %}
{% block contenuto %}
<article class="scheda">
<h1>{{ annuncio.titolo }}</h1>
<p class="meta">{{ annuncio.data | data_it }}</p>
<p>{{ annuncio.testo }}</p>
</article>
{% endblock %}
base.html annuncio.html pagina finale
ββββββββββββββββββββββββ βββββββββββββββββββββββ ββββββββββββββββββββββββ
β testata e menu β β extends "base.html" β β testata e menu β
β ββββββββββββββββββββ β + β block contenuto: β = β <h1>Uscitaβ¦</h1> β
β β block contenuto β β β <h1>β¦</h1> β β <p>Visita alβ¦</p> β
β ββββββββββββββββββββ β β <p>β¦</p> β β piΓ¨ di pagina β
β piΓ¨ di pagina β βββββββββββββββββββββββ ββββββββββββββββββββββββ
ββββββββββββββββββββββββ
Le regole da ricordare:
{% extends %}deve essere la prima riga del file figlio;- nel figlio, tutto quello che sta fuori dai blocchi viene ignorato;
- un blocco non riempito tiene il contenuto che ha in
base.html(il titolo "Home", per esempio).
Pezzi riutilizzabili: include
La scheda di un annuncio serve nell'elenco generale e nell'elenco per categoria. La scriviamo una volta in _scheda.html:
<div class="scheda">
<a class="etichetta {{ categorie[a.categoria] }}" href="{{ url_for('categoria', nome=a.categoria) }}">{{ a.categoria }}</a>
<h3><a href="{{ url_for('annuncio', id=a.id) }}">{{ a.titolo }}</a></h3>
<p class="meta">{{ a.data | data_it }}</p>
<p>{{ a.testo | truncate(60) }}</p>
</div>
e la inseriamo con {% include "_scheda.html" %}. Il pezzo incluso vede le variabili della pagina che lo include: per questo dentro il ciclo funziona a. Il trattino basso iniziale Γ¨ solo una convenzione: ricorda che non Γ¨ una pagina intera.
extends |
include |
|
|---|---|---|
| Idea | "questa pagina Γ¨ fatta come base.html" | "metti qui questo pezzo" |
| Quante volte | una sola, in cima | quante ne volete |
Un template, due pagine
Guardate la route per categoria:
@app.route("/categoria/<nome>")
def categoria(nome):
if nome not in CATEGORIE:
abort(404)
scelti = [a for a in annunci if a["categoria"] == nome]
return render_template("index.html", annunci=scelti, categoria_scelta=nome)
Usa lo stesso index.html della home, passando una lista filtrata e una variabile in piΓΉ, categoria_scelta. Nel template un {% if categoria_scelta %} cambia il titolo. Una pagina in piΓΉ, zero HTML in piΓΉ.
I file statici
CSS, immagini e JavaScript stanno in static/ e si collegano con url_for:
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<img src="{{ url_for('static', filename='img/logo.png') }}" alt="Logo">
Sicurezza gratis: l'escape automatico
Se un utente scrivesse come titolo <script>alert("hackerato")</script>, Jinja2 lo mostrerebbe come testo, non lo eseguirebbe: trasforma automaticamente < in < e > in >. Γ la prima difesa contro l'attacco XSS (Cross-Site Scripting), che vedremo nel laboratorio di sicurezza. Non disattivatela mai (il filtro | safe la spegne: si usa solo per HTML scritto da voi).
Gli errori piΓΉ comuni
| Errore | Causa |
|---|---|
TemplateNotFound |
nome del file sbagliato, o cartella templates rinominata |
UndefinedError: 'annuncio' is undefined |
variabile non passata con render_template (o nome diverso) |
TemplateSyntaxError: β¦ endfor |
manca un {% endfor %} o un {% endif %} |
| La pagina figlia Γ¨ vuota | il contenuto Γ¨ fuori da {% block contenuto %} |
BuildError |
in url_for c'Γ¨ un nome di funzione che non esiste |
Missioni
β Missione 1 β Il colore della bacheca
Aprite static/css/style.css e cambiate il valore di --accento (e --accento-chiaro) in cima al file. Ricaricate con Ctrl + F5: cambia il colore di tutto il sito. Poi aggiungete una quarta categoria, gita, con colore "" (nessuna classe): compare da sola tra i filtri.
β Missione 2 β Il menu
In base.html aggiungete al menu un link per ogni categoria, usando un ciclo su categorie. Compariranno in tutte le pagine.
ββ Missione 3 β Nuovo!
Mostrate un'etichetta "NUOVO" sulle schede degli annunci con data uguale o successiva a oggi.
Aiutino
Aggiungete "oggi": date.today().isoformat() nel context_processor. Le date nel formato "AAAA-MM-GG" si possono confrontare come stringhe: {% if a.data >= oggi %}.
ββ Missione 4 β Le categorie con i numeri
Create la pagina /categorie con una tabella: ogni categoria, quanti annunci contiene e il link alla sua pagina.
Aiutino
In Python costruite un dizionario conteggi = {nome: 0 for nome in CATEGORIE} e poi un ciclo sugli annunci che fa conteggi[a["categoria"]] += 1.
βββ Missione 5 β Un filtro tutto vostro
Scrivete il filtro giorni_mancanti: {{ a.data | giorni_mancanti }} restituisce "oggi", "domani", "tra 5 giorni" oppure "passato". Usatelo nella pagina dell'annuncio.
Aiutino
(date.fromisoformat(testo) - date.today()).days Γ¨ il numero di giorni di differenza (negativo se la data Γ¨ passata).