Skip to content

Pagination

Purpose: Provide a simple wrapper around Django's built-in paginator and a pair of template tags that generate pagination URLs and elided page ranges while preserving existing query parameters.


Why Pagination?

List views need consistent pagination across every feature. The Pagination system provides:

  • A single paginate_list() function with sensible defaults for page size
  • A pagination_url template tag that builds page links without discarding active filters or search terms
  • A get_elided_page_range template tag for rendering smart page number controls (e.g. 1 2 … 5 6 7 … 19 20)

Quick Start

1. Paginate a Queryset in Your View

from django_spire.contrib.pagination.pagination import paginate_list

def order_list_view(request):
    orders = Order.objects.active().order_by('-created_datetime')

    page = paginate_list(
        object_list=orders,
        page_number=request.GET.get('page', 1),
        per_page=25,
    )

    return render(request, 'orders/page/order_list_page.html', {'page': page})

2. Render Pagination Controls in Your Template

{% load pagination_tags %}

{% for order in page.object_list %}
    {% include 'orders/item/order_item.html' %}
{% endfor %}

{% if page.has_other_pages %}
    {% get_elided_page_range page as page_range %}

    <nav>
        <ul class="pagination">
            {% if page.has_previous %}
                <li class="page-item">
                    <a class="page-link" href="{% pagination_url page.previous_page_number %}">Previous</a>
                </li>
            {% endif %}

            {% for page_number in page_range %}
                {% if page_number == page.number %}
                    <li class="page-item active">
                        <span class="page-link">{{ page_number }}</span>
                    </li>
                {% elif page_number == '...' %}
                    <li class="page-item disabled">
                        <span class="page-link">&hellip;</span>
                    </li>
                {% else %}
                    <li class="page-item">
                        <a class="page-link" href="{% pagination_url page_number %}">{{ page_number }}</a>
                    </li>
                {% endif %}
            {% endfor %}

            {% if page.has_next %}
                <li class="page-item">
                    <a class="page-link" href="{% pagination_url page.next_page_number %}">Next</a>
                </li>
            {% endif %}
        </ul>
    </nav>
{% endif %}

Core Concepts

paginate_list

Wraps Django's Paginator and returns a Page object for the requested page number. If the page number exceeds the total number of pages, the last page is returned instead of raising an error.

from django_spire.contrib.pagination.pagination import paginate_list
Parameter Type Default Description
object_list list or QuerySet required The items to paginate
page_number int 1 The current page number
per_page int 50 Number of items per page

Returns a Django Page object. All standard Page methods apply: has_next(), has_previous(), next_page_number(), previous_page_number(), etc.

pagination_url Template Tag

Generates a query string for a pagination link. Preserves all existing GET parameters (search terms, filters, sort order) and updates only the page number.

{% load pagination_tags %}
Parameter Type Default Description
page_number int required The target page number
page_name str 'page' The query parameter name for the page number

Spaces in existing query parameter values are replaced with + to stay consistent with how Django session filters encode values.

get_elided_page_range Template Tag

Returns an elided page range from the paginator — a sequence of page numbers with '...' inserted where large gaps exist. Wraps Django's built-in get_elided_page_range.

{% load pagination_tags %}
Parameter Type Default Description
page_obj Page required The current Page object
on_each_side int 2 Number of pages to show on each side of the current page
on_ends int 2 Number of pages to show at each end of the range

Returns an iterator of integers and '...' strings.


Main Operations

Basic Pagination in a View

from django_spire.contrib.pagination.pagination import paginate_list

def invoice_list_view(request):
    invoices = Invoice.objects.active().order_by('-issued_date')

    page = paginate_list(
        object_list=invoices,
        page_number=request.GET.get('page', 1),
    )

    return render(request, 'invoices/page/invoice_list_page.html', {'page': page})

pagination_url automatically carries forward all existing GET parameters. No extra configuration is needed — if the current URL is ?search=acme&status=unpaid&page=1, a link to page 2 becomes ?search=acme&status=unpaid&page=2.

<a href="{% pagination_url page.next_page_number %}">Next</a>

Using a Custom Page Query Parameter

If your view uses a parameter name other than page (e.g. when multiple paginated lists appear on the same page):

<a href="{% pagination_url page.next_page_number page_name='invoice_page' %}">Next</a>

Match the same name in your view:

page = paginate_list(
    object_list=invoices,
    page_number=request.GET.get('invoice_page', 1),
)

Rendering an Elided Page Range

For long page ranges, get_elided_page_range inserts '...' where pages are omitted:

{% get_elided_page_range page as page_range %}

{% for page_number in page_range %}
    {% if page_number == '...' %}
        <span>&hellip;</span>
    {% else %}
        <a href="{% pagination_url page_number %}">{{ page_number }}</a>
    {% endif %}
{% endfor %}

To show more pages around the current one:

{% get_elided_page_range page on_each_side=3 on_ends=1 as page_range %}