浏览代码

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.
Sergei Maertens 8 年之前
父节点
当前提交
32c02f2a0e

+ 45 - 0
django/template/defaulttags.py

@@ -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):
     """

+ 54 - 0
docs/ref/templates/builtins.txt

@@ -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``

+ 3 - 0
docs/releases/1.11.txt

@@ -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
 ~~~~~
 

+ 95 - 0
tests/template_tests/syntax_tests/test_resetcycle.py

@@ -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')