Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/render/html/default.py: 0%
200 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-13 11:04 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-13 11:04 +0000
1import codecs
2import collections
3import enum
4import functools
5import logging
6import os
7import typing as t
9from jinja2 import ChoiceLoader, Environment, FileSystemLoader, Template
11from viur.core import conf, current, errors, securitykey
12from viur.core.bones import *
13from viur.core.i18n import translate, LanguageWrapper, TranslationExtension
14from viur.core.skeleton import SkelList, SkeletonInstance, remove_render_preparation_deep
15from . import utils as jinjaUtils
16from ..abstract import AbstractRenderer
17from ..json.default import CustomJsonEncoder
19KeyValueWrapper = collections.namedtuple("KeyValueWrapper", ["key", "descr"])
22class Render(AbstractRenderer):
23 """
24 The core jinja2 render.
26 This is the bridge between your ViUR modules and your templates.
27 First, the default jinja2-api is exposed to your templates. See http://jinja.pocoo.org/ for
28 more information. Second, we'll pass data das global variables to templates depending on the
29 current action.
31 - For list() a `skellist` is provided containing all requested skeletons to a limit
32 - For view(): skel - a dictionary with values from the skeleton prepared for use inside html
33 - For add()/edit: a dictionary as `skel` with `values`, `structure` and `errors` as keys.
35 Third, a bunch of global filters (like urlencode) and functions (getEntry, ..) are available to templates.
37 See the ViUR Documentation for more information about functions and data available to jinja2 templates.
39 Its possible for modules to extend the list of filters/functions available to templates by defining
40 a function called `jinjaEnv`. Its called from the render when the environment is first created and
41 can extend/override the functionality exposed to templates.
43 """
44 kind = "html"
46 listTemplate = "list"
47 viewTemplate = "view"
48 addTemplate = "add"
49 editTemplate = "edit"
51 addSuccessTemplate = "add_success"
52 editSuccessTemplate = "edit_success"
53 deleteSuccessTemplate = "delete_success"
55 listRepositoriesTemplate = "list_repositories" # fixme: This is a relict, should be solved differently (later!).
57 __haveEnvImported_ = False
59 def __init__(self, *args, **kwargs):
60 super().__init__(*args, **kwargs)
61 if not Render.__haveEnvImported_:
62 # We defer loading our plugins to this point to avoid circular imports
63 # noinspection PyUnresolvedReferences
64 from . import env
65 Render.__haveEnvImported_ = True
67 def getTemplateFileName(
68 self,
69 template: str | list[str] | tuple[str],
70 ignoreStyle: bool = False,
71 raise_exception: bool = True,
72 ) -> str | None:
73 """
74 Returns the filename of the template.
76 This function decides in which language and which style a given template is rendered.
77 The style is provided as get-parameters for special-case templates that differ from
78 their usual way.
80 It is advised to override this function in case that
81 :func:`viur.core.render.jinja2.default.Render.getLoaders` is redefined.
83 :param template: The basename of the template to use. This can optionally be also a sequence of names.
84 :param ignoreStyle: Ignore any maybe given style hints.
85 :param raise_exception: Defaults to raise an exception when not found, otherwise returns None.
87 :returns: Filename of the template
88 """
89 validChars = "abcdefghijklmnopqrstuvwxyz1234567890-"
90 htmlpath = getattr(self, "htmlpath", "html")
92 if (
93 not ignoreStyle
94 and (style := current.request.get().template_style)
95 and all(x in validChars for x in style.lower())
96 ):
97 style_postfix = f"_{style}"
98 else:
99 style_postfix = ""
101 lang = current.language.get()
103 if not isinstance(template, (tuple, list)):
104 template = (template,)
106 for tpl in template:
107 filenames = [tpl]
108 if style_postfix:
109 filenames.append(tpl + style_postfix)
111 if lang:
112 filenames += [
113 os.path.join(lang, _tpl)
114 for _tpl in filenames
115 ]
117 for filename in reversed(filenames):
118 filename += ".html"
120 if "_" in filename:
121 dirname, tail = filename.split("_", 1)
122 if tail:
123 if conf.instance.project_base_path.joinpath(htmlpath, dirname, filename).is_file():
124 return os.path.join(dirname, filename)
126 if conf.instance.project_base_path.joinpath(htmlpath, filename).is_file():
127 return filename
129 if conf.instance.core_base_path.joinpath("viur", "core", "template", filename).is_file():
130 return filename
132 msg = f"""Template {" or ".join((repr(tpl) for tpl in template))} not found."""
133 if raise_exception:
134 raise errors.NotFound(msg)
136 logging.error(msg)
137 return None
139 def getLoaders(self) -> ChoiceLoader:
140 """
141 Return the list of Jinja2 loaders which should be used.
143 May be overridden to provide an alternative loader
144 (e.g. for fetching templates from the datastore).
145 """
146 # fixme: Why not use ChoiceLoader directly for template loading?
147 return ChoiceLoader((
148 FileSystemLoader(getattr(self, "htmlpath", "html")),
149 FileSystemLoader(conf.instance.core_base_path / "viur" / "core" / "template"),
150 ))
152 def renderBoneValue(self,
153 bone: BaseBone,
154 skel: SkeletonInstance,
155 key: t.Any, # TODO: unused
156 boneValue: t.Any,
157 isLanguageWrapped: bool = False
158 ) -> list | dict | KeyValueWrapper | LanguageWrapper | str | None:
159 """
160 Renders the value of a bone.
162 It can be overridden and super-called from a custom renderer.
164 :param bone: The bone which value should be rendered.
165 (inherited from :class:`viur.core.bones.base.BaseBone`).
166 :param skel: The skeleton containing the bone instance.
167 :param key: The name of the bone.
168 :param boneValue: The value of the bone.
169 :param isLanguageWrapped: Is this bone wrapped inside a :class:`LanguageWrapper`?
171 :return: A dict containing the rendered attributes.
172 """
173 if bone.languages and not isLanguageWrapped:
174 res = LanguageWrapper(bone.languages)
175 if isinstance(boneValue, dict):
176 for language in bone.languages:
177 if language in boneValue:
178 res[language] = self.renderBoneValue(bone, skel, key, boneValue[language], True)
179 return res
180 elif bone.type == "select" or bone.type.startswith("select."):
181 def get_label(value) -> str:
182 if isinstance(value, enum.Enum):
183 return bone.values.get(value.value, value.name)
184 return bone.values.get(value, str(value))
186 if isinstance(boneValue, list):
187 return {val: get_label(val) for val in boneValue}
189 return KeyValueWrapper(boneValue, get_label(boneValue))
191 elif bone.type == "relational" or bone.type.startswith("relational."):
192 if isinstance(boneValue, list):
193 tmpList = []
194 for k in boneValue:
195 if not k:
196 continue
197 if bone.using is not None and k["rel"]:
198 k["rel"].renderPreparation = self.renderBoneValue
199 usingData = k["rel"]
200 else:
201 usingData = None
202 k["dest"].renderPreparation = self.renderBoneValue
203 tmpList.append({
204 "dest": k["dest"],
205 "rel": usingData
206 })
207 return tmpList
208 elif isinstance(boneValue, dict):
209 if bone.using is not None and boneValue["rel"]:
210 boneValue["rel"].renderPreparation = self.renderBoneValue
211 usingData = boneValue["rel"]
212 else:
213 usingData = None
214 boneValue["dest"].renderPreparation = self.renderBoneValue
215 return {
216 "dest": boneValue["dest"],
217 "rel": usingData
218 }
219 elif bone.type == "record" or bone.type.startswith("record."):
220 value = boneValue
221 if value:
222 if bone.multiple:
223 ret = []
224 for entry in value:
225 entry.renderPreparation = self.renderBoneValue
226 ret.append(entry)
227 return ret
228 value.renderPreparation = self.renderBoneValue
229 return value
230 elif bone.type == "password":
231 return ""
232 elif bone.type == "key":
233 return str(boneValue) if boneValue else None
235 else:
236 return boneValue
238 return None
240 def get_template(self, action: str, template: str) -> t.Optional[Template]:
241 """
242 Internal function for retrieving a template from an action name.
243 """
244 if not template:
245 default_template = action + "Template"
246 template = getattr(self.parent, default_template, None) or getattr(self, default_template, None)
248 if not template:
249 raise errors.NotImplemented(str(translate(
250 "core.html.error.template_not_configured",
251 "Template '{{default_template}}' not configured.",
252 default_variables={
253 "default_template": default_template,
254 }
255 )))
257 return template and self.getEnv().get_template(self.getTemplateFileName(template))
259 def render_action_template(
260 self,
261 default: str,
262 skel: SkeletonInstance,
263 action: str,
264 tpl: str = None,
265 params: dict = None,
266 **kwargs
267 ) -> str:
268 """
269 Internal action rendering that provides a variable structure to render an input-form.
270 The required information is passed via skel["structure"], skel["value"] and skel["errors"].
272 Any data in **kwargs is passed unmodified to the template.
274 :param default: The default action to render, which is used to construct a template name.
275 :param skel: Skeleton of which should be used for the action.
276 :param action: The name of the action, which is passed into the template.
277 :param tpl: Name of a different template, which should be used instead of the default one.
278 :param params: Optional data that will be passed unmodified to the template.
280 Any data in **kwargs is passed unmodified to the template.
282 :return: Returns the emitted HTML response.
283 """
284 template = self.get_template(default, tpl)
286 if skel is not None:
287 skel.skey = BaseBone(descr="SecurityKey", readOnly=True, visible=False)
288 skel["skey"] = securitykey.create()
290 # fixme: Is this still be used?
291 if current.request.get().kwargs.get("nomissing") == "1":
292 if isinstance(skel, SkeletonInstance):
293 super(SkeletonInstance, skel).__setattr__("errors", [])
295 skel.renderPreparation = self.renderBoneValue
297 return template.render(
298 skel={
299 "structure": skel.structure(),
300 "errors": skel.errors,
301 "value": skel
302 } if skel is not None else None,
303 action=action,
304 params=params,
305 **kwargs
306 )
308 def render_view_template(
309 self,
310 default: str,
311 skel: SkeletonInstance,
312 action: str,
313 tpl: str = None,
314 params: dict = None,
315 **kwargs
316 ) -> str:
317 """
318 Renders a page with an entry.
320 :param default: The default action to render, which is used to construct a template name.
321 :param skel: Skeleton which contains the data of the corresponding entity.
322 :param action: The name of the action, which is passed into the template.
323 :param tpl: Name of a different template, which should be used instead of the default one.
324 :param params: Optional data that will be passed unmodified to the template
326 Any data in **kwargs is passed unmodified to the template.
328 :return: Returns the emitted HTML response.
329 """
330 template = self.get_template(default, tpl)
332 if isinstance(skel, SkeletonInstance):
333 skel.renderPreparation = self.renderBoneValue
335 return template.render(
336 skel=skel,
337 action=action,
338 params=params,
339 **kwargs
340 )
342 def list(self, skellist: SkelList, action: str = "list", tpl: str = None, params: t.Any = None, **kwargs) -> str:
343 """
344 Renders a page with a list of entries.
346 :param skellist: List of Skeletons with entries to display.
347 :param action: The name of the action, which is passed into the template.
348 :param tpl: Name of a different template, which should be used instead of the default one.
350 :param params: Optional data that will be passed unmodified to the template
352 Any data in **kwargs is passed unmodified to the template.
354 :return: Returns the emitted HTML response.
355 """
356 template = self.get_template("list", tpl)
358 for skel in skellist:
359 skel.renderPreparation = self.renderBoneValue
361 return template.render(skellist=skellist, action=action, params=params, **kwargs)
363 def view(self, skel: SkeletonInstance, action: str = "view", tpl: str = None, params: t.Any = None,
364 **kwargs) -> str:
365 """
366 Renders a page for viewing an entry.
368 For details, see self.render_view_template().
369 """
370 return self.render_view_template("view", skel, action, tpl, params, **kwargs)
372 def add(self, skel: SkeletonInstance, action: str = "add", tpl: str = None, params: t.Any = None, **kwargs) -> str:
373 """
374 Renders a page for adding an entry.
376 For details, see self.render_action_template().
377 """
378 return self.render_action_template("add", skel, action, tpl, params, **kwargs)
380 def edit(self, skel: SkeletonInstance, action: str = "edit", tpl: str = None, params: t.Any = None,
381 **kwargs) -> str:
382 """
383 Renders a page for modifying an entry.
385 For details, see self.render_action_template().
386 """
387 return self.render_action_template("edit", skel, action, tpl, params, **kwargs)
389 def addSuccess(
390 self,
391 skel: SkeletonInstance,
392 action: str = "addSuccess",
393 tpl: str = None,
394 params: t.Any = None,
395 **kwargs
396 ) -> str:
397 """
398 Renders a page, informing that an entry has been successfully created.
400 For details, see self.render_view_template().
401 """
402 return self.render_view_template("addSuccess", skel, action, tpl, params, **kwargs)
404 def editSuccess(
405 self,
406 skel: SkeletonInstance,
407 action: str = "editSuccess",
408 tpl: str = None,
409 params: t.Any = None,
410 **kwargs
411 ) -> str:
412 """
413 Renders a page, informing that an entry has been successfully modified.
415 For details, see self.render_view_template().
416 """
417 return self.render_view_template("editSuccess", skel, action, tpl, params, **kwargs)
419 def deleteSuccess(
420 self,
421 skel: SkeletonInstance,
422 action: str = "deleteSuccess",
423 tpl: str = None,
424 params: t.Any = None,
425 **kwargs
426 ) -> str:
427 """
428 Renders a page, informing that an entry has been successfully deleted.
430 For details, see self.render_view_template().
431 """
432 return self.render_view_template("deleteSuccess", skel, action, tpl, params, **kwargs)
434 def listRootNodes( # fixme: This is a relict, should be solved differently (later!).
435 self,
436 repos: t.List[dict[t.Literal["key", "name"], t.Any]],
437 action: str = "listrootnodes",
438 tpl: str = None,
439 params: t.Any = None,
440 **kwargs
441 ) -> str:
442 """
443 Renders a list of available root nodes.
445 :param repos: List of repositories (dict with "key"=>Repo-Key and "name"=>Repo-Name)
446 :param action: The name of the action, which is passed into the template.
447 :param tpl: Name of a different template, which should be used instead of the default one.
448 :param params: Optional data that will be passed unmodified to the template
450 Any data in **kwargs is passed unmodified to the template.
452 :return: Returns the emitted HTML response.
453 """
454 template = self.get_template("listRootNodes", tpl)
455 return template.render(repos=repos, action=action, params=params, **kwargs)
457 def render(
458 self,
459 action: str,
460 skel: t.Optional[SkeletonInstance] = None,
461 *,
462 tpl: t.Optional[str] = None,
463 next_url: t.Optional[str] = None,
464 **kwargs
465 ):
466 """
467 Universal rendering function.
469 Handles an action and a skeleton. It shall be used by any action, in future.
470 It additionally allows for a tpl-parameter in HTML-renderer.
471 """
472 if next_url:
473 raise errors.Redirect(next_url)
475 return self.render_action_template(action, skel, action, tpl, params=kwargs)
477 def renderEmail(self,
478 dests: t.List[str],
479 file: str = None,
480 template: str = None,
481 skel: None | dict | SkeletonInstance | t.List["SkeletonInstance"] = None,
482 **kwargs) -> tuple[str, str]:
483 """
484 Renders an email.
485 Uses the first not-empty line as subject and the remaining template as body.
487 :param dests: Destination recipients.
488 :param file: The name of a template from the deploy/emails directory.
489 :param template: This string is interpreted as the template contents. Alternative to load from template file.
490 :param skel: Skeleton or dict which data to supply to the template.
491 :return: Returns the rendered email subject and body.
492 """
493 if isinstance(skel, SkeletonInstance):
494 skel.renderPreparation = self.renderBoneValue
496 elif isinstance(skel, list):
497 for x in skel:
498 if isinstance(x, SkeletonInstance):
499 x.renderPreparation = self.renderBoneValue
500 if file is not None:
501 try:
502 tpl = self.getEnv().from_string(codecs.open("emails/" + file + ".email", "r", "utf-8").read())
503 except Exception as err:
504 logging.exception(err)
505 tpl = self.getEnv().get_template(file + ".email")
506 else:
507 tpl = self.getEnv().from_string(template)
508 content = tpl.render(skel=skel, dests=dests, **kwargs).lstrip().splitlines()
509 if len(content) == 1:
510 content.insert(0, "") # add empty subject
512 remove_render_preparation_deep(skel)
514 return content[0], os.linesep.join(content[1:]).lstrip()
516 def getEnv(self) -> Environment:
517 """
518 Constructs the Jinja2 environment.
520 If an application specifies an jinja2Env function, this function
521 can alter the environment before its used to parse any template.
523 :return: Extended Jinja2 environment.
524 """
525 if "env" not in dir(self):
526 loaders = self.getLoaders()
527 self.env = Environment(loader=loaders,
528 extensions=["jinja2.ext.do", "jinja2.ext.loopcontrols", TranslationExtension])
529 self.env.trCache = {}
530 self.env.policies["json.dumps_kwargs"]["cls"] = CustomJsonEncoder
532 # Import functions.
533 for name, func in jinjaUtils.getGlobalFunctions().items():
534 self.env.globals[name] = functools.partial(func, self)
536 # Import filters.
537 for name, func in jinjaUtils.getGlobalFilters().items():
538 self.env.filters[name] = functools.partial(func, self)
540 # Import tests.
541 for name, func in jinjaUtils.getGlobalTests().items():
542 self.env.tests[name] = functools.partial(func, self)
544 # Import extensions.
545 for ext in jinjaUtils.getGlobalExtensions():
546 self.env.add_extension(ext)
548 # Import module-specific environment, if available.
549 if "jinjaEnv" in dir(self.parent):
550 self.env = self.parent.jinjaEnv(self.env)
552 return self.env