In Django la classe Form è il cuore del sistema di gestione degli input: definisce i campi, gestisce la validazione, il rendering nei template e la serializzazione dei dati puliti. Se stai costruendo interfacce affidabili e accessibili, comprendere bene Form è fondamentale.
Quando usare Form (e quando ModelForm)
Form: per input scollegati da un modello o per flussi “wizard/step” personalizzati.ModelForm: quando i campi del form derivano 1:1 da un modello e vuoi semplificare creazione/aggiornamento record.
Anatomia di una Form
# forms.py
from django import forms
class ContactForm(forms.Form):
name = forms.CharField(
max_length=80,
help_text="Inserisci il tuo nome e cognome.",
)
email = forms.EmailField(
label="Email",
help_text="Usa un indirizzo valido.",
)
message = forms.CharField(
widget=forms.Textarea(attrs={"rows": 5, "placeholder": "Scrivi qui..."}),
label="Messaggio",
)
Stati del form
- Unbound: creato senza dati, usato per mostrare il form vuoto.
- Bound: creato con
data(e/ofiles); può essere valido o contenere errori.
# Vista di esempio (function-based view)
from django.shortcuts import render, redirect
from .forms import ContactForm
def contact(request):
if request.method == "POST":
form = ContactForm(request.POST)
if form.is_valid():
dati = form.cleaned_data # <-- valori già validati e normalizzati
# ... usa i dati (es. invio email, salvataggio, ecc.)
return redirect("thank-you")
else:
form = ContactForm(initial={"name": "Mario Rossi"})
return render(request, "contact.html", {"form": form})
Rendering nei template
Django fornisce shortcut come form.as_p, ma nelle applicazioni reali è comune renderizzare manualmente per avere pieno controllo su markup e accessibilità.
<!-- contact.html -->
<h1>Contattaci</h1>
<form method="post" novalidate>
{% csrf_token %}
<p>
{{ form.name.label_tag }} {{ form.name }}
{% if form.name.help_text %}<small>{{ form.name.help_text }}</small>{% endif %}
{% for err in form.name.errors %}<span role="alert">{{ err }}</span>{% endfor %}
</p>
<p>
{{ form.email.label_tag }} {{ form.email }}
{% for err in form.email.errors %}<span role="alert">{{ err }}</span>{% endfor %}
</p>
<p>
{{ form.message.label_tag }} {{ form.message }}
{% for err in form.message.errors %}<span role="alert">{{ err }}</span>{% endfor %}
</p>
<button type="submit">Invia</button>
</form>
Validazione: dove e come
La validazione avviene in tre livelli: a) built-in dei campi, b) clean_<campo>, c) clean() per la validazione incrociata. Gli errori devono alzare ValidationError.
from django import forms
from django.core.exceptions import ValidationError
class RegisterForm(forms.Form):
username = forms.CharField(min_length=3, max_length=30)
password = forms.CharField(widget=forms.PasswordInput)
confirm_password = forms.CharField(widget=forms.PasswordInput)
def clean_username(self):
username = self.cleaned_data["username"]
if " " in username:
raise ValidationError("Niente spazi nello username.")
return username
def clean(self):
data = super().clean()
pw1 = data.get("password")
pw2 = data.get("confirm_password")
if pw1 and pw2 and pw1 != pw2:
raise ValidationError("Le password non coincidono.")
return data
Widget e attributi
Ogni campo usa un widget per il rendering HTML. In Django 5.2 ci sono anche widget per input moderni come ColorInput, SearchInput e TelInput, utili per UX e accessibilità migliori.
class ProfileForm(forms.Form):
favorite_color = forms.CharField(
label="Colore",
widget=forms.ColorInput
)
query = forms.CharField(
label="Cerca",
widget=forms.SearchInput(attrs={"placeholder": "Cosa cerchi?"})
)
phone = forms.CharField(
label="Telefono",
widget=forms.TelInput(attrs={"autocomplete": "tel"})
)
Personalizzare HTML e ARIA
Puoi passare attrs al widget per aggiungere classi, aria-*, placeholder, ecc. Questo approccio è compatibile con i miglioramenti di accessibilità introdotti nelle versioni recenti.
name = forms.CharField(
widget=forms.TextInput(attrs={
"class": "form-control",
"aria-describedby": "name",
"autocomplete": "name",
})
)
Messaggi di errore e label
email = forms.EmailField(
label="Email di contatto",
error_messages={
"required": "L'email è obbligatoria.",
"invalid": "Formato email non valido.",
}
)
File upload
Per gestire file, usa un FileField/ImageField e ricordati enctype="multipart/form-data" nel form HTML; in vista, passa request.FILES.
# forms.py
class UploadForm(forms.Form):
document = forms.FileField()
# views.py
def upload(request):
if request.method == "POST":
form = UploadForm(request.POST, request.FILES)
if form.is_valid():
f = form.cleaned_data["document"]
# gestisci il file...
# ...
else:
form = UploadForm()
return render(request, "upload.html", {"form": form})
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Carica</button>
</form>
Form multipli nella stessa pagina
Usa prefix per disambiguare i nomi dei campi e gli errori.
def account_settings(request):
profile = ProfileForm(request.POST or None, prefix="profile")
password = PasswordForm(request.POST or None, prefix="password")
if request.method == "POST":
valid = profile.is_valid() and password.is_valid()
if valid:
#...
return redirect("ok")
return render(request, "settings.html", {"profile": profile, "password": password})
Uso con class-based views
Le generic editing views riducono boilerplate nei flussi tipici GET/POST (vuoto → errori → successo). Le più usate sono FormView, CreateView, UpdateView e DeleteView.
from django.views.generic import FormView
from .forms import ContactForm
class ContactView(FormView):
template_name = "contact.html"
form_class = ContactForm
success_url = "/thank-you/"
def form_valid(self, form):
data = form.cleaned_data
# ... processa
return super().form_valid(form)
CSRF e sicurezza
- Includi sempre
{% csrf_token %}nei form POST. - Usa
form.cleaned_datae nonrequest.POSTper dati affidabili. - Evita di fidarti di campi nascosti per valori sensibili.
Integrazione con ModelForm
ModelForm genera automaticamente i campi a partire dal modello, convalidando anche vincoli a livello model (unique, blank/null, ecc.). Questo accelera CRUD e pannelli amministrativi personalizzati.
from django.forms import ModelForm
from .models import Article
class ArticleForm(ModelForm):
class Meta:
model = Article
fields = ["title", "content", "published"]
widgets = {
"title": forms.TextInput(attrs={"autofocus": True}),
}
Pattern di test per i form
# tests/test_forms.py
from django.test import SimpleTestCase
from .forms import RegisterForm
class RegisterFormTests(SimpleTestCase):
def test_password_mismatch(self):
form = RegisterForm(data={
"username": "mario",
"password": "abc",
"conferma_password": "xyz",
})
assert not form.is_valid()
assert "Le password non coincidono." in form.errors["**all**"]
Accessibilità e UX
- Usa
label_tage associazione implicitafor/idper ogni campo. - Fornisci
aria-describedbyquando usi help text personalizzati. - Sfrutta i nuovi widget nativi (
ColorInput,SearchInput,TelInput) per tastiere mobili e suggerimenti del browser.
Strumenti utili dell’ecosistema
- django-crispy-forms: layout dichiarativi e rendering pulito dei form, senza abbandonare le API core.
Checklist rapida
- Definisci i campi in
forms.Formo usaModelForm. - Convalida con
clean_<campo>eclean(). - Accedi ai valori tramite
cleaned_data. - Gestisci file con
request.FILESeenctypecorretto. - Rendi il markup accessibile (label, errori, help text) e proteggi CSRF.
- Valuta le generic views per ridurre boilerplate.