Code

Ticket #14262: 14262.assignment_tag.3.diff

File 14262.assignment_tag.3.diff, 12.3 KB (added by julien, 3 years ago)

Small doc fixes

Line 
1diff --git a/django/template/base.py b/django/template/base.py
2index b6470f3..37b6fab 100644
3--- a/django/template/base.py
4+++ b/django/template/base.py
5@@ -901,6 +901,60 @@ class Library(object):
6         else:
7             raise TemplateSyntaxError("Invalid arguments provided to simple_tag")
8 
9+    def assignment_tag(self, func=None, takes_context=None):
10+        def dec(func):
11+            params, xx, xxx, defaults = getargspec(func)
12+            if takes_context:
13+                if params[0] == 'context':
14+                    params = params[1:]
15+                else:
16+                    raise TemplateSyntaxError("Any tag function decorated with takes_context=True must have a first argument of 'context'")
17+
18+            class AssignmentNode(Node):
19+                def __init__(self, params_vars, target_var):
20+                    self.params_vars = map(Variable, params_vars)
21+                    self.target_var = target_var
22+
23+                def render(self, context):
24+                    resolved_vars = [var.resolve(context) for var in self.params_vars]
25+                    if takes_context:
26+                        func_args = [context] + resolved_vars
27+                    else:
28+                        func_args = resolved_vars
29+                    context[self.target_var] = func(*func_args)
30+                    return ''
31+
32+            def compile_func(parser, token):
33+                bits = token.split_contents()
34+                tag_name = bits[0]
35+                bits = bits[1:]
36+                params_max = len(params)
37+                defaults_length = defaults and len(defaults) or 0
38+                params_min = params_max - defaults_length
39+                if (len(bits) < 2 or bits[-2] != 'as'):
40+                    raise TemplateSyntaxError("'%s' tag takes at least 2 arguments and the second last argument must be 'as'" % tag_name)
41+                params_vars = bits[:-2]
42+                target_var = bits[-1]
43+                if (len(params_vars) < params_min or len(params_vars) > params_max):
44+                    if params_min == params_max:
45+                        raise TemplateSyntaxError("%s takes %s arguments" % (tag_name, params_min))
46+                    else:
47+                        raise TemplateSyntaxError("%s takes between %s and %s arguments" % (tag_name, params_min, params_max))
48+                return AssignmentNode(params_vars, target_var)
49+           
50+            compile_func.__doc__ = func.__doc__
51+            self.tag(getattr(func, "_decorated_function", func).__name__, compile_func)
52+            return func
53+
54+        if func is None:
55+            # @register.assignment_tag(...)
56+            return dec
57+        elif callable(func):
58+            # @register.assignment_tag
59+            return dec(func)
60+        else:
61+            raise TemplateSyntaxError("Invalid arguments provided to assignment_tag")
62+       
63     def inclusion_tag(self, file_name, context_class=Context, takes_context=False):
64         def dec(func):
65             params, xx, xxx, defaults = getargspec(func)
66diff --git a/docs/howto/custom-template-tags.txt b/docs/howto/custom-template-tags.txt
67index 362d358..16f25ae 100644
68--- a/docs/howto/custom-template-tags.txt
69+++ b/docs/howto/custom-template-tags.txt
70@@ -681,7 +681,70 @@ Or, using decorator syntax::
71         return your_get_current_time_method(timezone, format_string)
72 
73 For more information on how the ``takes_context`` option works, see the section
74-on `inclusion tags`_.
75+on :ref:`inclusion tags<howto-custom-template-tags-inclusion-tags>`.
76+
77+.. _howto-custom-template-tags-assignment-tags:
78+
79+Assignment tags
80+~~~~~~~~~~~~~~~
81+
82+.. versionadded:: 1.4
83+
84+Another common type of template tag is the type that fetches some data and
85+stores it in a context variable. To ease the creation of this type of tags,
86+Django provides a helper function, ``assignment_tag``. This function works
87+the same way as :ref:`simple_tag<howto-custom-template-tags-simple-tags>`,
88+except that it stores the tag's result in a specified context variable instead
89+of directly outputting it.
90+
91+Our earlier ``current_time`` function could thus be written like this:
92+
93+.. code-block:: python
94+
95+    def get_current_time(format_string):
96+        return datetime.datetime.now().strftime(format_string)
97+
98+    register.assignment_tag(get_current_time)
99+
100+The decorator syntax also works:
101+
102+.. code-block:: python
103+
104+    @register.assignment_tag
105+    def get_current_time(format_string):
106+        ...
107+
108+You may then store the result in a template variable using the ``as`` argument
109+followed by the variable name, and output it yourself where you see fit:
110+
111+.. code-block:: html+django
112+
113+    {% get_current_time "%Y-%m-%d %I:%M %p" as the_time %}
114+    <p>The time is {{ the_time }}.</p>
115+
116+If your template tag needs to access the current context, you can use the
117+``takes_context`` argument when registering your tag:
118+
119+.. code-block:: python
120+
121+    # The first argument *must* be called "context" here.
122+    def get_current_time(context, format_string):
123+        timezone = context['timezone']
124+        return your_get_current_time_method(timezone, format_string)
125+
126+    register.assignment_tag(takes_context=True)(get_current_time)
127+
128+Or, using decorator syntax:
129+
130+.. code-block:: python
131+
132+    @register.assignment_tag(takes_context=True)
133+    def get_current_time(context, format_string):
134+        timezone = context['timezone']
135+        return your_get_current_time_method(timezone, format_string)
136+
137+For more information on how the ``takes_context`` option works, see the section
138+on :ref:`inclusion tags<howto-custom-template-tags-inclusion-tags>`.
139 
140 .. _howto-custom-template-tags-inclusion-tags:
141 
142diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
143index 617c3ad..aea9c75 100644
144--- a/docs/releases/1.4.txt
145+++ b/docs/releases/1.4.txt
146@@ -43,6 +43,14 @@ override the provided templates to change the doctype.
147 A lazily evaluated version of :func:`django.core.urlresolvers.reverse` was
148 added to allow using URL reversals before the project's URLConf gets loaded.
149 
150+Assignment template tags
151+~~~~~~~~~~~~~~~~~~~~~~~~
152+
153+A new helper function,
154+:ref:`assignment_tag<howto-custom-template-tags-assignment-tags>`, was added to
155+``template.Library`` to ease the creation of template tags that store some
156+data in a specified context variable.
157+
158 .. _backwards-incompatible-changes-1.4:
159 
160 Backwards incompatible changes in 1.4
161diff --git a/tests/regressiontests/templates/custom.py b/tests/regressiontests/templates/custom.py
162index 7ca359d..5453657 100644
163--- a/tests/regressiontests/templates/custom.py
164+++ b/tests/regressiontests/templates/custom.py
165@@ -1,3 +1,5 @@
166+from __future__ import with_statement
167+
168 from django import template
169 from django.utils.unittest import TestCase
170 from templatetags import custom
171@@ -102,3 +104,54 @@ class CustomTagTests(TestCase):
172 
173         c.use_l10n = True
174         self.assertEquals(t.render(c).strip(), u'True')
175+
176+    def test_assignment_tags(self):
177+        c = template.Context({'value': 42})
178+
179+        t = template.Template('{% load custom %}{% assignment_no_params as var %}The result is: {{ var }}')
180+        self.assertEqual(t.render(c), u'The result is: assignment_no_params - Expected result')
181+
182+        t = template.Template('{% load custom %}{% assignment_one_param 37 as var %}The result is: {{ var }}')
183+        self.assertEqual(t.render(c), u'The result is: assignment_one_param - Expected result: 37')
184+
185+        t = template.Template('{% load custom %}{% assignment_explicit_no_context 37 as var %}The result is: {{ var }}')
186+        self.assertEqual(t.render(c), u'The result is: assignment_explicit_no_context - Expected result: 37')
187+
188+        t = template.Template('{% load custom %}{% assignment_no_params_with_context as var %}The result is: {{ var }}')
189+        self.assertEqual(t.render(c), u'The result is: assignment_no_params_with_context - Expected result (context value: 42)')
190+
191+        t = template.Template('{% load custom %}{% assignment_params_and_context 37 as var %}The result is: {{ var }}')
192+        self.assertEqual(t.render(c), u'The result is: assignment_params_and_context - Expected result (context value: 42): 37')
193+
194+        with self.assertRaises(template.TemplateSyntaxError) as context_manager:
195+            template.Template('{% load custom %}{% assignment_one_param 37 %}The result is: {{ var }}')
196+        self.assertEqual(context_manager.exception.message, "'assignment_one_param' tag takes at least 2 arguments and the second last argument must be 'as'")
197+       
198+        with self.assertRaises(template.TemplateSyntaxError) as context_manager:
199+            template.Template('{% load custom %}{% assignment_one_param 37 as %}The result is: {{ var }}')
200+        self.assertEqual(context_manager.exception.message, "'assignment_one_param' tag takes at least 2 arguments and the second last argument must be 'as'")
201+       
202+        with self.assertRaises(template.TemplateSyntaxError) as context_manager:
203+            template.Template('{% load custom %}{% assignment_one_param 37 ass var %}The result is: {{ var }}')
204+        self.assertEqual(context_manager.exception.message, "'assignment_one_param' tag takes at least 2 arguments and the second last argument must be 'as'")
205+       
206+    def test_assignment_tag_registration(self):
207+        # Test that the decorators preserve the decorated function's docstring, name and attributes.
208+        self.verify_tag(custom.assignment_no_params, 'assignment_no_params')
209+        self.verify_tag(custom.assignment_one_param, 'assignment_one_param')
210+        self.verify_tag(custom.assignment_explicit_no_context, 'assignment_explicit_no_context')
211+        self.verify_tag(custom.assignment_no_params_with_context, 'assignment_no_params_with_context')
212+        self.verify_tag(custom.assignment_params_and_context, 'assignment_params_and_context')
213+
214+    def test_assignment_tag_missing_context(self):
215+        # That the 'context' parameter must be present when takes_context is True
216+        def an_assignment_tag_without_parameters(arg):
217+            """Expected __doc__"""
218+            return "Expected result"
219+
220+        register = template.Library()
221+        decorator = register.assignment_tag(takes_context=True)
222+       
223+        with self.assertRaises(template.TemplateSyntaxError) as context_manager:
224+            decorator(an_assignment_tag_without_parameters)
225+        self.assertEqual(context_manager.exception.message, "Any tag function decorated with takes_context=True must have a first argument of 'context'")
226diff --git a/tests/regressiontests/templates/templatetags/custom.py b/tests/regressiontests/templates/templatetags/custom.py
227index 8db113a..2e281f7 100644
228--- a/tests/regressiontests/templates/templatetags/custom.py
229+++ b/tests/regressiontests/templates/templatetags/custom.py
230@@ -84,3 +84,33 @@ def use_l10n(context):
231 @register.inclusion_tag('test_incl_tag_use_l10n.html', takes_context=True)
232 def inclusion_tag_use_l10n(context):
233     return {}
234+
235+@register.assignment_tag
236+def assignment_no_params():
237+    """Expected assignment_no_params __doc__"""
238+    return "assignment_no_params - Expected result"
239+assignment_no_params.anything = "Expected assignment_no_params __dict__"
240+
241+@register.assignment_tag
242+def assignment_one_param(arg):
243+    """Expected assignment_one_param __doc__"""
244+    return "assignment_one_param - Expected result: %s" % arg
245+assignment_one_param.anything = "Expected assignment_one_param __dict__"
246+
247+@register.assignment_tag(takes_context=False)
248+def assignment_explicit_no_context(arg):
249+    """Expected assignment_explicit_no_context __doc__"""
250+    return "assignment_explicit_no_context - Expected result: %s" % arg
251+assignment_explicit_no_context.anything = "Expected assignment_explicit_no_context __dict__"
252+
253+@register.assignment_tag(takes_context=True)
254+def assignment_no_params_with_context(context):
255+    """Expected assignment_no_params_with_context __doc__"""
256+    return "assignment_no_params_with_context - Expected result (context value: %s)" % context['value']
257+assignment_no_params_with_context.anything = "Expected assignment_no_params_with_context __dict__"
258+
259+@register.assignment_tag(takes_context=True)
260+def assignment_params_and_context(context, arg):
261+    """Expected assignment_params_and_context __doc__"""
262+    return "assignment_params_and_context - Expected result (context value: %s): %s" % (context['value'], arg)
263+assignment_params_and_context.anything = "Expected assignment_params_and_context __dict__"