Recommended reading.
- The Custom template tags and filters how-to on the official Django documentation.
- Dealing With QueryString Parameters authored by Vitor Freitas.
- Inspired by the post Working with URLs in Python & Django authored by Adrienne Domingus. Not necessary for you to go through, just passing on some deserved credit. 🙂
Foundation.
We have a model that has multiple, independent choice fields. In a non-admin view, We want to be able to:
- Filter by one parameter
- Filter by multiple parameters
- 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:
- Renamed variables so that they make sense to me, the consumer of a piece of code.
- Added some optional type hints to get all that IDE goodness: autocomplete suggestions and docstrings for classes, methods, and functions.
- 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!
- 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.
- As a bonus, we also opted to use
QueryDict
across the function definition as inspired from Adrienne Domingus. This is in addition to receiving therequest.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>