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.6.3, created at 2024-10-16 22:16 +0000
« prev ^ index » next coverage.py v7.6.3, created at 2024-10-16 22:16 +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, DEFAULT_ORDER_TYPE
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 default_order: DEFAULT_ORDER_TYPE = None
24 """
25 Allows to specify a default order for this module, which is applied when no other order is specified.
27 Setting a default_order might result in the requirement of additional indexes, which are being raised
28 and must be specified.
29 """
31 def viewSkel(self, *args, **kwargs) -> SkeletonInstance:
32 """
33 Retrieve a new instance of a :class:`viur.core.skeleton.SkeletonInstance` that is used by the application
34 for viewing an existing entry from the list.
36 The default is a Skeleton instance returned by :func:`~baseSkel`.
38 This SkeletonInstance can be post-processed (just returning a subskel or manually removing single bones) - which
39 is the recommended way to ensure a given user cannot see certain fields. A Jinja-Template may choose not to
40 display certain bones, but if the json or xml render is attached (or the user can use the vi or admin render)
41 he could still see all values. This also prevents the user from filtering by these bones, so no binary search
42 is possible.
44 .. seealso:: :func:`addSkel`, :func:`editSkel`, :func:`~baseSkel`
46 :return: Returns a Skeleton instance for viewing an entry.
47 """
48 return self.baseSkel(*args, **kwargs)
50 def addSkel(self, *args, **kwargs) -> SkeletonInstance:
51 """
52 Retrieve a new instance of a :class:`viur.core.skeleton.Skeleton` that is used by the application
53 for adding an entry to the list.
55 The default is a Skeleton instance returned by :func:`~baseSkel`.
57 Like in :func:`viewSkel`, the skeleton can be post-processed. Bones that are being removed aren't visible
58 and cannot be set, but it's also possible to just set a bone to readOnly (revealing it's value to the user,
59 but preventing any modification. It's possible to pre-set values on that skeleton (and if that bone is
60 readOnly, enforcing these values).
62 .. seealso:: :func:`viewSkel`, :func:`editSkel`, :func:`~baseSkel`
64 :return: Returns a Skeleton instance for adding an entry.
65 """
66 return self.baseSkel(*args, **kwargs)
68 def editSkel(self, *args, **kwargs) -> SkeletonInstance:
69 """
70 Retrieve a new instance of a :class:`viur.core.skeleton.Skeleton` that is used by the application
71 for editing an existing entry from the list.
73 The default is a Skeleton instance returned by :func:`~baseSkel`.
75 Like in :func:`viewSkel`, the skeleton can be post-processed. Bones that are being removed aren't visible
76 and cannot be set, but it's also possible to just set a bone to readOnly (revealing it's value to the user,
77 but preventing any modification.
79 .. seealso:: :func:`viewSkel`, :func:`editSkel`, :func:`~baseSkel`
81 :return: Returns a Skeleton instance for editing an entry.
82 """
83 return self.baseSkel(*args, **kwargs)
85 def cloneSkel(self, *args, **kwargs) -> SkeletonInstance:
86 """
87 Retrieve a new instance of a :class:`viur.core.skeleton.Skeleton` that is used by the application
88 for cloning an existing entry from the list.
90 The default is a SkeletonInstance returned by :func:`~baseSkel`.
92 Like in :func:`viewSkel`, the skeleton can be post-processed. Bones that are being removed aren't visible
93 and cannot be set, but it's also possible to just set a bone to readOnly (revealing it's value to the user,
94 but preventing any modification.
96 .. seealso:: :func:`viewSkel`, :func:`editSkel`, :func:`~baseSkel`
98 :return: Returns a SkeletonInstance for editing an entry.
99 """
100 return self.baseSkel(*args, **kwargs)
102 ## External exposed functions
104 @exposed
105 @force_post
106 @skey
107 def preview(self, *args, **kwargs) -> t.Any:
108 """
109 Renders data for an entry, without reading from the database.
110 This function allows to preview an entry without writing it to the database.
112 Any entity values are provided via *kwargs*.
114 The function uses the viewTemplate of the application.
116 :returns: The rendered representation of the supplied data.
117 """
118 if not self.canPreview():
119 raise errors.Unauthorized()
121 skel = self.viewSkel()
122 skel.fromClient(kwargs)
124 return self.render.view(skel)
126 @exposed
127 def structure(self, *args, **kwargs) -> t.Any:
128 """
129 :returns: Returns the structure of our skeleton as used in list/view. Values are the defaultValues set
130 in each bone.
132 :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions.
133 """
134 skel = self.viewSkel()
135 if not self.canAdd(): # We can't use canView here as it would require passing a skeletonInstance.
136 # As a fallback, we'll check if the user has the permissions to view at least one entry
137 qry = self.listFilter(skel.all())
138 if not qry or not qry.getEntry():
139 raise errors.Unauthorized()
140 return self.render.view(skel)
142 @exposed
143 def view(self, key: db.Key | int | str, *args, **kwargs) -> t.Any:
144 """
145 Prepares and renders a single entry for viewing.
147 The entry is fetched by its entity key, which either is provided via *kwargs["key"]*,
148 or as the first parameter in *args*. The function performs several access control checks
149 on the requested entity before it is rendered.
151 .. seealso:: :func:`viewSkel`, :func:`canView`, :func:`onView`
153 :returns: The rendered representation of the requested entity.
155 :raises: :exc:`viur.core.errors.NotAcceptable`, when no *key* is provided.
156 :raises: :exc:`viur.core.errors.NotFound`, when no entry with the given *key* was found.
157 :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions.
158 """
159 skel = self.viewSkel()
160 if not skel.fromDB(key):
161 raise errors.NotFound()
163 if not self.canView(skel):
164 raise errors.Forbidden()
166 self.onView(skel)
167 return self.render.view(skel)
169 @exposed
170 def list(self, *args, **kwargs) -> t.Any:
171 """
172 Prepares and renders a list of entries.
174 All supplied parameters are interpreted as filters for the elements displayed.
176 Unlike other modules in ViUR, the access control in this function is performed
177 by calling the function :func:`listFilter`, which updates the query-filter to match only
178 elements which the user is allowed to see.
180 .. seealso:: :func:`listFilter`, :func:`viur.core.db.mergeExternalFilter`
182 :returns: The rendered list objects for the matching entries.
184 :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions.
185 """
186 # The general access control is made via self.listFilter()
187 query = self.listFilter(self.viewSkel().all().mergeExternalFilter(kwargs))
188 if query and query.queries and not isinstance(query.queries, list):
189 # Apply default order when specified
190 if self.default_order and not query.queries.orders and not current.request.get().kwargs.get("search"):
191 # TODO: refactor: Duplicate code in prototypes.Tree
192 if callable(default_order := self.default_order):
193 default_order = default_order(query)
195 if isinstance(default_order, dict):
196 logging.debug(f"Applying filter {default_order=}")
197 query.mergeExternalFilter(default_order)
199 elif default_order:
200 logging.debug(f"Applying {default_order=}")
202 # FIXME: This ugly test can be removed when there is type that abstracts SortOrders
203 if (
204 isinstance(default_order, str)
205 or (
206 isinstance(default_order, tuple)
207 and len(default_order) == 2
208 and isinstance(default_order[0], str)
209 and isinstance(default_order[1], db.SortOrder)
210 )
211 ):
212 query.order(default_order)
213 else:
214 query.order(*default_order)
216 return self.render.list(query.fetch())
218 raise errors.Unauthorized()
220 @force_ssl
221 @exposed
222 @skey(allow_empty=True)
223 def edit(self, key: db.Key | int | str, *args, **kwargs) -> t.Any:
224 """
225 Modify an existing entry, and render the entry, eventually with error notes on incorrect data.
226 Data is taken by any other arguments in *kwargs*.
228 The entry is fetched by its entity key, which either is provided via *kwargs["key"]*,
229 or as the first parameter in *args*. The function performs several access control checks
230 on the requested entity before it is modified.
232 .. seealso:: :func:`editSkel`, :func:`onEdit`, :func:`onEdited`, :func:`canEdit`
234 :returns: The rendered, edited object of the entry, eventually with error hints.
236 :raises: :exc:`viur.core.errors.NotAcceptable`, when no *key* is provided.
237 :raises: :exc:`viur.core.errors.NotFound`, when no entry with the given *key* was found.
238 :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions.
239 :raises: :exc:`viur.core.errors.PreconditionFailed`, if the *skey* could not be verified.
240 """
241 skel = self.editSkel()
242 if not skel.fromDB(key):
243 raise errors.NotFound()
245 if not self.canEdit(skel):
246 raise errors.Unauthorized()
248 if (
249 not kwargs # no data supplied
250 or not current.request.get().isPostRequest # failure if not using POST-method
251 or not skel.fromClient(kwargs, amend=True) # failure on reading into the bones
252 or utils.parse.bool(kwargs.get("bounce")) # review before changing
253 ):
254 # render the skeleton in the version it could as far as it could be read.
255 return self.render.edit(skel)
257 self.onEdit(skel)
258 skel.toDB() # write it!
259 self.onEdited(skel)
261 return self.render.editSuccess(skel)
263 @force_ssl
264 @exposed
265 @skey(allow_empty=True)
266 def add(self, *args, **kwargs) -> t.Any:
267 """
268 Add a new entry, and render the entry, eventually with error notes on incorrect data.
269 Data is taken by any other arguments in *kwargs*.
271 The function performs several access control checks on the requested entity before it is added.
273 .. seealso:: :func:`addSkel`, :func:`onAdd`, :func:`onAdded`, :func:`canAdd`
275 :returns: The rendered, added object of the entry, eventually with error hints.
277 :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions.
278 :raises: :exc:`viur.core.errors.PreconditionFailed`, if the *skey* could not be verified.
279 """
280 if not self.canAdd():
281 raise errors.Unauthorized()
283 skel = self.addSkel()
285 if (
286 not kwargs # no data supplied
287 or not current.request.get().isPostRequest # failure if not using POST-method
288 or not skel.fromClient(kwargs) # failure on reading into the bones
289 or utils.parse.bool(kwargs.get("bounce")) # review before adding
290 ):
291 # render the skeleton in the version it could as far as it could be read.
292 return self.render.add(skel)
294 self.onAdd(skel)
295 skel.toDB()
296 self.onAdded(skel)
298 return self.render.addSuccess(skel)
300 @force_ssl
301 @force_post
302 @exposed
303 @skey
304 def delete(self, key: db.Key | int | str, *args, **kwargs) -> t.Any:
305 """
306 Delete an entry.
308 The function runs several access control checks on the data before it is deleted.
310 .. seealso:: :func:`canDelete`, :func:`editSkel`, :func:`onDeleted`
312 :returns: The rendered, deleted object of the entry.
314 :raises: :exc:`viur.core.errors.NotFound`, when no entry with the given *key* was found.
315 :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions.
316 :raises: :exc:`viur.core.errors.PreconditionFailed`, if the *skey* could not be verified.
317 """
318 skel = self.editSkel()
319 if not skel.fromDB(key):
320 raise errors.NotFound()
322 if not self.canDelete(skel):
323 raise errors.Unauthorized()
325 self.onDelete(skel)
326 skel.delete()
327 self.onDeleted(skel)
329 return self.render.deleteSuccess(skel)
331 @exposed
332 def index(self, *args, **kwargs) -> t.Any:
333 """
334 Default, SEO-Friendly fallback for view and list.
336 :param args: The first argument - if provided - is interpreted as seoKey.
337 :param kwargs: Used for the fallback list.
338 :return: The rendered entity or list.
339 """
340 if args and args[0]:
341 # We probably have a Database or SEO-Key here
342 seoKey = str(args[0]).lower()
343 skel = self.viewSkel().all(_excludeFromAccessLog=True).filter("viur.viurActiveSeoKeys =", seoKey).getSkel()
344 if skel:
345 db.currentDbAccessLog.get(set()).add(skel["key"])
346 if not self.canView(skel):
347 raise errors.Forbidden()
348 seoUrl = utils.seoUrlToEntry(self.moduleName, skel)
349 # Check whether this is the current seo-key, otherwise redirect to it
351 if current.request.get().request.path.lower() != seoUrl:
352 raise errors.Redirect(seoUrl, status=301)
353 self.onView(skel)
354 return self.render.view(skel)
355 # This was unsuccessfully, we'll render a list instead
356 if not kwargs:
357 kwargs = self.getDefaultListParams()
358 return self.list(**kwargs)
360 def getDefaultListParams(self):
361 return {}
363 @exposed
364 @force_ssl
365 @skey(allow_empty=True)
366 def clone(self, key: db.Key | str | int, **kwargs):
367 """
368 Clone an existing entry, and render the entry, eventually with error notes on incorrect data.
369 Data is taken by any other arguments in *kwargs*.
371 The function performs several access control checks on the requested entity before it is added.
373 .. seealso:: :func:`canEdit`, :func:`canAdd`, :func:`onClone`, :func:`onCloned`
375 :param key: URL-safe key of the item to be edited.
377 :returns: The cloned object of the entry, eventually with error hints.
379 :raises: :exc:`viur.core.errors.NotAcceptable`, when no valid *skelType* was provided.
380 :raises: :exc:`viur.core.errors.NotFound`, when no *entry* to clone from was found.
381 :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions.
382 """
384 skel = self.cloneSkel()
385 if not skel.fromDB(key):
386 raise errors.NotFound()
388 # a clone-operation is some kind of edit and add...
389 if not (self.canEdit(skel) and self.canAdd()):
390 raise errors.Unauthorized()
392 # Remember source skel and unset the key for clone operation!
393 src_skel = skel
394 skel = skel.clone()
395 skel["key"] = None
397 # Check all required preconditions for clone
398 if (
399 not kwargs # no data supplied
400 or not current.request.get().isPostRequest # failure if not using POST-method
401 or not skel.fromClient(kwargs) # failure on reading into the bones
402 or utils.parse.bool(kwargs.get("bounce")) # review before changing
403 ):
404 return self.render.edit(skel, action="clone")
406 self.onClone(skel, src_skel=src_skel)
407 assert skel.toDB()
408 self.onCloned(skel, src_skel=src_skel)
410 return self.render.editSuccess(skel, action="cloneSuccess")
412 ## Default access control functions
414 def listFilter(self, query: db.Query) -> t.Optional[db.Query]:
415 """
416 Access control function on item listing.
418 This function is invoked by the :func:`list` renderer and the related Jinja2 fetching function,
419 and is used to modify the provided filter parameter to match only items that the current user
420 is allowed to see.
422 :param query: Query which should be altered.
424 :returns: The altered filter, or None if access is not granted.
425 """
427 if (user := current.user.get()) and (f"{self.moduleName}-view" in user["access"] or "root" in user["access"]):
428 return query
430 return None
432 def canView(self, skel: SkeletonInstance) -> bool:
433 """
434 Checks if the current user can view the given entry.
435 Should be identical to what's allowed by listFilter.
436 By default, `meth:listFilter` is used to determine what's allowed and whats not; but this
437 method can be overridden for performance improvements (to eliminate that additional database access).
438 :param skel: The entry we check for
439 :return: True if the current session is authorized to view that entry, False otherwise
440 """
441 # We log the key we're querying by hand so we don't have to lock on the entire kind in our query
442 db.currentDbAccessLog.get(set()).add(skel["key"])
443 query = self.viewSkel().all(_excludeFromAccessLog=True).mergeExternalFilter({"key": skel["key"]})
444 query = self.listFilter(query) # Access control
446 if query is None:
447 return False
449 if not query.getEntry():
450 return False
452 return True
454 def canAdd(self) -> bool:
455 """
456 Access control function for adding permission.
458 Checks if the current user has the permission to add a new entry.
460 The default behavior is:
461 - If no user is logged in, adding is generally refused.
462 - If the user has "root" access, adding is generally allowed.
463 - If the user has the modules "add" permission (module-add) enabled, adding is allowed.
465 It should be overridden for a module-specific behavior.
467 .. seealso:: :func:`add`
469 :returns: True, if adding entries is allowed, False otherwise.
470 """
471 if not (user := current.user.get()):
472 return False
474 # root user is always allowed.
475 if user["access"] and "root" in user["access"]:
476 return True
478 # user with add-permission is allowed.
479 if user and user["access"] and f"{self.moduleName}-add" in user["access"]:
480 return True
482 return False
484 def canPreview(self) -> bool:
485 """
486 Access control function for preview permission.
488 Checks if the current user has the permission to preview an entry.
490 The default behavior is:
491 - If no user is logged in, previewing is generally refused.
492 - If the user has "root" access, previewing is generally allowed.
493 - If the user has the modules "add" or "edit" permission (module-add, module-edit) enabled, \
494 previewing is allowed.
496 It should be overridden for module-specific behavior.
498 .. seealso:: :func:`preview`
500 :returns: True, if previewing entries is allowed, False otherwise.
501 """
502 if not (user := current.user.get()):
503 return False
505 if user["access"] and "root" in user["access"]:
506 return True
508 if (user and user["access"]
509 and (f"{self.moduleName}-add" in user["access"]
510 or f"{self.moduleName}-edit" in user["access"])):
511 return True
513 return False
515 def canEdit(self, skel: SkeletonInstance) -> bool:
516 """
517 Access control function for modification permission.
519 Checks if the current user has the permission to edit an entry.
521 The default behavior is:
522 - If no user is logged in, editing is generally refused.
523 - If the user has "root" access, editing is generally allowed.
524 - If the user has the modules "edit" permission (module-edit) enabled, editing is allowed.
526 It should be overridden for a module-specific behavior.
528 .. seealso:: :func:`edit`
530 :param skel: The Skeleton that should be edited.
532 :returns: True, if editing entries is allowed, False otherwise.
533 """
534 if not (user := current.user.get()):
535 return False
537 if user["access"] and "root" in user["access"]:
538 return True
540 if user and user["access"] and f"{self.moduleName}-edit" in user["access"]:
541 return True
543 return False
545 def canDelete(self, skel: SkeletonInstance) -> bool:
546 """
547 Access control function for delete permission.
549 Checks if the current user has the permission to delete an entry.
551 The default behavior is:
552 - If no user is logged in, deleting is generally refused.
553 - If the user has "root" access, deleting is generally allowed.
554 - If the user has the modules "deleting" permission (module-delete) enabled, \
555 deleting is allowed.
557 It should be overridden for a module-specific behavior.
559 :param skel: The Skeleton that should be deleted.
561 .. seealso:: :func:`delete`
563 :returns: True, if deleting entries is allowed, False otherwise.
564 """
565 if not (user := current.user.get()):
566 return False
568 if user["access"] and "root" in user["access"]:
569 return True
571 if user and user["access"] and f"{self.moduleName}-delete" in user["access"]:
572 return True
574 return False
576 ## Override-able event-hooks
578 def onAdd(self, skel: SkeletonInstance):
579 """
580 Hook function that is called before adding an entry.
582 It can be overridden for a module-specific behavior.
584 :param skel: The Skeleton that is going to be added.
586 .. seealso:: :func:`add`, :func:`onAdded`
587 """
588 pass
590 def onAdded(self, skel: SkeletonInstance):
591 """
592 Hook function that is called after adding an entry.
594 It should be overridden for a module-specific behavior.
595 The default is writing a log entry.
597 :param skel: The Skeleton that has been added.
599 .. seealso:: :func:`add`, , :func:`onAdd`
600 """
601 logging.info(f"""Entry added: {skel["key"]!r}""")
602 flushCache(kind=skel.kindName)
603 if user := current.user.get():
604 logging.info(f"""User: {user["name"]!r} ({user["key"]!r})""")
606 def onEdit(self, skel: SkeletonInstance):
607 """
608 Hook function that is called before editing an entry.
610 It can be overridden for a module-specific behavior.
612 :param skel: The Skeleton that is going to be edited.
614 .. seealso:: :func:`edit`, :func:`onEdited`
615 """
616 pass
618 def onEdited(self, skel: SkeletonInstance):
619 """
620 Hook function that is called after modifying an entry.
622 It should be overridden for a module-specific behavior.
623 The default is writing a log entry.
625 :param skel: The Skeleton that has been modified.
627 .. seealso:: :func:`edit`, :func:`onEdit`
628 """
629 logging.info(f"""Entry changed: {skel["key"]!r}""")
630 flushCache(key=skel["key"])
631 if user := current.user.get():
632 logging.info(f"""User: {user["name"]!r} ({user["key"]!r})""")
634 def onView(self, skel: SkeletonInstance):
635 """
636 Hook function that is called when viewing an entry.
638 It should be overridden for a module-specific behavior.
639 The default is doing nothing.
641 :param skel: The Skeleton that is viewed.
643 .. seealso:: :func:`view`
644 """
645 pass
647 def onDelete(self, skel: SkeletonInstance):
648 """
649 Hook function that is called before deleting an entry.
651 It can be overridden for a module-specific behavior.
653 :param skel: The Skeleton that is going to be deleted.
655 .. seealso:: :func:`delete`, :func:`onDeleted`
656 """
657 pass
659 def onDeleted(self, skel: SkeletonInstance):
660 """
661 Hook function that is called after deleting an entry.
663 It should be overridden for a module-specific behavior.
664 The default is writing a log entry.
666 :param skel: The Skeleton that has been deleted.
668 .. seealso:: :func:`delete`, :func:`onDelete`
669 """
670 logging.info(f"""Entry deleted: {skel["key"]!r}""")
671 flushCache(key=skel["key"])
672 if user := current.user.get():
673 logging.info(f"""User: {user["name"]!r} ({user["key"]!r})""")
675 def onClone(self, skel: SkeletonInstance, src_skel: SkeletonInstance):
676 """
677 Hook function that is called before cloning an entry.
679 It can be overwritten to a module-specific behavior.
681 :param skel: The new SkeletonInstance that is being created.
682 :param src_skel: The source SkeletonInstance `skel` is cloned from.
684 .. seealso:: :func:`clone`, :func:`onCloned`
685 """
686 pass
688 def onCloned(self, skel: SkeletonInstance, src_skel: SkeletonInstance):
689 """
690 Hook function that is called after cloning an entry.
692 It can be overwritten to a module-specific behavior.
694 :param skel: The new SkeletonInstance that was created.
695 :param src_skel: The source SkeletonInstance `skel` was cloned from.
697 .. seealso:: :func:`clone`, :func:`onClone`
698 """
699 logging.info(f"""Entry cloned: {skel["key"]!r}""")
700 flushCache(kind=skel.kindName)
702 if user := current.user.get():
703 logging.info(f"""User: {user["name"]!r} ({user["key"]!r})""")
706List.admin = True
707List.vi = True