Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/prototypes/list.py: 0%

220 statements  

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

1import logging 

2import typing as t 

3from viur.core import current, db, errors, utils 

4from viur.core.decorators import * 

5from viur.core.cache import flushCache 

6from viur.core.skeleton import SkeletonInstance 

7from viur.core.bones import BaseBone 

8from .skelmodule import SkelModule, DEFAULT_ORDER_TYPE 

9 

10 

11class List(SkelModule): 

12 """ 

13 List module prototype. 

14 

15 The list module prototype handles datasets in a flat list. It can be extended to filters and views to provide 

16 various use-cases. 

17 

18 It is undoubtedly the most frequently used prototype in any ViUR project. 

19 """ 

20 handler = "list" 

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

22 

23 default_order: DEFAULT_ORDER_TYPE = None 

24 """ 

25 Allows to specify a default order for this module, which is applied when no other order is specified. 

26 

27 Setting a default_order might result in the requirement of additional indexes, which are being raised 

28 and must be specified. 

29 """ 

30 

31 def viewSkel(self, *args, **kwargs) -> SkeletonInstance: 

32 """ 

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

34 for viewing an existing entry from the list. 

35 

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

37 

38 This SkeletonInstance can be post-processed (just returning a subskel or manually removing single bones) - which 

39 is the recommended way to ensure a given user cannot see certain fields. A Jinja-Template may choose not to 

40 display certain bones, but if the json or xml render is attached (or the user can use the vi or admin render) 

41 he could still see all values. This also prevents the user from filtering by these bones, so no binary search 

42 is possible. 

43 

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

45 

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

47 """ 

48 return self.baseSkel(*args, **kwargs) 

49 

50 def addSkel(self, *args, **kwargs) -> SkeletonInstance: 

51 """ 

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

53 for adding an entry to the list. 

54 

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

56 

57 Like in :func:`viewSkel`, the skeleton can be post-processed. Bones that are being removed aren't visible 

58 and cannot be set, but it's also possible to just set a bone to readOnly (revealing it's value to the user, 

59 but preventing any modification. It's possible to pre-set values on that skeleton (and if that bone is 

60 readOnly, enforcing these values). 

61 

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

63 

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

65 """ 

66 return self.baseSkel(*args, **kwargs) 

67 

68 def editSkel(self, *args, **kwargs) -> SkeletonInstance: 

69 """ 

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

71 for editing an existing entry from the list. 

72 

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

74 

75 Like in :func:`viewSkel`, the skeleton can be post-processed. Bones that are being removed aren't visible 

76 and cannot be set, but it's also possible to just set a bone to readOnly (revealing it's value to the user, 

77 but preventing any modification. 

78 

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

80 

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

82 """ 

83 return self.baseSkel(*args, **kwargs) 

84 

85 def cloneSkel(self, *args, **kwargs) -> SkeletonInstance: 

86 """ 

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

88 for cloning an existing entry from the list. 

89 

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

91 

92 Like in :func:`viewSkel`, the skeleton can be post-processed. Bones that are being removed aren't visible 

93 and cannot be set, but it's also possible to just set a bone to readOnly (revealing it's value to the user, 

94 but preventing any modification. 

95 

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

97 

98 :return: Returns a SkeletonInstance for editing an entry. 

99 """ 

100 return self.baseSkel(*args, **kwargs) 

101 

102 ## External exposed functions 

103 

104 @exposed 

105 @force_post 

106 @skey 

107 def preview(self, *args, **kwargs) -> t.Any: 

108 """ 

109 Renders data for an entry, without reading from the database. 

110 This function allows to preview an entry without writing it to the database. 

111 

112 Any entity values are provided via *kwargs*. 

113 

114 The function uses the viewTemplate of the application. 

115 

116 :returns: The rendered representation of the supplied data. 

117 """ 

118 if not self.canPreview(): 

119 raise errors.Unauthorized() 

120 

121 skel = self.viewSkel() 

122 skel.fromClient(kwargs) 

123 

124 return self.render.view(skel) 

125 

126 @exposed 

127 def structure(self, *args, **kwargs) -> t.Any: 

128 """ 

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

130 in each bone. 

131 

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

133 """ 

134 skel = self.viewSkel() 

135 if not self.canAdd(): # We can't use canView here as it would require passing a skeletonInstance. 

136 # As a fallback, we'll check if the user has the permissions to view at least one entry 

137 qry = self.listFilter(skel.all()) 

138 if not qry or not qry.getEntry(): 

139 raise errors.Unauthorized() 

140 return self.render.view(skel) 

141 

142 @exposed 

143 def view(self, key: db.Key | int | str, *args, **kwargs) -> t.Any: 

144 """ 

145 Prepares and renders a single entry for viewing. 

146 

147 The entry is fetched by its entity key, which either is provided via *kwargs["key"]*, 

148 or as the first parameter in *args*. The function performs several access control checks 

149 on the requested entity before it is rendered. 

150 

151 .. seealso:: :func:`viewSkel`, :func:`canView`, :func:`onView` 

152 

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

154 

155 :raises: :exc:`viur.core.errors.NotAcceptable`, when no *key* is provided. 

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

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

158 """ 

159 skel = self.viewSkel() 

160 if not skel.fromDB(key): 

161 raise errors.NotFound() 

162 

163 if not self.canView(skel): 

164 raise errors.Forbidden() 

165 

166 self.onView(skel) 

167 return self.render.view(skel) 

168 

169 @exposed 

170 def list(self, *args, **kwargs) -> t.Any: 

171 """ 

172 Prepares and renders a list of entries. 

173 

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

175 

176 Unlike other modules in ViUR, the access control in this function is performed 

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

178 elements which the user is allowed to see. 

179 

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

181 

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

183 

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

185 """ 

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

187 query = self.listFilter(self.viewSkel().all().mergeExternalFilter(kwargs)) 

188 if query and query.queries and not isinstance(query.queries, list): 

189 # Apply default order when specified 

190 if self.default_order and not query.queries.orders and not current.request.get().kwargs.get("search"): 

191 # TODO: refactor: Duplicate code in prototypes.Tree 

192 if callable(default_order := self.default_order): 

193 default_order = default_order(query) 

194 

195 if isinstance(default_order, dict): 

196 logging.debug(f"Applying filter {default_order=}") 

197 query.mergeExternalFilter(default_order) 

198 

199 elif default_order: 

200 logging.debug(f"Applying {default_order=}") 

201 

202 # FIXME: This ugly test can be removed when there is type that abstracts SortOrders 

203 if ( 

204 isinstance(default_order, str) 

205 or ( 

206 isinstance(default_order, tuple) 

207 and len(default_order) == 2 

208 and isinstance(default_order[0], str) 

209 and isinstance(default_order[1], db.SortOrder) 

210 ) 

211 ): 

212 query.order(default_order) 

213 else: 

214 query.order(*default_order) 

215 

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

217 

218 raise errors.Unauthorized() 

219 

220 @force_ssl 

221 @exposed 

222 @skey(allow_empty=True) 

223 def edit(self, key: db.Key | int | str, *args, **kwargs) -> t.Any: 

224 """ 

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

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

227 

228 The entry is fetched by its entity key, which either is provided via *kwargs["key"]*, 

229 or as the first parameter in *args*. The function performs several access control checks 

230 on the requested entity before it is modified. 

231 

232 .. seealso:: :func:`editSkel`, :func:`onEdit`, :func:`onEdited`, :func:`canEdit` 

233 

234 :returns: The rendered, edited object of the entry, eventually with error hints. 

235 

236 :raises: :exc:`viur.core.errors.NotAcceptable`, when no *key* is provided. 

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

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

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

240 """ 

241 skel = self.editSkel() 

242 if not skel.fromDB(key): 

243 raise errors.NotFound() 

244 

245 if not self.canEdit(skel): 

246 raise errors.Unauthorized() 

247 

248 if ( 

249 not kwargs # no data supplied 

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

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

252 or utils.parse.bool(kwargs.get("bounce")) # review before changing 

253 ): 

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

255 return self.render.edit(skel) 

256 

257 self.onEdit(skel) 

258 skel.toDB() # write it! 

259 self.onEdited(skel) 

260 

261 return self.render.editSuccess(skel) 

262 

263 @force_ssl 

264 @exposed 

265 @skey(allow_empty=True) 

266 def add(self, *args, **kwargs) -> t.Any: 

267 """ 

268 Add a new entry, and render the entry, eventually with error notes on incorrect data. 

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

270 

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

272 

273 .. seealso:: :func:`addSkel`, :func:`onAdd`, :func:`onAdded`, :func:`canAdd` 

274 

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

276 

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

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

279 """ 

280 if not self.canAdd(): 

281 raise errors.Unauthorized() 

282 

283 skel = self.addSkel() 

284 

285 if ( 

286 not kwargs # no data supplied 

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

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

289 or utils.parse.bool(kwargs.get("bounce")) # review before adding 

290 ): 

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

292 return self.render.add(skel) 

293 

294 self.onAdd(skel) 

295 skel.toDB() 

296 self.onAdded(skel) 

297 

298 return self.render.addSuccess(skel) 

299 

300 @force_ssl 

301 @force_post 

302 @exposed 

303 @skey 

304 def delete(self, key: db.Key | int | str, *args, **kwargs) -> t.Any: 

305 """ 

306 Delete an entry. 

307 

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

309 

310 .. seealso:: :func:`canDelete`, :func:`editSkel`, :func:`onDeleted` 

311 

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

313 

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

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

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

317 """ 

318 skel = self.editSkel() 

319 if not skel.fromDB(key): 

320 raise errors.NotFound() 

321 

322 if not self.canDelete(skel): 

323 raise errors.Unauthorized() 

324 

325 self.onDelete(skel) 

326 skel.delete() 

327 self.onDeleted(skel) 

328 

329 return self.render.deleteSuccess(skel) 

330 

331 @exposed 

332 def index(self, *args, **kwargs) -> t.Any: 

333 """ 

334 Default, SEO-Friendly fallback for view and list. 

335 

336 :param args: The first argument - if provided - is interpreted as seoKey. 

337 :param kwargs: Used for the fallback list. 

338 :return: The rendered entity or list. 

339 """ 

340 if args and args[0]: 

341 # We probably have a Database or SEO-Key here 

342 seoKey = str(args[0]).lower() 

343 skel = self.viewSkel().all(_excludeFromAccessLog=True).filter("viur.viurActiveSeoKeys =", seoKey).getSkel() 

344 if skel: 

345 db.currentDbAccessLog.get(set()).add(skel["key"]) 

346 if not self.canView(skel): 

347 raise errors.Forbidden() 

348 seoUrl = utils.seoUrlToEntry(self.moduleName, skel) 

349 # Check whether this is the current seo-key, otherwise redirect to it 

350 

351 if current.request.get().request.path.lower() != seoUrl: 

352 raise errors.Redirect(seoUrl, status=301) 

353 self.onView(skel) 

354 return self.render.view(skel) 

355 # This was unsuccessfully, we'll render a list instead 

356 if not kwargs: 

357 kwargs = self.getDefaultListParams() 

358 return self.list(**kwargs) 

359 

360 def getDefaultListParams(self): 

361 return {} 

362 

363 @exposed 

364 @force_ssl 

365 @skey(allow_empty=True) 

366 def clone(self, key: db.Key | str | int, **kwargs): 

367 """ 

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

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

370 

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

372 

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

374 

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

376 

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

378 

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

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

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

382 """ 

383 

384 skel = self.cloneSkel() 

385 if not skel.fromDB(key): 

386 raise errors.NotFound() 

387 

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

389 if not (self.canEdit(skel) and self.canAdd()): 

390 raise errors.Unauthorized() 

391 

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

393 src_skel = skel 

394 skel = skel.clone() 

395 skel["key"] = None 

396 

397 # Check all required preconditions for clone 

398 if ( 

399 not kwargs # no data supplied 

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

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

402 or utils.parse.bool(kwargs.get("bounce")) # review before changing 

403 ): 

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

405 

406 self.onClone(skel, src_skel=src_skel) 

407 assert skel.toDB() 

408 self.onCloned(skel, src_skel=src_skel) 

409 

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

411 

412 ## Default access control functions 

413 

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

415 """ 

416 Access control function on item listing. 

417 

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

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

420 is allowed to see. 

421 

422 :param query: Query which should be altered. 

423 

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

425 """ 

426 

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

428 return query 

429 

430 return None 

431 

432 def canView(self, skel: SkeletonInstance) -> bool: 

433 """ 

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

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

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

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

438 :param skel: The entry we check for 

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

440 """ 

441 # We log the key we're querying by hand so we don't have to lock on the entire kind in our query 

442 db.currentDbAccessLog.get(set()).add(skel["key"]) 

443 query = self.viewSkel().all(_excludeFromAccessLog=True).mergeExternalFilter({"key": skel["key"]}) 

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

445 

446 if query is None: 

447 return False 

448 

449 if not query.getEntry(): 

450 return False 

451 

452 return True 

453 

454 def canAdd(self) -> bool: 

455 """ 

456 Access control function for adding permission. 

457 

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

459 

460 The default behavior is: 

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

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

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

464 

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

466 

467 .. seealso:: :func:`add` 

468 

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

470 """ 

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

472 return False 

473 

474 # root user is always allowed. 

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

476 return True 

477 

478 # user with add-permission is allowed. 

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

480 return True 

481 

482 return False 

483 

484 def canPreview(self) -> bool: 

485 """ 

486 Access control function for preview permission. 

487 

488 Checks if the current user has the permission to preview an entry. 

489 

490 The default behavior is: 

491 - If no user is logged in, previewing is generally refused. 

492 - If the user has "root" access, previewing is generally allowed. 

493 - If the user has the modules "add" or "edit" permission (module-add, module-edit) enabled, \ 

494 previewing is allowed. 

495 

496 It should be overridden for module-specific behavior. 

497 

498 .. seealso:: :func:`preview` 

499 

500 :returns: True, if previewing entries is allowed, False otherwise. 

501 """ 

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

503 return False 

504 

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

506 return True 

507 

508 if (user and user["access"] 

509 and (f"{self.moduleName}-add" in user["access"] 

510 or f"{self.moduleName}-edit" in user["access"])): 

511 return True 

512 

513 return False 

514 

515 def canEdit(self, skel: SkeletonInstance) -> bool: 

516 """ 

517 Access control function for modification permission. 

518 

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

520 

521 The default behavior is: 

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

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

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

525 

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

527 

528 .. seealso:: :func:`edit` 

529 

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

531 

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

533 """ 

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

535 return False 

536 

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

538 return True 

539 

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

541 return True 

542 

543 return False 

544 

545 def canDelete(self, skel: SkeletonInstance) -> bool: 

546 """ 

547 Access control function for delete permission. 

548 

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

550 

551 The default behavior is: 

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

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

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

555 deleting is allowed. 

556 

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

558 

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

560 

561 .. seealso:: :func:`delete` 

562 

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

564 """ 

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

566 return False 

567 

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

569 return True 

570 

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

572 return True 

573 

574 return False 

575 

576 ## Override-able event-hooks 

577 

578 def onAdd(self, skel: SkeletonInstance): 

579 """ 

580 Hook function that is called before adding an entry. 

581 

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

583 

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

585 

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

587 """ 

588 pass 

589 

590 def onAdded(self, skel: SkeletonInstance): 

591 """ 

592 Hook function that is called after adding an entry. 

593 

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

595 The default is writing a log entry. 

596 

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

598 

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

600 """ 

601 logging.info(f"""Entry added: {skel["key"]!r}""") 

602 flushCache(kind=skel.kindName) 

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

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

605 

606 def onEdit(self, skel: SkeletonInstance): 

607 """ 

608 Hook function that is called before editing an entry. 

609 

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

611 

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

613 

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

615 """ 

616 pass 

617 

618 def onEdited(self, skel: SkeletonInstance): 

619 """ 

620 Hook function that is called after modifying an entry. 

621 

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

623 The default is writing a log entry. 

624 

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

626 

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

628 """ 

629 logging.info(f"""Entry changed: {skel["key"]!r}""") 

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

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

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

633 

634 def onView(self, skel: SkeletonInstance): 

635 """ 

636 Hook function that is called when viewing an entry. 

637 

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

639 The default is doing nothing. 

640 

641 :param skel: The Skeleton that is viewed. 

642 

643 .. seealso:: :func:`view` 

644 """ 

645 pass 

646 

647 def onDelete(self, skel: SkeletonInstance): 

648 """ 

649 Hook function that is called before deleting an entry. 

650 

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

652 

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

654 

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

656 """ 

657 pass 

658 

659 def onDeleted(self, skel: SkeletonInstance): 

660 """ 

661 Hook function that is called after deleting an entry. 

662 

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

664 The default is writing a log entry. 

665 

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

667 

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

669 """ 

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

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

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

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

674 

675 def onClone(self, skel: SkeletonInstance, src_skel: SkeletonInstance): 

676 """ 

677 Hook function that is called before cloning an entry. 

678 

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

680 

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

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

683 

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

685 """ 

686 pass 

687 

688 def onCloned(self, skel: SkeletonInstance, src_skel: SkeletonInstance): 

689 """ 

690 Hook function that is called after cloning an entry. 

691 

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

693 

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

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

696 

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

698 """ 

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

700 flushCache(kind=skel.kindName) 

701 

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

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

704 

705 

706List.admin = True 

707List.vi = True