Course outline

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.

About 4 min readContent reviewed
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.

python Isolated executable example
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())
Expected output
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.

python Application-context fragment
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")
Expected output
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.

Official references