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
« 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
11class List(SkelModule):
12 """
13 List module prototype.
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.
18 It is undoubtedly the most frequently used prototype in any ViUR project.
19 """
20 handler = "list"
21 accessRights = ("add", "edit", "view", "delete", "manage")
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.
28 The default is a Skeleton instance returned by :func:`~baseSkel`.
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.
36 .. seealso:: :func:`addSkel`, :func:`editSkel`, :func:`~baseSkel`
38 :return: Returns a Skeleton instance for viewing an entry.
39 """
40 return self.baseSkel(*args, **kwargs)
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.
47 The default is a Skeleton instance returned by :func:`~baseSkel`.
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).
54 .. seealso:: :func:`viewSkel`, :func:`editSkel`, :func:`~baseSkel`
56 :return: Returns a Skeleton instance for adding an entry.
57 """
58 return self.baseSkel(*args, **kwargs)
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.
65 The default is a Skeleton instance returned by :func:`~baseSkel`.
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.
71 .. seealso:: :func:`viewSkel`, :func:`editSkel`, :func:`~baseSkel`
73 :return: Returns a Skeleton instance for editing an entry.
74 """
75 return self.baseSkel(*args, **kwargs)
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.
82 The default is a SkeletonInstance returned by :func:`~baseSkel`.
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.
88 .. seealso:: :func:`viewSkel`, :func:`editSkel`, :func:`~baseSkel`
90 :return: Returns a SkeletonInstance for editing an entry.
91 """
92 return self.baseSkel(*args, **kwargs)
94 ## External exposed functions
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.
104 Any entity values are provided via *kwargs*.
106 The function uses the viewTemplate of the application.
108 :returns: The rendered representation of the supplied data.
109 """
110 if not self.canPreview():
111 raise errors.Unauthorized()
113 skel = self.viewSkel()
114 skel.fromClient(kwargs)
116 return self.render.view(skel)
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.
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()
133 case "edit":
134 skel = self.editSkel()
135 if not self.canEdit(skel):
136 raise errors.Unauthorized()
138 case "add":
139 if not self.canAdd():
140 raise errors.Unauthorized()
142 skel = self.addSkel()
144 case "clone":
145 skel = self.cloneSkel()
146 if not (self.canAdd() and self.canEdit(skel)):
147 raise errors.Unauthorized()
149 case _:
150 raise errors.NotImplemented(f"The action {action!r} is not implemented.")
152 return self.render.render(f"structure.{action}", skel)
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.
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.
163 .. seealso:: :func:`viewSkel`, :func:`canView`, :func:`onView`
165 :returns: The rendered representation of the requested entity.
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()
175 if not self.canView(skel):
176 raise errors.Forbidden()
178 self.onView(skel)
179 return self.render.view(skel)
181 @exposed
182 def list(self, *args, **kwargs) -> t.Any:
183 """
184 Prepares and renders a list of entries.
186 All supplied parameters are interpreted as filters for the elements displayed.
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.
192 .. seealso:: :func:`listFilter`, :func:`viur.core.db.mergeExternalFilter`
194 :returns: The rendered list objects for the matching entries.
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()
202 self._apply_default_order(query)
203 return self.render.list(query.fetch())
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*.
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.
217 .. seealso:: :func:`editSkel`, :func:`onEdit`, :func:`onEdited`, :func:`canEdit`
219 :returns: The rendered, edited object of the entry, eventually with error hints.
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()
230 if not self.canEdit(skel):
231 raise errors.Unauthorized()
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)
242 self.onEdit(skel)
243 skel.write() # write it!
244 self.onEdited(skel)
246 return self.render.editSuccess(skel)
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*.
256 The function performs several access control checks on the requested entity before it is added.
258 .. seealso:: :func:`addSkel`, :func:`onAdd`, :func:`onAdded`, :func:`canAdd`
260 :returns: The rendered, added object of the entry, eventually with error hints.
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()
268 skel = self.addSkel()
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)
279 self.onAdd(skel)
280 skel.write()
281 self.onAdded(skel)
283 return self.render.addSuccess(skel)
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.
293 The function runs several access control checks on the data before it is deleted.
295 .. seealso:: :func:`canDelete`, :func:`editSkel`, :func:`onDeleted`
297 :returns: The rendered, deleted object of the entry.
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()
307 if not self.canDelete(skel):
308 raise errors.Unauthorized()
310 self.onDelete(skel)
311 skel.delete()
312 self.onDeleted(skel)
314 return self.render.deleteSuccess(skel)
316 @exposed
317 def index(self, *args, **kwargs) -> t.Any:
318 """
319 Default, SEO-Friendly fallback for view and list.
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
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)
345 def getDefaultListParams(self):
346 return {}
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*.
356 The function performs several access control checks on the requested entity before it is added.
358 .. seealso:: :func:`canEdit`, :func:`canAdd`, :func:`onClone`, :func:`onCloned`
360 :param key: URL-safe key of the item to be edited.
362 :returns: The cloned object of the entry, eventually with error hints.
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 """
369 skel = self.cloneSkel()
370 if not skel.read(key):
371 raise errors.NotFound()
373 # a clone-operation is some kind of edit and add...
374 if not (self.canEdit(skel) and self.canAdd()):
375 raise errors.Unauthorized()
377 # Remember source skel and unset the key for clone operation!
378 src_skel = skel
379 skel = skel.clone()
380 skel["key"] = None
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")
391 self.onClone(skel, src_skel=src_skel)
392 assert skel.write()
393 self.onCloned(skel, src_skel=src_skel)
395 return self.render.editSuccess(skel, action="cloneSuccess")
397 ## Default access control functions
399 def listFilter(self, query: db.Query) -> t.Optional[db.Query]:
400 """
401 Access control function on item listing.
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.
407 :param query: Query which should be altered.
409 :returns: The altered filter, or None if access is not granted.
410 """
412 if (user := current.user.get()) and (f"{self.moduleName}-view" in user["access"] or "root" in user["access"]):
413 return query
415 return None
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)
429 if key := skel["key"]:
430 db.currentDbAccessLog.get(set()).add(key)
431 query.mergeExternalFilter({"key": key})
433 query = self.listFilter(query) # Access control
435 if query is None or (key and not query.getEntry()):
436 return False
438 return True
440 def canAdd(self) -> bool:
441 """
442 Access control function for adding permission.
444 Checks if the current user has the permission to add a new entry.
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.
451 It should be overridden for a module-specific behavior.
453 .. seealso:: :func:`add`
455 :returns: True, if adding entries is allowed, False otherwise.
456 """
457 if not (user := current.user.get()):
458 return False
460 # root user is always allowed.
461 if user["access"] and "root" in user["access"]:
462 return True
464 # user with add-permission is allowed.
465 if user and user["access"] and f"{self.moduleName}-add" in user["access"]:
466 return True
468 return False
470 def canPreview(self) -> bool:
471 """
472 Access control function for preview permission.
474 Checks if the current user has the permission to preview an entry.
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.
482 It should be overridden for module-specific behavior.
484 .. seealso:: :func:`preview`
486 :returns: True, if previewing entries is allowed, False otherwise.
487 """
488 if not (user := current.user.get()):
489 return False
491 if user["access"] and "root" in user["access"]:
492 return True
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
499 return False
501 def canEdit(self, skel: SkeletonInstance) -> bool:
502 """
503 Access control function for modification permission.
505 Checks if the current user has the permission to edit an entry.
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.
512 It should be overridden for a module-specific behavior.
514 .. seealso:: :func:`edit`
516 :param skel: The Skeleton that should be edited.
518 :returns: True, if editing entries is allowed, False otherwise.
519 """
520 if not (user := current.user.get()):
521 return False
523 if user["access"] and "root" in user["access"]:
524 return True
526 if user and user["access"] and f"{self.moduleName}-edit" in user["access"]:
527 return True
529 return False
531 def canDelete(self, skel: SkeletonInstance) -> bool:
532 """
533 Access control function for delete permission.
535 Checks if the current user has the permission to delete an entry.
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.
543 It should be overridden for a module-specific behavior.
545 :param skel: The Skeleton that should be deleted.
547 .. seealso:: :func:`delete`
549 :returns: True, if deleting entries is allowed, False otherwise.
550 """
551 if not (user := current.user.get()):
552 return False
554 if user["access"] and "root" in user["access"]:
555 return True
557 if user and user["access"] and f"{self.moduleName}-delete" in user["access"]:
558 return True
560 return False
562 ## Override-able event-hooks
564 def onAdd(self, skel: SkeletonInstance):
565 """
566 Hook function that is called before adding an entry.
568 It can be overridden for a module-specific behavior.
570 :param skel: The Skeleton that is going to be added.
572 .. seealso:: :func:`add`, :func:`onAdded`
573 """
574 pass
576 def onAdded(self, skel: SkeletonInstance):
577 """
578 Hook function that is called after adding an entry.
580 It should be overridden for a module-specific behavior.
581 The default is writing a log entry.
583 :param skel: The Skeleton that has been added.
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})""")
592 def onEdit(self, skel: SkeletonInstance):
593 """
594 Hook function that is called before editing an entry.
596 It can be overridden for a module-specific behavior.
598 :param skel: The Skeleton that is going to be edited.
600 .. seealso:: :func:`edit`, :func:`onEdited`
601 """
602 pass
604 def onEdited(self, skel: SkeletonInstance):
605 """
606 Hook function that is called after modifying an entry.
608 It should be overridden for a module-specific behavior.
609 The default is writing a log entry.
611 :param skel: The Skeleton that has been modified.
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})""")
620 def onView(self, skel: SkeletonInstance):
621 """
622 Hook function that is called when viewing an entry.
624 It should be overridden for a module-specific behavior.
625 The default is doing nothing.
627 :param skel: The Skeleton that is viewed.
629 .. seealso:: :func:`view`
630 """
631 pass
633 def onDelete(self, skel: SkeletonInstance):
634 """
635 Hook function that is called before deleting an entry.
637 It can be overridden for a module-specific behavior.
639 :param skel: The Skeleton that is going to be deleted.
641 .. seealso:: :func:`delete`, :func:`onDeleted`
642 """
643 pass
645 def onDeleted(self, skel: SkeletonInstance):
646 """
647 Hook function that is called after deleting an entry.
649 It should be overridden for a module-specific behavior.
650 The default is writing a log entry.
652 :param skel: The Skeleton that has been deleted.
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})""")
661 def onClone(self, skel: SkeletonInstance, src_skel: SkeletonInstance):
662 """
663 Hook function that is called before cloning an entry.
665 It can be overwritten to a module-specific behavior.
667 :param skel: The new SkeletonInstance that is being created.
668 :param src_skel: The source SkeletonInstance `skel` is cloned from.
670 .. seealso:: :func:`clone`, :func:`onCloned`
671 """
672 pass
674 def onCloned(self, skel: SkeletonInstance, src_skel: SkeletonInstance):
675 """
676 Hook function that is called after cloning an entry.
678 It can be overwritten to a module-specific behavior.
680 :param skel: The new SkeletonInstance that was created.
681 :param src_skel: The source SkeletonInstance `skel` was cloned from.
683 .. seealso:: :func:`clone`, :func:`onClone`
684 """
685 logging.info(f"""Entry cloned: {skel["key"]!r}""")
686 flushCache(kind=skel.kindName)
688 if user := current.user.get():
689 logging.info(f"""User: {user["name"]!r} ({user["key"]!r})""")
692List.admin = True
693List.vi = True