[Fixed]-Django template counter in nested loops

17👍

I found a better solution with itertools. (Better than my previous answer)
You can set current state of the loop to the itertools variable sent to the view context.
This time i tried on a dummy Django project and it works like a charm.

views.py:

from django.shortcuts import render_to_response
import itertools

def home(request):
    iterator=itertools.count()
    base_parts = [
        {'important item': 43},
        {'lesser item1': 22, 'lesser item2': 3, 'lesser item3': 45},
        {'most important item': 55}
    ]
    return render_to_response('index.html', 
                             {'base_parts': base_parts, 'iterator':iterator})

index.html:

{% for base_part in base_parts %}
    {% for k, v in base_part.items %}
        {{ iterator.next }} - {{ v }}<br/>
    {% endfor %}
{% endfor %}

HTML Output:

0 - 43
1 - 22
2 - 45
3 - 3
4 - 55

Sorted values:

(This part is not an answer to the actual question. It’s more like I’m playing around)

You can use Django’s SortedDict instead of Python’s built-in dictionary to keep items order.

views.py

from django.shortcuts import render_to_response
import itertools
from django.utils.datastructures import SortedDict

def home(request):
    iterator=itertools.count()
    base_parts = [
        SortedDict([('important item', 43)]),
        SortedDict([('lesser item1', 22), 
                    ('lesser item2', 3), 
                    ('lesser item3', 45)]),
        SortedDict([('most important item', 55)])
    ]
    print base_parts[1]
    return render_to_response('index.html', 
                             {'base_parts': base_parts, 'iterator':iterator})

HTML Output:

0 - 43
1 - 22
2 - 3
3 - 45
4 - 55

Edit 2014-May-25

You can also use collections.OrderedDict instead of Django’s SortedDict.

Edit 2016-June-28

Calling iterator.next doesn’t work in Python 3. You can create your own iterator class, inheriting from itertools.count:

import itertools
class TemplateIterator(itertools.count):
    def next(self):
        return next(self)

4👍

In an ideal world, you should avoid putting this kind of logic in the template. If you are not preserving the hierarchy in your output (eg displaying these items as a list of lists) flatten the list and use a simple for loop and the loop counter.

However, the ideal solution isn’t always an option. In theory, I believe the following could/should work

{% for base_part in base_parts %}     
    {% with outerCounter = forloop.parentloop.counter0 %}
    {% for k, v in base_part.items %}
        ...
        {% with innerCounter = forloop.counter %}
        {{ outerCounter|add:innerCounter }}
    {% endfor %}
{% endfor %}
👤Enrico

1👍

UPDATE: This is not a correct answer. I am just keeping it here to display what doesn’t work.

I have to admit that i haven’t tried this one but you can use with and add statements.

{% with total=0 %}
    {% for base_part in base_parts %}
        {% for k, v in base_part.items %}

        {# ...do stuff #}

        {# I try to get a running total of items to use as an ID #}
        totalId: {{ total|add:"1" }} <br/>
        {% endfor %}
    {% endfor %}
{% endwith %}

This would probably work on template level but i think a better approach is calculating it on the view level and passing a dictionary to the template which includes calculated values.

0👍

It would be quite and efficient to do it at the view code…

    #Calculate the count
    count = 0
    for item in base_parts:
       count+=len(item.keys())

    run_range = range(count)

    return to the template

    #template
    {% for x in run_range|slice:'1' %}
    {{x}}
    {% endfor %}

0👍

So I actually just appended the dictionary items to a list, ensuring that one I wanted was first:

base_parts = []
for k, v in rails_dic.items():
    base_parts.append({k: v})
for k, v in accessories_dic.items():
    base_parts.append({k: v})

This may not be the best way to do this, and I am open to a more pythonic way as the correct answer.

I think that taking care of this in the template is both tricky and not appropriate. This is why I my not accepting those answers. I would like that future visitors can see that the solution was sought for the overall problem in the more correct way, not just how to hack it in the templates.

0👍

A lot of good answers already, but I didn’t see a memoizing tag yet:

@register.simple_tag
def total_count(initial=None, _count=[0]):  # noqa
    if initial is not None:
        # reset counter and make sure nothing is printed
        _count[0] = initial
        return ''
    # increment counter
    _count[0] += 1
    return _count[0]

This is actually a generic counter that is incremented every time it is called. It uses a mutable default _count as its "memory."

The counter can be reset by specifying an initial value. For example, {% total_count 0 %} resets the "memory" to 0, so the next count will be 1. Similarly, {% total_count -1 %} will start the count at 0.

Here’s how to use it in a template:

{% total_count 0 %}
{% for ... %}
    {% for ... %}
        {% total_count %}
    {% endfor %}
{% endfor %}

Here’s another example that resets the counter on every iteration of the outermost loop:

{% for ... %}
    {% total_count 0 %}
    {% for ... %}
        {% for ... %}
            {% total_count %}
        {% endfor %}
    {% endfor %}
{% endfor %}

I really like the itertools.count() solution as well, but it requires us to adapt both the view and the template, and it cannot be easily reset from inside the template.

👤djvg

0👍

I was looking for the same thing but stubbornly wanted to do this in the template itself. I came up with this solution:

cart = {'fruit': ['apples', 'oranges'], 'veggies': ['sprouts', 'lettuce']}

In the template:

{% for category, products in cart.items %}
    {% with products_length=products|length %}
        {% for product in products %}
            {% widthratio forloop.parentloop.counter0 1 products_length as counter %}
            Counter: {{ counter|add:forloop.counter0 }}
        {% endfor %}
    {% endwith %}
{% endfor %}

Result:

Counter: 0

Counter: 1

Counter: 2

Counter: 3

Hat tip to @R. for the widthratio hack.

Leave a comment