Handling POST requests and CSRF protection
{% csrf_token %} has appeared in every form so far without explanation. This part covers what it actually protects against, finishes wiring up PostCreateView/PostUpdateView with a redirect target, and shows what happens when the token is missing.
What CSRF actually is
Cross-Site Request Forgery: a malicious site tricks a logged-in user's browser into submitting a request to your site — the browser automatically attaches that user's session cookie, so without protection, the request looks completely legitimate to your server. A hidden form on an attacker's page that auto-submits to your-blog.com/post/1/delete/ would, without CSRF protection, actually delete the post — because the victim's browser is authenticated, even though the victim never intended to visit your site at all.
What {% csrf_token %} does
<form method="post">
{% csrf_token %}
...
</form>This renders a hidden input containing a token tied to the user's session. Django's CsrfViewMiddleware (already active by default — check MIDDLEWARE in settings.py) rejects any POST request that doesn't include a matching token. An attacker's page, on a different domain, has no way to read that token to forge a valid request — which is exactly the protection.
Seeing it fail
Remove {% csrf_token %} from a form temporarily and submit it:
Forbidden (403)
CSRF verification failed. Request aborted.That's the middleware doing its job. It's worth seeing once deliberately, since the same error shows up for a real, accidental reason later — a form copy-pasted without its {% csrf_token %} line — and it's easier to fix in ten seconds when you recognize it on sight.
Finishing the create/update views
# blog/views.py
class PostCreateView(CreateView):
model = Post
form_class = PostForm
template_name = "blog/post_form.html"
success_url = reverse_lazy("post_list")
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class PostUpdateView(UpdateView):
model = Post
form_class = PostForm
template_name = "blog/post_form.html"
slug_field = "slug"
slug_url_kwarg = "slug"
def get_success_url(self):
return reverse_lazy("post_detail", kwargs={"slug": self.object.slug})success_url on PostCreateView is a fixed redirect target, since there's no existing post to link back to. PostUpdateView overrides get_success_url() instead — a method, not an attribute — because the target depends on self.object, the specific post that was just updated, which isn't known until the view runs.
Setting success_url as a plain string, like success_url = "/posts/", instead of reverse_lazy("post_list"). It works today — until the URL pattern changes and every hardcoded string like it silently points at a dead route.
Next: DeleteView — the last piece of full CRUD, plus the confirmation step that stops a stray click from deleting a post.