Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/render/html/default.py: 0%
198 statements
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-16 22:16 +0000
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-16 22:16 +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, db, errors, securitykey
12from viur.core.bones import *
13from viur.core.i18n import LanguageWrapper, TranslationExtension
14from viur.core.skeleton import SkelList, SkeletonInstance
15from . import utils as jinjaUtils
16from ..json.default import CustomJsonEncoder
18KeyValueWrapper = collections.namedtuple("KeyValueWrapper", ["key", "descr"])
21class Render(object):
22 """
23 The core jinja2 render.
25 This is the bridge between your ViUR modules and your templates.
26 First, the default jinja2-api is exposed to your templates. See http://jinja.pocoo.org/ for
27 more information. Second, we'll pass data das global variables to templates depending on the
28 current action.
30 - For list() a `skellist` is provided containing all requested skeletons to a limit
31 - For view(): skel - a dictionary with values from the skeleton prepared for use inside html
32 - For add()/edit: a dictionary as `skel` with `values`, `structure` and `errors` as keys.
34 Third, a bunch of global filters (like urlencode) and functions (getEntry, ..) are available to templates.
36 See the ViUR Documentation for more information about functions and data available to jinja2 templates.
38 Its possible for modules to extend the list of filters/functions available to templates by defining
39 a function called `jinjaEnv`. Its called from the render when the environment is first created and
40 can extend/override the functionality exposed to templates.
42 """
43 kind = "html"
45 listTemplate = "list"
46 viewTemplate = "view"
47 addTemplate = "add"
48 editTemplate = "edit"
50 addSuccessTemplate = "add_success"
51 editSuccessTemplate = "edit_success"
52 deleteSuccessTemplate = "delete_success"
54 listRepositoriesTemplate = "list_repositories" # fixme: This is a relict, should be solved differently (later!).
56 __haveEnvImported_ = False
58 def __init__(self, parent=None, *args, **kwargs):
59 super(Render, self).__init__(*args, **kwargs)
60 if not Render.__haveEnvImported_:
61 # We defer loading our plugins to this point to avoid circular imports
62 # noinspection PyUnresolvedReferences
63 from . import env
64 Render.__haveEnvImported_ = True
65 self.parent = parent
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 db.encodeKey(boneValue) if boneValue else None
235 else:
236 return boneValue
238 return None
240 def get_template(self, action: str, template: str) -> 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)
248 return self.getEnv().get_template(self.getTemplateFileName(template))
250 def render_action_template(
251 self,
252 default: str,
253 skel: SkeletonInstance,
254 action: str,
255 tpl: str = None,
256 params: dict = None,
257 **kwargs
258 ) -> str:
259 """
260 Internal action rendering that provides a variable structure to render an input-form.
261 The required information is passed via skel["structure"], skel["value"] and skel["errors"].
263 Any data in **kwargs is passed unmodified to the template.
265 :param default: The default action to render, which is used to construct a template name.
266 :param skel: Skeleton of which should be used for the action.
267 :param action: The name of the action, which is passed into the template.
268 :param tpl: Name of a different template, which should be used instead of the default one.
269 :param params: Optional data that will be passed unmodified to the template.
271 Any data in **kwargs is passed unmodified to the template.
273 :return: Returns the emitted HTML response.
274 """
275 template = self.get_template(default, tpl)
277 skel.skey = BaseBone(descr="SecurityKey", readOnly=True, visible=False)
278 skel["skey"] = securitykey.create()
280 # fixme: Is this still be used?
281 if current.request.get().kwargs.get("nomissing") == "1":
282 if isinstance(skel, SkeletonInstance):
283 super(SkeletonInstance, skel).__setattr__("errors", [])
285 skel.renderPreparation = self.renderBoneValue
287 return template.render(
288 skel={
289 "structure": skel.structure(),
290 "errors": skel.errors,
291 "value": skel
292 },
293 action=action,
294 params=params,
295 **kwargs
296 )
298 def render_view_template(
299 self,
300 default: str,
301 skel: SkeletonInstance,
302 action: str,
303 tpl: str = None,
304 params: dict = None,
305 **kwargs
306 ) -> str:
307 """
308 Renders a page with an entry.
310 :param default: The default action to render, which is used to construct a template name.
311 :param skel: Skeleton which contains the data of the corresponding entity.
312 :param action: The name of the action, which is passed into the template.
313 :param tpl: Name of a different template, which should be used instead of the default one.
314 :param params: Optional data that will be passed unmodified to the template
316 Any data in **kwargs is passed unmodified to the template.
318 :return: Returns the emitted HTML response.
319 """
320 template = self.get_template(default, tpl)
322 if isinstance(skel, SkeletonInstance):
323 skel.renderPreparation = self.renderBoneValue
325 return template.render(
326 skel=skel,
327 action=action,
328 params=params,
329 **kwargs
330 )
332 def list(self, skellist: SkelList, action: str = "list", tpl: str = None, params: t.Any = None, **kwargs) -> str:
333 """
334 Renders a page with a list of entries.
336 :param skellist: List of Skeletons with entries to display.
337 :param action: The name of the action, which is passed into the template.
338 :param tpl: Name of a different template, which should be used instead of the default one.
340 :param params: Optional data that will be passed unmodified to the template
342 Any data in **kwargs is passed unmodified to the template.
344 :return: Returns the emitted HTML response.
345 """
346 template = self.get_template("list", tpl)
348 for skel in skellist:
349 skel.renderPreparation = self.renderBoneValue
351 return template.render(skellist=skellist, action=action, params=params, **kwargs)
353 def view(self, skel: SkeletonInstance, action: str = "view", tpl: str = None, params: t.Any = None,
354 **kwargs) -> str:
355 """
356 Renders a page for viewing an entry.
358 For details, see self.render_view_template().
359 """
360 return self.render_view_template("view", skel, action, tpl, params, **kwargs)
362 def add(self, skel: SkeletonInstance, action: str = "add", tpl: str = None, params: t.Any = None, **kwargs) -> str:
363 """
364 Renders a page for adding an entry.
366 For details, see self.render_action_template().
367 """
368 return self.render_action_template("add", skel, action, tpl, params, **kwargs)
370 def edit(self, skel: SkeletonInstance, action: str = "edit", tpl: str = None, params: t.Any = None,
371 **kwargs) -> str:
372 """
373 Renders a page for modifying an entry.
375 For details, see self.render_action_template().
376 """
377 return self.render_action_template("edit", skel, action, tpl, params, **kwargs)
379 def addSuccess(
380 self,
381 skel: SkeletonInstance,
382 action: str = "addSuccess",
383 tpl: str = None,
384 params: t.Any = None,
385 **kwargs
386 ) -> str:
387 """
388 Renders a page, informing that an entry has been successfully created.
390 For details, see self.render_view_template().
391 """
392 return self.render_view_template("addSuccess", skel, action, tpl, params, **kwargs)
394 def editSuccess(
395 self,
396 skel: SkeletonInstance,
397 action: str = "editSuccess",
398 tpl: str = None,
399 params: t.Any = None,
400 **kwargs
401 ) -> str:
402 """
403 Renders a page, informing that an entry has been successfully modified.
405 For details, see self.render_view_template().
406 """
407 return self.render_view_template("editSuccess", skel, action, tpl, params, **kwargs)
409 def deleteSuccess(
410 self,
411 skel: SkeletonInstance,
412 action: str = "deleteSuccess",
413 tpl: str = None,
414 params: t.Any = None,
415 **kwargs
416 ) -> str:
417 """
418 Renders a page, informing that an entry has been successfully deleted.
420 For details, see self.render_view_template().
421 """
422 return self.render_view_template("deleteSuccess", skel, action, tpl, params, **kwargs)
424 def listRootNodes( # fixme: This is a relict, should be solved differently (later!).
425 self,
426 repos: t.List[dict[t.Literal["key", "name"], t.Any]],
427 action: str = "listrootnodes",
428 tpl: str = None,
429 params: t.Any = None,
430 **kwargs
431 ) -> str:
432 """
433 Renders a list of available root nodes.
435 :param repos: List of repositories (dict with "key"=>Repo-Key and "name"=>Repo-Name)
436 :param action: The name of the action, which is passed into the template.
437 :param tpl: Name of a different template, which should be used instead of the default one.
438 :param params: Optional data that will be passed unmodified to the template
440 Any data in **kwargs is passed unmodified to the template.
442 :return: Returns the emitted HTML response.
443 """
444 template = self.get_template("listRootNodes", tpl)
445 return template.render(repos=repos, action=action, params=params, **kwargs)
447 def renderEmail(self,
448 dests: t.List[str],
449 file: str = None,
450 template: str = None,
451 skel: None | dict | SkeletonInstance | t.List["SkeletonInstance"] = None,
452 **kwargs) -> tuple[str, str]:
453 """
454 Renders an email.
455 Uses the first not-empty line as subject and the remaining template as body.
457 :param dests: Destination recipients.
458 :param file: The name of a template from the deploy/emails directory.
459 :param template: This string is interpreted as the template contents. Alternative to load from template file.
460 :param skel: Skeleton or dict which data to supply to the template.
461 :return: Returns the rendered email subject and body.
462 """
463 if isinstance(skel, SkeletonInstance):
464 skel.renderPreparation = self.renderBoneValue
466 elif isinstance(skel, list):
467 for x in skel:
468 if isinstance(x, SkeletonInstance):
469 x.renderPreparation = self.renderBoneValue
470 if file is not None:
471 try:
472 tpl = self.getEnv().from_string(codecs.open("emails/" + file + ".email", "r", "utf-8").read())
473 except Exception as err:
474 logging.exception(err)
475 tpl = self.getEnv().get_template(file + ".email")
476 else:
477 tpl = self.getEnv().from_string(template)
478 content = tpl.render(skel=skel, dests=dests, **kwargs).lstrip().splitlines()
479 if len(content) == 1:
480 content.insert(0, "") # add empty subject
482 if isinstance(skel, SkeletonInstance):
483 skel.renderPreparation = None
485 elif isinstance(skel, list):
486 for x in skel:
487 if isinstance(x, SkeletonInstance):
488 x.renderPreparation = None
490 return content[0], os.linesep.join(content[1:]).lstrip()
492 def getEnv(self) -> Environment:
493 """
494 Constructs the Jinja2 environment.
496 If an application specifies an jinja2Env function, this function
497 can alter the environment before its used to parse any template.
499 :return: Extended Jinja2 environment.
500 """
501 if "env" not in dir(self):
502 loaders = self.getLoaders()
503 self.env = Environment(loader=loaders,
504 extensions=["jinja2.ext.do", "jinja2.ext.loopcontrols", TranslationExtension])
505 self.env.trCache = {}
506 self.env.policies["json.dumps_kwargs"]["cls"] = CustomJsonEncoder
508 # Import functions.
509 for name, func in jinjaUtils.getGlobalFunctions().items():
510 self.env.globals[name] = functools.partial(func, self)
512 # Import filters.
513 for name, func in jinjaUtils.getGlobalFilters().items():
514 self.env.filters[name] = functools.partial(func, self)
516 # Import tests.
517 for name, func in jinjaUtils.getGlobalTests().items():
518 self.env.tests[name] = functools.partial(func, self)
520 # Import extensions.
521 for ext in jinjaUtils.getGlobalExtensions():
522 self.env.add_extension(ext)
524 # Import module-specific environment, if available.
525 if "jinjaEnv" in dir(self.parent):
526 self.env = self.parent.jinjaEnv(self.env)
528 return self.env