Skip to content

Filtering

Learn the process of searching and filtering on a list page using Django Spire and Django Glue.

Warning

This tutorial does not go into the details of Django Glue. A basic understanding of Django Glue is recommended before starting this guide.

Step 1: Create a QuerySetFilter instance in the backend.

app/person/views.py
from django_spire.core.filtering.filters import QuerySetFilter


def person_list_view(request):
    queryset_filter = QuerySetFilter(request, filter_key='person_queryset_filter')

Default Filtering Options

Use the default_filtering_data parameter to set the default filtering options.

app/person/views.py
from django_spire.core.filtering.filters import QuerySetFilter

def person_list_view(request):
    queryset_filter = QuerySetFilter(
        request,
        filter_key='person_queryset_filter',
        default_filtering_data={'age': 25}
    )

Step 2: Inherit from FilterQuerySet or SearchQuerySet in the objects QuerySet class and override the abstract method.

For Filtering:

Inherit from FilterQuerySet and override the filter_by_query_dict method in the objects QuerySet class.

app/person/querysets.py
from django.db.models import Q, QuerySet
from django_spire.core.filtering.querysets import FilterQuerySet


class PersonQuerySet(FilterQuerySet):
    def filter_by_query_dict(self, filter_data: dict) -> QuerySet:
        query = Q()

        age = filter_data.get('age')
        if age:
            query &= Q(age=age)

        return self.filter(query)

For Searching:

Inherit from SearchQuerySet and override the search method in the objects QuerySet class.

app/person/querysets.py
from django.db.models import Q, QuerySet
from django_spire.core.filtering.querysets import SearchQuerySet

class PersonQuerySet(SearchQuerySet):
    def search(self, search_query: str) -> QuerySet:
        return self.filter(
            Q(first_name__icontains=search_query) |
            Q(last_name__icontains=search_query)
        )

Step 3: Query for the objects using the QuerySetFilter instance.

The filtering / searching will be handled inside the process_queryset method using the methods that were just overridden.

app/person/views.py
from app.person.models import Person
from django_spire.core.filtering.filters import QuerySetFilter


def person_list_view(request):
    queryset_filter = QuerySetFilter(request, filter_key='person_queryset_filter')

    people = queryset_filter.process_queryset(Person.objects.all())

Step 4: Pass the QuerySetFilter instance to the template.

app/person/views.py
from app.person.models import Person
from django_spire.core.filtering.filters import QuerySetFilter


def person_list_view(request):
    queryset_filter = QuerySetFilter(request, filter_key='person_queryset_filter')

    people = queryset_filter.process_queryset(Person.objects.all())

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

Step 5: Create a Django Glue form that extends one of the filter form html files.

Filtering

Searching can be included with filtering, see More Complex Example for an example.

person/form/filter_form.html
{% extends 'django_spire/filtering/form/filter_form.html' %}

{% block filter_key %}{{ queryset_filter.filter_key }}{% endblock %}

{% block filter_content %}
    <div
        class="row"
        x-data="{
            init() {
                this.search_value.set_attribute('placeholder', 'Search first and last name...')
            },
            age: new GlueIntegerField(
                'age',
                {
                    value: {{ queryset_filter.filter_data.age|default:'null' }},
                }
            )
        }"
    >
        Include block tags {% %} excluded due to mkdocs attempting to render file.
        <div class="col-12">
            include 'django_glue/form/field/number_field.html' with glue_field='age'
        </div>
    </div>
{% endblock %}

Searching

person/form/search_form.html
{% extends 'django_spire/filtering/form/search_form.html' %}

{% block filter_key %}{{ queryset_filter.filter_key }}{% endblock %}

{% block filter_content %}
    <div
        class="row"
        x-data="{
            init() {
                this.search_value.set_attribute('placeholder', 'Search first and last name...')
            },
            search_value: new GlueCharField(
                'search_value',
                {
                    value: '{{ queryset_filter.filter_data.search_value }}',
                    label: 'Search',
                    name: 'search_value',
                }
            )
        }"
    >
        Include block tags {% %} excluded due to mkdocs attempting to render file.
        <div class="col-12">
            include 'django_glue/form/field/char_field.html' with glue_field='search_value'
        </div>
    </div>
{% endblock %}

More Complex Example

app/person/views.py
from app.person.models import Person
from app.person.choices import PersonHairColourChoices
from django_spire.core.filtering.filters import QuerySetFilter


def person_list_view(request):
    queryset_filter = QuerySetFilter(
        request,
        filter_key='person_queryset_filter',
        default_filtering_data={
            'age': '25',
            'hair_colour_choices': [
                PersonHairColourChoices.BLACK,
                PersonHairColourChoices.BROWN
            ],
            'is_active': True
        }
    )

    people = queryset_filter.process_queryset(Person.objects.all())

    return TemplateResponse(
        request,
        'person/page/list_page.html',
        {
            'people': people,
            'queryset_filter': queryset_filter,
            'hair_colour_options': [
                [choice.value, choice.label]
                for choice in PersonHairColourChoices.choices
            ]
        }
    )
app/person/querysets.py
from django.db.models import Q, QuerySet
from django_spire.core.filtering.querysets import FilterQuerySet, SearchQuerySet
from django_spire.core.filtering.utils import query_multi_select_field, query_bool_select_field


class PersonQuerySet(FilterQuerySet, SearchQuerySet):
    def filter_by_query_dict(self, filter_data: dict) -> QuerySet:
        query = Q()

        age = filter_data.get('age')
        if age:
            query &= Q(age=age)

        query = query_bool_select_field(
            query,
            field_name='is_active',
            field_value=filter_data.get('is_active')
        )

        query = query_multi_select_field(
            query,
            field_name='hair_colour',
            field_value=filter_data.get('hair_colour_choices')
        )

        return self.filter(query).distinct().order_by('first_name', 'last_name')

    def search(self, search_query: str) -> QuerySet:
        return self.filter(
            Q(first_name__icontains=search_query) |
            Q(last_name__icontains=search_query)
        )
person/form/filter_form.html
{% extends 'django_spire/filtering/form/filter_form.html' %}

{% block filter_key %}{{ queryset_filter.filter_key }}{% endblock %}

{% block filter_content %}
    <div
        class="row"
        x-data="{
            init() {
                this.search_value.set_attribute('placeholder', 'Search first and last name...')
            },
            age: new GlueIntegerField(
                'age',
                {
                    value: {{ queryset_filter.filter_data.age|default:0 }},
                }
            ),
            search_value: new GlueCharField(
                'search_value',
                {
                    value: '{{ queryset_filter.filter_data.search_value }}',
                    label: 'Search',
                    name: 'search_value',
                }
            ),
            'hair_colour_choices': new GlueIntegerField(
                'hair_colour_choices',
                {
                    value: {{ queryset_filter.filter_data.hair_colour_choices|default:'null' }},
                    choices: {{ hair_colour_options }},
                }
            ),
            is_active: new GlueBooleanField(
                'is_active',
                {
                    label: 'Active',
                    choices: [[1, 'Yes'], [0, 'No']],
                    value: {{ queryset_filter.filter_data.is_active|default:'null' }}
                }
            ),
        }"
    >
        Include block tags {% %} excluded due to mkdocs attempting to render file.
        <div class="col-12">
            include 'django_glue/form/field/char_field.html' with glue_field='search_value'
        </div>
        <div class="col-12">
            include 'django_glue/form/field/number_field.html' with glue_field='age'
        </div>
        <div class="col-12">
            include 'django_glue/form/field/multi_select_field.html' with glue_field='hair_colour_choices'
        </div>
        <div class="col-12">
            include 'django_glue/form/field/select_field.html' with glue_field='is_active'
        </div>
    </div>
{% endblock %}