Browse Source

Fixed #15791 - method to signal that callable objects should not be called in templates

Thanks to ejucovy for the suggestion and patch!

git-svn-id: http://code.djangoproject.com/svn/django/trunk@16045 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Luke Plant 14 years ago
parent
commit
1286d78311

+ 3 - 1
django/template/base.py

@@ -692,7 +692,9 @@ class Variable(object):
                                 ):
                             raise VariableDoesNotExist("Failed lookup for key [%s] in %r", (bit, current)) # missing attribute
                 if callable(current):
-                    if getattr(current, 'alters_data', False):
+                    if getattr(current, 'do_not_call_in_templates', False):
+                        pass
+                    elif getattr(current, 'alters_data', False):
                         current = settings.TEMPLATE_STRING_IF_INVALID
                     else:
                         try: # method call (assuming no args required)

+ 12 - 2
docs/ref/templates/api.txt

@@ -207,8 +207,9 @@ straight lookups. Here are some things to keep in mind:
 
       To prevent this, set an ``alters_data`` attribute on the callable
       variable. The template system won't call a variable if it has
-      ``alters_data=True`` set. The dynamically-generated
-      :meth:`~django.db.models.Model.delete` and
+      ``alters_data=True`` set, and will instead replace the variable with
+      :setting:`TEMPLATE_STRING_IF_INVALID`, unconditionally.  The
+      dynamically-generated :meth:`~django.db.models.Model.delete` and
       :meth:`~django.db.models.Model.save` methods on Django model objects get
       ``alters_data=True`` automatically. Example::
 
@@ -216,6 +217,15 @@ straight lookups. Here are some things to keep in mind:
             self.database_record.delete()
         sensitive_function.alters_data = True
 
+    * .. versionadded:: 1.4
+         Occasionally you may want to turn off this feature for other reasons,
+         and tell the template system to leave a variable un-called no matter
+         what.  To do so, set a ``do_not_call_in_templates`` attribute on the
+         callable with the value ``True``.  The template system then will act as
+         if your variable is not callable (allowing you to access attributes of
+         the callable, for example).
+
+
 .. _invalid-template-variables:
 
 How invalid variables are handled

+ 111 - 0
tests/regressiontests/templates/callables.py

@@ -0,0 +1,111 @@
+from django import template
+from django.utils.unittest import TestCase
+
+class CallableVariablesTests(TestCase):
+
+    def test_callable(self):
+
+        class Doodad(object):
+            def __init__(self, value):
+                self.num_calls = 0
+                self.value = value
+            def __call__(self):
+                self.num_calls += 1
+                return {"the_value": self.value}
+
+        my_doodad = Doodad(42)
+        c = template.Context({"my_doodad": my_doodad})
+
+        # We can't access ``my_doodad.value`` in the template, because
+        # ``my_doodad.__call__`` will be invoked first, yielding a dictionary
+        # without a key ``value``.
+        t = template.Template('{{ my_doodad.value }}')
+        self.assertEqual(t.render(c), u'')
+
+        # We can confirm that the doodad has been called
+        self.assertEqual(my_doodad.num_calls, 1)
+
+        # But we can access keys on the dict that's returned
+        # by ``__call__``, instead.
+        t = template.Template('{{ my_doodad.the_value }}')
+        self.assertEqual(t.render(c), u'42')
+        self.assertEqual(my_doodad.num_calls, 2)
+
+    def test_alters_data(self):
+
+        class Doodad(object):
+            alters_data = True
+            def __init__(self, value):
+                self.num_calls = 0
+                self.value = value
+            def __call__(self):
+                self.num_calls += 1
+                return {"the_value": self.value}
+
+        my_doodad = Doodad(42)
+        c = template.Context({"my_doodad": my_doodad})
+
+        # Since ``my_doodad.alters_data`` is True, the template system will not
+        # try to call our doodad but will use TEMPLATE_STRING_IF_INVALID
+        t = template.Template('{{ my_doodad.value }}')
+        self.assertEqual(t.render(c), u'')
+        t = template.Template('{{ my_doodad.the_value }}')
+        self.assertEqual(t.render(c), u'')
+
+        # Double-check that the object was really never called during the
+        # template rendering.
+        self.assertEqual(my_doodad.num_calls, 0)
+
+    def test_do_not_call(self):
+
+        class Doodad(object):
+            do_not_call_in_templates = True
+            def __init__(self, value):
+                self.num_calls = 0
+                self.value = value
+            def __call__(self):
+                self.num_calls += 1
+                return {"the_value": self.value}
+
+        my_doodad = Doodad(42)
+        c = template.Context({"my_doodad": my_doodad})
+
+        # Since ``my_doodad.do_not_call_in_templates`` is True, the template
+        # system will not try to call our doodad.  We can access its attributes
+        # as normal, and we don't have access to the dict that it returns when
+        # called.
+        t = template.Template('{{ my_doodad.value }}')
+        self.assertEqual(t.render(c), u'42')
+        t = template.Template('{{ my_doodad.the_value }}')
+        self.assertEqual(t.render(c), u'')
+
+        # Double-check that the object was really never called during the
+        # template rendering.
+        self.assertEqual(my_doodad.num_calls, 0)
+
+    def test_do_not_call_and_alters_data(self):
+        # If we combine ``alters_data`` and ``do_not_call_in_templates``, the
+        # ``alters_data`` attribute will not make any difference in the
+        # template system's behavior.
+
+        class Doodad(object):
+            do_not_call_in_templates = True
+            alters_data = True
+            def __init__(self, value):
+                self.num_calls = 0
+                self.value = value
+            def __call__(self):
+                self.num_calls += 1
+                return {"the_value": self.value}
+
+        my_doodad = Doodad(42)
+        c = template.Context({"my_doodad": my_doodad})
+
+        t = template.Template('{{ my_doodad.value }}')
+        self.assertEqual(t.render(c), u'42')
+        t = template.Template('{{ my_doodad.the_value }}')
+        self.assertEqual(t.render(c), u'')
+
+        # Double-check that the object was really never called during the
+        # template rendering.
+        self.assertEqual(my_doodad.num_calls, 0)

+ 1 - 0
tests/regressiontests/templates/tests.py

@@ -25,6 +25,7 @@ from django.utils.translation import activate, deactivate, ugettext as _
 from django.utils.safestring import mark_safe
 from django.utils.tzinfo import LocalTimezone
 
+from callables import *
 from context import ContextTests
 from custom import CustomTagTests, CustomFilterTests
 from parser import ParserTests