Kaydet (Commit) 32c02f2a authored tarafından Sergei Maertens's avatar Sergei Maertens Kaydeden (comit) Tim Graham

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.
üst 2cfd48bc
......@@ -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
~~~~~
......
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')
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment