Pagination (template list views)
Paginate a QuerySet into fixed-size pages for server-rendered list views with Paginator and Page.
umbral::pagination is the page-of-rows helper for server-rendered (Jinja) list views. A Paginator counts a QuerySet once and slices it into fixed-size Pages; a Page exposes has_next(), start_index(), next_page_number(), and a windowed elided_page_range() for the 1 … 4 5 [6] 7 8 … 20 nav.
It lives in the core (no plugin to register): any handler can paginate a queryset directly. This is distinct from REST's JSON pagination - reach for this when you render HTML.
Handler example
use umbral::prelude::*;use umbral::pagination::Paginator;use umbral::templates::{render, context}; async fn post_list(query: Query<HashMap<String, String>>) -> Response { let page_num: i64 = query.get("page").and_then(|p| p.parse().ok()).unwrap_or(1); let paginator = Paginator::new(Post::objects().order_by(post::ID.asc()), 10); // page() errors on an out-of-range number; page_clamped() clamps into // [1, num_pages] - handy straight off an untrusted querystring. let page = paginator.page_clamped(page_num).await?; render("posts/list.html", &context! { posts => page.object_list, // the rows for this page page => page.context(), // the serialized nav context base_query => "", // current querystring, sans leading `?` })}page.context() produces a serializable PageContext carrying number, num_pages, total_count, has_next, has_previous, next_page_number, previous_page_number, start_index, end_index, and page_range (the elided window - each item is {n} or {ellipsis: true}).
The nav partial
A reusable _pagination.html partial ships under crates/umbral-core/templates/. Copy it into your project templates/ dir and {% include %} it from a list template; it renders a minimal, CSS-framework-agnostic <nav> with aria attributes from two context vars: page (the PageContext above) and base_query (the current querystring).
{# posts/list.html #}<ul> {% for post in posts %}<li>{{ post.title }}</li>{% endfor %}</ul> {% include "_pagination.html" %}Preserving filters across page links
The core engine registers a querystring_with(current_query, key, value) template global. It rebuilds the querystring replacing one key while preserving every other parameter, so a ?sort=name filter survives every ?page=N link:
<a href="?{{ querystring_with(base_query, "page", item.n) }}">{{ item.n }}</a>Strict vs. clamped
| Method | Out-of-range page |
|---|---|
paginator.page(n) | Errors PaginationError::InvalidPage (strict) |
paginator.page_clamped(n) | Clamps n into [1, num_pages] and returns that page |
See also
- Design rationale:
arch.md- the thin-core-plus-plugins split that keeps this an ORM-adjacent core utility rather than a plugin. - The ORM and QuerySets -
order_by,filter, and the by-valuelimit/offset/count/fetchthe paginator slices with.