Skip to content

Commit

Permalink
Fixed #15791 - method to signal that callable objects should not be c…
Browse files Browse the repository at this point in the history
…alled 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
  • Loading branch information
spookylukey committed Apr 19, 2011
1 parent 9587235 commit 1286d78
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 3 deletions.
4 changes: 3 additions & 1 deletion django/template/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -692,7 +692,9 @@ def _resolve_lookup(self, context):
):
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)
Expand Down
14 changes: 12 additions & 2 deletions docs/ref/templates/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -207,15 +207,25 @@ 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::

def sensitive_function(self):
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
Expand Down
111 changes: 111 additions & 0 deletions tests/regressiontests/templates/callables.py
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions tests/regressiontests/templates/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
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
Expand Down

0 comments on commit 1286d78

Please sign in to comment.