ModelForms: CreateView and UpdateView
CommentForm in part 12 declared every field by hand — reasonable for two fields, repetitive for Post's seven. ModelForm generates the form directly from the model, and CreateView/UpdateView are the generic views built to use one.
Defining a ModelForm
# blog/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "slug", "content", "published"]Meta.model points at what to generate the form from; Meta.fields is an explicit list of which model fields to include — deliberately not "__all__", since that silently exposes every future field added to Post (including ones that shouldn't be user-editable, like author) to whatever renders this form. PostForm() now behaves like any other Form: it validates, has cleaned_data, and renders with {{ form.as_p }} exactly as before.
CreateView
# blog/views.py
from django.views.generic.edit import CreateView
from django.urls import reverse_lazy
from .models import Post
from .forms import PostForm
class PostCreateView(CreateView):
model = Post
form_class = PostForm
template_name = "blog/post_form.html"
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)CreateView handles the entire GET/POST split from part 12's function view automatically: GET renders a blank form, POST validates and saves. form_valid() is the hook for anything that needs to happen to a valid form before it saves — here, setting author to the logged-in user, since that field is deliberately not in PostForm.Meta.fields for a user to set themselves. reverse_lazy (imported but used below) exists because CreateView needs a redirect target evaluated at class-definition time, before any URL has necessarily been resolved yet — reverse_lazy defers that resolution until it's actually needed.
UpdateView
class PostUpdateView(UpdateView):
model = Post
form_class = PostForm
template_name = "blog/post_form.html"
slug_field = "slug"
slug_url_kwarg = "slug"Nearly identical to CreateView — the difference is UpdateView looks up an existing object first (the same slug_field/slug_url_kwarg pattern from part 10) and pre-fills the form with its current values. Both views share the same post_form.html template, since a create form and an edit form are visually identical; only the surrounding context (a blank vs. pre-filled form) differs.
The shared template and URLs
{# blog/templates/blog/post_form.html #}
{% extends "blog/base.html" %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save</button>
</form>
{% endblock %}# blog/urls.py
path("post/new/", views.PostCreateView.as_view(), name="post_create"),
path("post/<slug:slug>/edit/", views.PostUpdateView.as_view(), name="post_update"),Both views also need a success_url — where to redirect after a successful save — which part 14 adds alongside a closer look at what actually protects that POST request.
Listing every model field in ModelForm.Meta.fields, including ones like author or created_at that should never come from user input. Anything in that list is something a submitted form can set — keep it to exactly what the person filling out the form should control.