Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

loader_tags.py 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. import posixpath
  2. from collections import defaultdict
  3. from django.utils.safestring import mark_safe
  4. from .base import (
  5. Node, Template, TemplateSyntaxError, TextNode, Variable, token_kwargs,
  6. )
  7. from .library import Library
  8. register = Library()
  9. BLOCK_CONTEXT_KEY = 'block_context'
  10. class BlockContext:
  11. def __init__(self):
  12. # Dictionary of FIFO queues.
  13. self.blocks = defaultdict(list)
  14. def add_blocks(self, blocks):
  15. for name, block in blocks.items():
  16. self.blocks[name].insert(0, block)
  17. def pop(self, name):
  18. try:
  19. return self.blocks[name].pop()
  20. except IndexError:
  21. return None
  22. def push(self, name, block):
  23. self.blocks[name].append(block)
  24. def get_block(self, name):
  25. try:
  26. return self.blocks[name][-1]
  27. except IndexError:
  28. return None
  29. class BlockNode(Node):
  30. def __init__(self, name, nodelist, parent=None):
  31. self.name, self.nodelist, self.parent = name, nodelist, parent
  32. def __repr__(self):
  33. return "<Block Node: %s. Contents: %r>" % (self.name, self.nodelist)
  34. def render(self, context):
  35. block_context = context.render_context.get(BLOCK_CONTEXT_KEY)
  36. with context.push():
  37. if block_context is None:
  38. context['block'] = self
  39. result = self.nodelist.render(context)
  40. else:
  41. push = block = block_context.pop(self.name)
  42. if block is None:
  43. block = self
  44. # Create new block so we can store context without thread-safety issues.
  45. block = type(self)(block.name, block.nodelist)
  46. block.context = context
  47. context['block'] = block
  48. result = block.nodelist.render(context)
  49. if push is not None:
  50. block_context.push(self.name, push)
  51. return result
  52. def super(self):
  53. if not hasattr(self, 'context'):
  54. raise TemplateSyntaxError(
  55. "'%s' object has no attribute 'context'. Did you use "
  56. "{{ block.super }} in a base template?" % self.__class__.__name__
  57. )
  58. render_context = self.context.render_context
  59. if (BLOCK_CONTEXT_KEY in render_context and
  60. render_context[BLOCK_CONTEXT_KEY].get_block(self.name) is not None):
  61. return mark_safe(self.render(self.context))
  62. return ''
  63. class ExtendsNode(Node):
  64. must_be_first = True
  65. context_key = 'extends_context'
  66. def __init__(self, nodelist, parent_name, template_dirs=None):
  67. self.nodelist = nodelist
  68. self.parent_name = parent_name
  69. self.template_dirs = template_dirs
  70. self.blocks = {n.name: n for n in nodelist.get_nodes_by_type(BlockNode)}
  71. def __repr__(self):
  72. return '<%s: extends %s>' % (self.__class__.__name__, self.parent_name.token)
  73. def find_template(self, template_name, context):
  74. """
  75. This is a wrapper around engine.find_template(). A history is kept in
  76. the render_context attribute between successive extends calls and
  77. passed as the skip argument. This enables extends to work recursively
  78. without extending the same template twice.
  79. """
  80. history = context.render_context.setdefault(
  81. self.context_key, [self.origin],
  82. )
  83. template, origin = context.template.engine.find_template(
  84. template_name, skip=history,
  85. )
  86. history.append(origin)
  87. return template
  88. def get_parent(self, context):
  89. parent = self.parent_name.resolve(context)
  90. if not parent:
  91. error_msg = "Invalid template name in 'extends' tag: %r." % parent
  92. if self.parent_name.filters or\
  93. isinstance(self.parent_name.var, Variable):
  94. error_msg += " Got this from the '%s' variable." %\
  95. self.parent_name.token
  96. raise TemplateSyntaxError(error_msg)
  97. if isinstance(parent, Template):
  98. # parent is a django.template.Template
  99. return parent
  100. if isinstance(getattr(parent, 'template', None), Template):
  101. # parent is a django.template.backends.django.Template
  102. return parent.template
  103. return self.find_template(parent, context)
  104. def render(self, context):
  105. compiled_parent = self.get_parent(context)
  106. if BLOCK_CONTEXT_KEY not in context.render_context:
  107. context.render_context[BLOCK_CONTEXT_KEY] = BlockContext()
  108. block_context = context.render_context[BLOCK_CONTEXT_KEY]
  109. # Add the block nodes from this node to the block context
  110. block_context.add_blocks(self.blocks)
  111. # If this block's parent doesn't have an extends node it is the root,
  112. # and its block nodes also need to be added to the block context.
  113. for node in compiled_parent.nodelist:
  114. # The ExtendsNode has to be the first non-text node.
  115. if not isinstance(node, TextNode):
  116. if not isinstance(node, ExtendsNode):
  117. blocks = {n.name: n for n in
  118. compiled_parent.nodelist.get_nodes_by_type(BlockNode)}
  119. block_context.add_blocks(blocks)
  120. break
  121. # Call Template._render explicitly so the parser context stays
  122. # the same.
  123. with context.render_context.push_state(compiled_parent, isolated_context=False):
  124. return compiled_parent._render(context)
  125. class IncludeNode(Node):
  126. context_key = '__include_context'
  127. def __init__(self, template, *args, extra_context=None, isolated_context=False, **kwargs):
  128. self.template = template
  129. self.extra_context = extra_context or {}
  130. self.isolated_context = isolated_context
  131. super().__init__(*args, **kwargs)
  132. def render(self, context):
  133. """
  134. Render the specified template and context. Cache the template object
  135. in render_context to avoid reparsing and loading when used in a for
  136. loop.
  137. """
  138. template = self.template.resolve(context)
  139. # Does this quack like a Template?
  140. if not callable(getattr(template, 'render', None)):
  141. # If not, try the cache and get_template().
  142. template_name = template
  143. cache = context.render_context.dicts[0].setdefault(self, {})
  144. template = cache.get(template_name)
  145. if template is None:
  146. template = context.template.engine.get_template(template_name)
  147. cache[template_name] = template
  148. # Use the base.Template of a backends.django.Template.
  149. elif hasattr(template, 'template'):
  150. template = template.template
  151. values = {
  152. name: var.resolve(context)
  153. for name, var in self.extra_context.items()
  154. }
  155. if self.isolated_context:
  156. return template.render(context.new(values))
  157. with context.push(**values):
  158. return template.render(context)
  159. @register.tag('block')
  160. def do_block(parser, token):
  161. """
  162. Define a block that can be overridden by child templates.
  163. """
  164. # token.split_contents() isn't useful here because this tag doesn't accept variable as arguments
  165. bits = token.contents.split()
  166. if len(bits) != 2:
  167. raise TemplateSyntaxError("'%s' tag takes only one argument" % bits[0])
  168. block_name = bits[1]
  169. # Keep track of the names of BlockNodes found in this template, so we can
  170. # check for duplication.
  171. try:
  172. if block_name in parser.__loaded_blocks:
  173. raise TemplateSyntaxError("'%s' tag with name '%s' appears more than once" % (bits[0], block_name))
  174. parser.__loaded_blocks.append(block_name)
  175. except AttributeError: # parser.__loaded_blocks isn't a list yet
  176. parser.__loaded_blocks = [block_name]
  177. nodelist = parser.parse(('endblock',))
  178. # This check is kept for backwards-compatibility. See #3100.
  179. endblock = parser.next_token()
  180. acceptable_endblocks = ('endblock', 'endblock %s' % block_name)
  181. if endblock.contents not in acceptable_endblocks:
  182. parser.invalid_block_tag(endblock, 'endblock', acceptable_endblocks)
  183. return BlockNode(block_name, nodelist)
  184. def construct_relative_path(current_template_name, relative_name):
  185. """
  186. Convert a relative path (starting with './' or '../') to the full template
  187. name based on the current_template_name.
  188. """
  189. if not relative_name.startswith(("'./", "'../", '"./', '"../')):
  190. # relative_name is a variable or a literal that doesn't contain a
  191. # relative path.
  192. return relative_name
  193. new_name = posixpath.normpath(
  194. posixpath.join(
  195. posixpath.dirname(current_template_name.lstrip('/')),
  196. relative_name.strip('\'"')
  197. )
  198. )
  199. if new_name.startswith('../'):
  200. raise TemplateSyntaxError(
  201. "The relative path '%s' points outside the file hierarchy that "
  202. "template '%s' is in." % (relative_name, current_template_name)
  203. )
  204. if current_template_name.lstrip('/') == new_name:
  205. raise TemplateSyntaxError(
  206. "The relative path '%s' was translated to template name '%s', the "
  207. "same template in which the tag appears."
  208. % (relative_name, current_template_name)
  209. )
  210. return '"%s"' % new_name
  211. @register.tag('extends')
  212. def do_extends(parser, token):
  213. """
  214. Signal that this template extends a parent template.
  215. This tag may be used in two ways: ``{% extends "base" %}`` (with quotes)
  216. uses the literal value "base" as the name of the parent template to extend,
  217. or ``{% extends variable %}`` uses the value of ``variable`` as either the
  218. name of the parent template to extend (if it evaluates to a string) or as
  219. the parent template itself (if it evaluates to a Template object).
  220. """
  221. bits = token.split_contents()
  222. if len(bits) != 2:
  223. raise TemplateSyntaxError("'%s' takes one argument" % bits[0])
  224. bits[1] = construct_relative_path(parser.origin.template_name, bits[1])
  225. parent_name = parser.compile_filter(bits[1])
  226. nodelist = parser.parse()
  227. if nodelist.get_nodes_by_type(ExtendsNode):
  228. raise TemplateSyntaxError("'%s' cannot appear more than once in the same template" % bits[0])
  229. return ExtendsNode(nodelist, parent_name)
  230. @register.tag('include')
  231. def do_include(parser, token):
  232. """
  233. Load a template and render it with the current context. You can pass
  234. additional context using keyword arguments.
  235. Example::
  236. {% include "foo/some_include" %}
  237. {% include "foo/some_include" with bar="BAZZ!" baz="BING!" %}
  238. Use the ``only`` argument to exclude the current context when rendering
  239. the included template::
  240. {% include "foo/some_include" only %}
  241. {% include "foo/some_include" with bar="1" only %}
  242. """
  243. bits = token.split_contents()
  244. if len(bits) < 2:
  245. raise TemplateSyntaxError(
  246. "%r tag takes at least one argument: the name of the template to "
  247. "be included." % bits[0]
  248. )
  249. options = {}
  250. remaining_bits = bits[2:]
  251. while remaining_bits:
  252. option = remaining_bits.pop(0)
  253. if option in options:
  254. raise TemplateSyntaxError('The %r option was specified more '
  255. 'than once.' % option)
  256. if option == 'with':
  257. value = token_kwargs(remaining_bits, parser, support_legacy=False)
  258. if not value:
  259. raise TemplateSyntaxError('"with" in %r tag needs at least '
  260. 'one keyword argument.' % bits[0])
  261. elif option == 'only':
  262. value = True
  263. else:
  264. raise TemplateSyntaxError('Unknown argument for %r tag: %r.' %
  265. (bits[0], option))
  266. options[option] = value
  267. isolated_context = options.get('only', False)
  268. namemap = options.get('with', {})
  269. bits[1] = construct_relative_path(parser.origin.template_name, bits[1])
  270. return IncludeNode(parser.compile_filter(bits[1]), extra_context=namemap,
  271. isolated_context=isolated_context)