test_callables.py 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. from unittest import TestCase
  2. from django.db.models.utils import AltersData
  3. from django.template import Context, Engine
  4. class CallableVariablesTests(TestCase):
  5. @classmethod
  6. def setUpClass(cls):
  7. cls.engine = Engine()
  8. super().setUpClass()
  9. def test_callable(self):
  10. class Doodad:
  11. def __init__(self, value):
  12. self.num_calls = 0
  13. self.value = value
  14. def __call__(self):
  15. self.num_calls += 1
  16. return {"the_value": self.value}
  17. my_doodad = Doodad(42)
  18. c = Context({"my_doodad": my_doodad})
  19. # We can't access ``my_doodad.value`` in the template, because
  20. # ``my_doodad.__call__`` will be invoked first, yielding a dictionary
  21. # without a key ``value``.
  22. t = self.engine.from_string("{{ my_doodad.value }}")
  23. self.assertEqual(t.render(c), "")
  24. # We can confirm that the doodad has been called
  25. self.assertEqual(my_doodad.num_calls, 1)
  26. # But we can access keys on the dict that's returned
  27. # by ``__call__``, instead.
  28. t = self.engine.from_string("{{ my_doodad.the_value }}")
  29. self.assertEqual(t.render(c), "42")
  30. self.assertEqual(my_doodad.num_calls, 2)
  31. def test_alters_data(self):
  32. class Doodad:
  33. alters_data = True
  34. def __init__(self, value):
  35. self.num_calls = 0
  36. self.value = value
  37. def __call__(self):
  38. self.num_calls += 1
  39. return {"the_value": self.value}
  40. my_doodad = Doodad(42)
  41. c = Context({"my_doodad": my_doodad})
  42. # Since ``my_doodad.alters_data`` is True, the template system will not
  43. # try to call our doodad but will use string_if_invalid
  44. t = self.engine.from_string("{{ my_doodad.value }}")
  45. self.assertEqual(t.render(c), "")
  46. t = self.engine.from_string("{{ my_doodad.the_value }}")
  47. self.assertEqual(t.render(c), "")
  48. # Double-check that the object was really never called during the
  49. # template rendering.
  50. self.assertEqual(my_doodad.num_calls, 0)
  51. def test_alters_data_propagation(self):
  52. class GrandParentLeft(AltersData):
  53. def my_method(self):
  54. return 42
  55. my_method.alters_data = True
  56. class ParentLeft(GrandParentLeft):
  57. def change_alters_data_method(self):
  58. return 63
  59. change_alters_data_method.alters_data = True
  60. def sub_non_callable_method(self):
  61. return 64
  62. sub_non_callable_method.alters_data = True
  63. class ParentRight(AltersData):
  64. def other_method(self):
  65. return 52
  66. other_method.alters_data = True
  67. class Child(ParentLeft, ParentRight):
  68. def my_method(self):
  69. return 101
  70. def other_method(self):
  71. return 102
  72. def change_alters_data_method(self):
  73. return 103
  74. change_alters_data_method.alters_data = False
  75. sub_non_callable_method = 104
  76. class GrandChild(Child):
  77. pass
  78. child = Child()
  79. self.assertIs(child.my_method.alters_data, True)
  80. self.assertIs(child.other_method.alters_data, True)
  81. self.assertIs(child.change_alters_data_method.alters_data, False)
  82. grand_child = GrandChild()
  83. self.assertIs(grand_child.my_method.alters_data, True)
  84. self.assertIs(grand_child.other_method.alters_data, True)
  85. self.assertIs(grand_child.change_alters_data_method.alters_data, False)
  86. c = Context({"element": grand_child})
  87. t = self.engine.from_string("{{ element.my_method }}")
  88. self.assertEqual(t.render(c), "")
  89. t = self.engine.from_string("{{ element.other_method }}")
  90. self.assertEqual(t.render(c), "")
  91. t = self.engine.from_string("{{ element.change_alters_data_method }}")
  92. self.assertEqual(t.render(c), "103")
  93. t = self.engine.from_string("{{ element.sub_non_callable_method }}")
  94. self.assertEqual(t.render(c), "104")
  95. def test_do_not_call(self):
  96. class Doodad:
  97. do_not_call_in_templates = True
  98. def __init__(self, value):
  99. self.num_calls = 0
  100. self.value = value
  101. def __call__(self):
  102. self.num_calls += 1
  103. return {"the_value": self.value}
  104. my_doodad = Doodad(42)
  105. c = Context({"my_doodad": my_doodad})
  106. # Since ``my_doodad.do_not_call_in_templates`` is True, the template
  107. # system will not try to call our doodad. We can access its attributes
  108. # as normal, and we don't have access to the dict that it returns when
  109. # called.
  110. t = self.engine.from_string("{{ my_doodad.value }}")
  111. self.assertEqual(t.render(c), "42")
  112. t = self.engine.from_string("{{ my_doodad.the_value }}")
  113. self.assertEqual(t.render(c), "")
  114. # Double-check that the object was really never called during the
  115. # template rendering.
  116. self.assertEqual(my_doodad.num_calls, 0)
  117. def test_do_not_call_and_alters_data(self):
  118. # If we combine ``alters_data`` and ``do_not_call_in_templates``, the
  119. # ``alters_data`` attribute will not make any difference in the
  120. # template system's behavior.
  121. class Doodad:
  122. do_not_call_in_templates = True
  123. alters_data = True
  124. def __init__(self, value):
  125. self.num_calls = 0
  126. self.value = value
  127. def __call__(self):
  128. self.num_calls += 1
  129. return {"the_value": self.value}
  130. my_doodad = Doodad(42)
  131. c = Context({"my_doodad": my_doodad})
  132. t = self.engine.from_string("{{ my_doodad.value }}")
  133. self.assertEqual(t.render(c), "42")
  134. t = self.engine.from_string("{{ my_doodad.the_value }}")
  135. self.assertEqual(t.render(c), "")
  136. # Double-check that the object was really never called during the
  137. # template rendering.
  138. self.assertEqual(my_doodad.num_calls, 0)