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

Out of interest, did anyone spot GitHub - ababic/django-querystring-tag: A single, powerful template tag for modifying and rendering safe, suitably-encoded querystring values. It's the clean and simple way to create pagination links, filters, and other state-preserving links? I built it a long time ago now, and it’s solid and well-tested.

Hi Andy :wave:

The patch has already landed so it’s a bit late for another alternative. However, I think your code wouldn’t have been suitable as-is as (IMO) the implementation is more complex and comes with things that I think are too specific to include in the framework (UTM stripping, for example). The patch we ended up with was much simpler and a btt more universal.

So I think I ran into an issue that we might consider before we release 5.1. One thing that I now ran into is that I would like to specify a variable for a key, ie

{% query_string some_var=some_other_var %}

which is currently not possible and if we added it later on would be ambiguous when manually setting strings. The only thing I can think of is change the behavior to:

{% query_string 'page'=some_other_var %} &
{% query_string page=some_other_var %}

So essentially literal keys would need quoting. This is rather ugly, but I still would like to throw it out here for (re)consideration.

/cc our fellows @nessita @sarahboyce for visibility.

Thank you for raising this @apollo13 and for trying out the feature during the alpha period :partying_face:

I have created a ticket to track this: #35523 (query_string cannot use variables as keys) – Django

FWIW I don’t have a strong opinion either way. The proposal sounds useful but also looks a bit confusing to me (do we do this for other tags? not sure)

I don’t have strong feelings either. I just found that django-tables2 has a valid usecase for it. You can paginate via ?page=123&per_page=25&order_by=yolo and it allows you to customize those keys. Hence their template uses (with their own implementation of querystring currently):

 <a href="{% querystring table.prefixed_order_by_field=column.order_by_alias.next %}">{{ column.header }}</a>

do we do this for other tags? not sure

Well kinda, for instance the cycle tag supports raw literals and variables: Built-in template tags and filters | Django documentation | Django

Same goes for other tags, so generally we do allow variables and then literal strings via quoting.

A better example is maybe the include/with tag though where we don’t allow it:

{% include "name_snippet.html" with person="Jane" greeting="Hello" %}
{% with total=business.employees.count %}

That said I would argue that it makes sense for those two tags to have static lists of variable because you know what the content will contain.

EDIT:// I guess query_string is the first tag that allows for key=value pairs aside from include/with, so we might be in uncharted land here :slight_smile:

I don’t have a super strong opinion about it but I’m not in favor of having to quote keys. I feel that it makes the “normal” use-case more complicated for the benefit of a rare one.

We also lose the parallel with standard Python syntax which adds a small barrier to learning.

To me, this could be fixed instead by adding support for *args and **kwargs syntax in the template language, but I realize that’s quite a big undertaking (though it would also solve similar issues with the {% cycle %} tag.

This is convincing to me. I think the right way to fix this is with *args and **kwargs support. The proposed syntax doesn’t seem to match any other code and feels a bit like an outlier.