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:
parent
2cfd48bccd
commit
32c02f2a0e
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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``
|
||||
|
|
|
@ -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
|
||||
~~~~~
|
||||
|
||||
|
|
|
@ -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')
|
Loading…
Reference in New Issue