16

I have a list of two dictionaries I am passing to a Django template:

base_parts = [
    {'important item': 43},
    {'lesser item': 22, 'lesser item': 3, 'lesser item': 45}
]

in my template I can do this:

{% 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 #}
    inner ID: {% forloop.counter0 %}< br/>
    outer ID: {% forloop.parentloop.counter0 %}< br/>

    {% endfor %}
{% endfor %}

As you can see, what I want is a running total of the total number of items I have iterated through, but both methods I have included return duplicates. I know I could concatenate the loops, but I am using a formset and really would like the ids to be indexed 0,1,2...etc.

Is there a way to achieve this type of count in the template?

Edit

Output at the moment looks like:

outerID: 0<br />
innerID: 0<br />
outerID: 0<br />
innerID: 1<br />
outerID: 1<br />
innerID: 0<br />
outerID: 1<br />
innerID: 1<br />
outerID: 1<br />
innerID: 2<br />

I want:

totalID: 0<br />
totalID: 1<br />
totalID: 2<br />
totalID: 3<br />
totalID: 4<br />
totalID: 5<br />
totalID: 6<br />
totalID: 7<br />
totalID: 8<br />
totalID: 9<br />
halfer
  • 19,824
  • 17
  • 99
  • 186
Darwin Tech
  • 18,449
  • 38
  • 112
  • 187

7 Answers7

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)
dprothero
  • 2,683
  • 2
  • 21
  • 28
username
  • 4,258
  • 1
  • 16
  • 26
  • 1
    Cool! works great with assignment tag! `@register.assignment_tag \n def get_counter(start=1): \n return itertools.count(start)` – laffuste Aug 27 '14 at 10:06
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 %}
djvg
  • 11,722
  • 5
  • 72
  • 103
Enrico
  • 10,377
  • 8
  • 44
  • 55
  • I don't think this would work. For example: Suppose both loops have three iterations, you would then get: `1, 2, 3, 2, 3, 4` – djvg Oct 17 '22 at 09:00
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.

username
  • 4,258
  • 1
  • 16
  • 26
  • But this never changes the value of `total` - it just adds 1. – Darwin Tech Dec 14 '12 at 01:04
  • Then this is a bad answer. I thought it might work. But as Django template philosophy suggests: `the template system is meant to express presentation, not program logic.` We have to follow these rules. So counting the list items in the view and passing that object to the template would be a better approach. – username Dec 14 '12 at 01:11
  • Yes, this seems like too much to do in the template. I think better approach is to compile the list of dictionaries to list of dictionaries where each element in the list is a single key value pair. – Darwin Tech Dec 14 '12 at 01:40
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 %}
Raunak Agarwal
  • 7,117
  • 6
  • 38
  • 62
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.

Darwin Tech
  • 18,449
  • 38
  • 112
  • 187
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
  • 11,722
  • 5
  • 72
  • 103
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.

Mathieu Dhondt
  • 8,405
  • 5
  • 37
  • 58