Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/render/html/env/viur.py: 0%
360 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
1from collections import OrderedDict
3import logging
4import os
5import typing as t
6import urllib
7import urllib.parse
8from datetime import timedelta
9from hashlib import sha512
10from qrcode import make as qrcode_make
11from qrcode.image import svg as qrcode_svg
13import string
14from viur.core import Method, current, db, errors, prototypes, securitykey, utils
15from viur.core.config import conf
16from viur.core.i18n import translate as translate_class
17from viur.core.render.html.utils import jinjaGlobalFilter, jinjaGlobalFunction
18from viur.core.request import TEMPLATE_STYLE_KEY
19from viur.core.skeleton import RelSkel, SkeletonInstance
20from viur.core.modules import file
21from ..default import Render
24@jinjaGlobalFunction
25def translate(
26 render: Render,
27 key: str,
28 default_text: str = None,
29 hint: str = None,
30 force_lang: str | None = None,
31 **kwargs
32) -> str:
33 """Jinja function for translations
35 See also :class:`core.i18n.TranslationExtension`.
36 """
37 return translate_class(key, default_text, hint, force_lang)(**kwargs)
40@jinjaGlobalFunction
41def execRequest(render: Render, path: str, *args, **kwargs) -> t.Any:
42 """
43 Jinja2 global: Perform an internal Request.
45 This function allows to embed the result of another request inside the current template.
46 All optional parameters are passed to the requested resource.
48 :param path: Local part of the url, e.g. user/list. Must not start with an /.
49 Must not include an protocol or hostname.
51 :returns: Whatever the requested resource returns. This is *not* limited to strings!
52 """
53 request = current.request.get()
54 cachetime = kwargs.pop("cachetime", 0)
55 if conf.debug.disable_cache or request.disableCache: # Caching disabled by config
56 cachetime = 0
57 cacheEnvKey = None
58 if conf.cache_environment_key:
59 try:
60 cacheEnvKey = conf.cache_environment_key()
61 except RuntimeError:
62 cachetime = 0
63 if cachetime:
64 # Calculate the cache key that entry would be stored under
65 tmpList = [f"{k}:{v}" for k, v in kwargs.items()]
66 tmpList.sort()
67 tmpList.extend(list(args))
68 tmpList.append(path)
69 if cacheEnvKey is not None:
70 tmpList.append(cacheEnvKey)
71 try:
72 appVersion = request.request.environ["CURRENT_VERSION_ID"].split('.')[0]
73 except:
74 appVersion = ""
75 logging.error("Could not determine the current application id! Caching might produce unexpected results!")
76 tmpList.append(appVersion)
77 mysha512 = sha512()
78 mysha512.update(str(tmpList).encode("UTF8"))
79 # TODO: Add hook for optional memcache or remove these remnants
80 cacheKey = f"jinja2_cache_{mysha512.hexdigest()}"
81 res = None # memcache.get(cacheKey)
82 if res:
83 return res
84 # Pop this key after building the cache string with it
85 template_style = kwargs.pop(TEMPLATE_STYLE_KEY, None)
86 tmp_params = request.kwargs.copy()
87 request.kwargs = {"__args": args, "__outer": tmp_params}
88 request.kwargs.update(kwargs)
89 lastRequestState = request.internalRequest
90 request.internalRequest = True
91 last_template_style = request.template_style
92 request.template_style = template_style
93 caller = conf.main_app
94 pathlist = path.split("/")
95 for currpath in pathlist:
96 if currpath in dir(caller):
97 caller = getattr(caller, currpath)
98 elif "index" in dir(caller) and hasattr(getattr(caller, "index"), '__call__'):
99 caller = getattr(caller, "index")
100 else:
101 request.kwargs = tmp_params # Reset RequestParams
102 request.internalRequest = lastRequestState
103 request.template_style = last_template_style
104 return f"{path=} not found (failed at {currpath!r})"
106 if not (isinstance(caller, Method) and caller.exposed is not None):
107 request.kwargs = tmp_params # Reset RequestParams
108 request.internalRequest = lastRequestState
109 request.template_style = last_template_style
110 return f"{caller!r} not callable or not exposed"
111 try:
112 resstr = caller(*args, **kwargs)
113 except Exception as e:
114 logging.error(f"Caught exception in execRequest while calling {path}")
115 logging.exception(e)
116 raise
117 request.kwargs = tmp_params
118 request.internalRequest = lastRequestState
119 request.template_style = last_template_style
120 if cachetime:
121 pass
122 # memcache.set(cacheKey, resstr, cachetime)
123 return resstr
126@jinjaGlobalFunction
127def getCurrentUser(render: Render) -> t.Optional[SkeletonInstance]:
128 """
129 Jinja2 global: Returns the current user from the session, or None if not logged in.
131 :return: A dict containing user data. Returns None if no user data is available.
132 """
133 currentUser = current.user.get()
134 if currentUser:
135 currentUser = currentUser.clone() # need to clone, as renderPreparation is changed
136 currentUser.renderPreparation = render.renderBoneValue
137 return currentUser
140@jinjaGlobalFunction
141def getSkel(render: Render, module: str, key: str = None, skel: str = "viewSkel") -> dict | bool | None:
142 """
143 Jinja2 global: Fetch an entry from a given module, and return the data as a dict,
144 prepared for direct use in the output.
146 It is possible to specify a different data-model as the one used for rendering
147 (e.g. an editSkel).
149 :param module: Name of the module, from which the data should be fetched.
150 :param key: Requested entity-key in an urlsafe-format. If the module is a Singleton
151 application, the parameter can be omitted.
152 :param skel: Specifies and optionally different data-model
154 :returns: dict on success, False on error.
155 """
156 if module not in dir(conf.main_app):
157 logging.error(f"getSkel called with unknown {module=}!")
158 return False
160 obj = getattr(conf.main_app, module)
162 if skel in dir(obj):
163 skel = getattr(obj, skel)()
165 if isinstance(obj, prototypes.singleton.Singleton) and not key:
166 # We fetching the entry from a singleton - No key needed
167 key = db.Key(skel.kindName, obj.getKey())
168 elif not key:
169 logging.info("getSkel called without a valid key")
170 return False
172 if not isinstance(skel, SkeletonInstance):
173 return False
175 if "canView" in dir(obj):
176 if not skel.fromDB(key):
177 logging.info(f"getSkel: Entry {key} not found")
178 return None
179 if isinstance(obj, prototypes.singleton.Singleton):
180 isAllowed = obj.canView()
181 elif isinstance(obj, prototypes.tree.Tree):
182 k = db.Key(key)
183 if k.kind.endswith("_rootNode"):
184 isAllowed = obj.canView("node", skel)
185 else:
186 isAllowed = obj.canView("leaf", skel)
187 else: # List and Hierarchies
188 isAllowed = obj.canView(skel)
189 if not isAllowed:
190 logging.error(f"getSkel: Access to {key} denied from canView")
191 return None
192 elif "listFilter" in dir(obj):
193 qry = skel.all().mergeExternalFilter({"key": str(key)})
194 qry = obj.listFilter(qry)
195 if not qry:
196 logging.info("listFilter permits getting entry, returning None")
197 return None
199 skel = qry.getSkel()
200 if not skel:
201 return None
203 else: # No Access-Test for this module
204 if not skel.fromDB(key):
205 return None
206 skel.renderPreparation = render.renderBoneValue
207 return skel
209 return False
212@jinjaGlobalFunction
213def getHostUrl(render: Render, forceSSL=False, *args, **kwargs):
214 """
215 Jinja2 global: Retrieve hostname with protocol.
217 :returns: Returns the hostname, including the currently used protocol, e.g: http://www.example.com
218 :rtype: str
219 """
220 url = current.request.get().request.url
221 url = url[:url.find("/", url.find("://") + 5)]
222 if forceSSL and url.startswith("http://"):
223 url = "https://" + url[7:]
224 return url
227@jinjaGlobalFunction
228def getVersionHash(render: Render) -> str:
229 """
230 Jinja2 global: Return the application hash for the current version. This can be used for cache-busting in
231 resource links (eg. /static/css/style.css?v={{ getVersionHash() }}. This hash is stable for each version
232 deployed (identical across all instances), but will change whenever a new version is deployed.
233 :return: The current version hash
234 """
235 return conf.instance.version_hash
238@jinjaGlobalFunction
239def getAppVersion(render: Render) -> str:
240 """
241 Jinja2 global: Return the application version for the current version as set on deployment.
242 :return: The current version
243 """
244 return conf.instance.app_version
247@jinjaGlobalFunction
248def redirect(render: Render, url: str) -> t.NoReturn:
249 """
250 Jinja2 global: Redirect to another URL.
252 :param url: URL to redirect to.
254 :raises: :exc:`viur.core.errors.Redirect`
255 """
256 raise errors.Redirect(url)
259@jinjaGlobalFunction
260def getLanguage(render: Render, resolveAlias: bool = False) -> str:
261 """
262 Jinja2 global: Returns the language used for this request.
264 :param resolveAlias: If True, the function tries to resolve the current language
265 using conf.i18n.language_alias_map.
266 """
267 lang = current.language.get()
268 if resolveAlias and lang in conf.i18n.language_alias_map:
269 lang = conf.i18n.language_alias_map[lang]
270 return lang
273@jinjaGlobalFunction
274def moduleName(render: Render) -> str:
275 """
276 Jinja2 global: Retrieve name of current module where this renderer is used within.
278 :return: Returns the name of the current module, or empty string if there is no module set.
279 """
280 if render.parent and "moduleName" in dir(render.parent):
281 return render.parent.moduleName
282 return ""
285@jinjaGlobalFunction
286def modulePath(render: Render) -> str:
287 """
288 Jinja2 global: Retrieve path of current module the renderer is used within.
290 :return: Returns the path of the current module, or empty string if there is no module set.
291 """
292 if render.parent and "modulePath" in dir(render.parent):
293 return render.parent.modulePath
294 return ""
297@jinjaGlobalFunction
298def getList(render: Render, module: str, skel: str = "viewSkel",
299 _noEmptyFilter: bool = False, *args, **kwargs) -> bool | None | list[SkeletonInstance]:
300 """
301 Jinja2 global: Fetches a list of entries which match the given filter criteria.
303 :param render: The html-renderer instance.
304 :param module: Name of the module from which list should be fetched.
305 :param skel: Name of the skeleton that is used to fetching the list.
306 :param _noEmptyFilter: If True, this function will not return any results if at least one
307 parameter is an empty list. This is useful to prevent filtering (e.g. by key) not being
308 performed because the list is empty.
309 :returns: Returns a dict that contains the "skellist" and "cursor" information,
310 or None on error case.
311 """
312 if module not in dir(conf.main_app):
313 logging.error(f"Jinja2-Render can't fetch a list from an unknown module {module}!")
314 return False
315 caller = getattr(conf.main_app, module)
316 if "viewSkel" not in dir(caller):
317 logging.error(f"Jinja2-Render cannot fetch a list from {module} due to missing viewSkel function")
318 return False
319 if _noEmptyFilter: # Test if any value of kwargs is an empty list
320 if any([isinstance(x, list) and not len(x) for x in kwargs.values()]):
321 return []
322 query = getattr(caller, "viewSkel")(skel).all()
323 query.mergeExternalFilter(kwargs)
324 if "listFilter" in dir(caller):
325 query = caller.listFilter(query)
326 if query is None:
327 return None
328 mylist = query.fetch()
329 if mylist:
330 for skel in mylist:
331 skel.renderPreparation = render.renderBoneValue
332 return mylist
335@jinjaGlobalFunction
336def getSecurityKey(render: Render, **kwargs) -> str:
337 """
338 Jinja2 global: Creates a new ViUR security key.
339 """
340 return securitykey.create(**kwargs)
343@jinjaGlobalFunction
344def getStructure(render: Render,
345 module: str,
346 skel: str = "viewSkel",
347 subSkel: t.Optional[str] = None) -> dict | bool:
348 """
349 Jinja2 global: Returns the skeleton structure instead of data for a module.
351 :param render: The html-renderer instance.
352 :param module: Module from which the skeleton is retrieved.
353 :param skel: Name of the skeleton.
354 :param subSkel: If set, return just that subskel instead of the whole skeleton
355 """
356 if module not in dir(conf.main_app):
357 return False
359 obj = getattr(conf.main_app, module)
361 if skel in dir(obj):
362 skel = getattr(obj, skel)()
364 if isinstance(skel, SkeletonInstance) or isinstance(skel, RelSkel):
365 if subSkel is not None:
366 try:
367 skel = skel.subSkel(subSkel)
369 except Exception as e:
370 logging.exception(e)
371 return False
373 return skel.structure()
375 return False
378@jinjaGlobalFunction
379def requestParams(render: Render) -> dict[str, str]:
380 """
381 Jinja2 global: Allows for accessing the request-parameters from the template.
383 These returned values are escaped, as users tend to use these in an unsafe manner.
385 :returns: dict of parameter and values.
386 """
387 res = {}
388 for k, v in current.request.get().kwargs.items():
389 res[utils.string.escape(k)] = utils.string.escape(v)
390 return res
393@jinjaGlobalFunction
394def updateURL(render: Render, **kwargs) -> str:
395 """
396 Jinja2 global: Constructs a new URL based on the current requests url.
398 Given parameters are replaced if they exists in the current requests url, otherwise there appended.
400 :returns: Returns a well-formed URL.
401 """
402 tmpparams = {}
403 tmpparams.update(current.request.get().kwargs)
405 for key in list(tmpparams.keys()):
406 if not key or key[0] == "_":
407 del tmpparams[key]
409 for key, value in list(kwargs.items()):
410 if value is None:
411 if key in tmpparams:
412 del tmpparams[key]
413 else:
414 tmpparams[key] = value
416 return "?" + urllib.parse.urlencode(tmpparams).replace("&", "&")
419@jinjaGlobalFilter
420def fileSize(render: Render, value: int | float, binary: bool = False) -> str:
421 """
422 Jinja2 filter: Format the value in an 'human-readable' file size (i.e. 13 kB, 4.1 MB, 102 Bytes, etc).
423 Per default, decimal prefixes are used (Mega, Giga, etc.). When the second parameter is set to True,
424 the binary prefixes are used (Mebi, Gibi).
426 :param render: The html-renderer instance.
427 :param value: Value to be calculated.
428 :param binary: Decimal prefixes behavior
430 :returns: The formatted file size string in human readable format.
431 """
432 bytes = float(value)
433 base = binary and 1024 or 1000
435 prefixes = [
436 (binary and 'KiB' or 'kB'),
437 (binary and 'MiB' or 'MB'),
438 (binary and 'GiB' or 'GB'),
439 (binary and 'TiB' or 'TB'),
440 (binary and 'PiB' or 'PB'),
441 (binary and 'EiB' or 'EB'),
442 (binary and 'ZiB' or 'ZB'),
443 (binary and 'YiB' or 'YB')
444 ]
446 if bytes == 1:
447 return '1 Byte'
448 elif bytes < base:
449 return '%d Bytes' % bytes
451 unit = 0
452 prefix = ""
454 for i, prefix in enumerate(prefixes):
455 unit = base ** (i + 2)
456 if bytes < unit:
457 break
459 return f'{(base * bytes / unit):.1f} {prefix}'
462@jinjaGlobalFilter
463def urlencode(render: Render, val: str) -> str:
464 """
465 Jinja2 filter: Make a string URL-safe.
467 :param render: The html-renderer instance.
468 :param val: String to be quoted.
469 :returns: Quoted string.
470 """
471 # quote_plus fails if val is None
472 if not val:
473 return ""
475 if isinstance(val, str):
476 val = val.encode("UTF-8")
478 return urllib.parse.quote_plus(val)
481# TODO
482'''
483This has been disabled until we are sure
484 a) what use-cases it has
485 b) how it's best implemented
486 c) doesn't introduce any XSS vulnerability
487 - TS 13.03.2016
488@jinjaGlobalFilter
489def className(render: Render, s: str) -> str:
490 """
491 Jinja2 filter: Converts a URL or name into a CSS-class name, by replacing slashes by underscores.
492 Example call could be```{{self|string|toClassName}}```.
494 :param s: The string to be converted, probably ``self|string`` in the Jinja2 template.
496 :return: CSS class name.
497 """
498 l = re.findall('\'([^\']*)\'', str(s))
499 if l:
500 l = set(re.split(r'/|_', l[0].replace(".html", "")))
501 return " ".join(l)
503 return ""
504'''
507@jinjaGlobalFilter
508def shortKey(render: Render, val: str) -> t.Optional[str]:
509 """
510 Jinja2 filter: Make a shortkey from an entity-key.
512 :param render: The html-renderer instance.
513 :param val: Entity-key as string.
515 :returns: Shortkey on success, None on error.
516 """
517 try:
518 k = db.Key.from_legacy_urlsafe(str(val))
519 return k.id_or_name
520 except:
521 return None
524@jinjaGlobalFunction
525def renderEditBone(render: Render, skel, boneName, boneErrors=None, prefix=None):
526 if not isinstance(skel, dict) or not all([x in skel for x in ["errors", "structure", "value"]]):
527 raise ValueError("This does not look like an editable Skeleton!")
529 boneParams = skel["structure"].get(boneName)
531 if not boneParams:
532 raise ValueError(f"Bone {boneName} is not part of that skeleton")
534 if not boneParams["visible"]:
535 fileName = "editform_bone_hidden"
536 else:
537 fileName = "editform_bone_" + boneParams["type"]
539 while fileName:
540 try:
541 fn = render.getTemplateFileName(fileName)
542 break
544 except errors.NotFound:
545 if "." in fileName:
546 fileName, unused = fileName.rsplit(".", 1)
547 else:
548 fn = render.getTemplateFileName("editform_bone_bone")
549 break
551 tpl = render.getEnv().get_template(fn)
553 return tpl.render(
554 boneName=((prefix + ".") if prefix else "") + boneName,
555 boneParams=boneParams,
556 boneValue=skel["value"][boneName] if boneName in skel["value"] else None,
557 boneErrors=boneErrors
558 )
561@jinjaGlobalFunction
562def renderEditForm(render: Render,
563 skel: dict,
564 ignore: list[str] = None,
565 hide: list[str] = None,
566 prefix=None,
567 bones: list[str] = None,
568 ) -> str:
569 """Render an edit-form based on a skeleton.
571 Render an HTML-form with lables and inputs from the skeleton structure
572 using templates for each bone-types.
574 :param render: The html-renderer instance.
575 :param skel: The skelton which provides the structure.
576 :param ignore: Don't render fields for these bones (name of the bone).
577 :param hide: Render these fields as hidden fields (name of the bone).
578 :param prefix: Prefix added to the bone names.
579 :param bones: If set only the bone with a name in the list would be rendered.
581 :return: A string containing the HTML-form.
582 """
583 if not isinstance(skel, dict) or not all([x in skel.keys() for x in ["errors", "structure", "value"]]):
584 raise ValueError("This does not look like an editable Skeleton!")
586 res = ""
588 sectionTpl = render.getEnv().get_template(render.getTemplateFileName("editform_category"))
589 rowTpl = render.getEnv().get_template(render.getTemplateFileName("editform_row"))
590 sections = OrderedDict()
592 if ignore and bones and (both := set(ignore).intersection(bones)):
593 raise ValueError(f"You have specified the same bones {', '.join(both)} in *ignore* AND *bones*!")
594 for boneName, boneParams in skel["structure"].items():
595 category = str("server.render.html.default_category")
596 if "params" in boneParams and isinstance(boneParams["params"], dict):
597 category = boneParams["params"].get("category", category)
598 if not category in sections:
599 sections[category] = []
601 sections[category].append((boneName, boneParams))
603 for category, boneList in sections.items():
604 allReadOnly = True
605 allHidden = True
606 categoryContent = ""
608 for boneName, boneParams in boneList:
609 if ignore and boneName in ignore:
610 continue
611 if bones and boneName not in bones:
612 continue
613 # print("--- skel[\"errors\"] ---")
614 # print(skel["errors"])
616 pathToBone = ((prefix + ".") if prefix else "") + boneName
617 boneErrors = [entry for entry in skel["errors"] if ".".join(entry.fieldPath).startswith(pathToBone)]
619 if hide and boneName in hide:
620 boneParams["visible"] = False
622 if not boneParams["readonly"]:
623 allReadOnly = False
625 if boneParams["visible"]:
626 allHidden = False
628 editWidget = renderEditBone(render, skel, boneName, boneErrors, prefix=prefix)
629 categoryContent += rowTpl.render(
630 boneName=pathToBone,
631 boneParams=boneParams,
632 boneErrors=boneErrors,
633 editWidget=editWidget
634 )
636 res += sectionTpl.render(
637 categoryName=category,
638 categoryClassName="".join(ch for ch in str(category) if ch in string.ascii_letters),
639 categoryContent=categoryContent,
640 allReadOnly=allReadOnly,
641 allHidden=allHidden
642 )
644 return res
647@jinjaGlobalFunction
648def embedSvg(render: Render, name: str, classes: list[str] | None = None, **kwargs: dict[str, str]) -> str:
649 """
650 jinja2 function to get an <img/>-tag for a SVG.
651 This method will not check the existence of a SVG!
653 :param render: The jinja renderer instance
654 :param name: Name of the icon (basename of file)
655 :param classes: A list of css-classes for the <img/>-tag
656 :param kwargs: Further html-attributes for this tag (e.g. "alt" or "title")
657 :return: A <img/>-tag
658 """
659 if any([x in name for x in ["..", "~", "/"]]):
660 return ""
662 if classes is None:
663 classes = ["js-svg", name.split("-", 1)[0]]
664 else:
665 assert isinstance(classes, list), "*classes* must be a list"
666 classes.extend(["js-svg", name.split("-", 1)[0]])
668 attributes = {
669 "src": os.path.join(conf.static_embed_svg_path, f"{name}.svg"),
670 "class": " ".join(classes),
671 **kwargs
672 }
673 return f"""<img {" ".join(f'{k}="{v}"' for k, v in attributes.items())}>"""
676@jinjaGlobalFunction
677def downloadUrlFor(
678 render: Render,
679 fileObj: dict,
680 expires: t.Optional[int] = conf.render_html_download_url_expiration,
681 derived: t.Optional[str] = None,
682 downloadFileName: t.Optional[str] = None
683) -> str:
684 """
685 Constructs a signed download-url for the given file-bone. Mostly a wrapper around
686 :meth:`file.File.create_download_url`.
688 :param render: The jinja renderer instance
689 :param fileObj: The file-bone (eg. skel["file"])
690 :param expires:
691 None if the file is supposed to be public
692 (which causes it to be cached on the google ede caches), otherwise it's lifetime in seconds.
693 :param derived:
694 Optional the filename of a derived file,
695 otherwise the download-link will point to the originally uploaded file.
696 :param downloadFileName: The filename to use when saving the response payload locally.
697 :return: THe signed download-url relative to the current domain (eg /download/...)
698 """
699 if "dlkey" not in fileObj and "dest" in fileObj:
700 fileObj = fileObj["dest"]
702 if expires:
703 expires = timedelta(minutes=expires)
705 if not isinstance(fileObj, (SkeletonInstance, dict)) or "dlkey" not in fileObj or "name" not in fileObj:
706 logging.error("Invalid fileObj supplied")
707 return ""
709 if derived and ("derived" not in fileObj or not isinstance(fileObj["derived"], dict)):
710 logging.error("No derivation for this fileObj")
711 return ""
713 if derived:
714 return file.File.create_download_url(
715 fileObj["dlkey"],
716 filename=derived,
717 derived=True,
718 expires=expires,
719 download_filename=downloadFileName,
720 )
722 return file.File.create_download_url(
723 fileObj["dlkey"],
724 filename=fileObj["name"],
725 expires=expires,
726 download_filename=downloadFileName
727 )
730@jinjaGlobalFunction
731def srcSetFor(
732 render: Render,
733 fileObj: dict,
734 expires: t.Optional[int] = conf.render_html_download_url_expiration,
735 width: t.Optional[int] = None,
736 height: t.Optional[int] = None
737) -> str:
738 """
739 Generates a string suitable for use as the srcset tag in html. This functionality provides the browser with a list
740 of images in different sizes and allows it to choose the smallest file that will fill it's viewport without
741 upscaling.
743 :param render:
744 The render instance that's calling this function.
745 :param fileObj:
746 The file-bone (or if multiple=True a single value from it) to generate the srcset for.
747 :param expires: None if the file is supposed to be public
748 (which causes it to be cached on the google ede caches), otherwise it's lifetime in seconds.
749 :param width: A list of widths that should be included in the srcset.
750 If a given width is not available, it will be skipped.
751 :param height: A list of heights that should be included in the srcset.
752 If a given height is not available, it will be skipped.
754 :return: The srctag generated or an empty string if a invalid file object was supplied
755 """
756 return file.File.create_src_set(fileObj, expires, width, height)
759@jinjaGlobalFunction
760def seoUrlForEntry(render: Render, *args, **kwargs):
761 return utils.seoUrlToEntry(*args, **kwargs)
764@jinjaGlobalFunction
765def seoUrlToFunction(render: Render, *args, **kwargs):
766 return utils.seoUrlToFunction(*args, **kwargs)
769@jinjaGlobalFunction
770def qrcode(render: Render, data: str) -> str:
771 """
772 Generates a SVG string for a html template
774 :param data: Any string data that should render to a QR Code.
776 :return: The SVG string representation.
777 """
778 return qrcode_make(data, image_factory=qrcode_svg.SvgPathImage, box_size=30).to_string().decode("utf-8")