Django: la classe Form

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/o files); 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_data e non request.POST per 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_tag e associazione implicita for/id per ogni campo.
  • Fornisci aria-describedby quando 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.Form o usa ModelForm.
  • Convalida con clean_<campo> e clean().
  • Accedi ai valori tramite cleaned_data.
  • Gestisci file con request.FILES e enctype corretto.
  • Rendi il markup accessibile (label, errori, help text) e proteggi CSRF.
  • Valuta le generic views per ridurre boilerplate.
Torna su