The recording for this lesson has not been published yet.
These are the first two views of the tutorial. Read them once more, because everything in this lesson is a direct translation of them.
# blog/views.py - before
from django.shortcuts import get_object_or_404, render
from django.utils import timezone
from .models import Post
def post_list(request):
posts = Post.objects.filter(published_date__lte=timezone.now()).order_by("-published_date")
return render(request, "blog/post_list.html", {"posts": posts})
def post_detail(request, pk):
post = get_object_or_404(Post, pk=pk)
return render(request, "blog/post_detail.html", {"post": post})
# blog/views.py - after
from django.utils import timezone
from django.views.generic import DetailView, ListView
from .models import Post
class PostListView(ListView):
model = Post
context_object_name = "posts"
paginate_by = 5
def get_queryset(self):
return Post.objects.filter(
published_date__lte=timezone.now()
).order_by("-published_date")
class PostDetailView(DetailView):
model = Post
context_object_name = "post"
# blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.PostListView.as_view(), name="post_list"),
path("post/<int:pk>/", views.PostDetailView.as_view(), name="post_detail"),
]
Same number of lines, but you also got pagination, a 404 for missing posts and a documented set of override points. Now let us see where each piece comes from.
A ListView needs to know which objects to show. You have three ways to
tell it, in increasing order of flexibility:
| You write | Django does | Use it when |
|---|---|---|
model = Post |
Post.objects.all() |
you want everything, unordered |
queryset = Post.objects.published() |
uses that queryset, re-evaluated per request | the filter never changes |
get_queryset(self) |
calls your method per request | the filter depends on the request |
Anything that depends on self.request, self.kwargs or the current
time belongs in get_queryset(). A search box, for example:
class PostListView(ListView):
model = Post
context_object_name = "posts"
paginate_by = 5
def get_queryset(self):
qs = Post.objects.published().select_related("author")
query = self.request.GET.get("q")
if query:
qs = qs.filter(title__icontains=query)
return qs
A custom manager method such as Post.objects.published() (module 2) keeps
the same rule in the list view, the sitemap and the feed. The view then only decides
what the request adds.
In the template, the objects arrive under two names. object_list always works;
context_object_name adds a readable alias, and without it Django also provides
post_list (the model name plus _list). Set it explicitly so your
templates do not depend on the model name:
{% for post in posts %}
<article class="mb-4">
<h2><a href="{% url 'blog:post_detail' post.pk %}">{{ post.title }}</a></h2>
<p class="text-secondary">{{ post.published_date|date:"j F Y" }}</p>
{{ post.text|truncatewords:30|linebreaksbr }}
</article>
{% empty %}
<p>No posts yet.</p>
{% endfor %}
DetailView looks for a pk or a slug in the URL
keyword arguments, calls get_object(), and raises Http404 if
nothing matches - the get_object_or_404 you wrote by hand.
class PostDetailView(DetailView):
model = Post
context_object_name = "post"
def get_queryset(self):
# Even a detail page must not leak drafts.
return Post.objects.published().select_related("author")
To use slugs instead of numbers, name the URL argument slug and point the view
at the field:
path("post/<slug:slug>/", views.PostDetailView.as_view(), name="post_detail")
class PostDetailView(DetailView):
model = Post
context_object_name = "post"
slug_field = "slug"
slug_url_kwarg = "slug"
Neither view above mentions a template, and both still render the ones you already have.
Generic views build the name from the model: <app_label>/<model_name>
plus a suffix.
| View | Default template | Suffix attribute |
|---|---|---|
ListView | blog/post_list.html | template_name_suffix = "_list" |
DetailView | blog/post_detail.html | template_name_suffix = "_detail" |
CreateView | blog/post_form.html | template_name_suffix = "_form" |
DeleteView | blog/post_confirm_delete.html | template_name_suffix = "_confirm_delete" |
The tutorial named its templates blog/post_list.html and
blog/post_detail.html, so the defaults match by luck - or rather, because the
tutorial follows Django's convention. When you need a different file, be explicit:
class PostArchiveView(ListView):
model = Post
template_name = "blog/post_archive.html"
ListView is a stack of four small classes. When you wonder which attribute is
read where, this picture is the answer.
One attribute replaces the whole Paginator dance:
class PostListView(ListView):
model = Post
context_object_name = "posts"
paginate_by = 5
paginate_orphans = 1 # avoid a last page with a single post
ListView then adds four names to the context:
paginator - the Paginator object (num_pages,
count).page_obj - the current Page
(number, has_next, next_page_number).is_paginated - True when there is more than one page.object_list - only the objects of this page. Your
context_object_name points at the same sliced list.{% if is_paginated %}
<ul class="pagination">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">prev</a>
</li>
{% endif %}
<li class="page-item disabled">
<span class="page-link">{{ page_obj.number }} / {{ paginator.num_pages }}</span>
</li>
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}">next</a>
</li>
{% endif %}
</ul>
{% endif %}
An unordered queryset may return rows in any order, so page 2 can repeat a post from
page 1. Order in get_queryset() or, better, give the model a
Meta.ordering. Also keep the query string: a link to
?page=2 silently drops ?q=django.
When the template needs more than the objects, override
get_context_data(). Always call super() first, then add your keys.
class PostListView(ListView):
model = Post
context_object_name = "posts"
paginate_by = 5
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["query"] = self.request.GET.get("q", "")
context["total_posts"] = self.get_queryset().count()
return context
class PostDetailView(DetailView):
model = Post
context_object_name = "post"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
# self.object is the Post, already fetched for you.
context["comments"] = self.object.comments.filter(approved=True)
return context
For values that do not depend on the request, extra_context is enough:
class PostListView(ListView):
model = Post
extra_context = {"page_title": "Blog"}
post_list with a PostListView that keeps the published
filter in get_queryset() and sets
context_object_name = "posts". The template must not change.paginate_by = 3 and build the pagination controls from
page_obj. Check that page 2 shows different posts.post_detail with a PostDetailView and confirm that an
unknown pk returns 404, and that a draft is not reachable by URL.?q= search to get_queryset() and expose the current
query through get_context_data() so the input keeps its value.ListView on ccbv.co.uk and find where
paginate_queryset() is called.