This version is in beta. Some features may change before release.

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.

Info
`per_page` is clamped to at least 1, and an empty queryset still reports `num_pages == 1` with one empty page.

Handler example

Code
rust
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).

Code
jinja
{# posts/list.html #}
<ul>
{% for post in posts %}<li>{{ post.title }}</li>{% endfor %}
</ul>
 
{% include "_pagination.html" %}

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:

Code
jinja
<a href="?{{ querystring_with(base_query, "page", item.n) }}">{{ item.n }}</a>

Strict vs. clamped

MethodOut-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-value limit/offset/count/fetch the paginator slices with.
webtemplatesormpagination