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

221 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-02-07 19:28 +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 

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 def viewSkel(self, *args, **kwargs) -> SkeletonInstance: 

24 """ 

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

26 for viewing an existing entry from the list. 

27 

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

29 

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

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

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

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

34 is possible. 

35 

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

37 

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

39 """ 

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

41 

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

43 """ 

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

45 for adding an entry to the list. 

46 

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

48 

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

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

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

52 readOnly, enforcing these values). 

53 

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

55 

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

57 """ 

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

59 

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

61 """ 

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

63 for editing an existing entry from the list. 

64 

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

66 

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

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

69 but preventing any modification. 

70 

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

72 

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

74 """ 

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

76 

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

78 """ 

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

80 for cloning an existing entry from the list. 

81 

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

83 

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

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

86 but preventing any modification. 

87 

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

89 

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

91 """ 

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

93 

94 ## External exposed functions 

95 

96 @exposed 

97 @force_post 

98 @skey 

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

100 """ 

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

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

103 

104 Any entity values are provided via *kwargs*. 

105 

106 The function uses the viewTemplate of the application. 

107 

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

109 """ 

110 if not self.canPreview(): 

111 raise errors.Unauthorized() 

112 

113 skel = self.viewSkel() 

114 skel.fromClient(kwargs) 

115 

116 return self.render.view(skel) 

117 

118 @exposed 

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

120 """ 

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

122 in each bone. 

123 

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

125 """ 

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

127 match action: 

128 case "view": 

129 skel = self.viewSkel() 

130 if not self.canView(skel): 

131 raise errors.Unauthorized() 

132 

133 case "edit": 

134 skel = self.editSkel() 

135 if not self.canEdit(skel): 

136 raise errors.Unauthorized() 

137 

138 case "add": 

139 if not self.canAdd(): 

140 raise errors.Unauthorized() 

141 

142 skel = self.addSkel() 

143 

144 case "clone": 

145 skel = self.cloneSkel() 

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

147 raise errors.Unauthorized() 

148 

149 case _: 

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

151 

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

153 

154 @exposed 

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

156 """ 

157 Prepares and renders a single entry for viewing. 

158 

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

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

161 on the requested entity before it is rendered. 

162 

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

164 

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

166 

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

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

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

170 """ 

171 skel = self.viewSkel() 

172 if not skel.read(key): 

173 raise errors.NotFound() 

174 

175 if not self.canView(skel): 

176 raise errors.Forbidden() 

177 

178 self.onView(skel) 

179 return self.render.view(skel) 

180 

181 @exposed 

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

183 """ 

184 Prepares and renders a list of entries. 

185 

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

187 

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

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

190 elements which the user is allowed to see. 

191 

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

193 

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

195 

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

197 """ 

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

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

200 raise errors.Unauthorized() 

201 

202 self._apply_default_order(query) 

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

204 

205 @force_ssl 

206 @exposed 

207 @skey(allow_empty=True) 

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

209 """ 

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

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

212 

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

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

215 on the requested entity before it is modified. 

216 

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

218 

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

220 

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

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

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

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

225 """ 

226 skel = self.editSkel() 

227 if not skel.read(key): 

228 raise errors.NotFound() 

229 

230 if not self.canEdit(skel): 

231 raise errors.Unauthorized() 

232 

233 if ( 

234 not kwargs # no data supplied 

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

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

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

238 ): 

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

240 return self.render.edit(skel) 

241 

242 self.onEdit(skel) 

243 skel.write() # write it! 

244 self.onEdited(skel) 

245 

246 return self.render.editSuccess(skel) 

247 

248 @force_ssl 

249 @exposed 

250 @skey(allow_empty=True) 

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

252 """ 

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

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

255 

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

257 

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

259 

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

261 

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

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

264 """ 

265 if not self.canAdd(): 

266 raise errors.Unauthorized() 

267 

268 skel = self.addSkel() 

269 

270 if ( 

271 not kwargs # no data supplied 

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

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

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

275 ): 

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

277 return self.render.add(skel) 

278 

279 self.onAdd(skel) 

280 skel.write() 

281 self.onAdded(skel) 

282 

283 return self.render.addSuccess(skel) 

284 

285 @force_ssl 

286 @force_post 

287 @exposed 

288 @skey 

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

290 """ 

291 Delete an entry. 

292 

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

294 

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

296 

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

298 

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

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

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

302 """ 

303 skel = self.editSkel() 

304 if not skel.read(key): 

305 raise errors.NotFound() 

306 

307 if not self.canDelete(skel): 

308 raise errors.Unauthorized() 

309 

310 self.onDelete(skel) 

311 skel.delete() 

312 self.onDeleted(skel) 

313 

314 return self.render.deleteSuccess(skel) 

315 

316 @exposed 

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

318 """ 

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

320 

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

322 :param kwargs: Used for the fallback list. 

323 :return: The rendered entity or list. 

324 """ 

325 if args and args[0]: 

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

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

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

329 if skel: 

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

331 if not self.canView(skel): 

332 raise errors.Forbidden() 

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

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

335 

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

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

338 self.onView(skel) 

339 return self.render.view(skel) 

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

341 if not kwargs: 

342 kwargs = self.getDefaultListParams() 

343 return self.list(**kwargs) 

344 

345 def getDefaultListParams(self): 

346 return {} 

347 

348 @exposed 

349 @force_ssl 

350 @skey(allow_empty=True) 

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

352 """ 

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

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

355 

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

357 

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

359 

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

361 

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

363 

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

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

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

367 """ 

368 

369 skel = self.cloneSkel() 

370 if not skel.read(key): 

371 raise errors.NotFound() 

372 

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

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

375 raise errors.Unauthorized() 

376 

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

378 src_skel = skel 

379 skel = skel.clone() 

380 skel["key"] = None 

381 

382 # Check all required preconditions for clone 

383 if ( 

384 not kwargs # no data supplied 

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

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

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

388 ): 

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

390 

391 self.onClone(skel, src_skel=src_skel) 

392 assert skel.write() 

393 self.onCloned(skel, src_skel=src_skel) 

394 

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

396 

397 ## Default access control functions 

398 

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

400 """ 

401 Access control function on item listing. 

402 

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

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

405 is allowed to see. 

406 

407 :param query: Query which should be altered. 

408 

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

410 """ 

411 

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

413 return query 

414 

415 return None 

416 

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

418 """ 

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

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

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

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

423 :param skel: The entry we check for 

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

425 """ 

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

427 query = self.viewSkel().all(_excludeFromAccessLog=True) 

428 

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

430 db.currentDbAccessLog.get(set()).add(key) 

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

432 

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

434 

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

436 return False 

437 

438 return True 

439 

440 def canAdd(self) -> bool: 

441 """ 

442 Access control function for adding permission. 

443 

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

445 

446 The default behavior is: 

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

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

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

450 

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

452 

453 .. seealso:: :func:`add` 

454 

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

456 """ 

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

458 return False 

459 

460 # root user is always allowed. 

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

462 return True 

463 

464 # user with add-permission is allowed. 

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

466 return True 

467 

468 return False 

469 

470 def canPreview(self) -> bool: 

471 """ 

472 Access control function for preview permission. 

473 

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

475 

476 The default behavior is: 

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

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

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

480 previewing is allowed. 

481 

482 It should be overridden for module-specific behavior. 

483 

484 .. seealso:: :func:`preview` 

485 

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

487 """ 

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

489 return False 

490 

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

492 return True 

493 

494 if (user and user["access"] 

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

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

497 return True 

498 

499 return False 

500 

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

502 """ 

503 Access control function for modification permission. 

504 

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

506 

507 The default behavior is: 

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

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

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

511 

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

513 

514 .. seealso:: :func:`edit` 

515 

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

517 

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

519 """ 

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

521 return False 

522 

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

524 return True 

525 

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

527 return True 

528 

529 return False 

530 

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

532 """ 

533 Access control function for delete permission. 

534 

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

536 

537 The default behavior is: 

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

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

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

541 deleting is allowed. 

542 

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

544 

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

546 

547 .. seealso:: :func:`delete` 

548 

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

550 """ 

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

552 return False 

553 

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

555 return True 

556 

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

558 return True 

559 

560 return False 

561 

562 ## Override-able event-hooks 

563 

564 def onAdd(self, skel: SkeletonInstance): 

565 """ 

566 Hook function that is called before adding an entry. 

567 

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

569 

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

571 

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

573 """ 

574 pass 

575 

576 def onAdded(self, skel: SkeletonInstance): 

577 """ 

578 Hook function that is called after adding an entry. 

579 

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

581 The default is writing a log entry. 

582 

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

584 

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

586 """ 

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

588 flushCache(kind=skel.kindName) 

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

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

591 

592 def onEdit(self, skel: SkeletonInstance): 

593 """ 

594 Hook function that is called before editing an entry. 

595 

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

597 

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

599 

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

601 """ 

602 pass 

603 

604 def onEdited(self, skel: SkeletonInstance): 

605 """ 

606 Hook function that is called after modifying an entry. 

607 

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

609 The default is writing a log entry. 

610 

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

612 

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

614 """ 

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

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

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

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

619 

620 def onView(self, skel: SkeletonInstance): 

621 """ 

622 Hook function that is called when viewing an entry. 

623 

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

625 The default is doing nothing. 

626 

627 :param skel: The Skeleton that is viewed. 

628 

629 .. seealso:: :func:`view` 

630 """ 

631 pass 

632 

633 def onDelete(self, skel: SkeletonInstance): 

634 """ 

635 Hook function that is called before deleting an entry. 

636 

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

638 

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

640 

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

642 """ 

643 pass 

644 

645 def onDeleted(self, skel: SkeletonInstance): 

646 """ 

647 Hook function that is called after deleting an entry. 

648 

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

650 The default is writing a log entry. 

651 

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

653 

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

655 """ 

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

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

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

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

660 

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

662 """ 

663 Hook function that is called before cloning an entry. 

664 

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

666 

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

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

669 

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

671 """ 

672 pass 

673 

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

675 """ 

676 Hook function that is called after cloning an entry. 

677 

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

679 

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

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

682 

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

684 """ 

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

686 flushCache(kind=skel.kindName) 

687 

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

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

690 

691 

692List.admin = True 

693List.vi = True