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

198 statements  

« prev     ^ index     » next       coverage.py v7.6.3, created at 2024-10-16 22:16 +0000

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, db, errors, securitykey 

12from viur.core.bones import * 

13from viur.core.i18n import LanguageWrapper, TranslationExtension 

14from viur.core.skeleton import SkelList, SkeletonInstance 

15from . import utils as jinjaUtils 

16from ..json.default import CustomJsonEncoder 

17 

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

19 

20 

21class Render(object): 

22 """ 

23 The core jinja2 render. 

24 

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

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

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

28 current action. 

29 

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

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

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

33 

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

35 

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

37 

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

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

40 can extend/override the functionality exposed to templates. 

41 

42 """ 

43 kind = "html" 

44 

45 listTemplate = "list" 

46 viewTemplate = "view" 

47 addTemplate = "add" 

48 editTemplate = "edit" 

49 

50 addSuccessTemplate = "add_success" 

51 editSuccessTemplate = "edit_success" 

52 deleteSuccessTemplate = "delete_success" 

53 

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

55 

56 __haveEnvImported_ = False 

57 

58 def __init__(self, parent=None, *args, **kwargs): 

59 super(Render, self).__init__(*args, **kwargs) 

60 if not Render.__haveEnvImported_: 

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

62 # noinspection PyUnresolvedReferences 

63 from . import env 

64 Render.__haveEnvImported_ = True 

65 self.parent = parent 

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 db.encodeKey(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) -> 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) 

247 

248 return self.getEnv().get_template(self.getTemplateFileName(template)) 

249 

250 def render_action_template( 

251 self, 

252 default: str, 

253 skel: SkeletonInstance, 

254 action: str, 

255 tpl: str = None, 

256 params: dict = None, 

257 **kwargs 

258 ) -> str: 

259 """ 

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

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

262 

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

264 

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

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

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

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

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

270 

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

272 

273 :return: Returns the emitted HTML response. 

274 """ 

275 template = self.get_template(default, tpl) 

276 

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

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

279 

280 # fixme: Is this still be used? 

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

282 if isinstance(skel, SkeletonInstance): 

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

284 

285 skel.renderPreparation = self.renderBoneValue 

286 

287 return template.render( 

288 skel={ 

289 "structure": skel.structure(), 

290 "errors": skel.errors, 

291 "value": skel 

292 }, 

293 action=action, 

294 params=params, 

295 **kwargs 

296 ) 

297 

298 def render_view_template( 

299 self, 

300 default: str, 

301 skel: SkeletonInstance, 

302 action: str, 

303 tpl: str = None, 

304 params: dict = None, 

305 **kwargs 

306 ) -> str: 

307 """ 

308 Renders a page with an entry. 

309 

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

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

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

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

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

315 

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

317 

318 :return: Returns the emitted HTML response. 

319 """ 

320 template = self.get_template(default, tpl) 

321 

322 if isinstance(skel, SkeletonInstance): 

323 skel.renderPreparation = self.renderBoneValue 

324 

325 return template.render( 

326 skel=skel, 

327 action=action, 

328 params=params, 

329 **kwargs 

330 ) 

331 

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

333 """ 

334 Renders a page with a list of entries. 

335 

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

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

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

339 

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

341 

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

343 

344 :return: Returns the emitted HTML response. 

345 """ 

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

347 

348 for skel in skellist: 

349 skel.renderPreparation = self.renderBoneValue 

350 

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

352 

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

354 **kwargs) -> str: 

355 """ 

356 Renders a page for viewing an entry. 

357 

358 For details, see self.render_view_template(). 

359 """ 

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

361 

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

363 """ 

364 Renders a page for adding an entry. 

365 

366 For details, see self.render_action_template(). 

367 """ 

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

369 

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

371 **kwargs) -> str: 

372 """ 

373 Renders a page for modifying an entry. 

374 

375 For details, see self.render_action_template(). 

376 """ 

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

378 

379 def addSuccess( 

380 self, 

381 skel: SkeletonInstance, 

382 action: str = "addSuccess", 

383 tpl: str = None, 

384 params: t.Any = None, 

385 **kwargs 

386 ) -> str: 

387 """ 

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

389 

390 For details, see self.render_view_template(). 

391 """ 

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

393 

394 def editSuccess( 

395 self, 

396 skel: SkeletonInstance, 

397 action: str = "editSuccess", 

398 tpl: str = None, 

399 params: t.Any = None, 

400 **kwargs 

401 ) -> str: 

402 """ 

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

404 

405 For details, see self.render_view_template(). 

406 """ 

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

408 

409 def deleteSuccess( 

410 self, 

411 skel: SkeletonInstance, 

412 action: str = "deleteSuccess", 

413 tpl: str = None, 

414 params: t.Any = None, 

415 **kwargs 

416 ) -> str: 

417 """ 

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

419 

420 For details, see self.render_view_template(). 

421 """ 

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

423 

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

425 self, 

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

427 action: str = "listrootnodes", 

428 tpl: str = None, 

429 params: t.Any = None, 

430 **kwargs 

431 ) -> str: 

432 """ 

433 Renders a list of available root nodes. 

434 

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

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

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

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

439 

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

441 

442 :return: Returns the emitted HTML response. 

443 """ 

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

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

446 

447 def renderEmail(self, 

448 dests: t.List[str], 

449 file: str = None, 

450 template: str = None, 

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

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

453 """ 

454 Renders an email. 

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

456 

457 :param dests: Destination recipients. 

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

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

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

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

462 """ 

463 if isinstance(skel, SkeletonInstance): 

464 skel.renderPreparation = self.renderBoneValue 

465 

466 elif isinstance(skel, list): 

467 for x in skel: 

468 if isinstance(x, SkeletonInstance): 

469 x.renderPreparation = self.renderBoneValue 

470 if file is not None: 

471 try: 

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

473 except Exception as err: 

474 logging.exception(err) 

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

476 else: 

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

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

479 if len(content) == 1: 

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

481 

482 if isinstance(skel, SkeletonInstance): 

483 skel.renderPreparation = None 

484 

485 elif isinstance(skel, list): 

486 for x in skel: 

487 if isinstance(x, SkeletonInstance): 

488 x.renderPreparation = None 

489 

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

491 

492 def getEnv(self) -> Environment: 

493 """ 

494 Constructs the Jinja2 environment. 

495 

496 If an application specifies an jinja2Env function, this function 

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

498 

499 :return: Extended Jinja2 environment. 

500 """ 

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

502 loaders = self.getLoaders() 

503 self.env = Environment(loader=loaders, 

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

505 self.env.trCache = {} 

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

507 

508 # Import functions. 

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

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

511 

512 # Import filters. 

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

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

515 

516 # Import tests. 

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

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

519 

520 # Import extensions. 

521 for ext in jinjaUtils.getGlobalExtensions(): 

522 self.env.add_extension(ext) 

523 

524 # Import module-specific environment, if available. 

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

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

527 

528 return self.env