diff --git a/django/template/base.py b/django/template/base.py index b8d6c139de2..08ff5c6d521 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 a6cc162f8da..c997a3c279c 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 00000000000..8afa703f63b --- /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 dedd7d5c920..75f3b4f412d 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