Coverage for  / home / runner / work / viur-core / viur-core / viur / src / viur / core / render / html / env / viur.py: 0%

344 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-24 12:35 +0000

1from collections import OrderedDict 

2import logging 

3import os 

4import typing as t 

5import urllib 

6import urllib.parse 

7from datetime import timedelta 

8from hashlib import sha512 

9import jinja2 

10from deprecated.sphinx import deprecated 

11from qrcode import make as qrcode_make 

12from qrcode.image import svg as qrcode_svg 

13 

14import string 

15from viur.core import Method, current, db, errors, prototypes, securitykey, utils 

16from viur.core.config import conf 

17from viur.core.i18n import LanguageWrapper 

18from viur.core.i18n import translate as translate_class 

19from viur.core.render.html.utils import jinjaGlobalFilter, jinjaGlobalFunction 

20from viur.core.request import TEMPLATE_STYLE_KEY 

21from viur.core.skeleton import RelSkel, SkeletonInstance 

22from viur.core.modules import file 

23from ..default import Render 

24 

25 

26@jinjaGlobalFunction 

27def translate( 

28 render: Render, 

29 key: str, 

30 default_text: str = None, 

31 hint: str = None, 

32 force_lang: str | None = None, 

33 **kwargs 

34) -> str: 

35 """Jinja function for translations 

36 

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

38 """ 

39 return translate_class(key, default_text, hint, force_lang, caller_is_jinja=True)(**kwargs) 

40 

41 

42@jinjaGlobalFunction 

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

44 """ 

45 Jinja2 global: Perform an internal Request. 

46 

47 This function allows to embed the result of another request inside the current template. 

48 All optional parameters are passed to the requested resource. 

49 

50 :param path: Local part of the url, e.g. user/list. Must not start with an /. 

51 Must not include an protocol or hostname. 

52 

53 :returns: Whatever the requested resource returns. This is *not* limited to strings! 

54 """ 

55 request = current.request.get() 

56 cachetime = kwargs.pop("cachetime", 0) 

57 if conf.debug.disable_cache or request.disableCache: # Caching disabled by config 

58 cachetime = 0 

59 cacheEnvKey = None 

60 if conf.cache_environment_key: 

61 try: 

62 cacheEnvKey = conf.cache_environment_key() 

63 except RuntimeError: 

64 cachetime = 0 

65 if cachetime: 

66 # Calculate the cache key that entry would be stored under 

67 tmpList = [f"{k}:{v}" for k, v in kwargs.items()] 

68 tmpList.sort() 

69 tmpList.extend(list(args)) 

70 tmpList.append(path) 

71 if cacheEnvKey is not None: 

72 tmpList.append(cacheEnvKey) 

73 try: 

74 appVersion = request.request.environ["CURRENT_VERSION_ID"].split('.')[0] 

75 except: 

76 appVersion = "" 

77 logging.error("Could not determine the current application id! Caching might produce unexpected results!") 

78 tmpList.append(appVersion) 

79 mysha512 = sha512() 

80 mysha512.update(str(tmpList).encode("UTF8")) 

81 # TODO: Add hook for optional memcache or remove these remnants 

82 cacheKey = f"jinja2_cache_{mysha512.hexdigest()}" 

83 res = None # memcache.get(cacheKey) 

84 if res: 

85 return res 

86 # Pop this key after building the cache string with it 

87 template_style = kwargs.pop(TEMPLATE_STYLE_KEY, None) 

88 tmp_params = request.kwargs.copy() 

89 request.kwargs = {"__args": args, "__outer": tmp_params} 

90 request.kwargs.update(kwargs) 

91 lastRequestState = request.internalRequest 

92 request.internalRequest = True 

93 last_template_style = request.template_style 

94 request.template_style = template_style 

95 caller = conf.main_app 

96 pathlist = path.split("/") 

97 for currpath in pathlist: 

98 if currpath in dir(caller): 

99 caller = getattr(caller, currpath) 

100 elif "index" in dir(caller) and hasattr(getattr(caller, "index"), '__call__'): 

101 caller = getattr(caller, "index") 

102 else: 

103 request.kwargs = tmp_params # Reset RequestParams 

104 request.internalRequest = lastRequestState 

105 request.template_style = last_template_style 

106 return f"{path=} not found (failed at {currpath!r})" 

107 

108 if not (isinstance(caller, Method) and caller.exposed is not None): 

109 request.kwargs = tmp_params # Reset RequestParams 

110 request.internalRequest = lastRequestState 

111 request.template_style = last_template_style 

112 return f"{caller!r} not callable or not exposed" 

113 try: 

114 resstr = caller(*args, **kwargs) 

115 except Exception as e: 

116 logging.error(f"Caught exception in execRequest while calling {path}") 

117 logging.exception(e) 

118 raise 

119 request.kwargs = tmp_params 

120 request.internalRequest = lastRequestState 

121 request.template_style = last_template_style 

122 if cachetime: 

123 pass 

124 # memcache.set(cacheKey, resstr, cachetime) 

125 return resstr 

126 

127 

128@jinjaGlobalFunction 

129def getCurrentUser(render: Render) -> t.Optional[SkeletonInstance]: 

130 """ 

131 Jinja2 global: Returns the current user from the session, or None if not logged in. 

132 

133 :return: A dict containing user data. Returns None if no user data is available. 

134 """ 

135 currentUser = current.user.get() 

136 if currentUser: 

137 currentUser = currentUser.clone() # need to clone, as renderPreparation is changed 

138 currentUser.renderPreparation = render.renderBoneValue 

139 return currentUser 

140 

141 

142@jinjaGlobalFunction 

143def getSkel( 

144 render: Render, 

145 module: str, 

146 key: str = None, 

147 skel: str = "viewSkel", 

148 skel_args: tuple[t.Any] = (), 

149) -> dict | bool | None: 

150 """ 

151 Jinja2 global: Fetch an entry from a given module, and return the data as a dict, 

152 prepared for direct use in the output. 

153 

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

155 (e.g. an editSkel). 

156 

157 :param module: Name of the module, from which the data should be fetched. 

158 :param key: Requested entity-key in an urlsafe-format. If the module is a Singleton 

159 application, the parameter can be omitted. 

160 :param skel: Specifies and optionally different data-model 

161 :param skel_arg: Optional skeleton arguments to be passed to the skel-function (e.g. for Tree-Modules) 

162 

163 :returns: dict on success, False on error. 

164 """ 

165 obj = conf.main_app 

166 for mod in module.split("."): 

167 if not (obj := getattr(obj, mod, None)): 

168 raise ValueError(f"getSkel: Can't read a skeleton from unknown module {module!r}") 

169 

170 if not getattr(obj, "html", False): 

171 raise PermissionError(f"getSkel: module {module!r} is not allowed to be accessed") 

172 

173 # Retrieve a skeleton 

174 skel = getattr(obj, skel)(*skel_args) 

175 if not isinstance(skel, SkeletonInstance): 

176 raise RuntimeError("getSkel: Invalid skel name provided") 

177 

178 if isinstance(obj, prototypes.singleton.Singleton) and not key: 

179 # We fetching the entry from a singleton - No key needed 

180 key = db.Key(skel.kindName, obj.getKey()) 

181 

182 elif not key: 

183 raise ValueError(f"getSkel has to be called with a valid key! Got {key!r}") 

184 

185 if hasattr(obj, "canView"): 

186 if not skel.read(key): 

187 logging.info(f"getSkel: Entry {key!r} not found") 

188 return None 

189 

190 if isinstance(obj, prototypes.singleton.Singleton): 

191 is_allowed = obj.canView() 

192 

193 elif isinstance(obj, prototypes.tree.Tree): 

194 if skel["key"].kind == obj.nodeSkelCls.kindName: 

195 is_allowed = obj.canView("node", skel) 

196 else: 

197 is_allowed = obj.canView("leaf", skel) 

198 

199 else: 

200 is_allowed = obj.canView(skel) 

201 

202 if not is_allowed: 

203 logging.error(f"getSkel: Access to {key} denied from canView") 

204 return None 

205 

206 elif hasattr(obj, "listFilter"): 

207 qry = skel.all().mergeExternalFilter({"key": str(key)}) 

208 qry = obj.listFilter(qry) 

209 if not qry: 

210 logging.info("listFilter permits getting entry, returning None") 

211 return None 

212 

213 if not (skel := qry.getSkel()): 

214 return None 

215 

216 else: # No Access-Test for this module 

217 if not skel.read(key): 

218 return None 

219 

220 skel.renderPreparation = render.renderBoneValue 

221 return skel 

222 

223 

224@jinjaGlobalFunction 

225def getHostUrl(render: Render, *args, **kwargs) -> str: 

226 """ 

227 Jinja2 global: Retrieve hostname with protocol. 

228 

229 :returns: Returns the hostname, including the currently used protocol, e.g: https://www.example.com 

230 :rtype: str 

231 """ 

232 return utils.get_base_url() 

233 

234 

235@jinjaGlobalFunction 

236def getVersionHash(render: Render) -> str: 

237 """ 

238 Jinja2 global: Return the application hash for the current version. This can be used for cache-busting in 

239 resource links (eg. /static/css/style.css?v={{ getVersionHash() }}. This hash is stable for each version 

240 deployed (identical across all instances), but will change whenever a new version is deployed. 

241 :return: The current version hash 

242 """ 

243 return conf.instance.version_hash 

244 

245 

246@jinjaGlobalFunction 

247def getAppVersion(render: Render) -> str: 

248 """ 

249 Jinja2 global: Return the application version for the current version as set on deployment. 

250 :return: The current version 

251 """ 

252 return conf.instance.app_version 

253 

254 

255@jinjaGlobalFunction 

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

257 """ 

258 Jinja2 global: Redirect to another URL. 

259 

260 :param url: URL to redirect to. 

261 

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

263 """ 

264 raise errors.Redirect(url) 

265 

266 

267@jinjaGlobalFunction 

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

269 """ 

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

271 

272 :param resolveAlias: If True, the function tries to resolve the current language 

273 using conf.i18n.language_alias_map. 

274 """ 

275 lang = current.language.get() 

276 if resolveAlias and lang in conf.i18n.language_alias_map: 

277 lang = conf.i18n.language_alias_map[lang] 

278 return lang 

279 

280 

281@jinjaGlobalFunction 

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

283 """ 

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

285 

286 :return: Returns the name of the current module, or empty string if there is no module set. 

287 """ 

288 if render.parent and "moduleName" in dir(render.parent): 

289 return render.parent.moduleName 

290 return "" 

291 

292 

293@jinjaGlobalFunction 

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

295 """ 

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

297 

298 :return: Returns the path of the current module, or empty string if there is no module set. 

299 """ 

300 if render.parent and "modulePath" in dir(render.parent): 

301 return render.parent.modulePath 

302 return "" 

303 

304 

305@jinjaGlobalFunction 

306def getList( 

307 render: Render, 

308 module: str, 

309 skel: str = "viewSkel", 

310 *, 

311 skel_args: tuple[t.Any] = (), 

312 _noEmptyFilter: bool = False, 

313 **kwargs 

314) -> bool | None | list[SkeletonInstance]: 

315 """ 

316 Jinja2 global: Fetches a list of entries which match the given filter criteria. 

317 

318 :param render: The html-renderer instance. 

319 :param module: Name of the module from which list should be fetched. 

320 :param skel: Name of the skeleton that is used to fetching the list. 

321 :param skel_arg: Optional skeleton arguments to be passed to the skel-function (e.g. for Tree-Modules) 

322 :param _noEmptyFilter: If True, this function will not return any results if at least one 

323 parameter is an empty list. This is useful to prevent filtering (e.g. by key) not being 

324 performed because the list is empty. 

325 

326 :returns: Returns a dict that contains the "skellist" and "cursor" information, 

327 or None on error case. 

328 """ 

329 if not (caller := getattr(conf.main_app, module, None)): 

330 raise ValueError(f"getList: Can't fetch a list from unknown module {module!r}") 

331 

332 if not getattr(caller, "html", False): 

333 raise PermissionError(f"getList: module {module!r} is not allowed to be accessed from html") 

334 

335 if not hasattr(caller, "listFilter"): 

336 raise NotImplementedError(f"getList: The module {module!r} is not designed for a list retrieval") 

337 

338 # Retrieve a skeleton 

339 skel = getattr(caller, skel)(*skel_args) 

340 if not isinstance(skel, SkeletonInstance): 

341 raise RuntimeError("getList: Invalid skel name provided") 

342 

343 # Test if any value of kwargs is an empty list 

344 if _noEmptyFilter and any(isinstance(value, list) and not value for value in kwargs.values()): 

345 return [] 

346 

347 # Create initial query 

348 query = skel.all().mergeExternalFilter(kwargs) 

349 

350 if query := caller.listFilter(query): 

351 caller._apply_default_order(query) 

352 

353 if query is None: 

354 return None 

355 

356 mylist = query.fetch() 

357 

358 if mylist: 

359 for skel in mylist: 

360 skel.renderPreparation = render.renderBoneValue 

361 

362 return mylist 

363 

364 

365@jinjaGlobalFunction 

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

367 """ 

368 Jinja2 global: Creates a new ViUR security key. 

369 """ 

370 return securitykey.create(**kwargs) 

371 

372 

373@jinjaGlobalFunction 

374def getStructure(render: Render, 

375 module: str, 

376 skel: str = "viewSkel", 

377 subSkel: t.Optional[str] = None) -> dict | bool: 

378 """ 

379 Jinja2 global: Returns the skeleton structure instead of data for a module. 

380 

381 :param render: The html-renderer instance. 

382 :param module: Module from which the skeleton is retrieved. 

383 :param skel: Name of the skeleton. 

384 :param subSkel: If set, return just that subskel instead of the whole skeleton 

385 """ 

386 if module not in dir(conf.main_app): 

387 return False 

388 

389 obj = getattr(conf.main_app, module) 

390 

391 if skel in dir(obj): 

392 skel = getattr(obj, skel)() 

393 

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

395 if subSkel is not None: 

396 try: 

397 skel = skel.subSkel(subSkel) 

398 

399 except Exception as e: 

400 logging.exception(e) 

401 return False 

402 

403 return skel.structure() 

404 

405 return False 

406 

407 

408@jinjaGlobalFunction 

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

410 """ 

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

412 

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

414 

415 :returns: dict of parameter and values. 

416 """ 

417 res = {} 

418 for k, v in current.request.get().kwargs.items(): 

419 res[utils.string.escape(k)] = utils.string.escape(v) 

420 return res 

421 

422 

423@jinjaGlobalFunction 

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

425 """ 

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

427 

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

429 

430 :returns: Returns a well-formed URL. 

431 """ 

432 tmpparams = {} 

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

434 

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

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

437 del tmpparams[key] 

438 

439 for key, value in list(kwargs.items()): 

440 if value is None: 

441 if key in tmpparams: 

442 del tmpparams[key] 

443 else: 

444 tmpparams[key] = value 

445 

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

447 

448 

449@jinjaGlobalFilter 

450@deprecated(version="3.7.0", reason="Use Jinja filter filesizeformat instead") 

451def fileSize(render: Render, value: int | float, binary: bool = False) -> str: 

452 """ 

453 Jinja2 filter: Format the value in an 'human-readable' file size (i.e. 13 kB, 4.1 MB, 102 Bytes, etc). 

454 Per default, decimal prefixes are used (Mega, Giga, etc.). When the second parameter is set to True, 

455 the binary prefixes are used (Mebi, Gibi). 

456 

457 :param render: The html-renderer instance. 

458 :param value: Value to be calculated. 

459 :param binary: Decimal prefixes behavior 

460 

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

462 """ 

463 return jinja2.filters.do_filesizeformat(value, binary) 

464 

465 

466# TODO 

467''' 

468This has been disabled until we are sure 

469 a) what use-cases it has 

470 b) how it's best implemented 

471 c) doesn't introduce any XSS vulnerability 

472 - TS 13.03.2016 

473@jinjaGlobalFilter 

474def className(render: Render, s: str) -> str: 

475 """ 

476 Jinja2 filter: Converts a URL or name into a CSS-class name, by replacing slashes by underscores. 

477 Example call could be```{{self|string|toClassName}}```. 

478 

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

480 

481 :return: CSS class name. 

482 """ 

483 l = re.findall('\'([^\']*)\'', str(s)) 

484 if l: 

485 l = set(re.split(r'/|_', l[0].replace(".html", ""))) 

486 return " ".join(l) 

487 

488 return "" 

489''' 

490 

491 

492@jinjaGlobalFilter 

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

494 """ 

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

496 

497 :param render: The html-renderer instance. 

498 :param val: Entity-key as string. 

499 

500 :returns: Shortkey on success, None on error. 

501 """ 

502 try: 

503 k = db.Key.from_legacy_urlsafe(str(val)) 

504 return k.id_or_name 

505 except: 

506 return None 

507 

508 

509@jinjaGlobalFunction 

510def renderEditBone(render: Render, skel, boneName, boneErrors=None, prefix=None): 

511 if not isinstance(skel, dict) or not all([x in skel for x in ["errors", "structure", "value"]]): 

512 raise ValueError("This does not look like an editable Skeleton!") 

513 

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

515 

516 if not boneParams: 

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

518 

519 if not boneParams["visible"]: 

520 fileName = "editform_bone_hidden" 

521 else: 

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

523 

524 while fileName: 

525 try: 

526 fn = render.getTemplateFileName(fileName) 

527 break 

528 

529 except errors.NotFound: 

530 if "." in fileName: 

531 fileName, unused = fileName.rsplit(".", 1) 

532 else: 

533 fn = render.getTemplateFileName("editform_bone_bone") 

534 break 

535 

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

537 

538 return tpl.render( 

539 boneName=((prefix + ".") if prefix else "") + boneName, 

540 boneParams=boneParams, 

541 boneValue=skel["value"][boneName] if boneName in skel["value"] else None, 

542 boneErrors=boneErrors 

543 ) 

544 

545 

546@jinjaGlobalFunction 

547def renderEditForm(render: Render, 

548 skel: dict, 

549 ignore: list[str] = None, 

550 hide: list[str] = None, 

551 prefix=None, 

552 bones: list[str] = None, 

553 ) -> str: 

554 """Render an edit-form based on a skeleton. 

555 

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

557 using templates for each bone-types. 

558 

559 :param render: The html-renderer instance. 

560 :param skel: The skelton which provides the structure. 

561 :param ignore: Don't render fields for these bones (name of the bone). 

562 :param hide: Render these fields as hidden fields (name of the bone). 

563 :param prefix: Prefix added to the bone names. 

564 :param bones: If set only the bone with a name in the list would be rendered. 

565 

566 :return: A string containing the HTML-form. 

567 """ 

568 if not isinstance(skel, dict) or not all([x in skel.keys() for x in ["errors", "structure", "value"]]): 

569 raise ValueError("This does not look like an editable Skeleton!") 

570 

571 res = "" 

572 

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

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

575 sections = OrderedDict() 

576 

577 if ignore and bones and (both := set(ignore).intersection(bones)): 

578 raise ValueError(f"You have specified the same bones {', '.join(both)} in *ignore* AND *bones*!") 

579 for boneName, boneParams in skel["structure"].items(): 

580 category = str("server.render.html.default_category") 

581 if "params" in boneParams and isinstance(boneParams["params"], dict): 

582 category = boneParams["params"].get("category", category) 

583 if not category in sections: 

584 sections[category] = [] 

585 

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

587 

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

589 allReadOnly = True 

590 allHidden = True 

591 categoryContent = "" 

592 

593 for boneName, boneParams in boneList: 

594 if ignore and boneName in ignore: 

595 continue 

596 if bones and boneName not in bones: 

597 continue 

598 # print("--- skel[\"errors\"] ---") 

599 # print(skel["errors"]) 

600 

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

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

603 

604 if hide and boneName in hide: 

605 boneParams["visible"] = False 

606 

607 if not boneParams["readonly"]: 

608 allReadOnly = False 

609 

610 if boneParams["visible"]: 

611 allHidden = False 

612 

613 editWidget = renderEditBone(render, skel, boneName, boneErrors, prefix=prefix) 

614 categoryContent += rowTpl.render( 

615 boneName=pathToBone, 

616 boneParams=boneParams, 

617 boneErrors=boneErrors, 

618 editWidget=editWidget 

619 ) 

620 

621 res += sectionTpl.render( 

622 categoryName=category, 

623 categoryClassName="".join(ch for ch in str(category) if ch in string.ascii_letters), 

624 categoryContent=categoryContent, 

625 allReadOnly=allReadOnly, 

626 allHidden=allHidden 

627 ) 

628 

629 return res 

630 

631 

632@jinjaGlobalFunction 

633def embedSvg(render: Render, name: str, classes: list[str] | None = None, **kwargs: dict[str, str]) -> str: 

634 """ 

635 jinja2 function to get an <img/>-tag for a SVG. 

636 This method will not check the existence of a SVG! 

637 

638 :param render: The jinja renderer instance 

639 :param name: Name of the icon (basename of file) 

640 :param classes: A list of css-classes for the <img/>-tag 

641 :param kwargs: Further html-attributes for this tag (e.g. "alt" or "title") 

642 :return: A <img/>-tag 

643 """ 

644 if any([x in name for x in ["..", "~", "/"]]): 

645 return "" 

646 

647 if classes is None: 

648 classes = ["js-svg", name.split("-", 1)[0]] 

649 else: 

650 assert isinstance(classes, list), "*classes* must be a list" 

651 classes.extend(["js-svg", name.split("-", 1)[0]]) 

652 

653 attributes = { 

654 "src": os.path.join(conf.static_embed_svg_path, f"{name}.svg"), 

655 "class": " ".join(classes), 

656 **kwargs 

657 } 

658 return f"""<img {" ".join(f'{k}="{v}"' for k, v in attributes.items())}>""" 

659 

660 

661@jinjaGlobalFunction 

662def downloadUrlFor( 

663 render: Render, 

664 fileObj: dict, 

665 expires: t.Optional[int] = conf.render_html_download_url_expiration, 

666 derived: t.Optional[str] = None, 

667 downloadFileName: t.Optional[str] = None, 

668 language: t.Optional[str] = None, 

669) -> str: 

670 """ 

671 Constructs a signed download-url for the given file-bone. Mostly a wrapper around 

672 :meth:`file.File.create_download_url`. 

673 

674 :param render: The jinja renderer instance 

675 :param fileObj: The file-bone (eg. skel["file"]) 

676 :param expires: 

677 None if the file is supposed to be public 

678 (which causes it to be cached on the google ede caches), otherwise it's lifetime in seconds. 

679 :param derived: 

680 Optional the filename of a derived file, 

681 otherwise the download-link will point to the originally uploaded file. 

682 :param downloadFileName: The filename to use when saving the response payload locally. 

683 :param language: Language overwrite if fileObj has multiple languages and we want to explicitly specify one 

684 :return: THe signed download-url relative to the current domain (eg /download/...) 

685 """ 

686 

687 if isinstance(fileObj, LanguageWrapper): 

688 language = language or current.language.get() 

689 if not language or not (fileObj := fileObj.get(language)): 

690 return "" 

691 

692 if "dlkey" not in fileObj and "dest" in fileObj: 

693 fileObj = fileObj["dest"] 

694 

695 if expires: 

696 expires = timedelta(minutes=expires) 

697 

698 if not isinstance(fileObj, (SkeletonInstance, dict)) or "dlkey" not in fileObj or "name" not in fileObj: 

699 logging.error("Invalid fileObj supplied") 

700 return "" 

701 

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

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

704 return "" 

705 

706 if derived: 

707 return conf.main_app.file.create_download_url( 

708 fileObj["dlkey"], 

709 filename=derived, 

710 derived=True, 

711 expires=expires, 

712 download_filename=downloadFileName, 

713 ) 

714 

715 return conf.main_app.file.create_download_url( 

716 fileObj["dlkey"], 

717 filename=fileObj["name"], 

718 expires=expires, 

719 download_filename=downloadFileName 

720 ) 

721 

722 

723@jinjaGlobalFunction 

724def srcSetFor( 

725 render: Render, 

726 fileObj: dict, 

727 expires: t.Optional[int] = conf.render_html_download_url_expiration, 

728 width: t.Optional[int] = None, 

729 height: t.Optional[int] = None, 

730 language: t.Optional[str] = None, 

731) -> str: 

732 """ 

733 Generates a string suitable for use as the srcset tag in html. This functionality provides the browser with a list 

734 of images in different sizes and allows it to choose the smallest file that will fill it's viewport without 

735 upscaling. 

736 

737 :param render: 

738 The render instance that's calling this function. 

739 :param fileObj: 

740 The file-bone (or if multiple=True a single value from it) to generate the srcset for. 

741 :param expires: None if the file is supposed to be public 

742 (which causes it to be cached on the google ede caches), otherwise it's lifetime in seconds. 

743 :param width: A list of widths that should be included in the srcset. 

744 If a given width is not available, it will be skipped. 

745 :param height: A list of heights that should be included in the srcset. 

746 If a given height is not available, it will be skipped. 

747 :param language: Language overwrite if fileObj has multiple languages and we want to explicitly specify one 

748 :return: The srctag generated or an empty string if a invalid file object was supplied 

749 """ 

750 return conf.main_app.file.create_src_set(fileObj, expires, width, height, language) 

751 

752 

753@jinjaGlobalFunction 

754def serving_url_for(render: Render, *args, **kwargs): 

755 """ 

756 Jinja wrapper for File.create_internal_serving_url(), see there for parameter information. 

757 """ 

758 return conf.main_app.file.create_internal_serving_url(*args, **kwargs) 

759 

760@jinjaGlobalFunction 

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

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

763 

764 

765@jinjaGlobalFunction 

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

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

768 

769 

770@jinjaGlobalFunction 

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

772 """ 

773 Generates a SVG string for a html template 

774 

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

776 

777 :return: The SVG string representation. 

778 """ 

779 return qrcode_make(data, image_factory=qrcode_svg.SvgPathImage, box_size=30).to_string().decode("utf-8")