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

200 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-13 11:04 +0000

1import codecs 

2import collections 

3import enum 

4import functools 

5import logging 

6import os 

7import typing as t 

8 

9from jinja2 import ChoiceLoader, Environment, FileSystemLoader, Template 

10 

11from viur.core import conf, current, errors, securitykey 

12from viur.core.bones import * 

13from viur.core.i18n import translate, LanguageWrapper, TranslationExtension 

14from viur.core.skeleton import SkelList, SkeletonInstance, remove_render_preparation_deep 

15from . import utils as jinjaUtils 

16from ..abstract import AbstractRenderer 

17from ..json.default import CustomJsonEncoder 

18 

19KeyValueWrapper = collections.namedtuple("KeyValueWrapper", ["key", "descr"]) 

20 

21 

22class Render(AbstractRenderer): 

23 """ 

24 The core jinja2 render. 

25 

26 This is the bridge between your ViUR modules and your templates. 

27 First, the default jinja2-api is exposed to your templates. See http://jinja.pocoo.org/ for 

28 more information. Second, we'll pass data das global variables to templates depending on the 

29 current action. 

30 

31 - For list() a `skellist` is provided containing all requested skeletons to a limit 

32 - For view(): skel - a dictionary with values from the skeleton prepared for use inside html 

33 - For add()/edit: a dictionary as `skel` with `values`, `structure` and `errors` as keys. 

34 

35 Third, a bunch of global filters (like urlencode) and functions (getEntry, ..) are available to templates. 

36 

37 See the ViUR Documentation for more information about functions and data available to jinja2 templates. 

38 

39 Its possible for modules to extend the list of filters/functions available to templates by defining 

40 a function called `jinjaEnv`. Its called from the render when the environment is first created and 

41 can extend/override the functionality exposed to templates. 

42 

43 """ 

44 kind = "html" 

45 

46 listTemplate = "list" 

47 viewTemplate = "view" 

48 addTemplate = "add" 

49 editTemplate = "edit" 

50 

51 addSuccessTemplate = "add_success" 

52 editSuccessTemplate = "edit_success" 

53 deleteSuccessTemplate = "delete_success" 

54 

55 listRepositoriesTemplate = "list_repositories" # fixme: This is a relict, should be solved differently (later!). 

56 

57 __haveEnvImported_ = False 

58 

59 def __init__(self, *args, **kwargs): 

60 super().__init__(*args, **kwargs) 

61 if not Render.__haveEnvImported_: 

62 # We defer loading our plugins to this point to avoid circular imports 

63 # noinspection PyUnresolvedReferences 

64 from . import env 

65 Render.__haveEnvImported_ = True 

66 

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. 

75 

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. 

79 

80 It is advised to override this function in case that 

81 :func:`viur.core.render.jinja2.default.Render.getLoaders` is redefined. 

82 

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. 

86 

87 :returns: Filename of the template 

88 """ 

89 validChars = "abcdefghijklmnopqrstuvwxyz1234567890-" 

90 htmlpath = getattr(self, "htmlpath", "html") 

91 

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

100 

101 lang = current.language.get() 

102 

103 if not isinstance(template, (tuple, list)): 

104 template = (template,) 

105 

106 for tpl in template: 

107 filenames = [tpl] 

108 if style_postfix: 

109 filenames.append(tpl + style_postfix) 

110 

111 if lang: 

112 filenames += [ 

113 os.path.join(lang, _tpl) 

114 for _tpl in filenames 

115 ] 

116 

117 for filename in reversed(filenames): 

118 filename += ".html" 

119 

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) 

125 

126 if conf.instance.project_base_path.joinpath(htmlpath, filename).is_file(): 

127 return filename 

128 

129 if conf.instance.core_base_path.joinpath("viur", "core", "template", filename).is_file(): 

130 return filename 

131 

132 msg = f"""Template {" or ".join((repr(tpl) for tpl in template))} not found.""" 

133 if raise_exception: 

134 raise errors.NotFound(msg) 

135 

136 logging.error(msg) 

137 return None 

138 

139 def getLoaders(self) -> ChoiceLoader: 

140 """ 

141 Return the list of Jinja2 loaders which should be used. 

142 

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

151 

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. 

161 

162 It can be overridden and super-called from a custom renderer. 

163 

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

170 

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

185 

186 if isinstance(boneValue, list): 

187 return {val: get_label(val) for val in boneValue} 

188 

189 return KeyValueWrapper(boneValue, get_label(boneValue)) 

190 

191 elif bone.type == "relational" or bone.type.startswith("relational."): 

192 if isinstance(boneValue, list): 

193 tmpList = [] 

194 for k in boneValue: 

195 if not k: 

196 continue 

197 if bone.using is not None and k["rel"]: 

198 k["rel"].renderPreparation = self.renderBoneValue 

199 usingData = k["rel"] 

200 else: 

201 usingData = None 

202 k["dest"].renderPreparation = self.renderBoneValue 

203 tmpList.append({ 

204 "dest": k["dest"], 

205 "rel": usingData 

206 }) 

207 return tmpList 

208 elif isinstance(boneValue, dict): 

209 if bone.using is not None and boneValue["rel"]: 

210 boneValue["rel"].renderPreparation = self.renderBoneValue 

211 usingData = boneValue["rel"] 

212 else: 

213 usingData = None 

214 boneValue["dest"].renderPreparation = self.renderBoneValue 

215 return { 

216 "dest": boneValue["dest"], 

217 "rel": usingData 

218 } 

219 elif bone.type == "record" or bone.type.startswith("record."): 

220 value = boneValue 

221 if value: 

222 if bone.multiple: 

223 ret = [] 

224 for entry in value: 

225 entry.renderPreparation = self.renderBoneValue 

226 ret.append(entry) 

227 return ret 

228 value.renderPreparation = self.renderBoneValue 

229 return value 

230 elif bone.type == "password": 

231 return "" 

232 elif bone.type == "key": 

233 return str(boneValue) if boneValue else None 

234 

235 else: 

236 return boneValue 

237 

238 return None 

239 

240 def get_template(self, action: str, template: str) -> t.Optional[Template]: 

241 """ 

242 Internal function for retrieving a template from an action name. 

243 """ 

244 if not template: 

245 default_template = action + "Template" 

246 template = getattr(self.parent, default_template, None) or getattr(self, default_template, None) 

247 

248 if not template: 

249 raise errors.NotImplemented(str(translate( 

250 "core.html.error.template_not_configured", 

251 "Template '{{default_template}}' not configured.", 

252 default_variables={ 

253 "default_template": default_template, 

254 } 

255 ))) 

256 

257 return template and self.getEnv().get_template(self.getTemplateFileName(template)) 

258 

259 def render_action_template( 

260 self, 

261 default: str, 

262 skel: SkeletonInstance, 

263 action: str, 

264 tpl: str = None, 

265 params: dict = None, 

266 **kwargs 

267 ) -> str: 

268 """ 

269 Internal action rendering that provides a variable structure to render an input-form. 

270 The required information is passed via skel["structure"], skel["value"] and skel["errors"]. 

271 

272 Any data in **kwargs is passed unmodified to the template. 

273 

274 :param default: The default action to render, which is used to construct a template name. 

275 :param skel: Skeleton of which should be used for the action. 

276 :param action: The name of the action, which is passed into the template. 

277 :param tpl: Name of a different template, which should be used instead of the default one. 

278 :param params: Optional data that will be passed unmodified to the template. 

279 

280 Any data in **kwargs is passed unmodified to the template. 

281 

282 :return: Returns the emitted HTML response. 

283 """ 

284 template = self.get_template(default, tpl) 

285 

286 if skel is not None: 

287 skel.skey = BaseBone(descr="SecurityKey", readOnly=True, visible=False) 

288 skel["skey"] = securitykey.create() 

289 

290 # fixme: Is this still be used? 

291 if current.request.get().kwargs.get("nomissing") == "1": 

292 if isinstance(skel, SkeletonInstance): 

293 super(SkeletonInstance, skel).__setattr__("errors", []) 

294 

295 skel.renderPreparation = self.renderBoneValue 

296 

297 return template.render( 

298 skel={ 

299 "structure": skel.structure(), 

300 "errors": skel.errors, 

301 "value": skel 

302 } if skel is not None else None, 

303 action=action, 

304 params=params, 

305 **kwargs 

306 ) 

307 

308 def render_view_template( 

309 self, 

310 default: str, 

311 skel: SkeletonInstance, 

312 action: str, 

313 tpl: str = None, 

314 params: dict = None, 

315 **kwargs 

316 ) -> str: 

317 """ 

318 Renders a page with an entry. 

319 

320 :param default: The default action to render, which is used to construct a template name. 

321 :param skel: Skeleton which contains the data of the corresponding entity. 

322 :param action: The name of the action, which is passed into the template. 

323 :param tpl: Name of a different template, which should be used instead of the default one. 

324 :param params: Optional data that will be passed unmodified to the template 

325 

326 Any data in **kwargs is passed unmodified to the template. 

327 

328 :return: Returns the emitted HTML response. 

329 """ 

330 template = self.get_template(default, tpl) 

331 

332 if isinstance(skel, SkeletonInstance): 

333 skel.renderPreparation = self.renderBoneValue 

334 

335 return template.render( 

336 skel=skel, 

337 action=action, 

338 params=params, 

339 **kwargs 

340 ) 

341 

342 def list(self, skellist: SkelList, action: str = "list", tpl: str = None, params: t.Any = None, **kwargs) -> str: 

343 """ 

344 Renders a page with a list of entries. 

345 

346 :param skellist: List of Skeletons with entries to display. 

347 :param action: The name of the action, which is passed into the template. 

348 :param tpl: Name of a different template, which should be used instead of the default one. 

349 

350 :param params: Optional data that will be passed unmodified to the template 

351 

352 Any data in **kwargs is passed unmodified to the template. 

353 

354 :return: Returns the emitted HTML response. 

355 """ 

356 template = self.get_template("list", tpl) 

357 

358 for skel in skellist: 

359 skel.renderPreparation = self.renderBoneValue 

360 

361 return template.render(skellist=skellist, action=action, params=params, **kwargs) 

362 

363 def view(self, skel: SkeletonInstance, action: str = "view", tpl: str = None, params: t.Any = None, 

364 **kwargs) -> str: 

365 """ 

366 Renders a page for viewing an entry. 

367 

368 For details, see self.render_view_template(). 

369 """ 

370 return self.render_view_template("view", skel, action, tpl, params, **kwargs) 

371 

372 def add(self, skel: SkeletonInstance, action: str = "add", tpl: str = None, params: t.Any = None, **kwargs) -> str: 

373 """ 

374 Renders a page for adding an entry. 

375 

376 For details, see self.render_action_template(). 

377 """ 

378 return self.render_action_template("add", skel, action, tpl, params, **kwargs) 

379 

380 def edit(self, skel: SkeletonInstance, action: str = "edit", tpl: str = None, params: t.Any = None, 

381 **kwargs) -> str: 

382 """ 

383 Renders a page for modifying an entry. 

384 

385 For details, see self.render_action_template(). 

386 """ 

387 return self.render_action_template("edit", skel, action, tpl, params, **kwargs) 

388 

389 def addSuccess( 

390 self, 

391 skel: SkeletonInstance, 

392 action: str = "addSuccess", 

393 tpl: str = None, 

394 params: t.Any = None, 

395 **kwargs 

396 ) -> str: 

397 """ 

398 Renders a page, informing that an entry has been successfully created. 

399 

400 For details, see self.render_view_template(). 

401 """ 

402 return self.render_view_template("addSuccess", skel, action, tpl, params, **kwargs) 

403 

404 def editSuccess( 

405 self, 

406 skel: SkeletonInstance, 

407 action: str = "editSuccess", 

408 tpl: str = None, 

409 params: t.Any = None, 

410 **kwargs 

411 ) -> str: 

412 """ 

413 Renders a page, informing that an entry has been successfully modified. 

414 

415 For details, see self.render_view_template(). 

416 """ 

417 return self.render_view_template("editSuccess", skel, action, tpl, params, **kwargs) 

418 

419 def deleteSuccess( 

420 self, 

421 skel: SkeletonInstance, 

422 action: str = "deleteSuccess", 

423 tpl: str = None, 

424 params: t.Any = None, 

425 **kwargs 

426 ) -> str: 

427 """ 

428 Renders a page, informing that an entry has been successfully deleted. 

429 

430 For details, see self.render_view_template(). 

431 """ 

432 return self.render_view_template("deleteSuccess", skel, action, tpl, params, **kwargs) 

433 

434 def listRootNodes( # fixme: This is a relict, should be solved differently (later!). 

435 self, 

436 repos: t.List[dict[t.Literal["key", "name"], t.Any]], 

437 action: str = "listrootnodes", 

438 tpl: str = None, 

439 params: t.Any = None, 

440 **kwargs 

441 ) -> str: 

442 """ 

443 Renders a list of available root nodes. 

444 

445 :param repos: List of repositories (dict with "key"=>Repo-Key and "name"=>Repo-Name) 

446 :param action: The name of the action, which is passed into the template. 

447 :param tpl: Name of a different template, which should be used instead of the default one. 

448 :param params: Optional data that will be passed unmodified to the template 

449 

450 Any data in **kwargs is passed unmodified to the template. 

451 

452 :return: Returns the emitted HTML response. 

453 """ 

454 template = self.get_template("listRootNodes", tpl) 

455 return template.render(repos=repos, action=action, params=params, **kwargs) 

456 

457 def render( 

458 self, 

459 action: str, 

460 skel: t.Optional[SkeletonInstance] = None, 

461 *, 

462 tpl: t.Optional[str] = None, 

463 next_url: t.Optional[str] = None, 

464 **kwargs 

465 ): 

466 """ 

467 Universal rendering function. 

468 

469 Handles an action and a skeleton. It shall be used by any action, in future. 

470 It additionally allows for a tpl-parameter in HTML-renderer. 

471 """ 

472 if next_url: 

473 raise errors.Redirect(next_url) 

474 

475 return self.render_action_template(action, skel, action, tpl, params=kwargs) 

476 

477 def renderEmail(self, 

478 dests: t.List[str], 

479 file: str = None, 

480 template: str = None, 

481 skel: None | dict | SkeletonInstance | t.List["SkeletonInstance"] = None, 

482 **kwargs) -> tuple[str, str]: 

483 """ 

484 Renders an email. 

485 Uses the first not-empty line as subject and the remaining template as body. 

486 

487 :param dests: Destination recipients. 

488 :param file: The name of a template from the deploy/emails directory. 

489 :param template: This string is interpreted as the template contents. Alternative to load from template file. 

490 :param skel: Skeleton or dict which data to supply to the template. 

491 :return: Returns the rendered email subject and body. 

492 """ 

493 if isinstance(skel, SkeletonInstance): 

494 skel.renderPreparation = self.renderBoneValue 

495 

496 elif isinstance(skel, list): 

497 for x in skel: 

498 if isinstance(x, SkeletonInstance): 

499 x.renderPreparation = self.renderBoneValue 

500 if file is not None: 

501 try: 

502 tpl = self.getEnv().from_string(codecs.open("emails/" + file + ".email", "r", "utf-8").read()) 

503 except Exception as err: 

504 logging.exception(err) 

505 tpl = self.getEnv().get_template(file + ".email") 

506 else: 

507 tpl = self.getEnv().from_string(template) 

508 content = tpl.render(skel=skel, dests=dests, **kwargs).lstrip().splitlines() 

509 if len(content) == 1: 

510 content.insert(0, "") # add empty subject 

511 

512 remove_render_preparation_deep(skel) 

513 

514 return content[0], os.linesep.join(content[1:]).lstrip() 

515 

516 def getEnv(self) -> Environment: 

517 """ 

518 Constructs the Jinja2 environment. 

519 

520 If an application specifies an jinja2Env function, this function 

521 can alter the environment before its used to parse any template. 

522 

523 :return: Extended Jinja2 environment. 

524 """ 

525 if "env" not in dir(self): 

526 loaders = self.getLoaders() 

527 self.env = Environment(loader=loaders, 

528 extensions=["jinja2.ext.do", "jinja2.ext.loopcontrols", TranslationExtension]) 

529 self.env.trCache = {} 

530 self.env.policies["json.dumps_kwargs"]["cls"] = CustomJsonEncoder 

531 

532 # Import functions. 

533 for name, func in jinjaUtils.getGlobalFunctions().items(): 

534 self.env.globals[name] = functools.partial(func, self) 

535 

536 # Import filters. 

537 for name, func in jinjaUtils.getGlobalFilters().items(): 

538 self.env.filters[name] = functools.partial(func, self) 

539 

540 # Import tests. 

541 for name, func in jinjaUtils.getGlobalTests().items(): 

542 self.env.tests[name] = functools.partial(func, self) 

543 

544 # Import extensions. 

545 for ext in jinjaUtils.getGlobalExtensions(): 

546 self.env.add_extension(ext) 

547 

548 # Import module-specific environment, if available. 

549 if "jinjaEnv" in dir(self.parent): 

550 self.env = self.parent.jinjaEnv(self.env) 

551 

552 return self.env