Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/prototypes/tree.py: 15%

480 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-25 15:02 +0000

1import logging 

2import time 

3import typing as t 

4 

5from deprecated.sphinx import deprecated 

6 

7from viur.core import current, db, errors 

8from viur.core.bones import BooleanBone, KeyBone, RelationalConsistency, SortIndexBone 

9from viur.core.cache import flushCache 

10from viur.core.decorators import * 

11from viur.core.skeleton import Skeleton, SkeletonInstance 

12from viur.core.tasks import CallDeferred 

13from .skelmodule import SkelModule 

14 

15SkelType = t.Literal["node", "leaf"] 

16 

17 

18class TreeSkel(Skeleton): 

19 parententry = KeyBone( # TODO VIUR4: Why is this not a RelationalBone? 

20 descr="Parent", 

21 visible=False, 

22 readOnly=True, 

23 ) 

24 

25 parentrepo = KeyBone( # TODO VIUR4: Why is this not a RelationalBone? 

26 descr="BaseRepo", 

27 visible=False, 

28 readOnly=True, 

29 ) 

30 

31 sortindex = SortIndexBone( 

32 visible=False, 

33 readOnly=True, 

34 ) 

35 

36 is_root_node = BooleanBone( 

37 defaultValue=False, 

38 readOnly=True, 

39 visible=False, 

40 ) 

41 

42 @classmethod 

43 def refresh(cls, skelValues): # ViUR2 Compatibility 

44 super().refresh(skelValues) 

45 if not skelValues["parententry"] and skelValues.dbEntity.get("parentdir"): # parentdir for viur2 compatibility 

46 skelValues["parententry"] = db.normalize_key(skelValues.dbEntity["parentdir"]) 

47 

48 

49class Tree(SkelModule): 

50 """ 

51 Tree module prototype. 

52 

53 It is used for hierarchical structures, either as a tree with nodes and leafs, or as a hierarchy with nodes only. 

54 """ 

55 accessRights = ("add", "edit", "view", "delete", "manage") 

56 

57 nodeSkelCls = None 

58 leafSkelCls = None 

59 

60 default_order = "sortindex" 

61 

62 def __init__(self, moduleName, modulePath, *args, **kwargs): 

63 assert self.nodeSkelCls, f"Need to specify at least nodeSkelCls for {self.__class__.__name__!r}" 

64 super().__init__(moduleName, modulePath, *args, **kwargs) 

65 

66 @property 

67 def handler(self): 

68 return "tree" if self.leafSkelCls else "tree.node" # either a tree or a tree with nodes only (former hierarchy) 

69 

70 def _checkSkelType(self, skelType: t.Any) -> t.Optional[SkelType]: 

71 """ 

72 Checks for correct skelType. 

73 

74 Either returns the type provided, or None in case it is invalid. 

75 """ 

76 skelType = skelType.lower() 

77 if skelType == "node" or (skelType == "leaf" and self.leafSkelCls): 

78 return skelType 

79 

80 return None 

81 

82 def _resolveSkelCls(self, skelType: SkelType, *args, **kwargs) -> t.Type[Skeleton]: 

83 if not (skelType := self._checkSkelType(skelType)): 

84 raise ValueError("Unsupported skelType") 

85 

86 if skelType == "leaf": 

87 return self.leafSkelCls 

88 

89 return self.nodeSkelCls 

90 

91 def baseSkel(self, skelType: SkelType, *args, **kwargs) -> SkeletonInstance: 

92 """ 

93 Return unmodified base skeleton for the given skelType. 

94 

95 .. seealso:: :func:`addSkel`, :func:`editSkel`, :func:`viewSkel`, :func:`~baseSkel` 

96 """ 

97 return self._resolveSkelCls(skelType, *args, **kwargs)() 

98 

99 def viewSkel(self, skelType: SkelType, *args, **kwargs) -> SkeletonInstance: 

100 """ 

101 Retrieve a new instance of a :class:`viur.core.skeleton.Skeleton` that is used by the application 

102 for viewing an existing entry from the tree. 

103 

104 The default is a Skeleton instance returned by :func:`~baseSkel`. 

105 

106 .. seealso:: :func:`addSkel`, :func:`editSkel`, :func:`~baseSkel` 

107 

108 :return: Returns a Skeleton instance for viewing an entry. 

109 """ 

110 return self.baseSkel(skelType, *args, **kwargs) 

111 

112 def addSkel(self, skelType: SkelType, *args, **kwargs) -> SkeletonInstance: 

113 """ 

114 Retrieve a new instance of a :class:`viur.core.skeleton.Skeleton` that is used by the application 

115 for adding an entry to the tree. 

116 

117 The default is a Skeleton instance returned by :func:`~baseSkel`. 

118 

119 .. seealso:: :func:`viewSkel`, :func:`editSkel`, :func:`~baseSkel` 

120 

121 :return: Returns a Skeleton instance for adding an entry. 

122 """ 

123 return self.baseSkel(skelType, *args, **kwargs) 

124 

125 def editSkel(self, skelType: SkelType, *args, **kwargs) -> SkeletonInstance: 

126 """ 

127 Retrieve a new instance of a :class:`viur.core.skeleton.Skeleton` that is used by the application 

128 for editing an existing entry from the tree. 

129 

130 The default is a Skeleton instance returned by :func:`~baseSkel`. 

131 

132 .. seealso:: :func:`viewSkel`, :func:`editSkel`, :func:`~baseSkel` 

133 

134 :return: Returns a Skeleton instance for editing an entry. 

135 """ 

136 return self.baseSkel(skelType, *args, **kwargs) 

137 

138 def cloneSkel(self, skelType: SkelType, *args, **kwargs) -> SkeletonInstance: 

139 """ 

140 Retrieve a new :class:`viur.core.skeleton.SkeletonInstance` that is used by the application 

141 for cloning an existing entry of the tree. 

142 

143 The default is a SkeletonInstance returned by :func:`~baseSkel`. 

144 

145 .. seealso:: :func:`viewSkel`, :func:`editSkel`, :func:`~baseSkel` 

146 

147 :return: Returns a SkeletonInstance for cloning an entry. 

148 """ 

149 return self.baseSkel(skelType, *args, **kwargs) 

150 

151 def rootnodeSkel( 

152 self, 

153 *, 

154 identifier: str = "rep_module_repo", 

155 ensure: bool | dict | t.Callable[[SkeletonInstance], None] = False, 

156 ) -> SkeletonInstance: 

157 """ 

158 Retrieve a new :class:`viur.core.skeleton.SkeletonInstance` that is used by the application 

159 for rootnode entries. 

160 

161 The default is a SkeletonInstance returned by :func:`~baseSkel`, with a preset key created from identifier. 

162 

163 :param identifier: Unique identifier (name) for this rootnode. 

164 :param ensure: If provided, ensures that the skeleton is available, and created with optionally provided values. 

165 

166 :return: Returns a SkeletonInstance for handling root nodes. 

167 """ 

168 skel = self.baseSkel("node") 

169 

170 skel["key"] = db.Key(skel.kindName, identifier) 

171 skel["is_root_node"] = True 

172 

173 if ensure not in (False, None): 

174 return skel.read(create=ensure) 

175 

176 return skel 

177 

178 @deprecated( 

179 version="3.7.0", 

180 reason="Use rootnodeSkel(ensure=True) instead.", 

181 action="always" 

182 ) 

183 def ensureOwnModuleRootNode(self) -> db.Entity: 

184 """ 

185 Ensures, that general root-node for the current module exists. 

186 If no root-node exists yet, it will be created. 

187 

188 :returns: The entity of the root-node. 

189 """ 

190 return self.rootnodeSkel(ensure=True).dbEntity 

191 

192 def getAvailableRootNodes(self, *args, **kwargs) -> list[dict[t.Literal["name", "key"], str]]: 

193 """ 

194 Default function for providing a list of root node items. 

195 This list is requested by several module-internal functions and *must* be 

196 overridden by a custom functionality. The default stub for this function 

197 returns an empty list. 

198 An example implementation could be the following: 

199 

200 .. code-block:: python 

201 

202 # Example 

203 def getAvailableRootNodes(self, *args, **kwargs): 

204 q = db.Query(self.rootKindName) 

205 ret = [{"key": str(e.key()), 

206 "name": e.get("name", str(e.key().id_or_name()))} #FIXME 

207 for e in q.run(limit=25)] 

208 return ret 

209 

210 :param args: Can be used in custom implementations. 

211 :param kwargs: Can be used in custom implementations. 

212 :return: Returns a list of dicts which must provide a "key" and a "name" entry with \ 

213 respective information. 

214 """ 

215 return [] 

216 

217 def getRootNode(self, key: db.Key | str) -> SkeletonInstance | None: 

218 """ 

219 Returns the root-node for a given child. 

220 

221 :param key: Key of the child node entry. 

222 

223 :returns: The skeleton of the root-node. 

224 """ 

225 skel = self.nodeSkelCls() 

226 

227 while key: 

228 if not skel.read(key): 

229 return None 

230 

231 key = skel["parententry"] 

232 

233 return skel 

234 

235 @CallDeferred 

236 def updateParentRepo(self, parentNode: str, newRepoKey: str, depth: int = 0): 

237 """ 

238 Recursively fixes the parentrepo key after a move operation. 

239 

240 This will delete all entries which are children of *nodeKey*, except *key* nodeKey. 

241 

242 :param parentNode: URL-safe key of the node which children should be fixed. 

243 :param newRepoKey: URL-safe key of the new repository. 

244 :param depth: Safety level depth preventing infinitive loops. 

245 """ 

246 if depth > 99: 

247 logging.critical(f"Maximum recursion depth reached in {self.updateParentRepo.__module__}/updateParentRepo") 

248 logging.critical("Your data is corrupt!") 

249 logging.debug(f"{parentNode=}, {newRepoKey=}") 

250 return 

251 

252 def fixTxn(nodeKey, newRepoKey): 

253 node = db.get(nodeKey) 

254 node["parentrepo"] = newRepoKey 

255 db.put(node) 

256 

257 # Fix all nodes 

258 q = db.Query(self.viewSkel("node").kindName).filter("parententry =", parentNode) 

259 for repo in q.iter(): 

260 self.updateParentRepo(repo.key, newRepoKey, depth=depth + 1) 

261 db.run_in_transaction(fixTxn, repo.key, newRepoKey) 

262 

263 # Fix the leafs on this level 

264 if self.leafSkelCls: 

265 q = db.Query(self.viewSkel("leaf").kindName).filter("parententry =", parentNode) 

266 for repo in q.iter(): 

267 db.run_in_transaction(fixTxn, repo.key, newRepoKey) 

268 

269 ## Internal exposed functions 

270 

271 @internal_exposed 

272 def pathToKey(self, key: db.Key): 

273 """ 

274 Returns the recursively expanded path through the Tree from the root-node to a 

275 requested node. 

276 :param key: Key of the destination *node*. 

277 :returns: An nested dictionary with information about all nodes in the path from root to the requested node. 

278 """ 

279 lastLevel = [] 

280 for x in range(0, 99): 

281 currentNodeSkel = self.viewSkel("node") 

282 if not currentNodeSkel.read(key): 

283 return [] # Either invalid key or listFilter prevented us from fetching anything 

284 if currentNodeSkel["parententry"] == currentNodeSkel["parentrepo"]: # We reached the top level 

285 break 

286 levelQry = self.viewSkel("node").all().filter("parententry =", currentNodeSkel["parententry"]) 

287 currentLevel = [{"skel": x, 

288 "active": x["key"] == currentNodeSkel["key"], 

289 "children": lastLevel if x["key"] == currentNodeSkel["key"] else []} 

290 for x in self.listFilter(levelQry).fetch(99)] 

291 assert currentLevel, "Got emtpy parent list?" 

292 lastLevel = currentLevel 

293 key = currentNodeSkel["parententry"] 

294 return lastLevel 

295 

296 ## External exposed functions 

297 

298 @exposed 

299 def index(self, skelType: SkelType = "node", parententry: t.Optional[db.KeyType] = None, **kwargs): 

300 if not parententry: 

301 repos = self.getAvailableRootNodes(**kwargs) 

302 match len(repos): 

303 case 0: 

304 raise errors.Unauthorized() 

305 case 1: 

306 parententry = repos[0]["key"] 

307 case _: 

308 raise errors.NotAcceptable(f"Missing required parameter {'parententry'!r}") 

309 

310 return self.list(skelType=skelType, parententry=parententry, **kwargs) 

311 

312 @exposed 

313 def listRootNodes(self, *args, **kwargs) -> t.Any: 

314 """ 

315 Renders a list of all available repositories for the current user using the 

316 modules default renderer. 

317 

318 :returns: The rendered representation of the available root-nodes. 

319 """ 

320 return self.render.listRootNodes(self.getAvailableRootNodes(*args, **kwargs)) 

321 

322 @exposed 

323 def list(self, skelType: SkelType, *args, **kwargs) -> t.Any: 

324 """ 

325 Prepares and renders a list of entries. 

326 

327 All supplied parameters are interpreted as filters for the elements displayed. 

328 

329 Unlike other module prototypes in ViUR, the access control in this function is performed 

330 by calling the function :func:`listFilter`, which updates the query-filter to match only 

331 elements which the user is allowed to see. 

332 

333 .. seealso:: :func:`listFilter`, :func:`viur.core.db.mergeExternalFilter` 

334 

335 :returns: The rendered list objects for the matching entries. 

336 

337 :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions. 

338 """ 

339 if not (skelType := self._checkSkelType(skelType)): 

340 raise errors.NotAcceptable("Invalid skelType provided.") 

341 

342 # The general access control is made via self.listFilter() 

343 if not (query := self.listFilter(self.viewSkel(skelType).all().mergeExternalFilter(kwargs))): 

344 raise errors.Unauthorized() 

345 

346 self._apply_default_order(query) 

347 return self.render.list(query.fetch()) 

348 

349 @exposed 

350 def structure(self, skelType: SkelType, action: t.Optional[str] = "view") -> t.Any: 

351 """ 

352 :returns: Returns the structure of our skeleton as used in list/view. Values are the defaultValues set 

353 in each bone. 

354 

355 :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions. 

356 """ 

357 # FIXME: In ViUR > 3.7 this could also become dynamic (ActionSkel paradigm). 

358 match action: 

359 case "view": 

360 skel = self.viewSkel(skelType) 

361 if not self.canView(skelType, skel): 

362 raise errors.Unauthorized() 

363 

364 case "edit": 

365 skel = self.editSkel(skelType) 

366 if not self.canEdit(skelType, skel): 

367 raise errors.Unauthorized() 

368 

369 case "add": 

370 if not self.canAdd(skelType): 

371 raise errors.Unauthorized() 

372 

373 skel = self.addSkel(skelType) 

374 

375 case "clone": 

376 skel = self.cloneSkel(skelType) 

377 if not (self.canAdd(skelType) and self.canEdit(skelType, skel)): 

378 raise errors.Unauthorized() 

379 

380 case _: 

381 raise errors.NotImplemented(f"The action {action!r} is not implemented.") 

382 

383 return self.render.render(f"structure.{skelType}.{action}", skel) 

384 

385 @exposed 

386 def view(self, skelType: SkelType, key: db.KeyType, *args, **kwargs) -> t.Any: 

387 """ 

388 Prepares and renders a single entry for viewing. 

389 

390 The entry is fetched by its *key* and its *skelType*. 

391 The function performs several access control checks on the requested entity before it is rendered. 

392 

393 .. seealso:: :func:`canView`, :func:`onView` 

394 

395 :returns: The rendered representation of the requested entity. 

396 

397 :param skelType: May either be "node" or "leaf". 

398 :param key: URL-safe key of the parent. 

399 

400 :raises: :exc:`viur.core.errors.NotAcceptable`, when an incorrect *skelType* is provided. 

401 :raises: :exc:`viur.core.errors.NotFound`, when no entry with the given *key* was found. 

402 :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions. 

403 """ 

404 if not (skelType := self._checkSkelType(skelType)): 

405 raise errors.NotAcceptable(f"Invalid skelType provided.") 

406 

407 skel = self.viewSkel(skelType) 

408 if not skel.read(key): 

409 raise errors.NotFound() 

410 

411 if not self.canView(skelType, skel): 

412 raise errors.Unauthorized() 

413 

414 self.onView(skelType, skel) 

415 return self.render.view(skel) 

416 

417 @exposed 

418 @force_ssl 

419 @skey(allow_empty=True) 

420 def add(self, skelType: SkelType, node: db.KeyType, *, bounce: bool = False, **kwargs) -> t.Any: 

421 # FIXME: VIUR4 rename node into key... 

422 """ 

423 Add a new entry with the given parent *node*, and render the entry, eventually with error notes 

424 on incorrect data. Data is taken by any other arguments in *kwargs*. 

425 

426 The function performs several access control checks on the requested entity before it is added. 

427 

428 .. seealso:: :func:`canAdd`, :func:`onAdd`, , :func:`onAdded` 

429 

430 :param skelType: Defines the type of the new entry and may either be "node" or "leaf". 

431 :param node: URL-safe key of the parent. 

432 

433 :returns: The rendered, added object of the entry, eventually with error hints. 

434 

435 :raises: :exc:`viur.core.errors.NotAcceptable`, when no valid *skelType* was provided. 

436 :raises: :exc:`viur.core.errors.NotFound`, when no valid *node* was found. 

437 :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions. 

438 :raises: :exc:`viur.core.errors.PreconditionFailed`, if the *skey* could not be verified. 

439 """ 

440 if not (skelType := self._checkSkelType(skelType)): 

441 raise errors.NotAcceptable(f"Invalid skelType provided.") 

442 

443 skel = self.addSkel(skelType) 

444 parentNodeSkel = self.editSkel("node") 

445 

446 # TODO VIUR4: Why is this parameter called "node"? 

447 if not parentNodeSkel.read(node): 

448 raise errors.NotFound("The provided parent node could not be found.") 

449 if not self.canAdd(skelType, parentNodeSkel): 

450 raise errors.Unauthorized() 

451 

452 skel["parententry"] = parentNodeSkel["key"] 

453 # parentrepo may not exist in parentNodeSkel as it may be an rootNode 

454 skel["parentrepo"] = parentNodeSkel["parentrepo"] or parentNodeSkel["key"] 

455 

456 if ( 

457 not kwargs # no data supplied 

458 or not current.request.get().isPostRequest # failure if not using POST-method 

459 or not skel.fromClient(kwargs, amend=bounce) # failure on reading into the bones 

460 or bounce # review before adding 

461 ): 

462 return self.render.add(skel) 

463 

464 self.onAdd(skelType, skel) 

465 skel.write() 

466 self.onAdded(skelType, skel) 

467 

468 return self.render.addSuccess(skel) 

469 

470 @force_ssl 

471 @force_post 

472 @exposed 

473 @skey 

474 @access("root") 

475 def add_or_edit(self, skelType: SkelType, key: db.KeyType, **kwargs) -> t.Any: 

476 """ 

477 This function is intended to be used by importers. 

478 Only "root"-users are allowed to use it. 

479 """ 

480 if not (skelType := self._checkSkelType(skelType)): 

481 raise errors.NotAcceptable("Invalid skelType provided.") 

482 

483 kind_name = self.nodeSkelCls.kindName if skelType == "node" else self.leafSkelCls.kindName 

484 

485 # Adjust key 

486 db_key = db.key_helper(key, target_kind=kind_name, adjust_kind=True) 

487 

488 # Retrieve and verify existing entry 

489 db_entity = db.get(db_key) 

490 is_add = not bool(db_entity) 

491 

492 # Instanciate relevant skeleton 

493 if is_add: 

494 skel = self.addSkel(skelType) 

495 else: 

496 skel = self.editSkel(skelType) 

497 skel.dbEntity = db_entity # assign existing entity 

498 

499 skel = skel.ensure_is_cloned() 

500 skel.parententry.required = True 

501 skel.parententry.readOnly = False 

502 

503 skel["key"] = db_key 

504 

505 if ( 

506 not kwargs # no data supplied 

507 or not skel.fromClient(kwargs) # failure on reading into the bones 

508 ): 

509 # render the skeleton in the version it could as far as it could be read. 

510 return self.render.render("add_or_edit", skel) 

511 

512 # Ensure the parententry exists 

513 parentNodeSkel = self.editSkel("node") 

514 if not parentNodeSkel.read(skel["parententry"]): 

515 raise errors.NotFound("The provided parent node could not be found.") 

516 if not self.canAdd(skelType, parentNodeSkel): 

517 raise errors.Unauthorized() 

518 

519 skel["parententry"] = parentNodeSkel["key"] 

520 # parentrepo may not exist in parentNodeSkel as it may be an rootNode 

521 skel["parentrepo"] = parentNodeSkel["parentrepo"] or parentNodeSkel["key"] 

522 

523 if is_add: 

524 self.onAdd(skelType, skel) 

525 else: 

526 self.onEdit(skelType, skel) 

527 

528 skel.write() 

529 

530 if is_add: 

531 self.onAdded(skelType, skel) 

532 return self.render.addSuccess(skel) 

533 

534 self.onEdited(skelType, skel) 

535 return self.render.editSuccess(skel) 

536 

537 @exposed 

538 @force_ssl 

539 @skey(allow_empty=True) 

540 def edit(self, skelType: SkelType, key: db.KeyType, *, bounce: bool = False, **kwargs) -> t.Any: 

541 """ 

542 Modify an existing entry, and render the entry, eventually with error notes on incorrect data. 

543 Data is taken by any other arguments in *kwargs*. 

544 

545 The function performs several access control checks on the requested entity before it is added. 

546 

547 .. seealso:: :func:`canEdit`, :func:`onEdit`, :func:`onEdited` 

548 

549 :param skelType: Defines the type of the entry that should be modified and may either be "node" or "leaf". 

550 :param key: URL-safe key of the item to be edited. 

551 

552 :returns: The rendered, modified object of the entry, eventually with error hints. 

553 

554 :raises: :exc:`viur.core.errors.NotAcceptable`, when no valid *skelType* was provided. 

555 :raises: :exc:`viur.core.errors.NotFound`, when no valid *node* was found. 

556 :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions. 

557 :raises: :exc:`viur.core.errors.PreconditionFailed`, if the *skey* could not be verified. 

558 """ 

559 if not (skelType := self._checkSkelType(skelType)): 

560 raise errors.NotAcceptable(f"Invalid skelType provided.") 

561 

562 skel = self.editSkel(skelType) 

563 if not skel.read(key): 

564 raise errors.NotFound() 

565 

566 if not self.canEdit(skelType, skel): 

567 raise errors.Unauthorized() 

568 

569 if ( 

570 not kwargs # no data supplied 

571 or not current.request.get().isPostRequest # failure if not using POST-method 

572 or not skel.fromClient(kwargs, amend=True) # failure on reading into the bones 

573 or bounce # review before adding 

574 ): 

575 return self.render.edit(skel) 

576 

577 self.onEdit(skelType, skel) 

578 skel.write() 

579 self.onEdited(skelType, skel) 

580 

581 return self.render.editSuccess(skel) 

582 

583 @exposed 

584 @force_ssl 

585 @force_post 

586 @skey 

587 def delete(self, skelType: SkelType, key: str, **kwargs) -> t.Any: 

588 """ 

589 Deletes an entry or an directory (including its contents). 

590 

591 The function runs several access control checks on the data before it is deleted. 

592 

593 For a node, the node itself and its entire subtree are deleted 

594 bottom-up as a single deferred job -- see :meth:`deleteRecursive` 

595 for why the node must not be deleted synchronously here. 

596 ``onDelete``/``onDeleted`` for the node still fire exactly once, 

597 just from within that deferred job instead of within this request. 

598 

599 If the node is locked by a ``RelationalConsistency.PreventDeletion`` 

600 relation, this is checked synchronously here (matching 

601 ``Skeleton.delete()``'s own check) so the caller gets an immediate 

602 error and no deferred job -- and therefore no cascading deletion of 

603 the subtree -- is ever started for it. 

604 

605 .. seealso:: :func:`canDelete`, :func:`onDelete`, :func:`onDeleted` 

606 

607 :param skelType: Defines the type of the entry that should be deleted and may either be "node" or "leaf". 

608 :param key: URL-safe key of the item to be deleted. 

609 

610 :returns: The rendered, deleted object of the entry. 

611 

612 :raises: :exc:`viur.core.errors.NotFound`, when no entry with the given *key* was found. 

613 :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions. 

614 :raises: :exc:`viur.core.errors.PreconditionFailed`, if the *skey* could not be verified. 

615 :raises: whatever :meth:`checkDeletePreconditions` raises (by default 

616 :exc:`viur.core.errors.Locked` for an entry still referenced by a 

617 ``PreventDeletion`` relation). 

618 """ 

619 if not (skelType := self._checkSkelType(skelType)): 

620 raise errors.NotAcceptable(f"Invalid skelType provided.") 

621 

622 skel = self.editSkel(skelType) 

623 if not skel.read(key): 

624 raise errors.NotFound() 

625 

626 if not self.canDelete(skelType, skel): 

627 raise errors.Unauthorized() 

628 

629 # Fail fast for the entry the delete was invoked on, so the caller 

630 # gets an immediate error and (for a node) no deferred cascade is 

631 # even started. Descendants are validated in deleteRecursive. 

632 self.checkDeletePreconditions(skelType, skel) 

633 

634 if skelType == "node": 

635 self.deleteRecursive(skel["key"], delete_self=True, call_hooks=True) 

636 else: 

637 self.onDelete(skelType, skel) 

638 skel.delete() 

639 self.onDeleted(skelType, skel) 

640 

641 return self.render.deleteSuccess(skel, skelType=skelType) 

642 

643 def checkDeletePreconditions(self, skelType: SkelType, skel: SkeletonInstance) -> None: 

644 """ 

645 Verify that *skel* may be deleted in its current state, raising if not. 

646 

647 This is a *state* precondition, distinct from :meth:`canDelete`, which 

648 answers whether the current *user* is permitted to delete. Raise an 

649 :class:`~viur.core.errors.HTTPException` (e.g. 

650 :class:`~viur.core.errors.Locked`, :class:`~viur.core.errors.Forbidden`) 

651 to veto the deletion. 

652 

653 The default refuses to delete an entry that is still referenced by a 

654 ``RelationalConsistency.PreventDeletion`` relation (mirroring the check 

655 inside ``Skeleton.delete()``, but *before* a cascade removes anything). 

656 

657 It is called 

658 

659 * synchronously in :meth:`delete` for the entry the delete was invoked 

660 on (immediate error to the caller), and 

661 * for **every** entry of a node's subtree in :meth:`deleteRecursive`, 

662 in a read-only pre-pass *before* anything is deleted -- so a veto 

663 anywhere in the subtree aborts the whole delete without leaving a 

664 partially-deleted, orphaned state behind. 

665 

666 Override it (calling ``super()``) to add domain-specific rules, e.g.:: 

667 

668 def checkDeletePreconditions(self, skelType, skel): 

669 super().checkDeletePreconditions(skelType, skel) 

670 if skel["is_locked"]: 

671 raise errors.Forbidden("This entry is locked.") 

672 

673 :param skelType: Type of the entry ("node" or "leaf"). 

674 :param skel: The already-read skeleton of the entry. 

675 """ 

676 if self._is_locked_by_relation(skel["key"]): 

677 raise errors.Locked("This entry is still referenced by other Skeletons, which prevents deleting!") 

678 

679 @staticmethod 

680 def _is_locked_by_relation(key: db.Key) -> bool: 

681 """ 

682 Check whether *key* is referenced by a ``RelationalConsistency.PreventDeletion`` relation. 

683 

684 Mirrors the check inside ``Skeleton.delete()``. 

685 

686 :param key: Key of the entity to check. 

687 :return: True if a ``PreventDeletion`` relation still points at *key*. 

688 """ 

689 return ( 

690 db.Query("viur-relations") 

691 .filter("dest.__key__ =", key) 

692 .filter("viur_relational_consistency =", RelationalConsistency.PreventDeletion.value) 

693 ).getEntry() is not None 

694 

695 @CallDeferred 

696 def deleteRecursive(self, parentKey: str, delete_self: bool = False, call_hooks: bool = False): 

697 """ 

698 Recursively processes a delete request. 

699 

700 Deletes all entries which are children of *parentKey*, bottom-up: 

701 leafs first, then each sub-node only after its own descendants 

702 have been removed. The whole subtree is processed within this 

703 single deferred call -- recursing into a sub-node does *not* 

704 spawn a separate deferred task for it, so there is no window in 

705 which a node could be deleted (or considered done) before its 

706 own children actually are. 

707 

708 Before anything is deleted, a read-only pre-pass validates the 

709 whole subtree (and, if *delete_self*, *parentKey* itself) via 

710 :meth:`checkDeletePreconditions`. If any entry vetoes deletion, the 

711 call aborts and logs without deleting a single entry -- so a veto 

712 (e.g. a ``PreventDeletion`` relation, or a domain rule added by a 

713 subclass) can never leave a partially-deleted, orphaned subtree 

714 behind. Validate-all-then-delete-all rather than checking each entry 

715 only as it is about to be deleted (which, being bottom-up, would 

716 already have removed the vetoed entry's own children). 

717 

718 If *delete_self* is set, *parentKey* itself is deleted last, once 

719 everything below it is already gone. This is what makes deferring 

720 the deletion of a node safe: if this task is lost entirely (queue 

721 purge, crash, a task pinned to an App Engine version that no 

722 longer exists, ...), *nothing* in the subtree has been touched 

723 yet, so nothing is left behind; if it fails partway through, a 

724 retry simply continues with what remains (deleting an 

725 already-deleted entry is a no-op read-then-skip). The previous 

726 behavior -- the node deleted synchronously by :meth:`delete` 

727 while its children's removal was merely enqueued as a separate, 

728 independent job -- could leave orphaned entries permanently 

729 behind: children whose ``parententry`` points to an 

730 already-deleted, nonexistent node, which then breaks anything 

731 that relies on the tree being intact (e.g. relation updates, 

732 aggregations). 

733 

734 :param parentKey: URL-safe key of the node whose children (and, 

735 if *delete_self*, the node itself) should be deleted. 

736 :param delete_self: If True, also delete the node identified by 

737 *parentKey*, after all of its descendants have been removed. 

738 :param call_hooks: If True, call :meth:`onDelete`/:meth:`onDeleted` 

739 for the *parentKey* node itself (only meaningful together 

740 with *delete_self*). Not applied recursively: cascaded 

741 descendants are removed without hooks, same as before. 

742 """ 

743 nodeKey = db.key_helper(parentKey, self.viewSkel("node").kindName) 

744 if not self._checkSubtreeDeletable(nodeKey, check_self=delete_self): 

745 # A veto was found (and logged) during the read-only pre-pass; 

746 # nothing has been deleted, keeping the tree consistent. 

747 return 

748 self._deleteSubtree(nodeKey) 

749 if delete_self: 

750 nodeSkel = self.viewSkel("node") 

751 if nodeSkel.read(nodeKey): 

752 if call_hooks: 

753 self.onDelete("node", nodeSkel) 

754 nodeSkel.delete() 

755 if call_hooks: 

756 self.onDeleted("node", nodeSkel) 

757 

758 def _checkSubtreeDeletable(self, nodeKey: db.Key, check_self: bool) -> bool: 

759 """ 

760 Read-only pre-pass for :meth:`deleteRecursive`. 

761 

762 Returns True only if every entry of *nodeKey*'s subtree (and 

763 *nodeKey* itself when *check_self*) passes 

764 :meth:`checkDeletePreconditions`. On the first veto it logs and 

765 returns False without having deleted anything. 

766 

767 :param nodeKey: Key of the node whose subtree is validated. 

768 :param check_self: Whether to also validate *nodeKey* itself. 

769 :return: True if the whole subtree may be deleted. 

770 """ 

771 try: 

772 if check_self: 

773 nodeSkel = self.viewSkel("node") 

774 if nodeSkel.read(nodeKey): 

775 self.checkDeletePreconditions("node", nodeSkel) 

776 if self.leafSkelCls: 

777 for leaf in db.Query(self.viewSkel("leaf").kindName).filter("parententry =", nodeKey).iter(): 

778 leafSkel = self.viewSkel("leaf") 

779 if leafSkel.read(leaf.key): 

780 self.checkDeletePreconditions("leaf", leafSkel) 

781 except errors.HTTPException as exc: 

782 logging.warning(f"Refusing to delete subtree of {nodeKey!r}: {exc}") 

783 return False 

784 for node in db.Query(self.viewSkel("node").kindName).filter("parententry =", nodeKey).iter(): 

785 if not self._checkSubtreeDeletable(node.key, check_self=True): 

786 return False 

787 return True 

788 

789 def onDeleteRecursive(self, skelType: SkelType, skel: SkeletonInstance) -> None: 

790 """ 

791 Hook, called for every *descendant* entry cascaded away during a 

792 recursive delete, right before that entry is deleted. 

793 

794 In contrast to :meth:`onDelete`/:meth:`onDeleted` — which fire once, 

795 for the very entry the delete was invoked on — this fires for each 

796 cascaded child/grandchild/... removed by :meth:`deleteRecursive`. 

797 The default implementation does nothing; override it to run 

798 per-entry cleanup (e.g. releasing external resources tied to a leaf). 

799 

800 :param skelType: Type of the descendant being deleted ("node" or "leaf"). 

801 :param skel: The already-read skeleton of the descendant. 

802 """ 

803 pass 

804 

805 def _deleteSubtree(self, nodeKey: db.Key) -> None: 

806 """ 

807 Synchronously delete all descendants of *nodeKey* (not *nodeKey* 

808 itself), bottom-up. 

809 

810 Internal helper for :meth:`deleteRecursive`: recurses directly 

811 instead of spawning a new deferred task per tree level, so the 

812 whole subtree is processed within a single execution and strictly 

813 in the correct order (a sub-node is only deleted once every one of 

814 *its* descendants is confirmed gone). :meth:`onDeleteRecursive` is 

815 called for each descendant right before it is deleted. 

816 

817 :param nodeKey: Key of the node whose descendants get deleted. 

818 """ 

819 if self.leafSkelCls: 

820 for leaf in db.Query(self.viewSkel("leaf").kindName).filter("parententry =", nodeKey).iter(): 

821 leafSkel = self.viewSkel("leaf") 

822 if not leafSkel.read(leaf.key): 

823 continue 

824 self.onDeleteRecursive("leaf", leafSkel) 

825 leafSkel.delete() 

826 for node in db.Query(self.viewSkel("node").kindName).filter("parententry =", nodeKey).iter(): 

827 self._deleteSubtree(node.key) 

828 nodeSkel = self.viewSkel("node") 

829 if nodeSkel.read(node.key): 

830 self.onDeleteRecursive("node", nodeSkel) 

831 nodeSkel.delete() 

832 

833 @exposed 

834 @force_ssl 

835 @force_post 

836 @skey 

837 def move( 

838 self, 

839 skelType: SkelType, 

840 key: db.KeyType, 

841 parentNode: db.KeyType, 

842 sortindex: t.Optional[float] = None 

843 ) -> str: 

844 """ 

845 Move a node (including its contents) or a leaf to another node. 

846 

847 .. seealso:: :func:`canMove` 

848 

849 :param skelType: Defines the type of the entry that should be moved and may either be "node" or "leaf". 

850 :param key: URL-safe key of the item to be moved. 

851 :param parentNode: URL-safe key of the destination node, which must be a node. 

852 :param sortindex: An optional sortindex for the key. 

853 

854 :returns: The rendered, edited object of the entry. 

855 

856 :raises: :exc:`viur.core.errors.NotFound`, when no entry with the given *key* was found. 

857 :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions. 

858 :raises: :exc:`viur.core.errors.PreconditionFailed`, if the *skey* could not be verified. 

859 """ 

860 if not (skelType := self._checkSkelType(skelType)): 

861 raise errors.NotAcceptable("Invalid skelType provided.") 

862 

863 skel = self.editSkel(skelType) 

864 parentnode_skel = self.baseSkel("node") 

865 

866 if not skel.read(key): 

867 raise errors.NotFound("Cannot find entity to move") 

868 

869 if not parentnode_skel.read(parentNode): 

870 parentNode = db.normalize_key(parentNode) 

871 

872 if parentNode.kind != parentnode_skel.kindName: 

873 raise errors.NotFound( 

874 f"You provided a key of kind {parentNode.kind}, but require a {parentnode_skel.kindName}." 

875 ) 

876 

877 raise errors.NotFound("Cannot find parentNode entity") 

878 

879 if skel["key"] == parentnode_skel["key"]: 

880 raise errors.NotAcceptable("Cannot move a node into itself") 

881 

882 # Test if we try to move a rootNode 

883 if not skel["parententry"]: 

884 raise errors.NotAcceptable("Can't move a rootNode to somewhere else") 

885 

886 if not self.canMove(skelType, skel, parentnode_skel): 

887 raise errors.Unauthorized() 

888 

889 # Check if parentNodeSkel is descendant of the skel 

890 walk_skel = parentnode_skel.clone() 

891 

892 while walk_skel and walk_skel["parententry"]: 

893 if walk_skel["parententry"] == skel["key"]: 

894 raise errors.NotAcceptable( 

895 f"Invalid move: Entry {key} cannot be moved below its own descendant {parentNode}." 

896 ) 

897 

898 walk_skel = walk_skel.read(walk_skel["parententry"]) 

899 

900 if not walk_skel: 

901 logging.warning(f"The parententry chain of {skel["key"]!r} seems to be broken") 

902 

903 old_parentrepo = skel["parentrepo"] 

904 

905 self.onEdit(skelType, skel) 

906 skel.patch({ 

907 "parententry": parentnode_skel["key"], 

908 "parentrepo": parentnode_skel["parentrepo"], 

909 "sortindex": sortindex or time.time() 

910 }) 

911 self.onEdited(skelType, skel) 

912 

913 # Ensure a changed parentRepo get's propagated 

914 if old_parentrepo != parentnode_skel["parentrepo"]: 

915 self.updateParentRepo(key, parentnode_skel["parentrepo"]) 

916 

917 return self.render.render("moveSuccess", skel) 

918 

919 @exposed 

920 @force_ssl 

921 @skey(allow_empty=True) 

922 def clone( 

923 self, 

924 skelType: SkelType, 

925 key: db.Key | str | int, 

926 *, 

927 bounce: bool = False, 

928 parententry: t.Optional[db.Key | str | int] = None, 

929 **kwargs, 

930 ): 

931 """ 

932 Clone an existing entry, and render the entry, eventually with error notes on incorrect data. 

933 Data is taken by any other arguments in *kwargs*. 

934 

935 The function performs several access control checks on the requested entity before it is added. 

936 

937 .. seealso:: :func:`canEdit`, :func:`canAdd`, :func:`onClone`, :func:`onCloned` 

938 

939 :param skelType: Defines the type of the entry that should be cloned and may either be "node" or "leaf". 

940 :param key: URL-safe key of the item to be edited. 

941 :param bounce: Return the skeleton after applying client data and validtion without writing. 

942 :param parententry: URL-safe key of the destination parent node. 

943 

944 :returns: The cloned object of the entry, eventually with error hints. 

945 

946 :raises: :exc:`viur.core.errors.NotAcceptable`, when no valid *skelType* was provided. 

947 :raises: :exc:`viur.core.errors.NotFound`, when no *entry* to clone from was found. 

948 :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions. 

949 """ 

950 

951 if not (skelType := self._checkSkelType(skelType)): 

952 raise errors.NotAcceptable(f"Invalid skelType provided.") 

953 

954 skel = self.cloneSkel(skelType) 

955 if not skel.read(key): 

956 raise errors.NotFound() 

957 

958 if parententry is not None: 

959 if not (parent_node_skel := self.viewSkel("node").read(parententry)): 

960 raise errors.NotFound("The provided parent node could not be found.") 

961 else: 

962 parent_node_skel = None 

963 

964 # a clone-operation is some kind of edit and add... 

965 if not (self.canEdit(skelType, skel) and self.canAdd(skelType, parent_node_skel)): 

966 raise errors.Unauthorized() 

967 

968 # Remember source skel and unset the key for clone operation! 

969 src_skel = skel 

970 skel = skel.clone(apply_clone_strategy=True) 

971 skel["key"] = None 

972 

973 # make parententry required and writeable when provided 

974 if "parententry" in kwargs: 

975 skel.parententry.readOnly = False 

976 skel.parententry.required = True 

977 else: 

978 _ = skel["parententry"] # TODO: because of accessedValues... 

979 

980 # make parentrepo required and writeable when provided 

981 if "parentrepo" in kwargs: 

982 skel.parentrepo.readOnly = False 

983 skel.parentrepo.required = True 

984 else: 

985 _ = skel["parentrepo"] # TODO: because of accessedValues... 

986 

987 # Check all required preconditions for clone 

988 if ( 

989 not kwargs # no data supplied 

990 or not current.request.get().isPostRequest # failure if not using POST-method 

991 or not skel.fromClient(kwargs, amend=bounce) # failure on reading into the bones 

992 or bounce # review before changing 

993 ): 

994 return self.render.edit(skel, action="clone") 

995 

996 self.onClone(skelType, skel, src_skel=src_skel) 

997 assert skel.write() 

998 self.onCloned(skelType, skel, src_skel=src_skel) 

999 

1000 return self.render.editSuccess(skel, action="cloneSuccess") 

1001 

1002 ## Default access control functions 

1003 

1004 def listFilter(self, query: db.Query) -> t.Optional[db.Query]: 

1005 """ 

1006 Access control function on item listing. 

1007 

1008 This function is invoked by the :func:`list` renderer and the related Jinja2 fetching function, 

1009 and is used to modify the provided filter parameter to match only items that the current user 

1010 is allowed to see. 

1011 

1012 :param query: Query which should be altered. 

1013 

1014 :returns: The altered filter, or None if access is not granted. 

1015 """ 

1016 

1017 if (user := current.user.get()) and (f"{self.moduleName}-view" in user["access"] or "root" in user["access"]): 

1018 return query 

1019 

1020 return None 

1021 

1022 def canView(self, skelType: SkelType, skel: SkeletonInstance) -> bool: 

1023 """ 

1024 Checks if the current user can view the given entry. 

1025 Should be identical to what's allowed by listFilter. 

1026 By default, `meth:listFilter` is used to determine what's allowed and whats not; but this 

1027 method can be overridden for performance improvements (to eliminate that additional database access). 

1028 :param skel: The entry we check for 

1029 :return: True if the current session is authorized to view that entry, False otherwise 

1030 """ 

1031 query = self.viewSkel(skelType).all() 

1032 

1033 if key := skel["key"]: 

1034 query.mergeExternalFilter({"key": key}) 

1035 

1036 query = self.listFilter(query) # Access control 

1037 

1038 if query is None or (key and not query.getEntry()): 

1039 return False 

1040 

1041 return True 

1042 

1043 def canAdd(self, skelType: SkelType, parentNodeSkel: t.Optional[SkeletonInstance] = None) -> bool: 

1044 """ 

1045 Access control function for adding permission. 

1046 

1047 Checks if the current user has the permission to add a new entry. 

1048 

1049 The default behavior is: 

1050 - If no user is logged in, adding is generally refused. 

1051 - If the user has "root" access, adding is generally allowed. 

1052 - If the user has the modules "add" permission (module-add) enabled, adding is allowed. 

1053 

1054 It should be overridden for a module-specific behavior. 

1055 

1056 .. seealso:: :func:`add` 

1057 

1058 :param skelType: Defines the type of the node that should be added. 

1059 :param parentNodeSkel: The parent node where a new entry should be added. 

1060 

1061 :returns: True, if adding entries is allowed, False otherwise. 

1062 """ 

1063 

1064 if not (user := current.user.get()): 

1065 return False 

1066 # root user is always allowed. 

1067 if user["access"] and "root" in user["access"]: 

1068 return True 

1069 # user with add-permission is allowed. 

1070 if user and user["access"] and f"{self.moduleName}-add" in user["access"]: 

1071 return True 

1072 return False 

1073 

1074 def canEdit(self, skelType: SkelType, skel: SkeletonInstance) -> bool: 

1075 """ 

1076 Access control function for modification permission. 

1077 

1078 Checks if the current user has the permission to edit an entry. 

1079 

1080 The default behavior is: 

1081 - If no user is logged in, editing is generally refused. 

1082 - If the user has "root" access, editing is generally allowed. 

1083 - If the user has the modules "edit" permission (module-edit) enabled, editing is allowed. 

1084 

1085 It should be overridden for a module-specific behavior. 

1086 

1087 .. seealso:: :func:`edit` 

1088 

1089 :param skelType: Defines the type of the node that should be edited. 

1090 :param skel: The Skeleton that should be edited. 

1091 

1092 :returns: True, if editing entries is allowed, False otherwise. 

1093 """ 

1094 if not (user := current.user.get()): 

1095 return False 

1096 if user["access"] and "root" in user["access"]: 

1097 return True 

1098 if user and user["access"] and f"{self.moduleName}-edit" in user["access"]: 

1099 return True 

1100 return False 

1101 

1102 def canDelete(self, skelType: SkelType, skel: SkeletonInstance) -> bool: 

1103 """ 

1104 Access control function for delete permission. 

1105 

1106 Checks if the current user has the permission to delete an entry. 

1107 

1108 The default behavior is: 

1109 - If no user is logged in, deleting is generally refused. 

1110 - If the user has "root" access, deleting is generally allowed. 

1111 - If the user has the modules "deleting" permission (module-delete) enabled, \ 

1112 deleting is allowed. 

1113 

1114 It should be overridden for a module-specific behavior. 

1115 

1116 :param skelType: Defines the type of the node that should be deleted. 

1117 :param skel: The Skeleton that should be deleted. 

1118 

1119 .. seealso:: :func:`delete` 

1120 

1121 :returns: True, if deleting entries is allowed, False otherwise. 

1122 """ 

1123 if not (user := current.user.get()): 

1124 return False 

1125 if user["access"] and "root" in user["access"]: 

1126 return True 

1127 if user and user["access"] and f"{self.moduleName}-delete" in user["access"]: 

1128 return True 

1129 return False 

1130 

1131 def canMove(self, skelType: SkelType, node: SkeletonInstance, destNode: SkeletonInstance) -> bool: 

1132 """ 

1133 Access control function for moving permission. 

1134 

1135 Checks if the current user has the permission to move an entry. 

1136 

1137 The default behavior is: 

1138 - If no user is logged in, deleting is generally refused. 

1139 - If the user has "root" access, deleting is generally allowed. 

1140 - If the user has the modules "edit" permission (module-edit) enabled, \ 

1141 moving is allowed. 

1142 

1143 It should be overridden for a module-specific behavior. 

1144 

1145 :param skelType: Defines the type of the node that shall be deleted. 

1146 :param node: URL-safe key of the node to be moved. 

1147 :param destNode: URL-safe key of the node where *node* should be moved to. 

1148 

1149 .. seealso:: :func:`move` 

1150 

1151 :returns: True, if deleting entries is allowed, False otherwise. 

1152 """ 

1153 if not (user := current.user.get()): 

1154 return False 

1155 if user["access"] and "root" in user["access"]: 

1156 return True 

1157 if user and user["access"] and f"{self.moduleName}-edit" in user["access"]: 

1158 return True 

1159 return False 

1160 

1161 ## Overridable eventhooks 

1162 

1163 def onAdd(self, skelType: SkelType, skel: SkeletonInstance): 

1164 """ 

1165 Hook function that is called before adding an entry. 

1166 

1167 It can be overridden for a module-specific behavior. 

1168 

1169 :param skelType: Defines the type of the node that shall be added. 

1170 :param skel: The Skeleton that is going to be added. 

1171 

1172 .. seealso:: :func:`add`, :func:`onAdded` 

1173 """ 

1174 pass 

1175 

1176 def onAdded(self, skelType: SkelType, skel: SkeletonInstance): 

1177 """ 

1178 Hook function that is called after adding an entry. 

1179 

1180 It should be overridden for a module-specific behavior. 

1181 The default is writing a log entry. 

1182 

1183 :param skelType: Defines the type of the node that has been added. 

1184 :param skel: The Skeleton that has been added. 

1185 

1186 .. seealso:: :func:`add`, :func:`onAdd` 

1187 """ 

1188 logging.info(f"""Entry of kind {skelType!r} added: {skel["key"]!r}""") 

1189 flushCache(kind=skel.kindName) 

1190 if user := current.user.get(): 

1191 logging.info(f"""User: {user["name"]!r} ({user["key"]!r})""") 

1192 

1193 def onEdit(self, skelType: SkelType, skel: SkeletonInstance): 

1194 """ 

1195 Hook function that is called before editing an entry. 

1196 

1197 It can be overridden for a module-specific behavior. 

1198 

1199 :param skelType: Defines the type of the node that shall be edited. 

1200 :param skel: The Skeleton that is going to be edited. 

1201 

1202 .. seealso:: :func:`edit`, :func:`onEdited` 

1203 """ 

1204 pass 

1205 

1206 def onEdited(self, skelType: SkelType, skel: SkeletonInstance): 

1207 """ 

1208 Hook function that is called after modifying an entry. 

1209 

1210 It should be overridden for a module-specific behavior. 

1211 The default is writing a log entry. 

1212 

1213 :param skelType: Defines the type of the node that has been edited. 

1214 :param skel: The Skeleton that has been modified. 

1215 

1216 .. seealso:: :func:`edit`, :func:`onEdit` 

1217 """ 

1218 logging.info(f"""Entry of kind {skelType!r} changed: {skel["key"]!r}""") 

1219 flushCache(key=skel["key"]) 

1220 if user := current.user.get(): 

1221 logging.info(f"""User: {user["name"]!r} ({user["key"]!r})""") 

1222 

1223 def onView(self, skelType: SkelType, skel: SkeletonInstance): 

1224 """ 

1225 Hook function that is called when viewing an entry. 

1226 

1227 It should be overridden for a module-specific behavior. 

1228 The default is doing nothing. 

1229 

1230 :param skelType: Defines the type of the node that is viewed. 

1231 :param skel: The Skeleton that is viewed. 

1232 

1233 .. seealso:: :func:`view` 

1234 """ 

1235 pass 

1236 

1237 def onDelete(self, skelType: SkelType, skel: SkeletonInstance): 

1238 """ 

1239 Hook function that is called before deleting an entry. 

1240 

1241 It can be overridden for a module-specific behavior. 

1242 

1243 :param skelType: Defines the type of the node that shall be deleted. 

1244 :param skel: The Skeleton that is going to be deleted. 

1245 

1246 .. seealso:: :func:`delete`, :func:`onDeleted` 

1247 """ 

1248 pass 

1249 

1250 def onDeleted(self, skelType: SkelType, skel: SkeletonInstance): 

1251 """ 

1252 Hook function that is called after deleting an entry. 

1253 

1254 It should be overridden for a module-specific behavior. 

1255 The default is writing a log entry. 

1256 

1257 ..warning: Saving the skeleton again will undo the deletion 

1258 (if the skeleton was a leaf or a node with no children). 

1259 

1260 :param skelType: Defines the type of the node that is deleted. 

1261 :param skel: The Skeleton that has been deleted. 

1262 

1263 .. seealso:: :func:`delete`, :func:`onDelete` 

1264 """ 

1265 logging.info(f"""Entry deleted: {skel["key"]!r} ({skelType!r})""") 

1266 flushCache(key=skel["key"]) 

1267 if user := current.user.get(): 

1268 logging.info(f"""User: {user["name"]!r} ({user["key"]!r})""") 

1269 

1270 def onClone(self, skelType: SkelType, skel: SkeletonInstance, src_skel: SkeletonInstance): 

1271 """ 

1272 Hook function that is called before cloning an entry. 

1273 

1274 It can be overwritten to a module-specific behavior. 

1275 

1276 :param skelType: Defines the type of the node that is cloned. 

1277 :param skel: The new SkeletonInstance that is being created. 

1278 :param src_skel: The source SkeletonInstance `skel` is cloned from. 

1279 

1280 .. seealso:: :func:`clone`, :func:`onCloned` 

1281 """ 

1282 pass 

1283 

1284 @CallDeferred 

1285 def _clone_recursive( 

1286 self, 

1287 skel_type: SkelType, 

1288 src_key: db.Key, 

1289 target_key: db.Key, 

1290 target_repo: db.Key, 

1291 cursor=None 

1292 ): 

1293 """ 

1294 Helper function which is used by default onCloned() to clone a recursive structure. 

1295 """ 

1296 assert (skel_type := self._checkSkelType(skel_type)) 

1297 

1298 logging.debug(f"_clone_recursive {skel_type=}, {src_key=}, {target_key=}, {target_repo=}, {cursor=}") 

1299 

1300 q = self.cloneSkel(skel_type).all().filter("parententry", src_key).order("sortindex") 

1301 q.setCursor(cursor) 

1302 

1303 count = 0 

1304 for skel in q.fetch(): 

1305 src_skel = skel 

1306 

1307 skel = skel.clone() 

1308 skel["key"] = None 

1309 skel["parententry"] = target_key 

1310 skel["parentrepo"] = target_repo 

1311 

1312 self.onClone(skel_type, skel, src_skel=src_skel) 

1313 logging.debug(f"copying {skel=}") # this logging _is_ needed, otherwise not all values are being written.. 

1314 assert skel.write() 

1315 self.onCloned(skel_type, skel, src_skel=src_skel) 

1316 count += 1 

1317 

1318 logging.debug(f"_clone_recursive {count=}") 

1319 

1320 if cursor := q.getCursor(): 

1321 self._clone_recursive(skel_type, src_key, target_key, target_repo, cursor) 

1322 

1323 def onCloned(self, skelType: SkelType, skel: SkeletonInstance, src_skel: SkeletonInstance): 

1324 """ 

1325 Hook function that is called after cloning an entry. 

1326 

1327 It can be overwritten to a module-specific behavior. 

1328 

1329 By default, when cloning a "node", this function calls :func:`_clone_recursive` 

1330 which recursively clones the entire structure below this node in the background. 

1331 If this is not wanted, or wanted by a specific setting, overwrite this function 

1332 without a super-call. 

1333 

1334 :param skelType: Defines the type of the node that is cloned. 

1335 :param skel: The new SkeletonInstance that was created. 

1336 :param src_skel: The source SkeletonInstance `skel` was cloned from. 

1337 

1338 .. seealso:: :func:`clone`, :func:`onClone` 

1339 """ 

1340 logging.info(f"""Entry cloned: {skel["key"]!r} ({skelType!r})""") 

1341 flushCache(kind=skel.kindName) 

1342 

1343 if user := current.user.get(): 

1344 logging.info(f"""User: {user["name"]!r} ({user["key"]!r})""") 

1345 

1346 # Clone entire structure below, in case this is a node. 

1347 if skelType == "node": 

1348 self._clone_recursive("node", src_skel["key"], skel["key"], skel["parentrepo"]) 

1349 

1350 if self.leafSkelCls: 

1351 self._clone_recursive("leaf", src_skel["key"], skel["key"], skel["parentrepo"]) 

1352 

1353 

1354Tree.vi = True 

1355Tree.admin = True