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.9.1, created at 2025-06-26 11:31 +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(**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(**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(**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 

93 # On clone, by default, behave as this is a skeleton for adding. 

94 return self.addSkel(**kwargs) 

95 

96 ## External exposed functions 

97 

98 @exposed 

99 @force_post 

100 @skey 

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

102 """ 

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

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

105 

106 Any entity values are provided via *kwargs*. 

107 

108 The function uses the viewTemplate of the application. 

109 

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

111 """ 

112 if not self.canPreview(): 

113 raise errors.Unauthorized() 

114 

115 skel = self.viewSkel(allow_client_defined=utils.string.is_prefix(self.render.kind, "json")) 

116 skel.fromClient(kwargs) 

117 

118 return self.render.view(skel) 

119 

120 @exposed 

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

122 """ 

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

124 in each bone. 

125 

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

127 """ 

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

129 match action: 

130 case "view": 

131 skel = self.viewSkel(allow_client_defined=utils.string.is_prefix(self.render.kind, "json")) 

132 if not self.canView(skel): 

133 raise errors.Unauthorized() 

134 

135 case "edit": 

136 skel = self.editSkel() 

137 if not self.canEdit(skel): 

138 raise errors.Unauthorized() 

139 

140 case "add": 

141 if not self.canAdd(): 

142 raise errors.Unauthorized() 

143 

144 skel = self.addSkel() 

145 

146 case "clone": 

147 skel = self.cloneSkel() 

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

149 raise errors.Unauthorized() 

150 

151 case _: 

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

153 

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

155 

156 @exposed 

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

158 """ 

159 Prepares and renders a single entry for viewing. 

160 

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

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

163 on the requested entity before it is rendered. 

164 

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

166 

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

168 

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

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

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

172 """ 

173 skel = self.viewSkel() 

174 if not skel.read(key): 

175 raise errors.NotFound() 

176 

177 if not self.canView(skel): 

178 raise errors.Forbidden() 

179 

180 self.onView(skel) 

181 return self.render.view(skel) 

182 

183 @exposed 

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

185 """ 

186 Prepares and renders a list of entries. 

187 

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

189 

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

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

192 elements which the user is allowed to see. 

193 

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

195 

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

197 

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

199 """ 

200 skel = self.viewSkel(allow_client_defined=utils.string.is_prefix(self.render.kind, "json")) 

201 

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

203 if not (query := self.listFilter(skel.all().mergeExternalFilter(kwargs))): 

204 raise errors.Unauthorized() 

205 

206 self._apply_default_order(query) 

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

208 

209 @force_ssl 

210 @exposed 

211 @skey(allow_empty=True) 

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

213 """ 

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

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

216 

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

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

219 on the requested entity before it is modified. 

220 

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

222 

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

224 

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

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

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

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

229 """ 

230 skel = self.editSkel() 

231 if not skel.read(key): 

232 raise errors.NotFound() 

233 

234 if not self.canEdit(skel): 

235 raise errors.Unauthorized() 

236 

237 if ( 

238 not kwargs # no data supplied 

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

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

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

242 ): 

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

244 return self.render.edit(skel) 

245 

246 self.onEdit(skel) 

247 skel.write() # write it! 

248 self.onEdited(skel) 

249 

250 return self.render.editSuccess(skel) 

251 

252 @force_ssl 

253 @exposed 

254 @skey(allow_empty=True) 

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

256 """ 

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

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

259 

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

261 

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

263 

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

265 

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

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

268 """ 

269 if not self.canAdd(): 

270 raise errors.Unauthorized() 

271 

272 skel = self.addSkel() 

273 

274 if ( 

275 not kwargs # no data supplied 

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

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

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

279 ): 

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

281 return self.render.add(skel) 

282 

283 self.onAdd(skel) 

284 skel.write() 

285 self.onAdded(skel) 

286 

287 return self.render.addSuccess(skel) 

288 

289 @force_ssl 

290 @force_post 

291 @exposed 

292 @skey 

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

294 """ 

295 Delete an entry. 

296 

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

298 

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

300 

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

302 

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

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

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

306 """ 

307 skel = self.editSkel() 

308 if not skel.read(key): 

309 raise errors.NotFound() 

310 

311 if not self.canDelete(skel): 

312 raise errors.Unauthorized() 

313 

314 self.onDelete(skel) 

315 skel.delete() 

316 self.onDeleted(skel) 

317 

318 return self.render.deleteSuccess(skel) 

319 

320 @exposed 

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

322 """ 

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

324 

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

326 :param kwargs: Used for the fallback list. 

327 :return: The rendered entity or list. 

328 """ 

329 if args and args[0]: 

330 skel = self.viewSkel( 

331 allow_client_defined=utils.string.is_prefix(self.render.kind, "json"), 

332 _excludeFromAccessLog=True, 

333 ) 

334 

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

336 if skel := skel.all().filter("viur.viurActiveSeoKeys =", str(args[0]).lower()).getSkel(): 

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

338 if not self.canView(skel): 

339 raise errors.Forbidden() 

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

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

342 

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

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

345 self.onView(skel) 

346 return self.render.view(skel) 

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

348 if not kwargs: 

349 kwargs = self.getDefaultListParams() 

350 return self.list(**kwargs) 

351 

352 def getDefaultListParams(self): 

353 return {} 

354 

355 @exposed 

356 @force_ssl 

357 @skey(allow_empty=True) 

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

359 """ 

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

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

362 

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

364 

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

366 

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

368 

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

370 

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

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

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

374 """ 

375 

376 skel = self.cloneSkel() 

377 if not skel.read(key): 

378 raise errors.NotFound() 

379 

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

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

382 raise errors.Unauthorized() 

383 

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

385 src_skel = skel 

386 skel = skel.clone(apply_clone_strategy=True) 

387 skel["key"] = None 

388 

389 # Check all required preconditions for clone 

390 if ( 

391 not kwargs # no data supplied 

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

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

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

395 ): 

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

397 

398 self.onClone(skel, src_skel=src_skel) 

399 assert skel.write() 

400 self.onCloned(skel, src_skel=src_skel) 

401 

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

403 

404 ## Default access control functions 

405 

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

407 """ 

408 Access control function on item listing. 

409 

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

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

412 is allowed to see. 

413 

414 :param query: Query which should be altered. 

415 

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

417 """ 

418 

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

420 return query 

421 

422 return None 

423 

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

425 """ 

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

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

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

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

430 :param skel: The entry we check for 

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

432 """ 

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

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

435 

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

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

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

439 

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

441 

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

443 return False 

444 

445 return True 

446 

447 def canAdd(self) -> bool: 

448 """ 

449 Access control function for adding permission. 

450 

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

452 

453 The default behavior is: 

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

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

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

457 

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

459 

460 .. seealso:: :func:`add` 

461 

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

463 """ 

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

465 return False 

466 

467 # root user is always allowed. 

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

469 return True 

470 

471 # user with add-permission is allowed. 

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

473 return True 

474 

475 return False 

476 

477 def canPreview(self) -> bool: 

478 """ 

479 Access control function for preview permission. 

480 

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

482 

483 The default behavior is: 

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

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

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

487 previewing is allowed. 

488 

489 It should be overridden for module-specific behavior. 

490 

491 .. seealso:: :func:`preview` 

492 

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

494 """ 

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

496 return False 

497 

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

499 return True 

500 

501 if (user and user["access"] 

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

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

504 return True 

505 

506 return False 

507 

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

509 """ 

510 Access control function for modification permission. 

511 

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

513 

514 The default behavior is: 

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

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

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

518 

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

520 

521 .. seealso:: :func:`edit` 

522 

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

524 

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

526 """ 

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

528 return False 

529 

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

531 return True 

532 

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

534 return True 

535 

536 return False 

537 

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

539 """ 

540 Access control function for delete permission. 

541 

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

543 

544 The default behavior is: 

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

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

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

548 deleting is allowed. 

549 

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

551 

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

553 

554 .. seealso:: :func:`delete` 

555 

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

557 """ 

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

559 return False 

560 

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

562 return True 

563 

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

565 return True 

566 

567 return False 

568 

569 ## Override-able event-hooks 

570 

571 def onAdd(self, skel: SkeletonInstance): 

572 """ 

573 Hook function that is called before adding an entry. 

574 

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

576 

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

578 

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

580 """ 

581 pass 

582 

583 def onAdded(self, skel: SkeletonInstance): 

584 """ 

585 Hook function that is called after adding an entry. 

586 

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

588 The default is writing a log entry. 

589 

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

591 

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

593 """ 

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

595 flushCache(kind=skel.kindName) 

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

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

598 

599 def onEdit(self, skel: SkeletonInstance): 

600 """ 

601 Hook function that is called before editing an entry. 

602 

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

604 

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

606 

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

608 """ 

609 pass 

610 

611 def onEdited(self, skel: SkeletonInstance): 

612 """ 

613 Hook function that is called after modifying an entry. 

614 

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

616 The default is writing a log entry. 

617 

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

619 

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

621 """ 

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

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

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

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

626 

627 def onView(self, skel: SkeletonInstance): 

628 """ 

629 Hook function that is called when viewing an entry. 

630 

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

632 The default is doing nothing. 

633 

634 :param skel: The Skeleton that is viewed. 

635 

636 .. seealso:: :func:`view` 

637 """ 

638 pass 

639 

640 def onDelete(self, skel: SkeletonInstance): 

641 """ 

642 Hook function that is called before deleting an entry. 

643 

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

645 

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

647 

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

649 """ 

650 pass 

651 

652 def onDeleted(self, skel: SkeletonInstance): 

653 """ 

654 Hook function that is called after deleting an entry. 

655 

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

657 The default is writing a log entry. 

658 

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

660 

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

662 """ 

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

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

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

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

667 

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

669 """ 

670 Hook function that is called before cloning an entry. 

671 

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

673 

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

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

676 

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

678 """ 

679 pass 

680 

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

682 """ 

683 Hook function that is called after cloning an entry. 

684 

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

686 

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

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

689 

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

691 """ 

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

693 flushCache(kind=skel.kindName) 

694 

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

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

697 

698 

699List.admin = True 

700List.vi = True