ModelForm in Django

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 Form che genera automaticamente i campi a partire dal modello indicato in Meta.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: widgets mappa un campo del form a un widget; labels, help_texts, error_messages personalizzano 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, chiama save().
  • Aggiornamento: passa instance=oggetto e chiama save() 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.FILES ed enctype nel form HTML.
  • M2M: se usi commit=False, chiama sempre save_m2m() dopo save().
  • Sicurezza: limita i campi esposti (fields esplicito) per evitare mass assignment accidentali.
  • Override: dichiara i campi che vuoi personalizzare prima della classe Meta nel form.
  • Accessibilità: usa label_tag, help_text e 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.

Torna su