The recording for this lesson has not been published yet.
post_new and post_edit from the tutorial are almost the same
function twice, and both contain the same four-line ritual: build the form, check the
method, validate, redirect.
# blog/views.py - before
from django.shortcuts import get_object_or_404, redirect, render
from .forms import PostForm
from .models import Post
def post_new(request):
if request.method == "POST":
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.published_date = timezone.now()
post.save()
return redirect("post_detail", pk=post.pk)
else:
form = PostForm()
return render(request, "blog/post_edit.html", {"form": form})
def post_edit(request, pk):
post = get_object_or_404(Post, pk=pk)
if request.method == "POST":
form = PostForm(request.POST, instance=post)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect("post_detail", pk=post.pk)
else:
form = PostForm(instance=post)
return render(request, "blog/post_edit.html", {"form": form})
CreateView and UpdateView already contain that ritual. What is
left for you is the part that is actually about your blog: which form, which fields, who the
author is, and where to go afterwards.
# blog/views.py - after
from django.urls import reverse_lazy
from django.views.generic import CreateView, DeleteView, UpdateView
from .forms import PostForm
from .models import Post
class PostCreateView(CreateView):
model = Post
form_class = PostForm
def form_valid(self, form):
form.instance.author = self.request.user
return super().form_valid(form)
class PostUpdateView(UpdateView):
model = Post
form_class = PostForm
class PostDeleteView(DeleteView):
model = Post
success_url = reverse_lazy("blog:post_list")
# blog/urls.py
urlpatterns = [
path("", views.PostListView.as_view(), name="post_list"),
path("post/<int:pk>/", views.PostDetailView.as_view(), name="post_detail"),
path("post/new/", views.PostCreateView.as_view(), name="post_new"),
path("post/<int:pk>/edit/", views.PostUpdateView.as_view(), name="post_edit"),
path("post/<int:pk>/delete/", views.PostDeleteView.as_view(), name="post_delete"),
]
Both views define post() for you. It builds the form from
request.POST (plus instance for updates), validates it, and calls
one of two hooks. Those two hooks are where you plug in.
Two consequences worth remembering. First, a successful POST always ends in a redirect - the post/redirect/get pattern, which is why reloading the page never saves twice. Second, an invalid POST returns status 200 with the bound form, so the user sees their input and the errors next to it.
A model form view needs to know which fields to render. Give it exactly one of these:
| Option | Code | When |
|---|---|---|
fields |
fields = ["title", "text"] |
quick CRUD, no custom validation or widgets |
form_class |
form_class = PostForm |
you have a real ModelForm - almost always |
Setting form_class and fields together raises
ImproperlyConfigured. And a form that includes author or
is_published lets any visitor post as somebody else - the field list is a
security boundary, not a convenience.
You already have PostForm, so keep it. All three views can share it, and both
form views render blog/post_form.html by default. The tutorial called that file
post_edit.html, so either rename it or say so:
class PostCreateView(CreateView):
model = Post
form_class = PostForm
template_name = "blog/post_edit.html"
form_valid() receives a validated form and is responsible for saving it. The
clean way to fill in a field the user must not control is to touch
form.instance before calling super():
class PostCreateView(CreateView):
model = Post
form_class = PostForm
def form_valid(self, form):
form.instance.author = self.request.user
form.instance.published_date = timezone.now()
return super().form_valid(form) # saves and returns the redirect
super().form_valid(form) does two things: self.object = form.save()
and return HttpResponseRedirect(self.get_success_url()). If you need the saved
object for something else, do it after the save:
def form_valid(self, form):
form.instance.author = self.request.user
response = super().form_valid(form) # self.object now exists
messages.success(self.request, f"Saved '{self.object.title}'.")
return response
You can also add errors from the view, when the rule needs the request:
def form_valid(self, form):
if Post.objects.filter(author=self.request.user, title=form.instance.title).exists():
form.add_error("title", "You already have a post with this title.")
return self.form_invalid(form)
form.instance.author = self.request.user
return super().form_valid(form)
To pass the user into the form itself, extend get_form_kwargs():
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["user"] = self.request.user
return kwargs
There are three ways to answer "where next", and they compose from most specific to least:
get_success_url() on the view - use it when the target depends on the
saved object.success_url on the view - a fixed destination.get_absolute_url() on the model - the default for
CreateView and UpdateView, and the one to prefer.# blog/models.py
from django.urls import reverse
class Post(models.Model):
...
def get_absolute_url(self):
return reverse("blog:post_detail", kwargs={"pk": self.pk})
With that method in place, PostCreateView and PostUpdateView need
no success_url at all, and {{ post.get_absolute_url }}
works in every template. When you do need an explicit URL:
class PostUpdateView(UpdateView):
model = Post
form_class = PostForm
def get_success_url(self):
return reverse("blog:post_detail", kwargs={"pk": self.object.pk})
A class attribute is evaluated at import time, before the URLconf is loaded, so
reverse() there raises an error. Use reverse_lazy for
success_url and plain reverse inside
get_success_url(), which runs per request.
DeleteView answers a GET with a confirmation page and only deletes on POST -
which is exactly right, because a link that deletes data will eventually be followed by a
crawler.
class PostDeleteView(DeleteView):
model = Post
success_url = reverse_lazy("blog:post_list")
# template: blog/post_confirm_delete.html
<!-- blog/templates/blog/post_confirm_delete.html -->
<h1>Delete post</h1>
<p>Really delete "{{ object.title }}"? This cannot be undone.</p>
<form method="post">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Delete</button>
<a href="{{ object.get_absolute_url }}" class="btn">Cancel</a>
</form>
A soft delete - hiding the row instead of removing it - is just another
form_valid():
class PostDeleteView(DeleteView):
model = Post
success_url = reverse_lazy("blog:post_list")
def form_valid(self, form):
self.object = self.get_object()
self.object.is_deleted = True
self.object.save(update_fields=["is_deleted"])
return HttpResponseRedirect(self.get_success_url())
Since Django 4.0 DeleteView is built on FormMixin, so the hook is
form_valid() - older tutorials override delete(), which no longer
runs.
<!-- blog/templates/blog/post_form.html -->
<h1>{% if form.instance.pk %}Edit post{% else %}New post{% endif %}</h1>
<form method="post" novalidate>
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Save</button>
</form>
Note that nothing in the template knows whether a CreateView or an
UpdateView rendered it - both provide form, and
UpdateView additionally provides object.
get_absolute_url() to Post and delete every
redirect("post_detail", pk=...) that becomes redundant.post_new with a PostCreateView that sets the author in
form_valid(), and confirm the author is correct even if you add
author to the POST data by hand.post_edit with an UpdateView reusing
PostForm and the same template.PostDeleteView with a confirmation template, then try to reach the
delete URL with GET and verify nothing is deleted.