Vai al contenuto

I template con Jinja2

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.

CATEGORIE collega ogni categoria al nome di una classe CSS (gialla, rossa, verde) già definita in style.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 &lt; e > in &gt;. È 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).