Video coming soon

The recording for this lesson has not been published yet.

What we cover

  • queryset, get_queryset and context_object_name
  • Template names generic views expect
  • paginate_by and paginator context
  • get_context_data for extra template variables

Lesson notes

1. The two views you are replacing

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.

2. queryset, get_queryset and context_object_name

A ListView needs to know which objects to show. You have three ways to tell it, in increasing order of flexibility:

You writeDjango doesUse 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

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 %}

3. DetailView and how it finds the object

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"

4. The template names generic views expect

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.

ViewDefault templateSuffix attribute
ListViewblog/post_list.htmltemplate_name_suffix = "_list"
DetailViewblog/post_detail.htmltemplate_name_suffix = "_detail"
CreateViewblog/post_form.htmltemplate_name_suffix = "_form"
DeleteViewblog/post_confirm_delete.htmltemplate_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"

5. Where the behaviour actually lives

ListView is a stack of four small classes. When you wonder which attribute is read where, this picture is the answer.

ListView MultipleObjectTemplateResponseMixin builds blog/post_list.html TemplateResponseMixin render_to_response(), template_name BaseListView get(): queryset -> context -> response MultipleObjectMixin + ContextMixin get_queryset(), paginate_by, get_context_data() View: as_view(), dispatch()

6. paginate_by and the paginator context

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 %}

7. get_context_data for everything else

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"}

8. Exercise

  1. Replace post_list with a PostListView that keeps the published filter in get_queryset() and sets context_object_name = "posts". The template must not change.
  2. Add paginate_by = 3 and build the pagination controls from page_obj. Check that page 2 shows different posts.
  3. Replace post_detail with a PostDetailView and confirm that an unknown pk returns 404, and that a draft is not reachable by URL.
  4. Add a ?q= search to get_queryset() and expose the current query through get_context_data() so the input keeps its value.
  5. Open ListView on ccbv.co.uk and find where paginate_queryset() is called.

Further reading

After this lesson you will

  • Replace post_list and post_detail with generic views
  • Add pagination without writing pagination code