Handle multiple query parameters gracefully in Django.

Ru

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

Recommended reading.

Foundation.

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

  1. Filter by one parameter
  2. Filter by multiple parameters
  3. Retain the filters across pagination

While Vitor Freitas’ guide was helpful, the syntax was quite out-dated. So we did what we sometimes do — and find incredibly helpful — in case we 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. We also made one small change in the order of the arguments, as it makes more sense to us 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 us.
  5. As a bonus, we 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 we opted for a small duplication/repetition as it favors readability: return f"?{new_querydict.urlencode()}".

# {app_label}/templatetags/query_helpers.py

from django import template
from django.http import QueryDict

register = template.Library()

@register.simple_tag
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()}"
    else:
        # 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 %}
<nav>
  <a href="{% query_helpers 'page' 1 request.GET %}"></a>
  <a href="{% query_helpers 'page' 2 request.GET %}"></a>
</nav>

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