Class-based views: ListView and DetailView
post_list from part 4 is still returning a static template with no real data. This part replaces it with ListView, a generic class-based view that already knows how to query a model, paginate it, and pass it to a template — the same pattern the standalone Django template views tutorial introduced with TemplateView, now applied to real data.
ListView
# blog/views.py
from django.views.generic import ListView
from .models import Post
class PostListView(ListView):
model = Post
template_name = "blog/post_list.html"
context_object_name = "posts"
queryset = Post.objects.filter(published=True)Set model = Post and ListView already knows to run Post.objects.all(), pass the result to the template as object_list, and paginate it once paginate_by is set (part 20). queryset overrides the default query — here, filtering to published posts only, using the exact .filter() pattern from part 8. context_object_name = "posts" is what makes the template variable posts instead of the default object_list, which reads better in the template below.
The list template
{# blog/templates/blog/post_list.html #}
{% extends "blog/base.html" %}
{% block title %}Latest posts{% endblock %}
{% block content %}
{% for post in posts %}
<article>
<h2><a href="{% url 'post_detail' post.slug %}">{{ post.title }}</a></h2>
<p>By {{ post.author }} on {{ post.created_at|date:"F j, Y" }}</p>
</article>
{% empty %}
<p>No posts yet.</p>
{% endfor %}
{% endblock %}{% url 'post_detail' post.slug %} builds a URL by name instead of a hardcoded path — the URL pattern it refers to doesn't exist yet (that's next), but the syntax is worth introducing here since every link in this project uses it from here on. {% empty %} inside a {% for %} renders only when the loop has nothing to iterate — cleaner than a separate {% if posts %} wrapped around the whole block.
DetailView
# blog/views.py
from django.views.generic import DetailView
class PostDetailView(DetailView):
model = Post
template_name = "blog/post_detail.html"
context_object_name = "post"{# blog/templates/blog/post_detail.html #}
{% extends "blog/base.html" %}
{% block title %}{{ post.title }}{% endblock %}
{% block content %}
<article>
<h1>{{ post.title }}</h1>
<p>By {{ post.author }} on {{ post.created_at|date:"F j, Y" }}</p>
<div>{{ post.content|linebreaks }}</div>
</article>
{% endblock %}DetailView fetches a single object and 404s automatically if the lookup fails — the get_object_or_404 pattern from part 8, built in. By default it looks up by primary key (pk) from the URL; part 10 switches that to the slug instead, for a URL worth actually reading.
Both do the same job here. ListView/DetailView remove boilerplate for exactly this shape — query a model, render a template — the same tradeoff the standalone TemplateView tutorial covers. A view with real branching logic (different behavior per HTTP method, complex permission checks) often stays clearer as a function.
Next: wiring these into URLs, and switching DetailView's lookup from an opaque numeric ID to a readable slug.