Code

Ticket #14262: 14262.assignment_tag.diff

File 14262.assignment_tag.diff, 13.6 KB (added by julien, 3 years ago)
Line 
1diff --git a/django/template/base.py b/django/template/base.py
2index 08ff5c6..ed1ae86 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 7dc6cce..770e823 100644
68--- a/docs/howto/custom-template-tags.txt
69+++ b/docs/howto/custom-template-tags.txt
70@@ -624,16 +624,18 @@ for example::
71 Variable resolution will throw a ``VariableDoesNotExist`` exception if it cannot
72 resolve the string passed to it in the current context of the page.
73 
74+.. _howto-custom-template-tags-simple-tags:
75+
76 Shortcut for simple tags
77 ~~~~~~~~~~~~~~~~~~~~~~~~
78 
79-Many template tags take a number of arguments -- strings or a template variables
80+Many template tags take a number of arguments -- strings or template variables
81 -- and return a string after doing some processing based solely on
82 the input argument and some external information. For example, the
83 ``current_time`` tag we wrote above is of this variety: we give it a format
84 string, it returns the time as a string.
85 
86-To ease the creation of the types of tags, Django provides a helper function,
87+To ease the creation of this type of tags, Django provides a helper function,
88 ``simple_tag``. This function, which is a method of
89 ``django.template.Library``, takes a function that accepts any number of
90 arguments, wraps it in a ``render`` function and the other necessary bits
91@@ -681,7 +683,66 @@ Or, using decorator syntax::
92         return your_get_current_time_method(timezone, format_string)
93 
94 For more information on how the ``takes_context`` option works, see the section
95-on `inclusion tags`_.
96+on :ref:`inclusion tags<howto-custom-template-tags-inclusion-tags>`.
97+
98+.. _howto-custom-template-tags-assignment-tags:
99+
100+Assignment tags
101+~~~~~~~~~~~~~~~
102+
103+.. versionadded:: 1.4
104+
105+Another common type of template tag is the type that fetches some data and
106+stores it in a context variable. To ease the creation of this type of tags,
107+Django provides a helper function, ``assignment_tag``. This function works
108+the same way as :ref:`simple_tag<howto-custom-template-tags-simple-tags>`,
109+except that it stores the tag's result in a specified context variable instead
110+of directly outputting it.
111+
112+Our earlier ``current_time`` function could thus be written like this::
113+
114+.. code-block:: python
115+
116+    def get_current_time(format_string):
117+        return datetime.datetime.now().strftime(format_string)
118+
119+    register.assignment_tag(get_current_time)
120+
121+The decorator syntax also works::
122+
123+.. code-block:: python
124+
125+    @register.assignment_tag
126+    def get_current_time(format_string):
127+        ...
128+
129+You may then store the result in a template variable using the ``as`` argument
130+followed by the variable name, and output it yourself where you see fit::
131+
132+.. code-block:: html+django
133+
134+    {% get_current_time "%Y-%m-%d %I:%M %p" as the_time %}
135+    <p>The time is {{ the_time }}.</p>
136+
137+If your template tag needs to access the current context, you can use the
138+``takes_context`` argument when registering your tag::
139+
140+    # The first argument *must* be called "context" here.
141+    def get_current_time(context, format_string):
142+        timezone = context['timezone']
143+        return your_get_current_time_method(timezone, format_string)
144+
145+    register.assignment_tag(takes_context=True)(get_current_time)
146+
147+Or, using decorator syntax::
148+
149+    @register.assignment_tag(takes_context=True)
150+    def get_current_time(context, format_string):
151+        timezone = context['timezone']
152+        return your_get_current_time_method(timezone, format_string)
153+
154+For more information on how the ``takes_context`` option works, see the section
155+on :ref:`inclusion tags<howto-custom-template-tags-inclusion-tags>`.
156 
157 .. _howto-custom-template-tags-inclusion-tags:
158 
159diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
160index a63fff7..162b493 100644
161--- a/docs/releases/1.4.txt
162+++ b/docs/releases/1.4.txt
163@@ -37,6 +37,14 @@ compatibility with old browsers, this change means that you can use any HTML5
164 features you need in admin pages without having to lose HTML validity or
165 override the provided templates to change the doctype.
166 
167+Assignment template tags
168+~~~~~~~~~~~~~~~~~~~~~~~~
169+
170+A new helper function,
171+:ref:`assignment_tag<howto-custom-template-tags-assignment-tags>`, was added to
172+``template.Library`` to ease the creation of template tags that store some
173+data in a specified context variable.
174+
175 .. _backwards-incompatible-changes-1.4:
176 
177 Backwards incompatible changes in 1.4
178diff --git a/tests/regressiontests/templates/custom.py b/tests/regressiontests/templates/custom.py
179index fe5b095..6e7f842 100644
180--- a/tests/regressiontests/templates/custom.py
181+++ b/tests/regressiontests/templates/custom.py
182@@ -1,3 +1,5 @@
183+from __future__ import with_statement
184+
185 from django import template
186 from django.utils.unittest import TestCase
187 from templatetags import custom
188@@ -78,3 +80,54 @@ class CustomTagTests(TestCase):
189         self.verify_tag(custom.inclusion_explicit_no_context, 'inclusion_explicit_no_context')
190         self.verify_tag(custom.inclusion_no_params_with_context, 'inclusion_no_params_with_context')
191         self.verify_tag(custom.inclusion_params_and_context, 'inclusion_params_and_context')
192+       
193+    def test_assignment_tags(self):
194+        c = template.Context({'value': 42})
195+
196+        t = template.Template('{% load custom %}{% assignment_no_params as var %}The result is: {{ var }}')
197+        self.assertEqual(t.render(c), u'The result is: assignment_no_params - Expected result')
198+
199+        t = template.Template('{% load custom %}{% assignment_one_param 37 as var %}The result is: {{ var }}')
200+        self.assertEqual(t.render(c), u'The result is: assignment_one_param - Expected result: 37')
201+
202+        t = template.Template('{% load custom %}{% assignment_explicit_no_context 37 as var %}The result is: {{ var }}')
203+        self.assertEqual(t.render(c), u'The result is: assignment_explicit_no_context - Expected result: 37')
204+
205+        t = template.Template('{% load custom %}{% assignment_no_params_with_context as var %}The result is: {{ var }}')
206+        self.assertEqual(t.render(c), u'The result is: assignment_no_params_with_context - Expected result (context value: 42)')
207+
208+        t = template.Template('{% load custom %}{% assignment_params_and_context 37 as var %}The result is: {{ var }}')
209+        self.assertEqual(t.render(c), u'The result is: assignment_params_and_context - Expected result (context value: 42): 37')
210+
211+        with self.assertRaises(template.TemplateSyntaxError) as context_manager:
212+            template.Template('{% load custom %}{% assignment_one_param 37 %}The result is: {{ var }}')
213+        self.assertEqual(context_manager.exception.message, "'assignment_one_param' tag takes at least 2 arguments and the second last argument must be 'as'")
214+       
215+        with self.assertRaises(template.TemplateSyntaxError) as context_manager:
216+            template.Template('{% load custom %}{% assignment_one_param 37 as %}The result is: {{ var }}')
217+        self.assertEqual(context_manager.exception.message, "'assignment_one_param' tag takes at least 2 arguments and the second last argument must be 'as'")
218+       
219+        with self.assertRaises(template.TemplateSyntaxError) as context_manager:
220+            template.Template('{% load custom %}{% assignment_one_param 37 ass var %}The result is: {{ var }}')
221+        self.assertEqual(context_manager.exception.message, "'assignment_one_param' tag takes at least 2 arguments and the second last argument must be 'as'")
222+       
223+    def test_assignment_tag_registration(self):
224+        # Test that the decorators preserve the decorated function's docstring, name and attributes.
225+        self.verify_tag(custom.assignment_no_params, 'assignment_no_params')
226+        self.verify_tag(custom.assignment_one_param, 'assignment_one_param')
227+        self.verify_tag(custom.assignment_explicit_no_context, 'assignment_explicit_no_context')
228+        self.verify_tag(custom.assignment_no_params_with_context, 'assignment_no_params_with_context')
229+        self.verify_tag(custom.assignment_params_and_context, 'assignment_params_and_context')
230+
231+    def test_assignment_tag_missing_context(self):
232+        # That the 'context' parameter must be present when takes_context is True
233+        def an_assignment_tag_without_parameters(arg):
234+            """Expected __doc__"""
235+            return "Expected result"
236+
237+        register = template.Library()
238+        decorator = register.assignment_tag(takes_context=True)
239+       
240+        with self.assertRaises(template.TemplateSyntaxError) as context_manager:
241+            decorator(an_assignment_tag_without_parameters)
242+        self.assertEqual(context_manager.exception.message, "Any tag function decorated with takes_context=True must have a first argument of 'context'")
243diff --git a/tests/regressiontests/templates/templatetags/custom.py b/tests/regressiontests/templates/templatetags/custom.py
244index b2e8a16..f27890e 100644
245--- a/tests/regressiontests/templates/templatetags/custom.py
246+++ b/tests/regressiontests/templates/templatetags/custom.py
247@@ -69,3 +69,32 @@ def inclusion_params_and_context(context, arg):
248     return {"result" : "inclusion_params_and_context - Expected result (context value: %s): %s" % (context['value'], arg)}
249 inclusion_params_and_context.anything = "Expected inclusion_params_and_context __dict__"
250 
251+@register.assignment_tag
252+def assignment_no_params():
253+    """Expected assignment_no_params __doc__"""
254+    return "assignment_no_params - Expected result"
255+assignment_no_params.anything = "Expected assignment_no_params __dict__"
256+
257+@register.assignment_tag
258+def assignment_one_param(arg):
259+    """Expected assignment_one_param __doc__"""
260+    return "assignment_one_param - Expected result: %s" % arg
261+assignment_one_param.anything = "Expected assignment_one_param __dict__"
262+
263+@register.assignment_tag(takes_context=False)
264+def assignment_explicit_no_context(arg):
265+    """Expected assignment_explicit_no_context __doc__"""
266+    return "assignment_explicit_no_context - Expected result: %s" % arg
267+assignment_explicit_no_context.anything = "Expected assignment_explicit_no_context __dict__"
268+
269+@register.assignment_tag(takes_context=True)
270+def assignment_no_params_with_context(context):
271+    """Expected assignment_no_params_with_context __doc__"""
272+    return "assignment_no_params_with_context - Expected result (context value: %s)" % context['value']
273+assignment_no_params_with_context.anything = "Expected assignment_no_params_with_context __dict__"
274+
275+@register.assignment_tag(takes_context=True)
276+def assignment_params_and_context(context, arg):
277+    """Expected assignment_params_and_context __doc__"""
278+    return "assignment_params_and_context - Expected result (context value: %s): %s" % (context['value'], arg)
279+assignment_params_and_context.anything = "Expected assignment_params_and_context __dict__"