Handle multiple query parameters gracefully in Django.

A snippet for handling single or multiple query parameters gracefully and across pagination in Django 3.

This is a part of the 100 Days To Offload challenge.

Recommended reading.


I have a model that has multiple, independent choice fields. In a non-admin view, I want to be able to:

  1. Filter by one parameter
  2. Filter by multiple parameters
  3. Retain the filters across pagination, which itself is another query parameter.

While Vitor Freitas’ guide was helpful, the syntax was quite out-dated, and much more importantly, I didn’t quite get what was happening.

So I did what I sometimes do — and find incredibly helpful — in case I want to integrate an internet snippet:

  1. Renamed variables so that they make sense to me, the consumer of a piece of code.
  2. Added some optional type hints to get all that IDE goodness: autocomplete suggestions and docstrings for classes, methods, and functions.
  3. Added a bunch of inline comments – never forget comments! They’re so helpfulโ€ฆ to you today, to you a few months from now, and to anyone else at any point in time!
  4. I also made one small change in the order of the arguments, as it makes more sense to me this way: swap value and key being passed. Now they read as, “What query key do I want to set, and what value do I want to set on it?” which feels way more natural to me.
  5. As a bonus, I also opted to use QueryDict across the function definition as inspired from Adrienne Domingus. This is in addition to receiving the request.GET QueryDict directly, instead of its string representation.

My snippet.

Note that I opted for a small duplication/repetition on line 28 and 39 as it favors readability.

# {app_label}/templatetags/query_helpers.py

from django import template
from django.http import QueryDict

register = template.Library()

def relative_url(
        param_key: str,
        param_value: str,
        querydict: QueryDict = None
    ) -> str:
    Construct and return a query params string.
    Arguments passed to this function take
    precedence over any existing query param
    for a request.
    # QueryDicts are immutable by default.
    new_querydict = QueryDict(mutable=True)
    new_querydict.appendlist(param_key, param_value)

    if not querydict:
        # No existing query params in the URL, so we're done.
        # We just needed to append the args that were
        # passed to this function.
        return f"?{new_querydict.urlencode()}"
        # This means there are some existing params in the URL.
        # We'd like to append them now and return that URL.
        # At the same time, we want to ensure that the
        # param_key and param_value passed to the function
        # take precedence over the one in the URL.
        # That happens when we check for key != param_key.
        for key, value in querydict.items():
            if key != param_key:
                new_querydict.appendlist(key, value)
        return f"?{new_querydict.urlencode()}"

Sample usage.

# templates/{app_label}/{model_name}_list.html

{% load query_helpers %}

{% comment %} A simple pagination for demonstration {% endcomment %}
  <a href="{% query_helpers 'page' 1 request.GET %}"></a>
  <a href="{% query_helpers 'page' 2 request.GET %}"></a>

{% comment %} Some other simple filter for demonstration {% endcomment %}
  <a href="{% query_helpers 'priority' 'HIGH' request.GET %}"></a>
  <a href="{% query_helpers 'priority' 'LOW' request.GET %}"></a>

Comment via email.