Tags: Filtering the Catalogue by Tag
Personal Catalogue: Django & PostgreSQL
Chapter 6 · Tags: Filtering the Catalogue by Tag
Chapter 5 let items be tagged. This chapter makes those tags actually useful for the app's own real core question — not just "do I already own this?" but the narrower, equally real "what Python books do I have?" — by building a genuine filter-by-tag feature into the list page.
Filtering via a Query Parameter
Rather than a separate page per tag, this course uses one real, shareable URL pattern — /?tag=python — read directly off the query string:
queryset.filter(tags__name=tag_name) follows the ManyToManyField relationship directly in the ORM — no manual join, no separate query, since Django's own field-lookup syntax (tags__name) turns straight into a real SQL join against the auto-generated join table from Chapter 2.
Making Tags Clickable
A small template change turns each tag shown on the list and detail pages into a real link back into this same filter:
And a small banner showing the active filter, with a real way back out of it:
paginate_by = 25, Chapter 4) generates its "next page" and "previous page" links from page_obj alone — those links only ever carry a ?page=2 query string by default, with no awareness that a ?tag=python filter might also be active. Clicking "next page" while a tag filter is active would silently drop the filter, landing on page 2 of the entire unfiltered catalogue instead of page 2 of the filtered results — a real, easy-to-miss bug that only shows up once a filtered list actually spans more than one page.
The fix is to build pagination links that explicitly carry the active tag forward:
&tag={{ active_tag }} suffix, and so would any other filter this project ever adds later (by item type, say). The real, general rule: any link that changes the page number, on a page whose own results are already filtered, has to explicitly re-include every active filter in its own query string — Django never does this automatically, since the pagination links and the filtering logic are two genuinely separate pieces of code that don't know about each other unless the template explicitly connects them.
A Small Tag Index Page
A real, genuinely useful companion to filtering: a page listing every tag alongside how many items actually carry it, using Django's own real aggregation support:
Count('items') uses the related_name='items' set on the Item.tags field back in Chapter 2, letting this query count, for each real Tag row, how many Item rows actually reference it — a single aggregated query rather than one count query per tag.
Hands-On Exercises
Add the tag-filtering logic to ItemListView, make tags clickable in the list template, and confirm visiting /?tag=python shows only items actually tagged Python.
Add enough real items tagged "Python" to force pagination onto a second page, then reproduce the real bug this chapter describes by clicking "next page" before applying the fix — confirm the filter is genuinely lost — then apply the fix and confirm it's preserved.
📄 View solutionBuild the TagIndexView and its template, and explain in your own words why Count('items') is a meaningfully faster approach than looping over every tag and calling tag.items.count() individually.
Chapter 6 Quick Reference
- Filtering —
get_queryset()reads a real?tag=query parameter and filters viatags__name - Clickable tags — each tag rendered as a link back into the same filter, plus a "clear filter" link
- Real bug found — pagination links silently drop the active tag filter unless the query string is explicitly carried forward on every page link
- TagIndexView — a real aggregated
Count('items')query listing every tag with its true item total, in one query - Next chapter: Search — real Django ORM querying across the whole catalogue