Fixed #5908 -- Added {% resetcycle %} template tag.

Thanks to Simon Litchfield for the report, Uninen for the initial
patch, akaihola, jamesp, b.schube, and Florian Appoloner for
subsequent patches, tests, and documentation.
This commit is contained in:
Sergei Maertens 2016-07-03 16:19:06 +02:00 committed by Tim Graham
parent 2cfd48bccd
commit 32c02f2a0e
4 changed files with 197 additions and 0 deletions

View File

@ -88,6 +88,12 @@ class CycleNode(Node):
return ''
return render_value_in_context(value, context)
def reset(self, context):
"""
Reset the cycle iteration back to the beginning.
"""
context.render_context[self] = itertools_cycle(self.cyclevars)
class DebugNode(Node):
def render(self, context):
@ -387,6 +393,15 @@ class NowNode(Node):
return formatted
class ResetCycleNode(Node):
def __init__(self, node):
self.node = node
def render(self, context):
self.node.reset(context)
return ''
class SpacelessNode(Node):
def __init__(self, nodelist):
self.nodelist = nodelist
@ -582,6 +597,9 @@ def cycle(parser, token):
# that names are only unique within each template (as opposed to using
# a global variable, which would make cycle names have to be unique across
# *all* templates.
#
# It keeps the last node in the parser to be able to reset it with
# {% resetcycle %}.
args = token.split_contents()
@ -621,6 +639,7 @@ def cycle(parser, token):
else:
values = [parser.compile_filter(arg) for arg in args[1:]]
node = CycleNode(values)
parser._last_cycle_node = node
return node
@ -1216,6 +1235,32 @@ def regroup(parser, token):
return RegroupNode(target, expression, var_name)
@register.tag
def resetcycle(parser, token):
"""
Resets a cycle tag.
If an argument is given, resets the last rendered cycle tag whose name
matches the argument, else resets the last rendered cycle tag (named or
unnamed).
"""
args = token.split_contents()
if len(args) > 2:
raise TemplateSyntaxError("%r tag accepts at most one argument." % args[0])
if len(args) == 2:
name = args[1]
try:
return ResetCycleNode(parser._named_cycle_nodes[name])
except (AttributeError, KeyError):
raise TemplateSyntaxError("Named cycle '%s' does not exist." % name)
try:
return ResetCycleNode(parser._last_cycle_node)
except AttributeError:
raise TemplateSyntaxError("No cycles in template.")
@register.tag
def spaceless(parser, token):
"""

View File

@ -185,6 +185,9 @@ call to ``{% cycle %}`` doesn't specify ``silent``::
{% cycle 'row1' 'row2' as rowcolors silent %}
{% cycle rowcolors %}
You can use the :ttag:`resetcycle` tag to make a ``{% cycle %}`` tag restart
from its first value when it's next encountered.
.. templatetag:: debug
``debug``
@ -994,6 +997,57 @@ attribute, allowing you to group on the display string rather than the
``{{ country.grouper }}`` will now display the value fields from the
``choices`` set rather than the keys.
.. templatetag:: resetcycle
``resetcycle``
--------------
.. versionadded:: 1.11
Resets a previous `cycle`_ so that it restarts from its first item at its next
encounter. Without arguments, ``{% resetcycle %}`` will reset the last
``{% cycle %}`` defined in the template.
Example usage::
{% for coach in coach_list %}
<h1>{{ coach.name }}</h1>
{% for athlete in coach.athlete_set.all %}
<p class="{% cycle 'odd' 'even' %}">{{ athlete.name }}</p>
{% endfor %}
{% resetcycle %}
{% endfor %}
This example would return this HTML::
<h1>José Mourinho</h1>
<p class="odd">Thibaut Courtois</p>
<p class="even">John Terry</p>
<p class="odd">Eden Hazard</p>
<h1>Carlo Ancelotti</h1>
<p class="odd">Manuel Neuer</p>
<p class="even">Thomas Müller</p>
Notice how the first block ends with ``class="odd"`` and the new one starts
with ``class="odd"``. Without the ``{% resetcycle %}`` tag, the second block
would start with ``class="even"``.
You can also reset named cycle tags::
{% for item in list %}
<p class="{% cycle 'odd' 'even' as stripe %} {% cycle 'major' 'minor' 'minor' 'minor' 'minor' as tick %}">
{{ item.data }}
</p>
{% ifchanged item.category %}
<h1>{{ item.category }}</h1>
{% if not forloop.first %}{% resetcycle tick %}{% endif %}
{% endifchanged %}
{% endfor %}
In this example, we have both the alternating odd/even rows and a "major" row
every fifth row. Only the five-row cycle is reset when a category changes.
.. templatetag:: spaceless
``spaceless``

View File

@ -313,6 +313,9 @@ Templates
so you can unpack the group object directly in a loop, e.g.
``{% for grouper, list in regrouped %}``.
* Added a :ttag:`resetcycle` template tag to allow resetting the sequence of
the :ttag:`cycle` template tag.
Tests
~~~~~

View File

@ -0,0 +1,95 @@
from django.template import TemplateSyntaxError
from django.test import SimpleTestCase
from ..utils import setup
class ResetCycleTagTests(SimpleTestCase):
@setup({'resetcycle01': "{% resetcycle %}"})
def test_resetcycle01(self):
with self.assertRaisesMessage(TemplateSyntaxError, "No cycles in template."):
self.engine.get_template('resetcycle01')
@setup({'resetcycle02': "{% resetcycle undefinedcycle %}"})
def test_resetcycle02(self):
with self.assertRaisesMessage(TemplateSyntaxError, "Named cycle 'undefinedcycle' does not exist."):
self.engine.get_template('resetcycle02')
@setup({'resetcycle03': "{% cycle 'a' 'b' %}{% resetcycle undefinedcycle %}"})
def test_resetcycle03(self):
with self.assertRaisesMessage(TemplateSyntaxError, "Named cycle 'undefinedcycle' does not exist."):
self.engine.get_template('resetcycle03')
@setup({'resetcycle04': "{% cycle 'a' 'b' as ab %}{% resetcycle undefinedcycle %}"})
def test_resetcycle04(self):
with self.assertRaisesMessage(TemplateSyntaxError, "Named cycle 'undefinedcycle' does not exist."):
self.engine.get_template('resetcycle04')
@setup({'resetcycle05': "{% for i in test %}{% cycle 'a' 'b' %}{% resetcycle %}{% endfor %}"})
def test_resetcycle05(self):
output = self.engine.render_to_string('resetcycle05', {'test': list(range(5))})
self.assertEqual(output, 'aaaaa')
@setup({'resetcycle06': "{% cycle 'a' 'b' 'c' as abc %}"
"{% for i in test %}"
"{% cycle abc %}"
"{% cycle '-' '+' %}"
"{% resetcycle %}"
"{% endfor %}"})
def test_resetcycle06(self):
output = self.engine.render_to_string('resetcycle06', {'test': list(range(5))})
self.assertEqual(output, 'ab-c-a-b-c-')
@setup({'resetcycle07': "{% cycle 'a' 'b' 'c' as abc %}"
"{% for i in test %}"
"{% resetcycle abc %}"
"{% cycle abc %}"
"{% cycle '-' '+' %}"
"{% endfor %}"})
def test_resetcycle07(self):
output = self.engine.render_to_string('resetcycle07', {'test': list(range(5))})
self.assertEqual(output, 'aa-a+a-a+a-')
@setup({'resetcycle08': "{% for i in outer %}"
"{% for j in inner %}"
"{% cycle 'a' 'b' %}"
"{% endfor %}"
"{% resetcycle %}"
"{% endfor %}"})
def test_resetcycle08(self):
output = self.engine.render_to_string('resetcycle08', {'outer': list(range(2)), 'inner': list(range(3))})
self.assertEqual(output, 'abaaba')
@setup({'resetcycle09': "{% for i in outer %}"
"{% cycle 'a' 'b' %}"
"{% for j in inner %}"
"{% cycle 'X' 'Y' %}"
"{% endfor %}"
"{% resetcycle %}"
"{% endfor %}"})
def test_resetcycle09(self):
output = self.engine.render_to_string('resetcycle09', {'outer': list(range(2)), 'inner': list(range(3))})
self.assertEqual(output, 'aXYXbXYX')
@setup({'resetcycle10': "{% for i in test %}"
"{% cycle 'X' 'Y' 'Z' as XYZ %}"
"{% cycle 'a' 'b' 'c' as abc %}"
"{% ifequal i 1 %}"
"{% resetcycle abc %}"
"{% endifequal %}"
"{% endfor %}"})
def test_resetcycle10(self):
output = self.engine.render_to_string('resetcycle10', {'test': list(range(5))})
self.assertEqual(output, 'XaYbZaXbYc')
@setup({'resetcycle11': "{% for i in test %}"
"{% cycle 'X' 'Y' 'Z' as XYZ %}"
"{% cycle 'a' 'b' 'c' as abc %}"
"{% ifequal i 1 %}"
"{% resetcycle XYZ %}"
"{% endifequal %}"
"{% endfor %}"})
def test_resetcycle11(self):
output = self.engine.render_to_string('resetcycle11', {'test': list(range(5))})
self.assertEqual(output, 'XaYbXcYaZb')