I ModelForm sono la scorciatoia “batterie incluse” di Django per creare form HTML direttamente dai tuoi modelli: generano campi, gestiscono validazione e salvataggio, riducendo il boilerplate.
Cos’è un ModelForm e perché usarlo
- È una sottoclasse di
Formche genera automaticamente i campi a partire dal modello indicato inMeta.model. - Rispetta le regole dei campi del modello (tipi,
blank,choices,unique, ecc.) e aggiunge validazione lato form. - Offre il metodo
save()per creare/aggiornare l’istanza del modello in modo sicuro e conciso.
In breve
1) Modello
# models.py
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=150)
author = models.CharField(max_length=120)
published = models.DateField(null=True, blank=True)
cover = models.ImageField(upload_to="covers/", blank=True)
in_print = models.BooleanField(default=True)
def __str__(self):
return f"{self.title} — {self.author}"
2) ModelForm
# forms.py
from django import forms
from .models import Book
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ["title", "author", "published", "cover", "in_print"]
labels = {
"in_print": "Disponibile a catalogo",
}
help_texts = {
"published": "Formato: AAAA-MM-GG",
}
widgets = {
"published": forms.DateInput(attrs={"type": "date"}),
}
error_messages = {
"title": {"max_length": "Titolo troppo lungo." },
}
3) View
# views.py
from django.shortcuts import render, redirect, get_object_or_404
from .forms import BookForm
from .models import Book
def create_book(request):
if request.method == "POST":
form = BookForm(request.POST, request.FILES) # attenzione ai file!
if form.is_valid():
form.save() # crea Book
return redirect("books:list")
else:
form = BookForm()
return render(request, "books/book_form.html", {"form": form})
def update_book(request, pk):
book = get_object_or_404(Book, pk=pk)
if request.method == "POST":
form = BookForm(request.POST, request.FILES, instance=book)
if form.is_valid():
form.save() # aggiorna Book
return redirect("books:detail", pk=book.pk)
else:
form = BookForm(instance=book)
return render(request, "books/book_form.html", {"form": form, "book": book})
4) Template
<!-- templates/books/book_form.html -->
<h1>{{ book|default:"Nuovo libro" }}</h1>
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.non_field_errors }}
<p>{{ form.title.label_tag }} {{ form.title }} {{ form.title.errors }}</p>
<p>{{ form.author.label_tag }} {{ form.author }} {{ form.author.errors }}</p>
<p>{{ form.published.label_tag }} {{ form.published }} {{ form.published.errors }}</p>
<p>{{ form.cover.label_tag }} {{ form.cover }} {{ form.cover.errors }}</p>
<p>{{ form.in_print.label_tag }} {{ form.in_print }} {{ form.in_print.errors }}</p>
<button type="submit">Salva</button>
</form>
Nota: per i file usa sempre enctype="multipart/form-data" e passa request.FILES alla costruzione del form.
Come ModelForm genera i campi
- Precedenza: se dichiari un campo nel form, quello override il campo generato automaticamente.
- Selezione campi:
fields = "__all__"include tutti i campi editabili.fields = ["..."]include solo i campi elencati.exclude = ["..."]include tutto tranne i campi esclusi.
- Rappresentazioni:
widgetsmappa un campo del form a un widget;labels,help_texts,error_messagespersonalizzano UI e messaggi.
Override di un campo
class BookForm(forms.ModelForm):
title = forms.CharField(min_length=3) # override del CharField generato
class Meta:
model = Book
fields = "__all__"
Creazione vs aggiornamento e il ruolo di instance
- Creazione: costruisci il form senza
instance, chiamasave(). - Aggiornamento: passa
instance=oggettoe chiamasave()per aggiornare solo i campi cambiati.
Salvataggio differito e relazione ManyToMany
form = BookForm(request.POST, request.FILES)
if form.is_valid():
book = form.save(commit=False) # istanza non ancora in DB
book.author = book.author.strip() # modifica di servizio
book.save()
form.save_m2m() # necessario se il form ha campi M2M
Validazione
clean()per validazioni che coinvolgono più campi.clean_nomecampo()per la logica di un singolo campo.- Gli errori globali vanno in
non_field_errors().
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = "__all__"
def clean(self):
data = super().clean()
title = data.get("title")
author = data.get("author")
if title and author and title.lower() == author.lower():
# errore "non di campo"
self.add_error(None, "Titolo e autore non possono coincidere.")
return data
def clean_title(self):
title = self.cleaned_data["title"].strip()
if title.endswith("!"):
raise forms.ValidationError("Evita i punti esclamativi nel titolo.")
return title
Rendering del form
Puoi renderizzare campo per campo (massimo controllo) oppure affidarti a scorciatoie come {{ form.as_p }}. Con Django 5.x è consigliato usare template personalizzati o librerie di componenti per un markup accessibile e coerente.
Personalizzazione dei widget
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ["title", "published", "in_print"]
widgets = {
"title": forms.TextInput(attrs={"placeholder": "Il titolo..."}),
"published": forms.DateInput(attrs={"type": "date"}),
"in_print": forms.CheckboxInput(attrs={"aria-label": "Disponibile"}),
}
Limitare le scelte dinamicamente
Quando il form viene istanziato puoi alterare queryset o gli attrs a runtime (es. filtrare su utente loggato).
class BookForm(forms.ModelForm):
class Meta:
model = Book
fields = ["title", "author"]
def __init__(self, *args, user=None, **kwargs):
super().__init__(*args, **kwargs)
if user and not user.is_superuser:
# esempio: campo fittizio ModelChoiceField "author" se fosse FK
# self.fields["author"].queryset = Author.objects.filter(active=True)
pass
ModelFormSet e InlineFormSet
Per editare più istanze contemporaneamente usa modelformset_factory; per modificare oggetti “figli” legati a un “genitore” tramite FK usa inlineformset_factory.
ModelFormSet
from django.forms import modelformset_factory
from .models import Book
BookFormSet = modelformset_factory(
Book,
fields=["title", "author", "in_print"],
extra=1,
can_delete=True
)
def edit_books(request):
qs = Book.objects.order_by("title")
if request.method == "POST":
formset = BookFormSet(request.POST, queryset=qs)
if formset.is_valid():
formset.save()
return redirect("books:list")
else:
formset = BookFormSet(queryset=qs)
return render(request, "books/books_formset.html", {"formset": formset})
InlineFormSet
from django.forms import inlineformset_factory
from .models import Author, Book
BookInlineFormSet = inlineformset_factory(
parent_model=Author,
model=Book,
fields=["title", "published", "in_print"],
extra=1,
can_delete=True
)
def edit_author_books(request, author_id):
author = Author.objects.get(pk=author_id)
if request.method == "POST":
formset = BookInlineFormSet(request.POST, request.FILES, instance=author)
if formset.is_valid():
formset.save()
return redirect("authors:detail", pk=author.pk)
else:
formset = BookInlineFormSet(instance=author)
return render(request, "authors/author_books.html", {"author": author, "formset": formset})
Gestione delle performance
- Usa
only()/defer()sui queryset pre-caricati in form/formset quando servono pochi campi. - Evita validazioni che colpiscono il DB per ogni campo; spostale su
clean()o in task asincroni quando possibile.
Test dei ModelForm
# tests/test_forms.py
import pytest
from books.forms import BookForm
@pytest.mark.django_db
def test_book_form_valid():
form = BookForm(data={"title": "Refactoring", "author": "M. Fowler", "in_print": True})
assert form.is_valid()
obj = form.save()
assert obj.pk is not None
def test_book_form_errors():
form = BookForm(data={"title": "A!", "author": "A!"})
assert not form.is_valid()
assert "non_field_errors" in form.errors
Best practice e insidie comuni
- File: ricorda
request.FILESedenctypenel form HTML. - M2M: se usi
commit=False, chiama sempresave_m2m()doposave(). - Sicurezza: limita i campi esposti (
fieldsesplicito) per evitare mass assignment accidentali. - Override: dichiara i campi che vuoi personalizzare prima della classe
Metanel form. - Accessibilità: usa
label_tag,help_texte messaggi d’errore chiari; aggiungi attributi ARIA ai widget quando utile.
Conclusione
Con i ModelForm ottieni un flusso end-to-end per la creazione e l’aggiornamento di dati persistenti con pochissimo codice e convalidhe aderenti al tuo modello. Parti dalle basi (Meta.model, fields), personalizza widgets e validazione, quindi scala verso formset e inline formset per casi multi-oggetto. Se stai aggiornando o iniziando oggi, Django 5.2 LTS è una base stabile e supportata a lungo termine.