summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Plant <L.Plant.98@cantab.net>2011-04-19 22:06:19 +0000
committerLuke Plant <L.Plant.98@cantab.net>2011-04-19 22:06:19 +0000
commit1286d783110e8b4d1df8068ed0d9093b49cdcaec (patch)
tree0cbaaad3f19a45763e402988130906678f68f09b
parent9587235530b5e3315e3348b1e0d0899ec5408f6d (diff)
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
-rw-r--r--django/template/base.py4
-rw-r--r--docs/ref/templates/api.txt14
-rw-r--r--tests/regressiontests/templates/callables.py111
-rw-r--r--tests/regressiontests/templates/tests.py1
4 files changed, 127 insertions, 3 deletions
diff --git a/django/template/base.py b/django/template/base.py
index b8d6c139de..08ff5c6d52 100644
--- a/django/template/base.py
+++ b/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)
diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt
index a6cc162f8d..c997a3c279 100644
--- a/docs/ref/templates/api.txt
+++ b/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
diff --git a/tests/regressiontests/templates/callables.py b/tests/regressiontests/templates/callables.py
new file mode 100644
index 0000000000..8afa703f63
--- /dev/null
+++ b/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)
diff --git a/tests/regressiontests/templates/tests.py b/tests/regressiontests/templates/tests.py
index dedd7d5c92..75f3b4f412 100644
--- a/tests/regressiontests/templates/tests.py
+++ b/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