Building the Public-Facing Views: List & Detail

Personal Catalogue: Django & PostgreSQL

Chapter 4 · Building the Public-Facing Views: List & Detail

Chapter 3 gave this project a real, working way to add items — but only through the admin, a login-gated internal tool. This chapter builds the pages the catalogue is actually for: a real list of every item, and a detail page for each one, both genuinely readable without ever touching /admin/.

Two Real Views: List & Detail

Django's own built-in generic class-based views handle both of these directly. Rather than writing a function that manually fetches a queryset, passes it to a template, and returns an HttpResponse, ListView and DetailView do that real, repetitive plumbing automatically from a small amount of configuration.

The List View

# catalogue/views.py from django.views.generic import ListView, DetailView from .models import Item class ItemListView(ListView): model = Item template_name = 'catalogue/item_list.html' context_object_name = 'items' paginate_by = 25 queryset = Item.objects.select_related('item_type').prefetch_related('tags')

select_related('item_type') and prefetch_related('tags') are both deliberate, not decoration — the reasoning for each is covered directly below, once the template that actually needs them exists.

The Detail View

class ItemDetailView(DetailView): model = Item template_name = 'catalogue/item_detail.html' context_object_name = 'item'

DetailView looks up a single Item by its primary key (taken from the URL) and returns a real, genuine HTTP 404 automatically if no item with that ID exists — no manual get_object_or_404 call needed, since the generic view already wraps that lookup internally.

URL Routing

# catalogue/urls.py from django.urls import path from . import views urlpatterns = [ path('', views.ItemListView.as_view(), name='item_list'), path('item/<int:pk>/', views.ItemDetailView.as_view(), name='item_detail'), ]

Include this app-level urls.py from the project's own root catalogue_site/urls.py:

# catalogue_site/urls.py from django.urls import path, include urlpatterns = [ path('admin/', admin.site.urls), path('', include('catalogue.urls')), ]

Minimal Templates

A small base template, extended by both real pages:

<!-- catalogue/templates/catalogue/base.html --> <!DOCTYPE html> <html> <head><title>{% block title %}My Catalogue{% endblock %}</title></head> <body> <h1><a href="{% url 'item_list' %}">My Catalogue</a></h1> {% block content %}{% endblock %} </body> </html>
<!-- catalogue/templates/catalogue/item_list.html --> {% extends 'catalogue/base.html' %} {% block content %} <ul> {% for item in items %} <li> <a href="{% url 'item_detail' item.pk %}">{{ item.title }}</a> ({{ item.item_type }}{% if item.creator %}, {{ item.creator }}{% endif %}) {% for tag in item.tags.all %}<span>#{{ tag.name }}</span>{% endfor %} </li> {% endfor %} </ul> {% endblock %}
Real Template Inheritance
{% extends %} and {% block %} are Django's own real template inheritance mechanism — item_list.html doesn't repeat the <html>/<head>/navigation markup at all, it only fills in the content block base.html already declared. Every later template in this course (detail page, add-item form, Chapter 8's own styled versions) extends the same base.html.
A Real N+1 Query Bug, Caught Before It Shipped
The {% for tag in item.tags.all %} loop inside item_list.html runs once per item on the page — meaning without prefetch_related('tags') in the view's own queryset, displaying 25 items would trigger 25 separate database queries just to fetch tags, on top of the one query that fetched the items themselves: a real, classic N+1 query problem. prefetch_related('tags') fixes this by issuing exactly one additional query up front, fetching every tag for every item on the page at once, which Django then serves from memory for each item.tags.all call inside the template loop. select_related('item_type') solves the same underlying problem one level earlier, for the single-valued item.item_type lookup the template also uses — a real JOIN folded into the original query instead of a second query per item.
select_related vs. prefetch_related — Not Interchangeable
select_related only works for single-valued relationships (a ForeignKey like item_type), since it works by joining the related table directly into the original SQL query. prefetch_related is required for multi-valued relationships (a ManyToManyField like tags), since a single SQL join can't cleanly represent "many tags per item" in one flat result set — it runs as a real, separate second query instead. Using the wrong one, or skipping both, either raises an error or silently reintroduces the N+1 problem this chapter just fixed.

item_detail.html follows the identical pattern, extending the same base.html and rendering the single item the view resolved, including its full tag list — with no N+1 risk here at all, since only one item's own tags are ever fetched on this page.

Trying It Out

Visit the site's own root URL and confirm every item added in Chapter 3 — through the admin — now shows up on this real, public-facing list page too, since both interfaces read from the exact same Item table. Click through to a detail page, and confirm visiting a nonexistent item ID (e.g. /item/9999/) returns a real 404 rather than an error page or a blank screen.

Hands-On Exercises

Exercise 1

Build both views, both templates, and the URL routing from this chapter, then confirm the list page correctly shows every item added through the admin in Chapter 3, tags included.

📄 View solution
Exercise 2

Explain, step by step, why removing prefetch_related('tags') from the list view's own queryset would produce exactly 26 real database queries to render a page of 25 items with tags — not 25, and not 1.

📄 View solution
Exercise 3

Explain why select_related can't be used for the tags relationship, even though it's used successfully for item_type on the exact same queryset.

📄 View solution

Chapter 4 Quick Reference

  • ListView / DetailView — Django's own generic class-based views, handling querying and 404s automatically from minimal configuration
  • URL routing — an app-level urls.py included from the project's own root urls.py
  • Template inheritance{% extends %} / {% block %} share a common base.html across every page
  • Real N+1 bug found and fixed — looping over item.tags.all per item without prefetch_related('tags') would trigger one extra query per item
  • select_related vs. prefetch_related — single-valued relationships (ForeignKey) use select_related; multi-valued relationships (ManyToManyField) need prefetch_related instead
  • Next chapter: a real public-facing add-item form, handling several genuinely different item shapes outside the admin