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
« 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
10class List(SkelModule):
11 """
12 List module prototype.
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.
17 It is undoubtedly the most frequently used prototype in any ViUR project.
18 """
19 handler = "list"
20 accessRights = ("add", "edit", "view", "delete", "manage")
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.
27 The default is a Skeleton instance returned by :func:`~baseSkel`.
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.
35 .. seealso:: :func:`addSkel`, :func:`editSkel`, :func:`~baseSkel`
37 :return: Returns a Skeleton instance for viewing an entry.
38 """
39 return self.baseSkel(**kwargs)
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.
46 The default is a Skeleton instance returned by :func:`~baseSkel`.
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).
53 .. seealso:: :func:`viewSkel`, :func:`editSkel`, :func:`~baseSkel`
55 :return: Returns a Skeleton instance for adding an entry.
56 """
57 return self.baseSkel(**kwargs)
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.
64 The default is a Skeleton instance returned by :func:`~baseSkel`.
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.
70 .. seealso:: :func:`viewSkel`, :func:`editSkel`, :func:`~baseSkel`
72 :return: Returns a Skeleton instance for editing an entry.
73 """
74 return self.baseSkel(**kwargs)
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.
81 The default is a SkeletonInstance returned by :func:`~baseSkel`.
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.
87 .. seealso:: :func:`viewSkel`, :func:`editSkel`, :func:`~baseSkel`
89 :return: Returns a SkeletonInstance for editing an entry.
90 """
92 # On clone, by default, behave as this is a skeleton for adding.
93 return self.addSkel(**kwargs)
95 ## External exposed functions
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.
105 Any entity values are provided via *kwargs*.
107 The function uses the viewTemplate of the application.
109 :returns: The rendered representation of the supplied data.
110 """
111 if not self.canPreview():
112 raise errors.Unauthorized()
114 skel = self.viewSkel(allow_client_defined=utils.string.is_prefix(self.render.kind, "json"))
115 skel.fromClient(kwargs)
117 return self.render.view(skel)
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.
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()
134 case "edit":
135 skel = self.editSkel()
136 if not self.canEdit(skel):
137 raise errors.Unauthorized()
139 case "add":
140 if not self.canAdd():
141 raise errors.Unauthorized()
143 skel = self.addSkel()
145 case "clone":
146 skel = self.cloneSkel()
147 if not (self.canAdd() and self.canEdit(skel)):
148 raise errors.Unauthorized()
150 case _:
151 raise errors.NotImplemented(f"The action {action!r} is not implemented.")
153 return self.render.render(f"structure.{action}", skel)
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.
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.
164 .. seealso:: :func:`viewSkel`, :func:`canView`, :func:`onView`
166 :returns: The rendered representation of the requested entity.
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()
176 if not self.canView(skel):
177 raise errors.Forbidden()
179 self.onView(skel)
180 return self.render.view(skel)
182 @exposed
183 def list(self, *args, **kwargs) -> t.Any:
184 """
185 Prepares and renders a list of entries.
187 All supplied parameters are interpreted as filters for the elements displayed.
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.
193 .. seealso:: :func:`listFilter`, :func:`viur.core.db.mergeExternalFilter`
195 :returns: The rendered list objects for the matching entries.
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"))
201 # The general access control is made via self.listFilter()
202 if not (query := self.listFilter(skel.all().mergeExternalFilter(kwargs))):
203 raise errors.Unauthorized()
205 self._apply_default_order(query)
206 return self.render.list(query.fetch())
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*.
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.
220 .. seealso:: :func:`editSkel`, :func:`onEdit`, :func:`onEdited`, :func:`canEdit`
222 :returns: The rendered, edited object of the entry, eventually with error hints.
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()
233 if not self.canEdit(skel):
234 raise errors.Unauthorized()
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)
245 self.onEdit(skel)
246 skel.write() # write it!
247 self.onEdited(skel)
249 return self.render.editSuccess(skel)
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*.
259 The function performs several access control checks on the requested entity before it is added.
261 .. seealso:: :func:`addSkel`, :func:`onAdd`, :func:`onAdded`, :func:`canAdd`
263 :returns: The rendered, added object of the entry, eventually with error hints.
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()
271 skel = self.addSkel()
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)
282 self.onAdd(skel)
283 skel.write()
284 self.onAdded(skel)
286 return self.render.addSuccess(skel)
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.
296 The function runs several access control checks on the data before it is deleted.
298 .. seealso:: :func:`canDelete`, :func:`editSkel`, :func:`onDeleted`
300 :returns: The rendered, deleted object of the entry.
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()
310 if not self.canDelete(skel):
311 raise errors.Unauthorized()
313 self.onDelete(skel)
314 skel.delete()
315 self.onDeleted(skel)
317 return self.render.deleteSuccess(skel)
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 )
334 if (
335 isinstance(key, db.Key) and skel.read(key) or
336 (skel := skel.all().filter("viur.viurActiveSeoKeys =", str(key).lower()).getSkel())
337 ):
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
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)
350 if not kwargs:
351 kwargs = self.getDefaultListParams()
352 return self.list(**kwargs)
354 def getDefaultListParams(self):
355 return {}
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*.
365 The function performs several access control checks on the requested entity before it is added.
367 .. seealso:: :func:`canEdit`, :func:`canAdd`, :func:`onClone`, :func:`onCloned`
369 :param key: URL-safe key of the item to be edited.
371 :returns: The cloned object of the entry, eventually with error hints.
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 """
378 skel = self.cloneSkel()
379 if not skel.read(key):
380 raise errors.NotFound()
382 # a clone-operation is some kind of edit and add...
383 if not (self.canEdit(skel) and self.canAdd()):
384 raise errors.Unauthorized()
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
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")
400 self.onClone(skel, src_skel=src_skel)
401 assert skel.write()
402 self.onCloned(skel, src_skel=src_skel)
404 return self.render.editSuccess(skel, action="cloneSuccess")
406 ## Default access control functions
408 def listFilter(self, query: db.Query) -> t.Optional[db.Query]:
409 """
410 Access control function on item listing.
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.
416 :param query: Query which should be altered.
418 :returns: The altered filter, or None if access is not granted.
419 """
421 if (user := current.user.get()) and (f"{self.moduleName}-view" in user["access"] or "root" in user["access"]):
422 return query
424 return None
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)
438 if key := skel["key"]:
439 db.current_db_access_log.get(set()).add(key)
440 query.mergeExternalFilter({"key": key})
442 query = self.listFilter(query) # Access control
444 if query is None or (key and not query.getEntry()):
445 return False
447 return True
449 def canAdd(self) -> bool:
450 """
451 Access control function for adding permission.
453 Checks if the current user has the permission to add a new entry.
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.
460 It should be overridden for a module-specific behavior.
462 .. seealso:: :func:`add`
464 :returns: True, if adding entries is allowed, False otherwise.
465 """
466 if not (user := current.user.get()):
467 return False
469 # root user is always allowed.
470 if user["access"] and "root" in user["access"]:
471 return True
473 # user with add-permission is allowed.
474 if user and user["access"] and f"{self.moduleName}-add" in user["access"]:
475 return True
477 return False
479 def canPreview(self) -> bool:
480 """
481 Access control function for preview permission.
483 Checks if the current user has the permission to preview an entry.
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.
491 It should be overridden for module-specific behavior.
493 .. seealso:: :func:`preview`
495 :returns: True, if previewing entries is allowed, False otherwise.
496 """
497 if not (user := current.user.get()):
498 return False
500 if user["access"] and "root" in user["access"]:
501 return True
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
508 return False
510 def canEdit(self, skel: SkeletonInstance) -> bool:
511 """
512 Access control function for modification permission.
514 Checks if the current user has the permission to edit an entry.
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.
521 It should be overridden for a module-specific behavior.
523 .. seealso:: :func:`edit`
525 :param skel: The Skeleton that should be edited.
527 :returns: True, if editing entries is allowed, False otherwise.
528 """
529 if not (user := current.user.get()):
530 return False
532 if user["access"] and "root" in user["access"]:
533 return True
535 if user and user["access"] and f"{self.moduleName}-edit" in user["access"]:
536 return True
538 return False
540 def canDelete(self, skel: SkeletonInstance) -> bool:
541 """
542 Access control function for delete permission.
544 Checks if the current user has the permission to delete an entry.
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.
552 It should be overridden for a module-specific behavior.
554 :param skel: The Skeleton that should be deleted.
556 .. seealso:: :func:`delete`
558 :returns: True, if deleting entries is allowed, False otherwise.
559 """
560 if not (user := current.user.get()):
561 return False
563 if user["access"] and "root" in user["access"]:
564 return True
566 if user and user["access"] and f"{self.moduleName}-delete" in user["access"]:
567 return True
569 return False
571 ## Override-able event-hooks
573 def onAdd(self, skel: SkeletonInstance):
574 """
575 Hook function that is called before adding an entry.
577 It can be overridden for a module-specific behavior.
579 :param skel: The Skeleton that is going to be added.
581 .. seealso:: :func:`add`, :func:`onAdded`
582 """
583 pass
585 def onAdded(self, skel: SkeletonInstance):
586 """
587 Hook function that is called after adding an entry.
589 It should be overridden for a module-specific behavior.
590 The default is writing a log entry.
592 :param skel: The Skeleton that has been added.
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})""")
601 def onEdit(self, skel: SkeletonInstance):
602 """
603 Hook function that is called before editing an entry.
605 It can be overridden for a module-specific behavior.
607 :param skel: The Skeleton that is going to be edited.
609 .. seealso:: :func:`edit`, :func:`onEdited`
610 """
611 pass
613 def onEdited(self, skel: SkeletonInstance):
614 """
615 Hook function that is called after modifying an entry.
617 It should be overridden for a module-specific behavior.
618 The default is writing a log entry.
620 :param skel: The Skeleton that has been modified.
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})""")
629 def onView(self, skel: SkeletonInstance):
630 """
631 Hook function that is called when viewing an entry.
633 It should be overridden for a module-specific behavior.
634 The default is doing nothing.
636 :param skel: The Skeleton that is viewed.
638 .. seealso:: :func:`view`
639 """
640 pass
642 def onDelete(self, skel: SkeletonInstance):
643 """
644 Hook function that is called before deleting an entry.
646 It can be overridden for a module-specific behavior.
648 :param skel: The Skeleton that is going to be deleted.
650 .. seealso:: :func:`delete`, :func:`onDeleted`
651 """
652 pass
654 def onDeleted(self, skel: SkeletonInstance):
655 """
656 Hook function that is called after deleting an entry.
658 It should be overridden for a module-specific behavior.
659 The default is writing a log entry.
661 :param skel: The Skeleton that has been deleted.
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})""")
670 def onClone(self, skel: SkeletonInstance, src_skel: SkeletonInstance):
671 """
672 Hook function that is called before cloning an entry.
674 It can be overwritten to a module-specific behavior.
676 :param skel: The new SkeletonInstance that is being created.
677 :param src_skel: The source SkeletonInstance `skel` is cloned from.
679 .. seealso:: :func:`clone`, :func:`onCloned`
680 """
681 pass
683 def onCloned(self, skel: SkeletonInstance, src_skel: SkeletonInstance):
684 """
685 Hook function that is called after cloning an entry.
687 It can be overwritten to a module-specific behavior.
689 :param skel: The new SkeletonInstance that was created.
690 :param src_skel: The source SkeletonInstance `skel` was cloned from.
692 .. seealso:: :func:`clone`, :func:`onClone`
693 """
694 logging.info(f"""Entry cloned: {skel["key"]!r}""")
695 flushCache(kind=skel.kindName)
697 if user := current.user.get():
698 logging.info(f"""User: {user["name"]!r} ({user["key"]!r})""")
701List.admin = True
702List.vi = True