Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/template/loader_tags.py: 18%

187 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1import posixpath 

2from collections import defaultdict 

3 

4from django.utils.safestring import mark_safe 

5 

6from .base import Node, Template, TemplateSyntaxError, TextNode, Variable, token_kwargs 

7from .library import Library 

8 

9register = Library() 

10 

11BLOCK_CONTEXT_KEY = "block_context" 

12 

13 

14class BlockContext: 

15 def __init__(self): 

16 # Dictionary of FIFO queues. 

17 self.blocks = defaultdict(list) 

18 

19 def __repr__(self): 

20 return f"<{self.__class__.__qualname__}: blocks={self.blocks!r}>" 

21 

22 def add_blocks(self, blocks): 

23 for name, block in blocks.items(): 

24 self.blocks[name].insert(0, block) 

25 

26 def pop(self, name): 

27 try: 

28 return self.blocks[name].pop() 

29 except IndexError: 

30 return None 

31 

32 def push(self, name, block): 

33 self.blocks[name].append(block) 

34 

35 def get_block(self, name): 

36 try: 

37 return self.blocks[name][-1] 

38 except IndexError: 

39 return None 

40 

41 

42class BlockNode(Node): 

43 def __init__(self, name, nodelist, parent=None): 

44 self.name, self.nodelist, self.parent = name, nodelist, parent 

45 

46 def __repr__(self): 

47 return "<Block Node: %s. Contents: %r>" % (self.name, self.nodelist) 

48 

49 def render(self, context): 

50 block_context = context.render_context.get(BLOCK_CONTEXT_KEY) 

51 with context.push(): 

52 if block_context is None: 

53 context["block"] = self 

54 result = self.nodelist.render(context) 

55 else: 

56 push = block = block_context.pop(self.name) 

57 if block is None: 

58 block = self 

59 # Create new block so we can store context without thread-safety issues. 

60 block = type(self)(block.name, block.nodelist) 

61 block.context = context 

62 context["block"] = block 

63 result = block.nodelist.render(context) 

64 if push is not None: 

65 block_context.push(self.name, push) 

66 return result 

67 

68 def super(self): 

69 if not hasattr(self, "context"): 

70 raise TemplateSyntaxError( 

71 "'%s' object has no attribute 'context'. Did you use " 

72 "{{ block.super }} in a base template?" % self.__class__.__name__ 

73 ) 

74 render_context = self.context.render_context 

75 if ( 

76 BLOCK_CONTEXT_KEY in render_context 

77 and render_context[BLOCK_CONTEXT_KEY].get_block(self.name) is not None 

78 ): 

79 return mark_safe(self.render(self.context)) 

80 return "" 

81 

82 

83class ExtendsNode(Node): 

84 must_be_first = True 

85 context_key = "extends_context" 

86 

87 def __init__(self, nodelist, parent_name, template_dirs=None): 

88 self.nodelist = nodelist 

89 self.parent_name = parent_name 

90 self.template_dirs = template_dirs 

91 self.blocks = {n.name: n for n in nodelist.get_nodes_by_type(BlockNode)} 

92 

93 def __repr__(self): 

94 return "<%s: extends %s>" % (self.__class__.__name__, self.parent_name.token) 

95 

96 def find_template(self, template_name, context): 

97 """ 

98 This is a wrapper around engine.find_template(). A history is kept in 

99 the render_context attribute between successive extends calls and 

100 passed as the skip argument. This enables extends to work recursively 

101 without extending the same template twice. 

102 """ 

103 history = context.render_context.setdefault( 

104 self.context_key, 

105 [self.origin], 

106 ) 

107 template, origin = context.template.engine.find_template( 

108 template_name, 

109 skip=history, 

110 ) 

111 history.append(origin) 

112 return template 

113 

114 def get_parent(self, context): 

115 parent = self.parent_name.resolve(context) 

116 if not parent: 

117 error_msg = "Invalid template name in 'extends' tag: %r." % parent 

118 if self.parent_name.filters or isinstance(self.parent_name.var, Variable): 

119 error_msg += ( 

120 " Got this from the '%s' variable." % self.parent_name.token 

121 ) 

122 raise TemplateSyntaxError(error_msg) 

123 if isinstance(parent, Template): 

124 # parent is a django.template.Template 

125 return parent 

126 if isinstance(getattr(parent, "template", None), Template): 

127 # parent is a django.template.backends.django.Template 

128 return parent.template 

129 return self.find_template(parent, context) 

130 

131 def render(self, context): 

132 compiled_parent = self.get_parent(context) 

133 

134 if BLOCK_CONTEXT_KEY not in context.render_context: 

135 context.render_context[BLOCK_CONTEXT_KEY] = BlockContext() 

136 block_context = context.render_context[BLOCK_CONTEXT_KEY] 

137 

138 # Add the block nodes from this node to the block context 

139 block_context.add_blocks(self.blocks) 

140 

141 # If this block's parent doesn't have an extends node it is the root, 

142 # and its block nodes also need to be added to the block context. 

143 for node in compiled_parent.nodelist: 

144 # The ExtendsNode has to be the first non-text node. 

145 if not isinstance(node, TextNode): 

146 if not isinstance(node, ExtendsNode): 

147 blocks = { 

148 n.name: n 

149 for n in compiled_parent.nodelist.get_nodes_by_type(BlockNode) 

150 } 

151 block_context.add_blocks(blocks) 

152 break 

153 

154 # Call Template._render explicitly so the parser context stays 

155 # the same. 

156 with context.render_context.push_state(compiled_parent, isolated_context=False): 

157 return compiled_parent._render(context) 

158 

159 

160class IncludeNode(Node): 

161 context_key = "__include_context" 

162 

163 def __init__( 

164 self, template, *args, extra_context=None, isolated_context=False, **kwargs 

165 ): 

166 self.template = template 

167 self.extra_context = extra_context or {} 

168 self.isolated_context = isolated_context 

169 super().__init__(*args, **kwargs) 

170 

171 def __repr__(self): 

172 return f"<{self.__class__.__qualname__}: template={self.template!r}>" 

173 

174 def render(self, context): 

175 """ 

176 Render the specified template and context. Cache the template object 

177 in render_context to avoid reparsing and loading when used in a for 

178 loop. 

179 """ 

180 template = self.template.resolve(context) 

181 # Does this quack like a Template? 

182 if not callable(getattr(template, "render", None)): 

183 # If not, try the cache and select_template(). 

184 template_name = template or () 

185 if isinstance(template_name, str): 

186 template_name = ( 

187 construct_relative_path( 

188 self.origin.template_name, 

189 template_name, 

190 ), 

191 ) 

192 else: 

193 template_name = tuple(template_name) 

194 cache = context.render_context.dicts[0].setdefault(self, {}) 

195 template = cache.get(template_name) 

196 if template is None: 

197 template = context.template.engine.select_template(template_name) 

198 cache[template_name] = template 

199 # Use the base.Template of a backends.django.Template. 

200 elif hasattr(template, "template"): 

201 template = template.template 

202 values = { 

203 name: var.resolve(context) for name, var in self.extra_context.items() 

204 } 

205 if self.isolated_context: 

206 return template.render(context.new(values)) 

207 with context.push(**values): 

208 return template.render(context) 

209 

210 

211@register.tag("block") 

212def do_block(parser, token): 

213 """ 

214 Define a block that can be overridden by child templates. 

215 """ 

216 # token.split_contents() isn't useful here because this tag doesn't accept 

217 # variable as arguments. 

218 bits = token.contents.split() 

219 if len(bits) != 2: 

220 raise TemplateSyntaxError("'%s' tag takes only one argument" % bits[0]) 

221 block_name = bits[1] 

222 # Keep track of the names of BlockNodes found in this template, so we can 

223 # check for duplication. 

224 try: 

225 if block_name in parser.__loaded_blocks: 

226 raise TemplateSyntaxError( 

227 "'%s' tag with name '%s' appears more than once" % (bits[0], block_name) 

228 ) 

229 parser.__loaded_blocks.append(block_name) 

230 except AttributeError: # parser.__loaded_blocks isn't a list yet 

231 parser.__loaded_blocks = [block_name] 

232 nodelist = parser.parse(("endblock",)) 

233 

234 # This check is kept for backwards-compatibility. See #3100. 

235 endblock = parser.next_token() 

236 acceptable_endblocks = ("endblock", "endblock %s" % block_name) 

237 if endblock.contents not in acceptable_endblocks: 

238 parser.invalid_block_tag(endblock, "endblock", acceptable_endblocks) 

239 

240 return BlockNode(block_name, nodelist) 

241 

242 

243def construct_relative_path(current_template_name, relative_name): 

244 """ 

245 Convert a relative path (starting with './' or '../') to the full template 

246 name based on the current_template_name. 

247 """ 

248 has_quotes = (relative_name.startswith('"') and relative_name.endswith('"')) or ( 

249 relative_name.startswith("'") and relative_name.endswith("'") 

250 ) 

251 new_name = relative_name.strip("'\"") 

252 if not new_name.startswith(("./", "../")): 

253 # relative_name is a variable or a literal that doesn't contain a 

254 # relative path. 

255 return relative_name 

256 

257 new_name = posixpath.normpath( 

258 posixpath.join( 

259 posixpath.dirname(current_template_name.lstrip("/")), 

260 new_name, 

261 ) 

262 ) 

263 if new_name.startswith("../"): 

264 raise TemplateSyntaxError( 

265 "The relative path '%s' points outside the file hierarchy that " 

266 "template '%s' is in." % (relative_name, current_template_name) 

267 ) 

268 if current_template_name.lstrip("/") == new_name: 

269 raise TemplateSyntaxError( 

270 "The relative path '%s' was translated to template name '%s', the " 

271 "same template in which the tag appears." 

272 % (relative_name, current_template_name) 

273 ) 

274 return f'"{new_name}"' if has_quotes else new_name 

275 

276 

277@register.tag("extends") 

278def do_extends(parser, token): 

279 """ 

280 Signal that this template extends a parent template. 

281 

282 This tag may be used in two ways: ``{% extends "base" %}`` (with quotes) 

283 uses the literal value "base" as the name of the parent template to extend, 

284 or ``{% extends variable %}`` uses the value of ``variable`` as either the 

285 name of the parent template to extend (if it evaluates to a string) or as 

286 the parent template itself (if it evaluates to a Template object). 

287 """ 

288 bits = token.split_contents() 

289 if len(bits) != 2: 

290 raise TemplateSyntaxError("'%s' takes one argument" % bits[0]) 

291 bits[1] = construct_relative_path(parser.origin.template_name, bits[1]) 

292 parent_name = parser.compile_filter(bits[1]) 

293 nodelist = parser.parse() 

294 if nodelist.get_nodes_by_type(ExtendsNode): 

295 raise TemplateSyntaxError( 

296 "'%s' cannot appear more than once in the same template" % bits[0] 

297 ) 

298 return ExtendsNode(nodelist, parent_name) 

299 

300 

301@register.tag("include") 

302def do_include(parser, token): 

303 """ 

304 Load a template and render it with the current context. You can pass 

305 additional context using keyword arguments. 

306 

307 Example:: 

308 

309 {% include "foo/some_include" %} 

310 {% include "foo/some_include" with bar="BAZZ!" baz="BING!" %} 

311 

312 Use the ``only`` argument to exclude the current context when rendering 

313 the included template:: 

314 

315 {% include "foo/some_include" only %} 

316 {% include "foo/some_include" with bar="1" only %} 

317 """ 

318 bits = token.split_contents() 

319 if len(bits) < 2: 

320 raise TemplateSyntaxError( 

321 "%r tag takes at least one argument: the name of the template to " 

322 "be included." % bits[0] 

323 ) 

324 options = {} 

325 remaining_bits = bits[2:] 

326 while remaining_bits: 

327 option = remaining_bits.pop(0) 

328 if option in options: 

329 raise TemplateSyntaxError( 

330 "The %r option was specified more than once." % option 

331 ) 

332 if option == "with": 

333 value = token_kwargs(remaining_bits, parser, support_legacy=False) 

334 if not value: 

335 raise TemplateSyntaxError( 

336 '"with" in %r tag needs at least one keyword argument.' % bits[0] 

337 ) 

338 elif option == "only": 

339 value = True 

340 else: 

341 raise TemplateSyntaxError( 

342 "Unknown argument for %r tag: %r." % (bits[0], option) 

343 ) 

344 options[option] = value 

345 isolated_context = options.get("only", False) 

346 namemap = options.get("with", {}) 

347 bits[1] = construct_relative_path(parser.origin.template_name, bits[1]) 

348 return IncludeNode( 

349 parser.compile_filter(bits[1]), 

350 extra_context=namemap, 

351 isolated_context=isolated_context, 

352 )