ModelForms
Picture writing a plain forms.Form for creating a TaskFlow task. You'd need a
CharField for title, a CharField for
description, a DateField for due_date, a
ChoiceField for priority — every one of those already exists, spelled
out, on the Task model itself. Retyping them in a second class is pure duplication,
and the two definitions can quietly drift out of sync as the model changes. A
ModelForm solves this directly: it's a form class generated automatically from a
model's fields, so you describe the model once and let Django build the form from it.
A ModelForm, Built From Task
A ModelForm looks like an ordinary form class, except it has no field definitions of
its own — instead, an inner Meta class points at a model and lists which of that
model's fields to expose:
# taskflow/forms.py
from django import forms
from .models import Task
class TaskForm(forms.ModelForm):
class Meta:
model = Task
fields = ['title', 'description', 'due_date', 'priority']
From those four lines, Django inspects the Task model and builds a
CharField for title, a Textarea-backed field for
description, a date-picker-friendly field for due_date, and a
ChoiceField for priority — matching each model field's type and
constraints automatically. Notice project, tags, and
completed aren't listed here: they're deliberately left off this particular form,
since a brand-new task doesn't need to set its completion status or tags at creation time.
fields vs exclude
Meta.fields, as used above, explicitly lists which model fields should appear on the
form — everything not listed is simply left out. The alternative, Meta.exclude, does
the opposite: it lists which fields to leave off, and everything else on the
model is included automatically.
# Equivalent in effect to the fields=[...] version above,
# but only as long as Task's fields don't change
class TaskForm(forms.ModelForm):
class Meta:
model = Task
exclude = ['project', 'tags', 'completed']
exclude looks like less typing today, but it has a real security cost: if someone
later adds a new field to Task — say, an internal_notes field meant to
stay admin-only — exclude exposes it on this form automatically, with no code change
required to do so. fields is the safer default, since a new model field simply
doesn't appear on the form until you deliberately add it to that list.
Saving a ModelForm: Creating a New Task
A ModelForm's biggest convenience is its .save() method. Where a plain
form only gives you cleaned_data and leaves object creation to you,
form.save() builds the model instance and writes it to the database in one
call:
# taskflow/views.py
from django.shortcuts import render, redirect
from .forms import TaskForm
def task_create(request):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
form.save()
return redirect('task-list')
else:
form = TaskForm()
return render(request, 'taskflow/task_form.html', {'form': form})
That single form.save() call replaces what would otherwise be several manual lines —
reading each value out of cleaned_data, constructing a Task(...)
instance field by field, and calling .save() on it yourself. Because
TaskForm already knows which model it belongs to, it handles all of that internally.
commit=False: Setting Extra Fields Before Saving
Sometimes a value needs to be attached to the new object before it's written to the database, but
that value isn't something the visitor should be filling in on the form itself — the clearest
TaskFlow example is stamping a new task's project with whichever project the current
logged-in user actually owns. Calling form.save(commit=False) builds the model
instance in memory without saving it yet, giving you a chance to set additional
fields first:
def task_create(request, project_id):
if request.method == 'POST':
form = TaskForm(request.POST)
if form.is_valid():
task = form.save(commit=False)
task.project_id = project_id
task.save()
return redirect('task-list')
else:
form = TaskForm()
return render(request, 'taskflow/task_form.html', {'form': form})
task here is a real Task instance, already populated from the form's
cleaned data — it just hasn't hit the database yet. Setting task.project_id and then
calling task.save() ourselves finishes the job with the extra field in place. This
exact pattern is how a future view will attach the current logged-in user via
request.user, once User Authentication
introduces that concept properly.
Editing an Existing Task
Creating and editing use the same TaskForm, with one difference: passing an existing
model instance as instance= pre-fills every field with that object's current values,
and points .save() at updating that same row instead of inserting a new one.
from django.shortcuts import render, redirect, get_object_or_404
from .models import Task
from .forms import TaskForm
def task_edit(request, task_id):
task = get_object_or_404(Task, pk=task_id)
if request.method == 'POST':
form = TaskForm(request.POST, instance=task)
if form.is_valid():
form.save()
return redirect('task-list')
else:
form = TaskForm(instance=task)
return render(request, 'taskflow/task_form.html', {'form': form})
On GET, TaskForm(instance=task) renders with task's current title,
description, due date, and priority already filled in. On POST,
TaskForm(request.POST, instance=task) validates the submitted data and, on
.save(), updates that exact task row rather than creating a second one —
the same template and the same view function handle both creating and editing, just with
instance as the difference.