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

1from collections import OrderedDict 

2 

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 

12 

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 

22 

23 

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 

34 

35 See also :class:`core.i18n.TranslationExtension`. 

36 """ 

37 return translate_class(key, default_text, hint, force_lang)(**kwargs) 

38 

39 

40@jinjaGlobalFunction 

41def execRequest(render: Render, path: str, *args, **kwargs) -> t.Any: 

42 """ 

43 Jinja2 global: Perform an internal Request. 

44 

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. 

47 

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. 

50 

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})" 

105 

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 

124 

125 

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. 

130 

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 

138 

139 

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. 

145 

146 It is possible to specify a different data-model as the one used for rendering 

147 (e.g. an editSkel). 

148 

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 

153 

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 

159 

160 obj = getattr(conf.main_app, module) 

161 

162 if skel in dir(obj): 

163 skel = getattr(obj, skel)() 

164 

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 

171 

172 if not isinstance(skel, SkeletonInstance): 

173 return False 

174 

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 

198 

199 skel = qry.getSkel() 

200 if not skel: 

201 return None 

202 

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 

208 

209 return False 

210 

211 

212@jinjaGlobalFunction 

213def getHostUrl(render: Render, forceSSL=False, *args, **kwargs): 

214 """ 

215 Jinja2 global: Retrieve hostname with protocol. 

216 

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 

225 

226 

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 

236 

237 

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 

245 

246 

247@jinjaGlobalFunction 

248def redirect(render: Render, url: str) -> t.NoReturn: 

249 """ 

250 Jinja2 global: Redirect to another URL. 

251 

252 :param url: URL to redirect to. 

253 

254 :raises: :exc:`viur.core.errors.Redirect` 

255 """ 

256 raise errors.Redirect(url) 

257 

258 

259@jinjaGlobalFunction 

260def getLanguage(render: Render, resolveAlias: bool = False) -> str: 

261 """ 

262 Jinja2 global: Returns the language used for this request. 

263 

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 

271 

272 

273@jinjaGlobalFunction 

274def moduleName(render: Render) -> str: 

275 """ 

276 Jinja2 global: Retrieve name of current module where this renderer is used within. 

277 

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 "" 

283 

284 

285@jinjaGlobalFunction 

286def modulePath(render: Render) -> str: 

287 """ 

288 Jinja2 global: Retrieve path of current module the renderer is used within. 

289 

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 "" 

295 

296 

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. 

302 

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 

333 

334 

335@jinjaGlobalFunction 

336def getSecurityKey(render: Render, **kwargs) -> str: 

337 """ 

338 Jinja2 global: Creates a new ViUR security key. 

339 """ 

340 return securitykey.create(**kwargs) 

341 

342 

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. 

350 

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 

358 

359 obj = getattr(conf.main_app, module) 

360 

361 if skel in dir(obj): 

362 skel = getattr(obj, skel)() 

363 

364 if isinstance(skel, SkeletonInstance) or isinstance(skel, RelSkel): 

365 if subSkel is not None: 

366 try: 

367 skel = skel.subSkel(subSkel) 

368 

369 except Exception as e: 

370 logging.exception(e) 

371 return False 

372 

373 return skel.structure() 

374 

375 return False 

376 

377 

378@jinjaGlobalFunction 

379def requestParams(render: Render) -> dict[str, str]: 

380 """ 

381 Jinja2 global: Allows for accessing the request-parameters from the template. 

382 

383 These returned values are escaped, as users tend to use these in an unsafe manner. 

384 

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 

391 

392 

393@jinjaGlobalFunction 

394def updateURL(render: Render, **kwargs) -> str: 

395 """ 

396 Jinja2 global: Constructs a new URL based on the current requests url. 

397 

398 Given parameters are replaced if they exists in the current requests url, otherwise there appended. 

399 

400 :returns: Returns a well-formed URL. 

401 """ 

402 tmpparams = {} 

403 tmpparams.update(current.request.get().kwargs) 

404 

405 for key in list(tmpparams.keys()): 

406 if not key or key[0] == "_": 

407 del tmpparams[key] 

408 

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 

415 

416 return "?" + urllib.parse.urlencode(tmpparams).replace("&", "&") 

417 

418 

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). 

425 

426 :param render: The html-renderer instance. 

427 :param value: Value to be calculated. 

428 :param binary: Decimal prefixes behavior 

429 

430 :returns: The formatted file size string in human readable format. 

431 """ 

432 bytes = float(value) 

433 base = binary and 1024 or 1000 

434 

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 ] 

445 

446 if bytes == 1: 

447 return '1 Byte' 

448 elif bytes < base: 

449 return '%d Bytes' % bytes 

450 

451 unit = 0 

452 prefix = "" 

453 

454 for i, prefix in enumerate(prefixes): 

455 unit = base ** (i + 2) 

456 if bytes < unit: 

457 break 

458 

459 return f'{(base * bytes / unit):.1f} {prefix}' 

460 

461 

462@jinjaGlobalFilter 

463def urlencode(render: Render, val: str) -> str: 

464 """ 

465 Jinja2 filter: Make a string URL-safe. 

466 

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 "" 

474 

475 if isinstance(val, str): 

476 val = val.encode("UTF-8") 

477 

478 return urllib.parse.quote_plus(val) 

479 

480 

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}}```. 

493 

494 :param s: The string to be converted, probably ``self|string`` in the Jinja2 template. 

495 

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) 

502 

503 return "" 

504''' 

505 

506 

507@jinjaGlobalFilter 

508def shortKey(render: Render, val: str) -> t.Optional[str]: 

509 """ 

510 Jinja2 filter: Make a shortkey from an entity-key. 

511 

512 :param render: The html-renderer instance. 

513 :param val: Entity-key as string. 

514 

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 

522 

523 

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!") 

528 

529 boneParams = skel["structure"].get(boneName) 

530 

531 if not boneParams: 

532 raise ValueError(f"Bone {boneName} is not part of that skeleton") 

533 

534 if not boneParams["visible"]: 

535 fileName = "editform_bone_hidden" 

536 else: 

537 fileName = "editform_bone_" + boneParams["type"] 

538 

539 while fileName: 

540 try: 

541 fn = render.getTemplateFileName(fileName) 

542 break 

543 

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 

550 

551 tpl = render.getEnv().get_template(fn) 

552 

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 ) 

559 

560 

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. 

570 

571 Render an HTML-form with lables and inputs from the skeleton structure 

572 using templates for each bone-types. 

573 

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. 

580 

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!") 

585 

586 res = "" 

587 

588 sectionTpl = render.getEnv().get_template(render.getTemplateFileName("editform_category")) 

589 rowTpl = render.getEnv().get_template(render.getTemplateFileName("editform_row")) 

590 sections = OrderedDict() 

591 

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] = [] 

600 

601 sections[category].append((boneName, boneParams)) 

602 

603 for category, boneList in sections.items(): 

604 allReadOnly = True 

605 allHidden = True 

606 categoryContent = "" 

607 

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"]) 

615 

616 pathToBone = ((prefix + ".") if prefix else "") + boneName 

617 boneErrors = [entry for entry in skel["errors"] if ".".join(entry.fieldPath).startswith(pathToBone)] 

618 

619 if hide and boneName in hide: 

620 boneParams["visible"] = False 

621 

622 if not boneParams["readonly"]: 

623 allReadOnly = False 

624 

625 if boneParams["visible"]: 

626 allHidden = False 

627 

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 ) 

635 

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 ) 

643 

644 return res 

645 

646 

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! 

652 

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 "" 

661 

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]]) 

667 

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())}>""" 

674 

675 

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`. 

687 

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"] 

701 

702 if expires: 

703 expires = timedelta(minutes=expires) 

704 

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 "" 

708 

709 if derived and ("derived" not in fileObj or not isinstance(fileObj["derived"], dict)): 

710 logging.error("No derivation for this fileObj") 

711 return "" 

712 

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 ) 

721 

722 return file.File.create_download_url( 

723 fileObj["dlkey"], 

724 filename=fileObj["name"], 

725 expires=expires, 

726 download_filename=downloadFileName 

727 ) 

728 

729 

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. 

742 

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. 

753 

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) 

757 

758 

759@jinjaGlobalFunction 

760def seoUrlForEntry(render: Render, *args, **kwargs): 

761 return utils.seoUrlToEntry(*args, **kwargs) 

762 

763 

764@jinjaGlobalFunction 

765def seoUrlToFunction(render: Render, *args, **kwargs): 

766 return utils.seoUrlToFunction(*args, **kwargs) 

767 

768 

769@jinjaGlobalFunction 

770def qrcode(render: Render, data: str) -> str: 

771 """ 

772 Generates a SVG string for a html template 

773 

774 :param data: Any string data that should render to a QR Code. 

775 

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")