📄 Modulo 3: Templates HTML e Jinja2
Separa la logica dal design: crea pagine HTML professionali
3 di 8
60 minuti
Intermedio
🤔 Il Problema: HTML nel Codice Python
Finora abbiamo restituito HTML come stringhe dentro le funzioni Python. Questo approccio ha grossi problemi:
@app.route('/')
def home():
return '''
<html>
<head>
<title>Il Mio Sito</title>
<style>
body { font-family: Arial; }
h1 { color: blue; }
</style>
</head>
<body>
<h1>Benvenuto</h1>
<p>Questo è molto scomodo!</p>
</body>
</html>
'''
HTML e Python mescolati
L'editor non colora l'HTML
Modifiche complicate
Codice duplicato ovunque
✅ La Soluzione: Templates
I templates sono file HTML separati che Flask carica e "riempie" con i dati. Così:
- Python gestisce la logica (route, database, calcoli)
- HTML gestisce la presentazione (layout, stile)
- Jinja2 fa da ponte, inserendo i dati Python nell'HTML
📁 Struttura delle Cartelle
Flask cerca i templates in una cartella specifica chiamata templates. Questa cartella DEVE essere allo stesso livello di app.py.
flask-corso/ │ ├── app.py ← File Python principale │ ├── templates/ ← Cartella per i file HTML │ ├── index.html │ ├── about.html │ └── contatti.html │ └── venv/ ← Ambiente virtuale (non toccare)
In PyCharm:
- Clic destro sulla cartella del progetto (quella con app.py)
- New → Directory
- Digita:
templates - Premi Invio
⚠️ IMPORTANTE: Nome Obbligatorio
La cartella DEVE chiamarsi esattamente templates (minuscolo, plurale). Flask la cerca automaticamente con questo nome. Se la chiami diversamente, non funzionerà!
🎨 Il Tuo Primo Template
Creiamo il nostro primo template HTML separato.
Nella cartella templates:
- Clic destro su
templates - New → File
- Digita:
index.html
Nel file templates/index.html, digita:
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Homepage</title>
</head>
<body>
<h1>Benvenuto nel mio sito!</h1>
<p>Questo è il mio primo template Flask.</p>
</body>
</html>
Nel file app.py, cambia il codice in questo modo:
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
return render_template('index.html')
if __name__ == '__main__':
app.run(debug=True)
Avvia il server e vai su http://localhost:5000
Vedrai la pagina HTML caricata dal template! 🎉
🔍 Cosa fa render_template()?
render_template('index.html') dice a Flask:
- Vai nella cartella
templates/ - Trova il file
index.html - Leggilo e restituiscilo al browser
🔧 Jinja2: Il Motore di Template
Jinja2 è il linguaggio che Flask usa nei templates. Ti permette di inserire variabili Python, creare cicli, condizioni e molto altro direttamente nell'HTML.
| Sintassi | Scopo | Esempio |
|---|---|---|
{{ variabile }} |
Stampa una variabile | {{ nome }} |
{% istruzione %} |
Esegue logica (if, for, ecc.) | {% if utente %} |
{# commento #} |
Commento (non visibile) | {# Questo è un commento #} |
💬 Passare Variabili ai Templates
Ora vediamo come passare dati da Python ai templates HTML.
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
nome = "Mario"
eta = 25
citta = "Roma"
return render_template('index.html',
nome=nome,
eta=eta,
citta=citta)
if __name__ == '__main__':
app.run(debug=True)
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>Homepage</title>
</head>
<body>
<h1>Benvenuto {{ nome }}!</h1>
<p>Hai {{ eta }} anni e vivi a {{ citta }}.</p>
</body>
</html>
Risultato nel browser: "Benvenuto Mario! Hai 25 anni e vivi a Roma."
💡 Sintassi Alternativa (più pulita)
Puoi passare variabili in modo più compatto:
return render_template('index.html',
nome="Mario",
eta=25,
citta="Roma")
📋 Passare Liste e Dizionari
Puoi passare anche strutture dati complesse come liste e dizionari.
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
utente = {
'nome': 'Mario',
'cognome': 'Rossi',
'email': 'mario@email.com'
}
hobby = ['Calcio', 'Cinema', 'Lettura']
return render_template('index.html',
utente=utente,
hobby=hobby)
if __name__ == '__main__':
app.run(debug=True)
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>Profilo</title>
</head>
<body>
<h1>Profilo di {{ utente.nome }} {{ utente.cognome }}</h1>
<p>Email: {{ utente.email }}</p>
<h2>Hobby:</h2>
<p>Primo hobby: {{ hobby[0] }}</p>
</body>
</html>
🔍 Accesso ai Dati
- Dizionari:
{{ utente.nome }}o{{ utente['nome'] }} - Liste:
{{ hobby[0] }}(primo elemento)
🔄 Cicli For nei Templates
Jinja2 permette di creare cicli per iterare su liste e dizionari.
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
prodotti = [
{'nome': 'Laptop', 'prezzo': 899},
{'nome': 'Mouse', 'prezzo': 25},
{'nome': 'Tastiera', 'prezzo': 45},
{'nome': 'Monitor', 'prezzo': 199}
]
return render_template('index.html', prodotti=prodotti)
if __name__ == '__main__':
app.run(debug=True)
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>Prodotti</title>
</head>
<body>
<h1>I Nostri Prodotti</h1>
<ul>
{% for prodotto in prodotti %}
<li>
{{ prodotto.nome }} - €{{ prodotto.prezzo }}
</li>
{% endfor %}
</ul>
</body>
</html>
| Variabile | Descrizione |
|---|---|
loop.index |
Indice corrente (parte da 1) |
loop.index0 |
Indice corrente (parte da 0) |
loop.first |
True se è il primo elemento |
loop.last |
True se è l'ultimo elemento |
loop.length |
Numero totale di elementi |
<ul>
{% for prodotto in prodotti %}
<li>
#{{ loop.index }} - {{ prodotto.nome }}
{% if loop.first %}⭐ PIÙ VENDUTO{% endif %}
</li>
{% endfor %}
</ul>
❓ Condizioni If/Else
Puoi usare condizioni per mostrare o nascondere parti del template.
from flask import Flask, render_template
app = Flask(__name__)
@app.route('/')
def home():
utente_loggato = True
nome_utente = "Mario"
eta = 17
return render_template('index.html',
loggato=utente_loggato,
nome=nome_utente,
eta=eta)
if __name__ == '__main__':
app.run(debug=True)
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>Homepage</title>
</head>
<body>
{% if loggato %}
<h1>Benvenuto {{ nome }}!</h1>
<p>Sei loggato nel sistema.</p>
{% else %}
<h1>Benvenuto Visitatore</h1>
<p>Effettua il login per accedere.</p>
{% endif %}
<hr>
{% if eta >= 18 %}
<p>✅ Sei maggiorenne</p>
{% else %}
<p>❌ Sei minorenne</p>
{% endif %}
</body>
</html>
Operatori di Confronto
== |
Uguale | {% if eta == 18 %} |
!= |
Diverso | {% if nome != "Admin" %} |
> < |
Maggiore/Minore | {% if eta > 18 %} |
>= <= |
Maggiore o uguale / Minore o uguale | {% if prezzo >= 100 %} |
and or |
Operatori logici | {% if eta > 18 and loggato %} |
not |
Negazione | {% if not loggato %} |
in |
Contenuto in | {% if "admin" in ruoli %} |
🎨 Filtri Jinja2
I filtri modificano le variabili prima di visualizzarle. Si applicano con il simbolo | (pipe).
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>Filtri</title>
</head>
<body>
<h1>{{ nome|upper }}</h1> <!-- MARIO -->
<p>{{ nome|lower }}</p> <!-- mario -->
<p>{{ nome|capitalize }}</p> <!-- Mario -->
<p>{{ nome|title }}</p> <!-- Mario Rossi -->
<p>{{ descrizione|truncate(50) }}</p> <!-- Tronca a 50 caratteri -->
<p>{{ lista|length }}</p> <!-- Numero di elementi -->
<p>{{ prezzo|round(2) }}</p> <!-- Arrotonda a 2 decimali -->
<p>{{ testo|safe }}</p> <!-- Non escapa HTML -->
</body>
</html>
| Filtro | Scopo | Esempio |
|---|---|---|
|upper |
MAIUSCOLO | {{ nome|upper }} |
|lower |
minuscolo | {{ nome|lower }} |
|capitalize |
Prima lettera maiuscola | {{ nome|capitalize }} |
|title |
Ogni Parola Maiuscola | {{ testo|title }} |
|length |
Lunghezza lista/stringa | {{ lista|length }} |
|default('val') |
Valore di default se vuoto | {{ nome|default('Ospite') }} |
|join(',') |
Unisce lista con separatore | {{ tags|join(', ') }} |
|safe |
Non escapa HTML | {{ contenuto_html|safe }} |
🏗️ Template Inheritance (Ereditarietà)
L'ereditarietà dei template ti permette di creare un layout base (header, footer, menu) e riutilizzarlo in tutte le pagine. Questo evita duplicazione di codice!
🎯 Il Concetto
Immagina un palazzo:
- base.html = struttura del palazzo (muri, finestre, porte)
- home.html, about.html = appartamenti interni (arredi diversi, ma struttura uguale)
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Sito Flask{% endblock %}</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contatti">Contatti</a>
</nav>
</header>
<main>
{% block content %}
<!-- Il contenuto delle pagine figlie va qui -->
{% endblock %}
</main>
<footer>
<p>© 2024 Il Mio Sito Flask</p>
</footer>
</body>
</html>
🔑 Note importanti:
{% block title %}{% endblock %}→ Area per il titolo della pagina{% block content %}{% endblock %}→ Area per il contenuto principale- Header e Footer sono FISSI in tutte le pagine
{% extends "base.html" %}
{% block title %}Homepage - Il Mio Sito{% endblock %}
{% block content %}
<h1>Benvenuto nella Homepage!</h1>
<p>Questo è il contenuto specifico della homepage.</p>
<p>Data attuale: {{ data_attuale }}</p>
<div class="features">
<h2>Le Nostre Caratteristiche:</h2>
<ul>
{% for feature in features %}
<li>{{ feature }}</li>
{% endfor %}
</ul>
</div>
{% endblock %}
🔑 Cosa succede:
{% extends "base.html" %}→ Dice a Jinja2 di usare base.html come template{% block title %}→ Sostituisce il titolo nel base.html{% block content %}→ Inserisce questo contenuto nell'area "content"
from flask import Flask, render_template
from datetime import datetime
app = Flask(__name__)
@app.route('/')
def home():
# Dati da passare al template
features = [
"Templates con Jinja2",
"Design responsive",
"Database integrato",
"API REST"
]
return render_template('home.html',
data_attuale=datetime.now().strftime("%d/%m/%Y"),
features=features)
if __name__ == '__main__':
app.run(debug=True)
{% extends "base.html" %}
{% block title %}Chi Siamo - Il Mio Sito{% endblock %}
{% block content %}
<h1>Chi Siamo</h1>
<p>Siamo un'azienda specializzata in sviluppo web con Flask.</p>
<h2>Il Nostro Team:</h2>
<div class="team-grid">
{% for membro in team %}
<div class="team-member">
<h3>{{ membro.nome }}</h3>
<p>{{ membro.ruolo }}</p>
<p>{{ membro.descrizione }}</p>
</div>
{% endfor %}
</div>
{% endblock %}
🎯 Vantaggi dell'Ereditarietà
Non ripetere header/footer in ogni file
Cambi il menu in UN SOLO file (base.html)
Tutte le pagine hanno lo stesso layout
Nuove pagine in pochi minuti
| Comando | Scopo | Esempio |
|---|---|---|
{% include "header.html" %} |
Includi un template dentro un altro | Per riutilizzare componenti specifici |
{{ super() }} |
Mantieni il contenuto del blocco padre | {% block title %}{{ super() }} - Pagina{% endblock %} |
{% block nome %}{% endblock %} |
Definisci un blocco personalizzato | Per script specifici o metatag |
💪 Esercizi Pratici
Facile
📝 Template per una Lista della Spesa
Crea una pagina che mostra una lista della spesa con:
- Un titolo "La Mia Lista della Spesa"
- Una lista non ordinata con 5 prodotti
- Accanto a ogni prodotto, mostra la quantità
- Usa un ciclo for e passalo da Python
lista_spesa = [
{"prodotto": "Pane", "quantita": 2},
{"prodotto": "Latte", "quantita": 1},
# ... aggiungi altri 3 prodotti
]
Medio
📊 Tabella Studenti con Condizioni
Crea una pagina che mostra una tabella di studenti con:
- Nome, Cognome, Voto, Stato (Promosso/Bocciato)
- Se voto ≥ 18 → Stato = "✅ Promosso" (verde)
- Se voto < 18 → Stato = "❌ Bocciato" (rosso)
- Calcola la media dei voti alla fine
<tr class="{% if studente.voto >= 18 %}promosso{% else %}bocciato{% endif %}">
Difficile
🏗️ Sito Completo con Template Base
Crea un piccolo sito con 3 pagine:
- Home - Presentazione del sito
- Prodotti - Tabella di prodotti (nome, prezzo, categoria)
- Contatti - Form fittizio di contatto
Requisiti:
- Usa un template base con header e footer
- Il menu deve mostrare la pagina corrente in grassetto
- Ogni pagina deve avere un titolo diverso nel browser tab
- Usa almeno un filtro Jinja2 in ogni pagina
🔐 Validazione Dati nei Form
Quando gli utenti compilano form, è fondamentale validare i dati prima di usarli. Ecco come fare con Flask:
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<h1>🔐 Accedi al Sito</h1>
<!-- Mostra messaggi di errore -->
{% if errore %}
<div style="background: #fee2e2; color: #991b1b; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
❌ {{ errore }}
</div>
{% endif %}
<!-- Form di login -->
<form method="POST" action="/login">
<div style="margin-bottom: 15px;">
<label for="username">Username:</label><br>
<input type="text" id="username" name="username" style="width: 300px; padding: 8px;" required>
</div>
<div style="margin-bottom: 15px;">
<label for="password">Password:</label><br>
<input type="password" id="password" name="password" style="width: 300px; padding: 8px;" required>
</div>
<button type="submit" style="background: #4f46e5; color: white; padding: 10px 20px; border: none; border-radius: 6px;">
Accedi
</button>
</form>
<p style="margin-top: 20px;">
Non hai un account? <a href="/registrazione">Registrati qui</a>
</p>
</body>
</html>
from flask import Flask, render_template, request, session, redirect, url_for
app = Flask(__name__)
app.secret_key = 'tua_chiave_segreta' # ⚠️ IMPORTANTE per le sessioni!
# Utenti di esempio (in un'app reale sarebbe un database)
utenti = {
'admin': 'password123',
'mario': 'mario2024',
'lucia': 'luciaSecure'
}
@app.route('/')
def home():
# Controlla se l'utente è loggato
if 'username' in session:
return f'''
<h1>Benvenuto {session['username']}!</h1>
<p>Sei correttamente loggato nel sistema.</p>
<a href="/dashboard">Vai alla Dashboard</a> |
<a href="/logout">Logout</a>
'''
return '''
<h1>Benvenuto Ospite</h1>
<p>Per accedere alle funzionalità del sito:</p>
<a href="/login">Login</a> |
<a href="/registrazione">Registrati</a>
'''
@app.route('/login', methods=['GET', 'POST'])
def login():
# Se l'utente è già loggato, reindirizza alla home
if 'username' in session:
return redirect('/')
if request.method == 'POST':
# 1. Prendi i dati dal form
username = request.form.get('username', '').strip()
password = request.form.get('password', '').strip()
# 2. VALIDAZIONE BASE
errori = []
# Controllo campi vuoti
if not username:
errori.append("Inserisci l'username")
if not password:
errori.append("Inserisci la password")
# Controllo credenziali (se i campi non sono vuoti)
if username and password:
# Verifica se l'utente esiste
if username not in utenti:
errori.append("Username non trovato")
# Verifica se la password è corretta
elif utenti[username] != password:
errori.append("Password errata")
# 3. Se ci sono errori, mostra il form con errori
if errori:
return render_template('login.html', errore=" | ".join(errori))
# 4. Se tutto OK, salva in sessione e reindirizza
session['username'] = username
session['logged_in'] = True
return redirect('/dashboard')
# Se è una GET, mostra il form vuoto
return render_template('login.html')
@app.route('/dashboard')
def dashboard():
# 🔒 CONTROLLO ACCESSO: solo utenti loggati
if 'username' not in session:
return redirect('/login')
username = session['username']
return f'''
<!DOCTYPE html>
<html lang="it">
<head>
<meta charset="UTF-8">
<title>Dashboard</title>
</head>
<body>
<h1>👋 Benvenuto nella Dashboard, {username}!</h1>
<h3>🔐 Area Riservata</h3>
<p>Questa pagina è accessibile solo agli utenti autenticati.</p>
<div style="background: #f0f9ff; padding: 20px; border-radius: 8px; margin: 20px 0;">
<h4>💡 I tuoi dati di sessione:</h4>
<ul>
<li>Username: <strong>{username}</strong></li>
<li>Stato: ✅ Loggato</li>
<li>Session ID: {session.get('_id', 'N/A')}</li>
</ul>
</div>
<a href="/">🏠 Home</a> |
<a href="/logout">🚪 Logout</a>
</body>
</html>
'''
@app.route('/logout')
def logout():
# Cancella la sessione
session.clear()
return '''
<h1>Logout effettuato</h1>
<p>Sei stato disconnesso correttamente.</p>
<a href="/">Torna alla Home</a>
'''
if __name__ == '__main__':
app.run(debug=True)
💡 Come Funziona il Sistema di Validazione
request.form.get('campo') prende i dati inviati dal form
.strip() rimuove spazi all'inizio e alla fine
Controlla se i campi sono vuoti o non validi
Mostra errori specifici all'utente se ci sono problemi
Per proteggere le pagine riservate (come /dashboard), usa questo pattern:
@app.route('/pagina-riservata')
def pagina_riservata():
# Controlla se l'utente è loggato
if 'username' not in session:
# Se non è loggato, reindirizza al login
return redirect('/login')
# Se è loggato, mostra la pagina
return render_template('pagina_riservata.html')
⚠️ IMPORTANTE: Senza app.secret_key le sessioni non funzionano! La secret key cripta i dati della sessione.
🎯 Esempio Pratico di Validazione Avanzata
@app.route('/registrazione', methods=['GET', 'POST'])
def registrazione():
if request.method == 'POST':
username = request.form.get('username', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '').strip()
conferma = request.form.get('conferma_password', '').strip()
errori = []
# Validazione Username
if len(username) < 3:
errori.append("L'username deve essere di almeno 3 caratteri")
elif not username.isalnum():
errori.append("L'username può contenere solo lettere e numeri")
# Validazione Email
if '@' not in email or '.' not in email:
errori.append("Email non valida")
# Validazione Password
if len(password) < 6:
errori.append("La password deve essere di almeno 6 caratteri")
elif password != conferma:
errori.append("Le password non coincidono")
if errori:
return render_template('registrazione.html', errori=errori)
# Se tutto OK, registra l'utente
# (qui salveresti nel database)
session['username'] = username
return redirect('/dashboard')
return render_template('registrazione.html')
📝 Template per Mostrare Errori di Validazione
<!-- Se ci sono errori, mostrali -->
{% if errori %}
<div style="background: #fee2e2; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<h4 style="color: #991b1b; margin-top: 0;">❌ Si sono verificati errori:</h4>
<ul style="margin-bottom: 0;">
{% for errore in errori %}
<li>{{ errore }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
⚠️ Errori Comuni e Soluzioni
Errore: jinja2.exceptions.TemplateNotFound: index.html
Causa: Flask non trova il template nella cartella templates/
Soluzione:
- Verifica che la cartella si chiami ESATTAMENTE
templates - Deve essere allo stesso livello di
app.py - Controlla i permessi di lettura della cartella
Errore: jinja2.exceptions.UndefinedError: 'variabile' is undefined
Causa: Stai usando una variabile nel template che non hai passato da Python
Soluzione:
- Controlla l'ortografia della variabile
- Verifica di averla passata in
render_template() - Usa
{{ variabile|default('Valore di default') }}
Errore: jinja2.exceptions.TemplateSyntaxError: expected token 'end of statement block'
Causa: Dimenticato {% endblock %} o {% endfor %}
Soluzione:
- Assicurati che ogni
{% block %}abbia il suo{% endblock %} - Ogni
{% for %}deve avere{% endfor %} - Ogni
{% if %}deve avere{% endif %}
📋 Riepilogo Concetti Chiave
Struttura
- Cartella
templates/obbligatoria - File
.htmldentro templates/ render_template('file.html')
Jinja2 Sintassi
{{ variabile }}→ Stampa{% for %}→ Ciclo{% if %}→ Condizione|filtro→ Modifica variabile
Template Inheritance
{% extends "base.html" %}{% block nome %}{% endblock %}- DRY: Don't Repeat Yourself
- Layout consistente
Best Practices
- Separa logica da presentazione
- Usa sempre template base
- Nomi file chiari e descrittivi
- Organizza templates in sottocartelle
✅ Modulo 3 Completato!
Ora sai creare pagine HTML professionali con Flask e Jinja2. Pronto per il prossimo passo?
🎯 Cosa Aspettarti nel Prossimo Modulo
CSS e Stili
Rendi i tuoi templates bellissimi
Immagini e Media
Aggiungi foto e icone al tuo sito
Cartella Static
Organizza file CSS, JS, immagini