Django / Reliable application workflows / Lesson 11
Generic views and bounded pagination
Use class-based view contracts deliberately and paginate a scoped, deterministically ordered dataset without loading everything first.
On this page
Lesson goals
- Abstraction keeps its lifecycle obligations
- Inspect a deterministic in-memory paginator
- Test view composition and query costs together
Abstraction keeps its lifecycle obligations
Generic class-based views can remove routine list, detail, and form plumbing while exposing documented methods for queryset, context, and response behavior. Learn that lifecycle before overriding dispatch or mixing several unrelated behaviors. A mixinโs order can affect which method runs, so inspect the method-resolution contract rather than copying an inheritance list blindly. A generic view does not automatically enforce every tenant or object rule. Keep state that varies per request on the request instance, not in a shared mutable class attribute. The goal is less repeated mechanics with equally visible ownership, not an opaque controller whose behavior no reader can trace.
Pagination needs a contract beyond page numbers
Define the visible queryset, stable ordering, maximum page size, empty behavior, and invalid-page response. A unique tie-breaker matters when the main sort key can repeat. Paginating an already materialized full list can still consume all database rows and memory first. Count-based page navigation also has a cost, and offset pages can shift under concurrent inserts or deletes. A browsing interface is not automatically a stable export snapshot. Choose the pagination strategy and freshness policy according to the task, and do not promise that page two remains identical throughout a changing dataset merely because the first request returned a page count.
Inspect a deterministic in-memory paginator
The first complete example paginates five synthetic values at two items per page. It checks count, number of pages, and the second pageโs contents independently. The final page naturally has fewer items. This fixture demonstrates Paginator behavior without claiming ORM performance. get_page offers forgiving handling for malformed or out-of-range page input, while page has stricter documented exceptions; choose intentionally rather than assuming both interfaces reject the same values. A web endpoint may wrap either behavior in its own response contract, so tests should use the actual public path and expected missing or fallback policy.
from django.conf import settings
settings.configure(
SECRET_KEY="synthetic-course-key",
USE_I18N=False, USE_TZ=True,
INSTALLED_APPS=[],
DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
DEFAULT_AUTO_FIELD="django.db.models.AutoField",
ALLOWED_HOSTS=["testserver"],
)
import django
django.setup()
from django.core.paginator import Paginator
paginator = Paginator([1, 2, 3, 4, 5], 2)
page = paginator.page(2)
print(paginator.count, paginator.num_pages)
print(list(page.object_list))
print(page.has_previous(), page.has_next())
5 3
[3, 4]
True True
Five values produce three pages, and the middle page contains three and four. This is an in-memory pagination fixture, not a database benchmark.
Scope a class-based list per request
The second fragment assumes the catalog Course ownership schema, authentication/session middleware, a real list template, and named routing. LoginRequiredMixin protects entry, and get_queryset filters by the current user before pagination and applies a deterministic title/pk order. The template can iterate page_obj and render appropriate navigation through the declared context. This fragment is not executed against an application database. A permission mixin can add a capability rule, but allowing the listing operation alone would not replace this row scope. If a request-controlled page size is supported later, validate and cap it rather than reading an unbounded number directly into paginate_by.
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views.generic import ListView
from .models import Course
class MyCourseListView(LoginRequiredMixin, ListView):
model = Course
template_name = "catalog/my_courses.html"
paginate_by = 20
def get_queryset(self):
return Course.objects.filter(owner=self.request.user).order_by("title", "pk")
In the declared app: each authenticated user sees only their courses, up to 20 per page.
Requires the declared application context. The ownership filter and unique tie-breaker apply before the generic view paginates the queryset.
Test view composition and query costs together
Verify the anonymous path, two users with separate records, tied titles, empty results, the first and final pages, and the invalid-page policy. Ensure links preserve only approved filters and do not echo sensitive query parameters unintentionally. Related-object rendering can still introduce N+1 queries after correct pagination, so load the required graph on the scoped queryset. Inspect context names and template expectations when customizing a generic view. A successful 200 response cannot prove that records were scoped, ordered, bounded, or accessible through keyboard navigation. Keep those outcomes as separate assertions and reviews under representative data.
Common errors and fixes
A list mixin allows access but rows remain cross-user
- Why it happens
- Capability and queryset visibility were conflated.
- How to fix it
- Scope get_queryset under the current request.
- Prevent it next time
- Test two owners and excluded rows.
Pages repeat tied records unpredictably
- Why it happens
- Ordering lacks a unique tie-breaker.
- How to fix it
- Add deterministic ordering and document concurrent-change behavior.
- Prevent it next time
- Include tied primary sort values in fixtures.
Small pages still use excessive memory
- Why it happens
- The entire queryset was converted to a list first.
- How to fix it
- Paginate the appropriate bounded query rather than all materialized data.
- Prevent it next time
- Measure retrieval as well as rendered row count.
Check your understanding
1. Why include pk after title in a paginated order?
Show worked solution
Titles can repeat, while the primary key supplies a stable tie-breaker for the declared dataset. This makes ordering deterministic for equal titles. It does not freeze the dataset across separate requests, so concurrent changes and export consistency still need their own policy.
2. What must be added before allowing a client to choose page size?
Show worked solution
Validate its syntax and range, choose a documented default, cap the maximum, and propagate only approved filters into links. Test malformed, zero, negative, and excessive values. Pagination is a resource boundary as well as a navigation feature.
Key takeaways
- Generic views keep their documented lifecycle.
- Scope and order before paginating.
- Page navigation does not create a frozen snapshot.