Skip to content

Filtering

Purpose: how list pages in Django Spire handle searching, filtering, and sorting — server-side querysets and the client-side Glue scroll list.

Warning

This guide assumes a working knowledge of Django Glue. The scroll list below is built on QuerySetGlue and does not replace the core Glue primitives.


Server-Side

1 · Compose a QuerySet

List-page querysets combine HistoryQuerySet (soft-delete/active state) with SearchQuerySetMixin (default search):

# app/person/querysets.py
from __future__ import annotations

from typing import TYPE_CHECKING

from django_spire.core.querysets import SearchQuerySetMixin
from django_spire.history.querysets import HistoryQuerySet

if TYPE_CHECKING:
    from django.db.models import QuerySet

    from app.person.models import Person


class PersonQuerySet(HistoryQuerySet, SearchQuerySetMixin):
    def bulk_filter(self, filter_data: dict) -> QuerySet[Person]:
        queryset = self

        search = filter_data.get('search', '')
        if search:
            queryset = queryset.search(search)

        return queryset

On the model, attach it through a manager:

# app/person/models.py
from app.person.querysets import PersonQuerySet

class Person(HistoryModelMixin):
    objects = PersonQuerySet.as_manager()

2 · Searching

SearchQuerySetMixin.search(search_value) provides a sensible default — every word in the query must match at least one CharField (case-insensitive). Override search() on the queryset when the default is wrong:

class PersonQuerySet(HistoryQuerySet, SearchQuerySetMixin):
    def search(self, search_value: str | None) -> QuerySet:
        if not search_value:
            return self

        search_value = search_value.strip()
        return self.filter(
            Q(first_name__icontains=search_value) | Q(last_name__icontains=search_value)
        )

3 · Applying the Filter in a View

bulk_filter() receives the raw request GET params and is the single entry point for all list-page filtering:

# app/person/views.py
from django.template.response import TemplateResponse
from app.person.models import Person


def person_list_view(request):
    people = Person.objects.active().bulk_filter(filter_data=request.GET.dict())

    context = {'people': people}
    return TemplateResponse(request, context=context, template='person/page/list_page.html')

Pagination is plain Django Paginator on top of the filtered queryset; column sorting can be exposed through a sort_by_column(sort_column, sort_direction) queryset method that maps request params to a safe order_by.


Client-Side (Glue Scroll Lists)

For interactive list pages, Spire ships a base scroll template that wires a QuerySetGlue queryset to search, sort, and infinite scroll:

django_spire/glue/scroll/scroll.html

{% extends 'django_spire/glue/scroll/scroll.html' %}

{% block scroll_queryset_order_by_field %}name{% endblock %}
{% block scroll_queryset_unique_name %}{{ person_queryset_name }}{% endblock %}
{% block scroll_queryset_filter_field %}description{% endblock %}
{% block increment %}25{% endblock %}

{% block scroll_header %}
    <div class="row">
        <div class="col">
            <input class="form-control" placeholder="Search ..." type="search" x-model.debounce="searchQuery">
        </div>
        <div class="col">
            <button class="btn btn-outline-secondary" type="button" @click="toggleOrder()">
                <i class="bi" :class="orderBy.startsWith('-') ? 'bi-sort-alpha-down-alt' : 'bi-sort-alpha-down'"></i>
            </button>
        </div>
    </div>
{% endblock %}

{% block scroll_item %}
    {# one row of the list #}
{% endblock %}

The view binds the queryset with Glue.queryset:

Glue.queryset(
    request,
    'people',
    Person.objects.active(),
    Glue.Access.CHANGE,
    fields=['id', 'first_name', 'last_name', 'description'],
)

Available on the Alpine component:

State / Method Purpose
searchQuery bound to the search input; debounced, resets and reloads the list
filterField the field name the search input filters on
orderBy current sort field; prefix with - for descending (toggleOrder() flips it)
items the loaded rows
hasMore / loadMoreItems() infinite-scroll state; loads increment more rows
updateItem(pk, removed) refresh or remove one row after a mutation

Search and sort are applied client-side against the glued data:

this.scrollQuerySet = this.scrollQuerySet
    .filter({ [this.filterField + '__icontains']: this.searchQuery })
    .orderBy(this.orderBy)
    .slice(start, stop)