Handle multiple query parameters gracefully in Django.

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

I’m looking to hire a full-time junior Django developer on contract residing in any timezone from UTC+05:00 to UTC+08:00. Apply here.

Recommended reading.

Foundation.

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

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 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>

If you think I’m doing something wrong here, please let me know. I don’t know everything, no matter the programming language or the framework. It’s quite possible I might have made a silly or unintended mistake. 🙂

Join the discussion on Mastodon or Twitter, or write me an email.