Adding a template tag to generate query strings

https://code.djangoproject.com/ticket/10941

This is a common use-case. It mostly appears when you have a view with a bunch of filters and also pagination, but there are other times it would be handy.

The ticket (and mailing list post) seems to have stalled, so I’m looking for input here. One suggestion is that this doesn’t belong in Django core. Maybe, in which case we should close the ticket. However, the suggested package, django-spurl hasn’t had an update in two years (it probably still works, but I don’t like having to rely on unmaintained packages).

I also think the use-case is common enough that we should support it in core, if the solution is simple enough. I think a variation of Carsten’s snippet in the ticket strikes me as the right balance. It’s just a handful of lines and quite easy to test. supports changing query string values as well as adding and removing them from an existing query string.

What do people think?

2 Likes

This comes up again and again and again. I’d be keen to have tags and utilities in core for it. (DRF has similar for the Browsable API)

+1, my opinion on this hasn’t changed in the last eight years (cough, #10941 (Add a templatetag to generate querystrings) – Django )

1 Like

+1, a very common use case. And yes let’s keep it simple like Carsten’s approach. I’d rather the tag took request than request.GET—or at least, it works if passed either, to support other QueryDicts.

Thanks, then I will take a look at this tonight / next week, time & motivation permitting.

3 Likes

We have difficulties to reach a consensus on use and implementation, check out

As far as I’m aware, we have two options:

  1. template tag {% query_string %} to modify and return the current query string (from request.GET) by passing keyword arguments.
  2. template tag {% query_string %} to modify any query dict by passing keyword argument, other dicts, or query dicts.

Personally, I prefer the first option because the second is unnecessarily complicated, it seems rather niche, and IMO template tags are not the place for such complex modifications.

I agree with Mariusz here.

There is a third half-way option that the PR currently implements where you can optionally specify a QueryDict other than request.GET - or ebe very explicit and pass request.GET yourself - as the first positional argument, but doesn’t allow to add any extra dicts or QueryDicts to merge. I’m not sure I’ve ever needed this personally, and I’m struggling to think of any common cases for it.

First of all, many thanks to everyone who is spending effort on this!

Let me summarize my arguments in favor of the second option above as described by @felixxm, alternatively the third option as currently implemented by @tom:

My main use case is monthly reports that are queried with parameters like ?year=2023&month=12. Each report links to the reports of the previous and next months, e. g. ?year=2024&month=1. I think we agree that computing such values for the offset periods should be done in the view, not in the template. However, passing such values from the view via the template context to the query_string tag comes with a lot of bloat, which could easily avoided if such parameters could be grouped and passed around in a dict.

In my opinion, passing previous and next time periods is not niche, rare or more complicated than pagination. In fact, I think that both types of next/previous-linking match quite well and that both have frequent use cases.
Pagination dominates the examples, blog posts and teaching material, but I believe that there are many projects that use time periods for pagination as well.

I have some problems with coming up with more elaborative, expressive examples, but please note that the scope and usefulness of using dicts is obviously not limited to time periods. It easily scales up for any report that has lot of options for the user to filter what the view shows.

About the API, I don’t feel like the making the request.GET parameter implicit leads to a clearer and easier to understand user experience. In my opinion, short-circuiting request.GET comes with a high price: It gives up power and flexibility while making the tag too limited and thus unusable for even moderately complex use cases.
Also, while I see value in implicity, too, the implicity inherently goes along with (a different kind of) complexity as well. Personally, this tag is one of the cases where I’d prefer to see what’s going on explicitly and to me, this even seems less than more complex.

Thanks a lot for your consideration!

On my backlog I have this one…

https://code.djangoproject.com/ticket/5865

You want to be able to do this: {% cycle colors %} but can’t.

I’d like to allow * and ** expansion here, so you’d do {% cycle *colors %} or (the relevance here)…

{% query_string **mydict extra=value %}

Whether or not we get there, @carstenfuchs could I get what you need wrapping the suggested function from the PR:

from django.template.defaulttags import query_string


@register.simple_tag(takes_context=True)
def with_dicts_query_string(context, query_dict=None, *args, **kwargs):
    """
    Wraps query_string() to accept a list of dicts before the kwargs. 
    """
    new_kwargs = dict(
        {k:v for d in args for k,v in d.items()},
        **kwargs,
    )
    return query_string(context, query_dict, **new_kwargs)

?

(That’s untested so answer may well be no.)

I was thinking this is probably worth having… — I’m redirecting to another view and need to build the query string for that.

If I have the QueryDict I can do query_dict.urlencode in the template. Do I need to override keys from that based on other template vars? Maybe/Not sure/Probably :thinking:

1 Like

Yes, this is what I’ll have to fall back to if the agreement is to not support dicts.

Thanks for pointing to the bigger picture about * and ** expansion!

As I think I mentioned in the PR, if there is a great demand for this, it would be a backwards compatible change to add support for multiple dicts and QueryDicts via positional args to the tag. So for me, I think it’s okay to go ahead with the current implementation (if there’s enough agreement), then it’s a fairly small patch if we decide we want to add support for more later.

3 Likes