Django’s template syntax, enhanced ifchanged

This article generally discusses Django’s template syntax and in detail offers a patch for the existing tag ifchanged. The patch allows to pass a parameter to ifchanged, which is then evaluated and checked if it has changed, instead of the tag’s content. If it has changed the content is being printed, as usual. I.e. you can do
{% ifchanged variable %}print if variable's value has changed{% ifchanged %}
You can download the patch here, the Django-ticket is here. To know more about the details, read on.

I am writing an application using Django and doing that I am using the Django templates, which have a very clean syntax in my eyes. The logic is all implemented in the tags itself. But is also implies that for every special case that can not be achieved by any combination of tags you are either stuck or need to write custom tags, which seems not very handy to me. With my current knowledge I think I would prefer using a bit more intelligent logic inside templates, not a full blown programming language but a simple enough one for easier building presentation logic. Some examples follow to explain what I mean.

Alternating rows
For alternating rows inside a for loop I normally use the modulo operator (i.e. in PHP) which is row%2. That would look like:

{% if forloop.counter%2==0 %}
    row1
{% else %}
    row2
{% endif %}

But that doesn’t work in Django templates :-(. That is what I mean by simple logic inside templates, using modulo and comparing it to a number. That is pretty simple I think.
Instead you have to learn another tag (you have to find out first!):
{% cycle row1,row2 %}
“row1″ and “row2″ are strings that are printed alternately. But they better have no spaces! So that is quite limiting I think.

Nested loops
If a for-loop is nested inside another referring to “forloop.counter” is ambiguous.

{% for country in countries %}
    {% for city in country.cities %}
        {{ forloop.counter }}
    {% endfor %}
{% endfor %}

What value does forloop.counter print? If it is always printing the inner for loop’s counter, how do I access the outer for loop’s counter?
I am not an expert yet in Django templates, may be someone can even teach me that all my concerns are invalid.

My use case
My problem was a mix of the both above mentioned cases. Imagine listing all cities for a country in a certain way (I use country and cities for easier explaining, since the relationship between them doesn’t need to be explained). For some design reason I just want to put every city in it’s own div box and style them just alternately for each country. I.e. like so:


Berlin
Munich
Hamburg
Barcelona
Valencia
Madrid
Moscow
Kiev
St. Petersburg

I think you get the idea. So, how do I implement that in a Django template? I want to pass the pure data into the template and inside the template I have to figure out how to style it. That’s what I assume as presentation logic, and in my eyes that belongs there, but let’s not start a flame war about those points of view here. Having a strong PHP background my first thoughts were like that:

{% for country in counter=>countries %}
    {% for city in country.cities %}
{{ city.name }}
{% endfor %} {% endfor %}

There are two problems. There is no such for-syntax as {% for country in counter=>countries %} the part in italic font is no valid Django template syntax. Ok, no big deal I thought first, I can use forloop.counter0. But I have two nested for loops, so how does it know I refer to the outer for loop? It can’t. Bummer, no solution here. Let’s keep moving.
The second problem is the {% if counter%2==1 %}, the modulo and comparison operations are both not allowed in Django templates. And using {% ifchanged %} at this position doesn’t help either sinice it for one refers to the inner loop (which are the cities) and second it would look like this: {% ifchanged %}Alternate{% endifchanged %} and that is not how the ifchanged works, since the content inside the tags doesn’t change.

The enhanced ifchanged syntax
Having ifchanged accept a parameter would be phantastic. Like so:

{% for country in countries %}
    {% for city in country.cities %}
{{ city.name }}
{% endfor %} {% endfor %}

The ifchanged tag should be able to check if a certain variable has changed if given, otherwise work as usual.

Pros:

  • This way ifchanged can be used as before
  • and you can additionally use it inside loops,
  • even in nested loops of any depth.
  • You can refer to whatever variable you want to check and
  • the syntax is very readable in my eyes.

Cons:

  • The command doesn’t look so consistent anymore. Without parameters it refer to the tag’c content, with parameters it refers to the parameters value.

So this is just an enhancement of the current syntax. And a very useful (for me at least). I wouldn’t have known how to do it otherwise. I was even asking on #django and got some great help there. But it was unfortunately not really satisfying, since the suggestion was a CSS based solution, which I was not in favor of, simply because it puts the logic in a place where I don’t expect it. But that is my very subjective opinion.

The patch
Of course I have written a patch for it, which simply enhances the ifchanged syntax, which you can download here. Open source is not all about getting but also giving. So here it is, I would be happy to see it go into Django’s template system. I opened a trac ticket and see what happens.
Btw, I got another patch pending, but I will have to test it out first and will provide it when I feel comfortable.

Finally
There is an alike ticket already in Django’s trac, I just don’t really understand the problem there, though the screenshots look very much as if the problem is the same, but a different approach. May be my patch helps here.
If I have missed a simple solution to how my problem can be solved without the need of enhancing ifchanged with my patch, I would be greatly interested. I just simply don’t know all the details and facets of Django’s template language yet, may be I missed something there. If so please tell me!

[Update]
I just realized that the usage of my enhanced ifchanged doesn’t even finally get me where I want, since it only is true when for the first element of each country … hehe. So my reasoning is not all correct, but the patch is there anyway. And might be useful too.

[Update2]
I have added a correction about what I have done wrong here, you can find it here.

5 Comments »

  1. Thiago said,

    December 19, 2008 at 5:47 pm

    i have the same problem and found this:

    Variable Description
    forloop.counter The current iteration of the loop (1-indexed)
    forloop.counter0 The current iteration of the loop (0-indexed)
    forloop.revcounter The number of iterations from the end of the loop (1-indexed)
    forloop.revcounter0 The number of iterations from the end of the loop (0-indexed)
    forloop.first True if this is the first time through the loop
    forloop.last True if this is the last time through the loop
    forloop.parentloop For nested loops, this is the loop “above” the current one

    I hope it works for you.

  2. monokrome said,

    January 20, 2010 at 8:03 pm

    This is the standard format for alternating rows in dJango:

    {% for item in item_list %}
    {{ item }}
    {% endfor %}

  3. monokrome said,

    January 20, 2010 at 8:05 pm

    My last comment would have looked a lot more interesting if the HTML was properly changed. The template tag you put in your class is:

    {% cycle “odd” “even” %}

    This will give you an odd and an even class throughout the loop on the cycled items.

  4. ksamuel said,

    September 17, 2011 at 9:34 pm

    What you would do using modulus, you can do with the template filter “divisibleby”:

    {% if forloop.counter|divisibleby:”3″ %}
    do stuff
    {% endif %}

    If the equivalent of

    {% if forloop.counter % 3 == 0 %}
    do stuff
    {% endif %}

  5. Johnie said,

    October 19, 2011 at 6:55 am

    Solche Artikel mag ich!

RSS feed for comments on this post · TrackBack URL

Leave a Comment