Code

Ticket #5908: 5908-resetcycle.3.diff

File 5908-resetcycle.3.diff, 7.3 KB (added by jamesp, 3 years ago)

Updated patch to current trunk; added TemplateSyntaxError tests

Line 
1diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py
2index 06838cf..4b1f8ce 100644
3--- a/django/template/defaulttags.py
4+++ b/django/template/defaulttags.py
5@@ -111,6 +111,9 @@ class CycleNode(Node):
6         self.variable_name = variable_name
7         self.silent = silent
8 
9+    def reset(self, context):
10+        context.render_context[self] = itertools_cycle(self.cyclevars)
11+
12     def render(self, context):
13         if self not in context.render_context:
14             # First time the node is rendered in template
15@@ -123,6 +126,14 @@ class CycleNode(Node):
16             return ''
17         return value
18 
19+class ResetCycleNode(Node):
20+    def __init__(self, node):
21+        self.node = node
22+
23+    def render(self, context):
24+        self.node.reset(context)
25+        return ''
26+
27 class DebugNode(Node):
28     def render(self, context):
29         from pprint import pformat
30@@ -582,7 +593,7 @@ def cycle(parser, token):
31     # Ugly hack warning: This stuffs the named template dict into parser so
32     # that names are only unique within each template (as opposed to using
33     # a global variable, which would make cycle names have to be unique across
34-    # *all* templates.
35+    # *all* templates. Also keeps last unnamed node in the parser.
36 
37     args = token.split_contents()
38 
39@@ -627,9 +638,38 @@ def cycle(parser, token):
40     else:
41         values = [parser.compile_filter(arg) for arg in args[1:]]
42         node = CycleNode(values)
43+        parser._lastUnnamedCycleNode = node
44     return node
45 
46 @register.tag
47+def resetcycle(parser, token):
48+    """
49+    Resets a cycle tag.
50+
51+    If an argument is given, resets the last rendered cycle tag whose name
52+    matches the argument, else resets last rendered unnamed cycle tag.
53+
54+    """
55+    args = token.split_contents()
56+
57+    if len(args) > 2:
58+        raise TemplateSyntaxError("%r tag accepts at most one argument."
59+                                  % args[0])
60+    if len(args) == 2:
61+        name = args[1]
62+        try:
63+            return ResetCycleNode(parser._namedCycleNodes[name])
64+        except AttributeError:
65+            raise TemplateSyntaxError("No named cycles in template. "
66+                                      "%r is not defined" % name)
67+        except KeyError:
68+            raise TemplateSyntaxError("Named cycle %r does not exist" % name)
69+    try:
70+        return ResetCycleNode(parser._lastUnnamedCycleNode)
71+    except AttributeError:
72+        raise TemplateSyntaxError("No unnamed cycles in template.")
73+
74+@register.tag
75 def csrf_token(parser, token):
76     return CsrfTokenNode()
77 
78diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt
79index 3638ce9..6f440e5 100644
80--- a/docs/ref/templates/builtins.txt
81+++ b/docs/ref/templates/builtins.txt
82@@ -144,6 +144,9 @@ explicitly::
83         {% cycle var1 var2 var3 %}
84     {% endfilter %}
85 
86+You can use the `resetcycle`_ tag to reset a ``{% cycle %}`` tag to restart
87+from its first value when it's next encountered.
88+
89 For backwards compatibility, the ``{% cycle %}`` tag supports the much inferior
90 old syntax from previous Django versions. You shouldn't use this in any new
91 projects, but for the sake of the people who are still using it, here's what it
92@@ -864,6 +867,44 @@ attribute, allowing  you to group on the display string rather than the
93 ``{{ gender.grouper }}`` will now display the value fields from the
94 ``choices`` set rather than the keys.
95 
96+.. templatetag:: resetcycle
97+
98+resetcycle
99+^^^^^^^^^^
100+
101+Resets a previous `cycle`_ so that it restarts from its first item at its next
102+invocation.  Without arguments, ``{% resetcycle %}`` will reset the last
103+unnamed ``{% cycle %}`` defined in the template.
104+
105+Example usage::
106+
107+    {% for coach in coach_list %}
108+        <h1>{{ coach.name }}</h1>
109+        {% for athlete in coach.athlete_set.all %}
110+            <p class="{% cycle 'odd' 'even' %}">{{ athlete.name }}</p>
111+        {% endfor %}
112+        {% resetcycle %}
113+    {% endfor %}
114+
115+The athlete list for every coach starts with ``class="odd"``.  Without the
116+``{% resetcycle %}`` tag, the first athlete of a coach might be rendered with
117+``class="even"`` if the last athlete of the previous coach had ``class="odd"``.
118+
119+You can also reset named cycle tags::
120+
121+    {% for item in list %}
122+        <p class="{% cycle 'odd' 'evn' as stripe %} {% cycle 'majr' 'minr' 'minr' 'minr' 'minr' as tick %}">
123+           {{ item.data }}
124+       </p>
125+        {% ifchanged item.category %}
126+            <h1>{{ item.category }}</h1>
127+            {% if not forloop.first %}{% resetcycle tick %}{% endif %}
128+        {% endifchanged %}
129+    {% endfor %}
130+
131+In this example we have both the alternating odd/even rows and a "major" row
132+every fifth row.  Only the five-row cycle is reset when a category changes.
133+
134 .. templatetag:: spaceless
135 
136 spaceless
137diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py
138index c5c65fd..9c1d2f2 100644
139--- a/tests/regressiontests/templates/tests.py
140+++ b/tests/regressiontests/templates/tests.py
141@@ -750,6 +750,20 @@ class Templates(unittest.TestCase):
142             'included-cycle': ('{{ abc }}', {'abc': 'xxx'}, 'xxx'),
143             'cycle24': ("{% for x in values %}{% cycle 'a' 'b' 'c' as abc silent %}{% include 'included-cycle' %}{% endfor %}", {'values': [1,2,3,4]}, "abca"),
144 
145+            ### RESETCYCLE TAG ########################################################
146+            'resetcycle01': ("{% resetcycle %}", {}, template.TemplateSyntaxError),
147+            'resetcycle02': ("{% resetcycle undefinedcycle %}", {}, template.TemplateSyntaxError),
148+            'resetcycle03': ("{% cycle 'a' 'b' 'c' as abc %}{% resetcycle %}", {}, template.TemplateSyntaxError),
149+            'resetcycle04': ("{% cycle 'a' 'b' 'c' %}{% resetcycle undefinedcycle %}", {}, template.TemplateSyntaxError),
150+            'resetcycle05': ("{% cycle 'a' 'b' 'c' as abc %}{% resetcycle undefinedcycle %}", {}, template.TemplateSyntaxError),
151+            'resetcycle06': ("{% for i in test %}{% cycle 'a' 'b' %}{% resetcycle %}{% endfor %}", {'test': range(5)}, 'aaaaa'),
152+            'resetcycle07': ("{% cycle 'a' 'b' 'c' as abc %}{% for i in test %}{% cycle abc %}{% cycle '-' '+' %}{% resetcycle %}{% endfor %}", {'test': range(5)}, 'ab-c-a-b-c-'),
153+            'resetcycle08': ("{% cycle 'a' 'b' 'c' as abc %}{% for i in test %}{% resetcycle abc %}{% cycle abc %}{% cycle '-' '+' %}{% endfor %}", {'test': range(5)}, 'aa-a+a-a+a-'),
154+            'resetcycle09': ("{% for i in outer %}{% for j in inner %}{% cycle 'a' 'b' %}{% endfor %}{% resetcycle %}{% endfor %}", {'outer': range(2), 'inner': range(3)}, 'abaaba'),
155+            'resetcycle10': ("{% for i in outer %}{% cycle 'a' 'b' %}{% for j in inner %}{% cycle 'X' 'Y' %}{% endfor %}{% resetcycle %}{% endfor %}", {'outer': range(2), 'inner': range(3)}, 'aXYXbXYX'),
156+            'resetcycle11': ("{% for i in test %}{% cycle 'X' 'Y' 'Z' as XYZ %}{% cycle 'a' 'b' 'c' as abc %}{% ifequal i 1 %}{% resetcycle abc %}{% endifequal %}{% endfor %}", {'test': range(5)}, 'XaYbZaXbYc'),
157+            'resetcycle12': ("{% for i in test %}{% cycle 'X' 'Y' 'Z' as XYZ %}{% cycle 'a' 'b' 'c' as abc %}{% ifequal i 1 %}{% resetcycle XYZ %}{% endifequal %}{% endfor %}", {'test': range(5)}, 'XaYbXcYaZb'),
158+
159             ### EXCEPTIONS ############################################################
160 
161             # Raise exception for invalid template name