From 16269c4d0a5d2e61c7555fec438440abee9be9f5 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 8 Jun 2007 11:58:03 +0000 Subject: [PATCH] Fixed #3523 -- Added list unpacking to for loops in templates. Thanks to SmileyChris and Honza Kral for their work. git-svn-id: http://code.djangoproject.com/svn/django/trunk@5443 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/template/defaulttags.py | 56 +++++++++++++++++------- docs/templates.txt | 22 +++++++++- tests/regressiontests/templates/tests.py | 14 ++++++ 3 files changed, 74 insertions(+), 18 deletions(-) diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index 371b57e430..4f232f0c70 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -5,6 +5,7 @@ from django.template import TemplateSyntaxError, VariableDoesNotExist, BLOCK_TAG from django.template import get_library, Library, InvalidTemplateLibrary from django.conf import settings import sys +import re register = Library() @@ -61,8 +62,8 @@ class FirstOfNode(Node): return '' class ForNode(Node): - def __init__(self, loopvar, sequence, reversed, nodelist_loop): - self.loopvar, self.sequence = loopvar, sequence + def __init__(self, loopvars, sequence, reversed, nodelist_loop): + self.loopvars, self.sequence = loopvars, sequence self.reversed = reversed self.nodelist_loop = nodelist_loop @@ -72,7 +73,7 @@ class ForNode(Node): else: reversed = '' return "" % \ - (self.loopvar, self.sequence, len(self.nodelist_loop), reversed) + (', '.join( self.loopvars ), self.sequence, len(self.nodelist_loop), reversed) def __iter__(self): for node in self.nodelist_loop: @@ -107,6 +108,7 @@ class ForNode(Node): for index in range(len(data)-1, -1, -1): yield data[index] values = reverse(values) + unpack = len(self.loopvars) > 1 for i, item in enumerate(values): context['forloop'] = { # shortcuts for current loop iteration number @@ -120,9 +122,20 @@ class ForNode(Node): 'last': (i == len_values - 1), 'parentloop': parentloop, } - context[self.loopvar] = item + if unpack: + # If there are multiple loop variables, unpack the item into them. + context.update(dict(zip(self.loopvars, item))) + else: + context[self.loopvars[0]] = item for node in self.nodelist_loop: nodelist.append(node.render(context)) + if unpack: + # The loop variables were pushed on to the context so pop them + # off again. This is necessary because the tag lets the length + # of loopvars differ to the length of each set of items and we + # don't want to leave any vars from the previous loop on the + # context. + context.pop() context.pop() return nodelist.render(context) @@ -486,7 +499,7 @@ def do_filter(parser, token): nodelist = parser.parse(('endfilter',)) parser.delete_first_token() return FilterNode(filter_expr, nodelist) -filter = register.tag("filter", do_filter) +do_filter = register.tag("filter", do_filter) #@register.tag def firstof(parser, token): @@ -530,8 +543,14 @@ def do_for(parser, token): {% endfor %} - You can also loop over a list in reverse by using + You can loop over a list in reverse by using ``{% for obj in list reversed %}``. + + You can also unpack multiple values from a two-dimensional array:: + + {% for key,value in dict.items %} + {{ key }}: {{ value }} + {% endfor %} The for loop sets a number of variables available within the loop: @@ -552,18 +571,23 @@ def do_for(parser, token): """ bits = token.contents.split() - if len(bits) == 5 and bits[4] != 'reversed': - raise TemplateSyntaxError, "'for' statements with five words should end in 'reversed': %s" % token.contents - if len(bits) not in (4, 5): - raise TemplateSyntaxError, "'for' statements should have either four or five words: %s" % token.contents - if bits[2] != 'in': - raise TemplateSyntaxError, "'for' statement must contain 'in' as the second word: %s" % token.contents - loopvar = bits[1] - sequence = parser.compile_filter(bits[3]) - reversed = (len(bits) == 5) + if len(bits) < 4: + raise TemplateSyntaxError, "'for' statements should have at least four words: %s" % token.contents + + reversed = bits[-1] == 'reversed' + in_index = reversed and -3 or -2 + if bits[in_index] != 'in': + raise TemplateSyntaxError, "'for' statements should use the format 'for x in y': %s" % token.contents + + loopvars = re.sub(r' *, *', ',', ' '.join(bits[1:in_index])).split(',') + for var in loopvars: + if not var or ' ' in var: + raise TemplateSyntaxError, "'for' tag received an invalid argument: %s" % token.contents + + sequence = parser.compile_filter(bits[in_index+1]) nodelist_loop = parser.parse(('endfor',)) parser.delete_first_token() - return ForNode(loopvar, sequence, reversed, nodelist_loop) + return ForNode(loopvars, sequence, reversed, nodelist_loop) do_for = register.tag("for", do_for) def do_ifequal(parser, token, negate): diff --git a/docs/templates.txt b/docs/templates.txt index d8b511dedc..cb8e238f43 100644 --- a/docs/templates.txt +++ b/docs/templates.txt @@ -447,7 +447,7 @@ for ~~~ Loop over each item in an array. For example, to display a list of athletes -given ``athlete_list``:: +provided in ``athlete_list``:: -You can also loop over a list in reverse by using ``{% for obj in list reversed %}``. +You can loop over a list in reverse by using ``{% for obj in list reversed %}``. + +**New in Django development version** +If you need to loop over a list of lists, you can unpack the values +in eachs sub-list into a set of known names. For example, if your context contains +a list of (x,y) coordinates called ``points``, you could use the following +to output the list of points:: + + {% for x, y in points %} + There is a point at {{ x }},{{ y }} + {% endfor %} + +This can also be useful if you need to access the items in a dictionary. +For example, if your context contained a dictionary ``data``, the following +would display the keys and values of the dictionary:: + + {% for key, value in data.items %} + {{ key }}: {{ value }} + {% endfor %} The for loop sets a number of variables available within the loop: diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py index a5ed2dbf56..e983a539ae 100644 --- a/tests/regressiontests/templates/tests.py +++ b/tests/regressiontests/templates/tests.py @@ -289,6 +289,20 @@ class Templates(unittest.TestCase): 'for-tag-vars02': ("{% for val in values %}{{ forloop.counter0 }}{% endfor %}", {"values": [6, 6, 6]}, "012"), 'for-tag-vars03': ("{% for val in values %}{{ forloop.revcounter }}{% endfor %}", {"values": [6, 6, 6]}, "321"), 'for-tag-vars04': ("{% for val in values %}{{ forloop.revcounter0 }}{% endfor %}", {"values": [6, 6, 6]}, "210"), + 'for-tag-unpack01': ("{% for key,value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"), + 'for-tag-unpack03': ("{% for key, value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"), + 'for-tag-unpack04': ("{% for key , value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"), + 'for-tag-unpack05': ("{% for key ,value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"), + 'for-tag-unpack06': ("{% for key value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, template.TemplateSyntaxError), + 'for-tag-unpack07': ("{% for key,,value in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, template.TemplateSyntaxError), + 'for-tag-unpack08': ("{% for key,value, in items %}{{ key }}:{{ value }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, template.TemplateSyntaxError), + # Ensure that a single loopvar doesn't truncate the list in val. + 'for-tag-unpack09': ("{% for val in items %}{{ val.0 }}:{{ val.1 }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, "one:1/two:2/"), + # Otherwise, silently truncate if the length of loopvars differs to the length of each set of items. + 'for-tag-unpack10': ("{% for x,y in items %}{{ x }}:{{ y }}/{% endfor %}", {"items": (('one', 1, 'carrot'), ('two', 2, 'orange'))}, "one:1/two:2/"), + 'for-tag-unpack11': ("{% for x,y,z in items %}{{ x }}:{{ y }},{{ z }}/{% endfor %}", {"items": (('one', 1), ('two', 2))}, ("one:1,/two:2,/", "one:1,INVALID/two:2,INVALID/")), + 'for-tag-unpack12': ("{% for x,y,z in items %}{{ x }}:{{ y }},{{ z }}/{% endfor %}", {"items": (('one', 1, 'carrot'), ('two', 2))}, ("one:1,carrot/two:2,/", "one:1,carrot/two:2,INVALID/")), + 'for-tag-unpack13': ("{% for x,y,z in items %}{{ x }}:{{ y }},{{ z }}/{% endfor %}", {"items": (('one', 1, 'carrot'), ('two', 2, 'cheese'))}, ("one:1,carrot/two:2,cheese/", "one:1,carrot/two:2,cheese/")), ### IF TAG ################################################################ 'if-tag01': ("{% if foo %}yes{% else %}no{% endif %}", {"foo": True}, "yes"),