Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #14262 -- Added new assignment_tag as a simple way to assign th…

…e result of a template tag to a context variable. Thanks, Julien Phalip.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16149 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 950e05c3ff5daa360c3bddb84831d40b167062f1 1 parent 8ce352c
Jannis Leidel authored May 03, 2011
60  django/template/base.py
@@ -901,6 +901,66 @@ def render(self, context):
901 901
         else:
902 902
             raise TemplateSyntaxError("Invalid arguments provided to simple_tag")
903 903
 
  904
+    def assignment_tag(self, func=None, takes_context=None):
  905
+        def dec(func):
  906
+            params, xx, xxx, defaults = getargspec(func)
  907
+            if takes_context:
  908
+                if params[0] == 'context':
  909
+                    params = params[1:]
  910
+                else:
  911
+                    raise TemplateSyntaxError("Any tag function decorated with takes_context=True must have a first argument of 'context'")
  912
+
  913
+            class AssignmentNode(Node):
  914
+                def __init__(self, params_vars, target_var):
  915
+                    self.params_vars = map(Variable, params_vars)
  916
+                    self.target_var = target_var
  917
+
  918
+                def render(self, context):
  919
+                    resolved_vars = [var.resolve(context) for var in self.params_vars]
  920
+                    if takes_context:
  921
+                        func_args = [context] + resolved_vars
  922
+                    else:
  923
+                        func_args = resolved_vars
  924
+                    context[self.target_var] = func(*func_args)
  925
+                    return ''
  926
+
  927
+            def compile_func(parser, token):
  928
+                bits = token.split_contents()
  929
+                tag_name = bits[0]
  930
+                bits = bits[1:]
  931
+                params_max = len(params)
  932
+                defaults_length = defaults and len(defaults) or 0
  933
+                params_min = params_max - defaults_length
  934
+                if (len(bits) < 2 or bits[-2] != 'as'):
  935
+                    raise TemplateSyntaxError(
  936
+                        "'%s' tag takes at least 2 arguments and the "
  937
+                        "second last argument must be 'as'" % tag_name)
  938
+                params_vars = bits[:-2]
  939
+                target_var = bits[-1]
  940
+                if (len(params_vars) < params_min or
  941
+                        len(params_vars) > params_max):
  942
+                    if params_min == params_max:
  943
+                        raise TemplateSyntaxError(
  944
+                            "%s takes %s arguments" % (tag_name, params_min))
  945
+                    else:
  946
+                        raise TemplateSyntaxError(
  947
+                            "%s takes between %s and %s arguments"
  948
+                            % (tag_name, params_min, params_max))
  949
+                return AssignmentNode(params_vars, target_var)
  950
+
  951
+            compile_func.__doc__ = func.__doc__
  952
+            self.tag(getattr(func, "_decorated_function", func).__name__, compile_func)
  953
+            return func
  954
+
  955
+        if func is None:
  956
+            # @register.assignment_tag(...)
  957
+            return dec
  958
+        elif callable(func):
  959
+            # @register.assignment_tag
  960
+            return dec(func)
  961
+        else:
  962
+            raise TemplateSyntaxError("Invalid arguments provided to assignment_tag")
  963
+
904 964
     def inclusion_tag(self, file_name, context_class=Context, takes_context=False):
905 965
         def dec(func):
906 966
             params, xx, xxx, defaults = getargspec(func)
65  docs/howto/custom-template-tags.txt
@@ -681,7 +681,70 @@ Or, using decorator syntax::
681 681
         return your_get_current_time_method(timezone, format_string)
682 682
 
683 683
 For more information on how the ``takes_context`` option works, see the section
684  
-on `inclusion tags`_.
  684
+on :ref:`inclusion tags<howto-custom-template-tags-inclusion-tags>`.
  685
+
  686
+.. _howto-custom-template-tags-assignment-tags:
  687
+
  688
+Assignment tags
  689
+~~~~~~~~~~~~~~~
  690
+
  691
+.. versionadded:: 1.4
  692
+
  693
+Another common type of template tag is the type that fetches some data and
  694
+stores it in a context variable. To ease the creation of this type of tags,
  695
+Django provides a helper function, ``assignment_tag``. This function works
  696
+the same way as :ref:`simple_tag<howto-custom-template-tags-simple-tags>`,
  697
+except that it stores the tag's result in a specified context variable instead
  698
+of directly outputting it.
  699
+
  700
+Our earlier ``current_time`` function could thus be written like this:
  701
+
  702
+.. code-block:: python
  703
+
  704
+    def get_current_time(format_string):
  705
+        return datetime.datetime.now().strftime(format_string)
  706
+
  707
+    register.assignment_tag(get_current_time)
  708
+
  709
+The decorator syntax also works:
  710
+
  711
+.. code-block:: python
  712
+
  713
+    @register.assignment_tag
  714
+    def get_current_time(format_string):
  715
+        ...
  716
+
  717
+You may then store the result in a template variable using the ``as`` argument
  718
+followed by the variable name, and output it yourself where you see fit:
  719
+
  720
+.. code-block:: html+django
  721
+
  722
+    {% get_current_time "%Y-%m-%d %I:%M %p" as the_time %}
  723
+    <p>The time is {{ the_time }}.</p>
  724
+
  725
+If your template tag needs to access the current context, you can use the
  726
+``takes_context`` argument when registering your tag:
  727
+
  728
+.. code-block:: python
  729
+
  730
+    # The first argument *must* be called "context" here.
  731
+    def get_current_time(context, format_string):
  732
+        timezone = context['timezone']
  733
+        return your_get_current_time_method(timezone, format_string)
  734
+
  735
+    register.assignment_tag(takes_context=True)(get_current_time)
  736
+
  737
+Or, using decorator syntax:
  738
+
  739
+.. code-block:: python
  740
+
  741
+    @register.assignment_tag(takes_context=True)
  742
+    def get_current_time(context, format_string):
  743
+        timezone = context['timezone']
  744
+        return your_get_current_time_method(timezone, format_string)
  745
+
  746
+For more information on how the ``takes_context`` option works, see the section
  747
+on :ref:`inclusion tags<howto-custom-template-tags-inclusion-tags>`.
685 748
 
686 749
 .. _howto-custom-template-tags-inclusion-tags:
687 750
 
8  docs/releases/1.4.txt
@@ -52,6 +52,14 @@ documentation for :attr:`~django.contrib.admin.ModelAdmin.list_filter`.
52 52
 A lazily evaluated version of :func:`django.core.urlresolvers.reverse` was
53 53
 added to allow using URL reversals before the project's URLConf gets loaded.
54 54
 
  55
+Assignment template tags
  56
+~~~~~~~~~~~~~~~~~~~~~~~~
  57
+
  58
+A new helper function,
  59
+:ref:`assignment_tag<howto-custom-template-tags-assignment-tags>`, was added to
  60
+``template.Library`` to ease the creation of template tags that store some
  61
+data in a specified context variable.
  62
+
55 63
 .. _backwards-incompatible-changes-1.4:
56 64
 
57 65
 Backwards incompatible changes in 1.4
53  tests/regressiontests/templates/custom.py
... ...
@@ -1,3 +1,5 @@
  1
+from __future__ import with_statement
  2
+
1 3
 from django import template
2 4
 from django.utils.unittest import TestCase
3 5
 from templatetags import custom
@@ -102,3 +104,54 @@ def test_15070_use_l10n(self):
102 104
 
103 105
         c.use_l10n = True
104 106
         self.assertEquals(t.render(c).strip(), u'True')
  107
+
  108
+    def test_assignment_tags(self):
  109
+        c = template.Context({'value': 42})
  110
+
  111
+        t = template.Template('{% load custom %}{% assignment_no_params as var %}The result is: {{ var }}')
  112
+        self.assertEqual(t.render(c), u'The result is: assignment_no_params - Expected result')
  113
+
  114
+        t = template.Template('{% load custom %}{% assignment_one_param 37 as var %}The result is: {{ var }}')
  115
+        self.assertEqual(t.render(c), u'The result is: assignment_one_param - Expected result: 37')
  116
+
  117
+        t = template.Template('{% load custom %}{% assignment_explicit_no_context 37 as var %}The result is: {{ var }}')
  118
+        self.assertEqual(t.render(c), u'The result is: assignment_explicit_no_context - Expected result: 37')
  119
+
  120
+        t = template.Template('{% load custom %}{% assignment_no_params_with_context as var %}The result is: {{ var }}')
  121
+        self.assertEqual(t.render(c), u'The result is: assignment_no_params_with_context - Expected result (context value: 42)')
  122
+
  123
+        t = template.Template('{% load custom %}{% assignment_params_and_context 37 as var %}The result is: {{ var }}')
  124
+        self.assertEqual(t.render(c), u'The result is: assignment_params_and_context - Expected result (context value: 42): 37')
  125
+
  126
+        self.assertRaisesRegexp(template.TemplateSyntaxError,
  127
+            "'assignment_one_param' tag takes at least 2 arguments and the second last argument must be 'as'",
  128
+            template.Template, '{% load custom %}{% assignment_one_param 37 %}The result is: {{ var }}')
  129
+
  130
+        self.assertRaisesRegexp(template.TemplateSyntaxError,
  131
+            "'assignment_one_param' tag takes at least 2 arguments and the second last argument must be 'as'",
  132
+            template.Template, '{% load custom %}{% assignment_one_param 37 as %}The result is: {{ var }}')
  133
+
  134
+        self.assertRaisesRegexp(template.TemplateSyntaxError,
  135
+            "'assignment_one_param' tag takes at least 2 arguments and the second last argument must be 'as'",
  136
+            template.Template, '{% load custom %}{% assignment_one_param 37 ass var %}The result is: {{ var }}')
  137
+
  138
+    def test_assignment_tag_registration(self):
  139
+        # Test that the decorators preserve the decorated function's docstring, name and attributes.
  140
+        self.verify_tag(custom.assignment_no_params, 'assignment_no_params')
  141
+        self.verify_tag(custom.assignment_one_param, 'assignment_one_param')
  142
+        self.verify_tag(custom.assignment_explicit_no_context, 'assignment_explicit_no_context')
  143
+        self.verify_tag(custom.assignment_no_params_with_context, 'assignment_no_params_with_context')
  144
+        self.verify_tag(custom.assignment_params_and_context, 'assignment_params_and_context')
  145
+
  146
+    def test_assignment_tag_missing_context(self):
  147
+        # That the 'context' parameter must be present when takes_context is True
  148
+        def an_assignment_tag_without_parameters(arg):
  149
+            """Expected __doc__"""
  150
+            return "Expected result"
  151
+
  152
+        register = template.Library()
  153
+        decorator = register.assignment_tag(takes_context=True)
  154
+
  155
+        self.assertRaisesRegexp(template.TemplateSyntaxError,
  156
+            "Any tag function decorated with takes_context=True must have a first argument of 'context'",
  157
+            decorator, an_assignment_tag_without_parameters)
30  tests/regressiontests/templates/templatetags/custom.py
@@ -84,3 +84,33 @@ def use_l10n(context):
84 84
 @register.inclusion_tag('test_incl_tag_use_l10n.html', takes_context=True)
85 85
 def inclusion_tag_use_l10n(context):
86 86
     return {}
  87
+
  88
+@register.assignment_tag
  89
+def assignment_no_params():
  90
+    """Expected assignment_no_params __doc__"""
  91
+    return "assignment_no_params - Expected result"
  92
+assignment_no_params.anything = "Expected assignment_no_params __dict__"
  93
+
  94
+@register.assignment_tag
  95
+def assignment_one_param(arg):
  96
+    """Expected assignment_one_param __doc__"""
  97
+    return "assignment_one_param - Expected result: %s" % arg
  98
+assignment_one_param.anything = "Expected assignment_one_param __dict__"
  99
+
  100
+@register.assignment_tag(takes_context=False)
  101
+def assignment_explicit_no_context(arg):
  102
+    """Expected assignment_explicit_no_context __doc__"""
  103
+    return "assignment_explicit_no_context - Expected result: %s" % arg
  104
+assignment_explicit_no_context.anything = "Expected assignment_explicit_no_context __dict__"
  105
+
  106
+@register.assignment_tag(takes_context=True)
  107
+def assignment_no_params_with_context(context):
  108
+    """Expected assignment_no_params_with_context __doc__"""
  109
+    return "assignment_no_params_with_context - Expected result (context value: %s)" % context['value']
  110
+assignment_no_params_with_context.anything = "Expected assignment_no_params_with_context __dict__"
  111
+
  112
+@register.assignment_tag(takes_context=True)
  113
+def assignment_params_and_context(context, arg):
  114
+    """Expected assignment_params_and_context __doc__"""
  115
+    return "assignment_params_and_context - Expected result (context value: %s): %s" % (context['value'], arg)
  116
+assignment_params_and_context.anything = "Expected assignment_params_and_context __dict__"

0 notes on commit 950e05c

Please sign in to comment.
Something went wrong with that request. Please try again.