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.10.6, created at 2025-09-13 11:04 +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 .skelmodule import SkelModule 

8 

9 

10class List(SkelModule): 

11 """ 

12 List module prototype. 

13 

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

15 various use-cases. 

16 

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

18 """ 

19 handler = "list" 

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

21 

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

23 """ 

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

25 for viewing an existing entry from the list. 

26 

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

28 

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

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

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

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

33 is possible. 

34 

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

36 

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

38 """ 

39 return self.baseSkel(**kwargs) 

40 

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

42 """ 

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

44 for adding an entry to the list. 

45 

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

47 

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

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

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

51 readOnly, enforcing these values). 

52 

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

54 

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

56 """ 

57 return self.baseSkel(**kwargs) 

58 

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

60 """ 

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

62 for editing an existing entry from the list. 

63 

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

65 

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

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

68 but preventing any modification. 

69 

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

71 

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

73 """ 

74 return self.baseSkel(**kwargs) 

75 

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

77 """ 

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

79 for cloning an existing entry from the list. 

80 

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

82 

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

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

85 but preventing any modification. 

86 

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

88 

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

90 """ 

91 

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

93 return self.addSkel(**kwargs) 

94 

95 ## External exposed functions 

96 

97 @exposed 

98 @force_post 

99 @skey 

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

101 """ 

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

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

104 

105 Any entity values are provided via *kwargs*. 

106 

107 The function uses the viewTemplate of the application. 

108 

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

110 """ 

111 if not self.canPreview(): 

112 raise errors.Unauthorized() 

113 

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

115 skel.fromClient(kwargs) 

116 

117 return self.render.view(skel) 

118 

119 @exposed 

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

121 """ 

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

123 in each bone. 

124 

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

126 """ 

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

128 match action: 

129 case "view": 

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

131 if not self.canView(skel): 

132 raise errors.Unauthorized() 

133 

134 case "edit": 

135 skel = self.editSkel() 

136 if not self.canEdit(skel): 

137 raise errors.Unauthorized() 

138 

139 case "add": 

140 if not self.canAdd(): 

141 raise errors.Unauthorized() 

142 

143 skel = self.addSkel() 

144 

145 case "clone": 

146 skel = self.cloneSkel() 

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

148 raise errors.Unauthorized() 

149 

150 case _: 

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

152 

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

154 

155 @exposed 

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

157 """ 

158 Prepares and renders a single entry for viewing. 

159 

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

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

162 on the requested entity before it is rendered. 

163 

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

165 

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

167 

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

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

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

171 """ 

172 skel = self.viewSkel() 

173 if not skel.read(key): 

174 raise errors.NotFound() 

175 

176 if not self.canView(skel): 

177 raise errors.Forbidden() 

178 

179 self.onView(skel) 

180 return self.render.view(skel) 

181 

182 @exposed 

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

184 """ 

185 Prepares and renders a list of entries. 

186 

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

188 

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

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

191 elements which the user is allowed to see. 

192 

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

194 

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

196 

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

198 """ 

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

200 

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

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

203 raise errors.Unauthorized() 

204 

205 self._apply_default_order(query) 

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

207 

208 @force_ssl 

209 @exposed 

210 @skey(allow_empty=True) 

211 def edit(self, key: db.Key | int | str, *, bounce: bool = False, **kwargs) -> t.Any: 

212 """ 

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

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

215 

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

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

218 on the requested entity before it is modified. 

219 

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

221 

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

223 

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

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

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

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

228 """ 

229 skel = self.editSkel() 

230 if not skel.read(key): 

231 raise errors.NotFound() 

232 

233 if not self.canEdit(skel): 

234 raise errors.Unauthorized() 

235 

236 if ( 

237 not kwargs # no data supplied 

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

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

240 or bounce # review before changing 

241 ): 

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

243 return self.render.edit(skel) 

244 

245 self.onEdit(skel) 

246 skel.write() # write it! 

247 self.onEdited(skel) 

248 

249 return self.render.editSuccess(skel) 

250 

251 @force_ssl 

252 @exposed 

253 @skey(allow_empty=True) 

254 def add(self, *, bounce: bool = False, **kwargs) -> t.Any: 

255 """ 

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

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

258 

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

260 

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

262 

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

264 

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

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

267 """ 

268 if not self.canAdd(): 

269 raise errors.Unauthorized() 

270 

271 skel = self.addSkel() 

272 

273 if ( 

274 not kwargs # no data supplied 

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

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

277 or bounce # review before adding 

278 ): 

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

280 return self.render.add(skel) 

281 

282 self.onAdd(skel) 

283 skel.write() 

284 self.onAdded(skel) 

285 

286 return self.render.addSuccess(skel) 

287 

288 @force_ssl 

289 @force_post 

290 @exposed 

291 @skey 

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

293 """ 

294 Delete an entry. 

295 

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

297 

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

299 

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

301 

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

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

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

305 """ 

306 skel = self.editSkel() 

307 if not skel.read(key): 

308 raise errors.NotFound() 

309 

310 if not self.canDelete(skel): 

311 raise errors.Unauthorized() 

312 

313 self.onDelete(skel) 

314 skel.delete() 

315 self.onDeleted(skel) 

316 

317 return self.render.deleteSuccess(skel) 

318 

319 @exposed 

320 def index(self, key: db.Key | int | str = None, *args, **kwargs) -> t.Any: 

321 """ 

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

323 :param key: The key can be a database key or a seoKey. 

324 :param args: Unused. 

325 :param kwargs: Used for the fallback list. 

326 :return: The rendered entity or list. 

327 """ 

328 if key: 

329 skel = self.viewSkel( 

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

331 _excludeFromAccessLog=True, 

332 ) 

333 

334 if ( 

335 isinstance(key, db.Key) and skel.read(key) or 

336 (skel := skel.all().filter("viur.viurActiveSeoKeys =", str(key).lower()).getSkel()) 

337 ): 

338 

339 db.current_db_access_log.get(set()).add(skel["key"]) 

340 if not self.canView(skel): 

341 raise errors.Forbidden() 

342 seo_url = utils.seoUrlToEntry(self.moduleName, skel) 

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

344 

345 if current.request.get().request.path.lower() != seo_url: 

346 raise errors.Redirect(seo_url, status=301) 

347 self.onView(skel) 

348 return self.render.view(skel) 

349 

350 if not kwargs: 

351 kwargs = self.getDefaultListParams() 

352 return self.list(**kwargs) 

353 

354 def getDefaultListParams(self): 

355 return {} 

356 

357 @exposed 

358 @force_ssl 

359 @skey(allow_empty=True) 

360 def clone(self, key: db.Key | str | int, *, bounce: bool = False, **kwargs): 

361 """ 

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

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

364 

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

366 

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

368 

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

370 

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

372 

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

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

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

376 """ 

377 

378 skel = self.cloneSkel() 

379 if not skel.read(key): 

380 raise errors.NotFound() 

381 

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

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

384 raise errors.Unauthorized() 

385 

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

387 src_skel = skel 

388 skel = skel.clone(apply_clone_strategy=True) 

389 skel["key"] = None 

390 

391 # Check all required preconditions for clone 

392 if ( 

393 not kwargs # no data supplied 

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

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

396 or bounce # review before changing 

397 ): 

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

399 

400 self.onClone(skel, src_skel=src_skel) 

401 assert skel.write() 

402 self.onCloned(skel, src_skel=src_skel) 

403 

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

405 

406 ## Default access control functions 

407 

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

409 """ 

410 Access control function on item listing. 

411 

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

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

414 is allowed to see. 

415 

416 :param query: Query which should be altered. 

417 

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

419 """ 

420 

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

422 return query 

423 

424 return None 

425 

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

427 """ 

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

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

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

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

432 :param skel: The entry we check for 

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

434 """ 

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

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

437 

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

439 db.current_db_access_log.get(set()).add(key) 

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

441 

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

443 

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

445 return False 

446 

447 return True 

448 

449 def canAdd(self) -> bool: 

450 """ 

451 Access control function for adding permission. 

452 

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

454 

455 The default behavior is: 

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

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

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

459 

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

461 

462 .. seealso:: :func:`add` 

463 

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

465 """ 

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

467 return False 

468 

469 # root user is always allowed. 

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

471 return True 

472 

473 # user with add-permission is allowed. 

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

475 return True 

476 

477 return False 

478 

479 def canPreview(self) -> bool: 

480 """ 

481 Access control function for preview permission. 

482 

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

484 

485 The default behavior is: 

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

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

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

489 previewing is allowed. 

490 

491 It should be overridden for module-specific behavior. 

492 

493 .. seealso:: :func:`preview` 

494 

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

496 """ 

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

498 return False 

499 

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

501 return True 

502 

503 if (user and user["access"] 

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

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

506 return True 

507 

508 return False 

509 

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

511 """ 

512 Access control function for modification permission. 

513 

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

515 

516 The default behavior is: 

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

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

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

520 

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

522 

523 .. seealso:: :func:`edit` 

524 

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

526 

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

528 """ 

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

530 return False 

531 

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

533 return True 

534 

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

536 return True 

537 

538 return False 

539 

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

541 """ 

542 Access control function for delete permission. 

543 

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

545 

546 The default behavior is: 

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

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

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

550 deleting is allowed. 

551 

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

553 

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

555 

556 .. seealso:: :func:`delete` 

557 

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

559 """ 

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

561 return False 

562 

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

564 return True 

565 

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

567 return True 

568 

569 return False 

570 

571 ## Override-able event-hooks 

572 

573 def onAdd(self, skel: SkeletonInstance): 

574 """ 

575 Hook function that is called before adding an entry. 

576 

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

578 

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

580 

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

582 """ 

583 pass 

584 

585 def onAdded(self, skel: SkeletonInstance): 

586 """ 

587 Hook function that is called after adding an entry. 

588 

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

590 The default is writing a log entry. 

591 

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

593 

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

595 """ 

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

597 flushCache(kind=skel.kindName) 

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

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

600 

601 def onEdit(self, skel: SkeletonInstance): 

602 """ 

603 Hook function that is called before editing an entry. 

604 

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

606 

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

608 

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

610 """ 

611 pass 

612 

613 def onEdited(self, skel: SkeletonInstance): 

614 """ 

615 Hook function that is called after modifying an entry. 

616 

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

618 The default is writing a log entry. 

619 

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

621 

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

623 """ 

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

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

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

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

628 

629 def onView(self, skel: SkeletonInstance): 

630 """ 

631 Hook function that is called when viewing an entry. 

632 

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

634 The default is doing nothing. 

635 

636 :param skel: The Skeleton that is viewed. 

637 

638 .. seealso:: :func:`view` 

639 """ 

640 pass 

641 

642 def onDelete(self, skel: SkeletonInstance): 

643 """ 

644 Hook function that is called before deleting an entry. 

645 

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

647 

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

649 

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

651 """ 

652 pass 

653 

654 def onDeleted(self, skel: SkeletonInstance): 

655 """ 

656 Hook function that is called after deleting an entry. 

657 

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

659 The default is writing a log entry. 

660 

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

662 

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

664 """ 

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

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

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

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

669 

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

671 """ 

672 Hook function that is called before cloning an entry. 

673 

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

675 

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

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

678 

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

680 """ 

681 pass 

682 

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

684 """ 

685 Hook function that is called after cloning an entry. 

686 

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

688 

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

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

691 

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

693 """ 

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

695 flushCache(kind=skel.kindName) 

696 

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

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

699 

700 

701List.admin = True 

702List.vi = True