From b9dcb36a86886d2d60f9c7fe61ab21291eb8d6bc Mon Sep 17 00:00:00 2001 From: Jan Max Meyer Date: Wed, 5 Feb 2025 23:55:23 +0100 Subject: [PATCH] wip: Entire rework of the `skel()`/`*Skel()` behavior - faces some review issues of @sveneberth - no satisfying integration with existing code --- src/viur/core/prototypes/list.py | 63 ++++---------- src/viur/core/prototypes/singleton.py | 32 +------ src/viur/core/prototypes/skelmodule.py | 71 ++++++++++++---- src/viur/core/prototypes/tree.py | 112 +++++++++++++++---------- 4 files changed, 141 insertions(+), 137 deletions(-) diff --git a/src/viur/core/prototypes/list.py b/src/viur/core/prototypes/list.py index b84909c9d..d2f0c2fd6 100644 --- a/src/viur/core/prototypes/list.py +++ b/src/viur/core/prototypes/list.py @@ -20,26 +20,7 @@ class List(SkelModule): handler = "list" accessRights = ("add", "edit", "view", "delete", "manage") - def viewSkel(self, *args, **kwargs) -> SkeletonInstance: - """ - Retrieve a new instance of a :class:`viur.core.skeleton.SkeletonInstance` that is used by the application - for viewing an existing entry from the list. - - The default is a Skeleton instance returned by :func:`~baseSkel`. - - This SkeletonInstance can be post-processed (just returning a subskel or manually removing single bones) - which - is the recommended way to ensure a given user cannot see certain fields. A Jinja-Template may choose not to - display certain bones, but if the json or xml render is attached (or the user can use the vi or admin render) - he could still see all values. This also prevents the user from filtering by these bones, so no binary search - is possible. - - .. seealso:: :func:`addSkel`, :func:`editSkel`, :func:`~baseSkel` - - :return: Returns a Skeleton instance for viewing an entry. - """ - return self.skel(bones_from_request=True, **kwargs) - - def addSkel(self, *args, **kwargs) -> SkeletonInstance: + def addSkel(self, **kwargs) -> SkeletonInstance: """ Retrieve a new instance of a :class:`viur.core.skeleton.Skeleton` that is used by the application for adding an entry to the list. @@ -55,26 +36,9 @@ def addSkel(self, *args, **kwargs) -> SkeletonInstance: :return: Returns a Skeleton instance for adding an entry. """ - return self.baseSkel(*args, **kwargs) - - def editSkel(self, *args, **kwargs) -> SkeletonInstance: - """ - Retrieve a new instance of a :class:`viur.core.skeleton.Skeleton` that is used by the application - for editing an existing entry from the list. - - The default is a Skeleton instance returned by :func:`~baseSkel`. - - Like in :func:`viewSkel`, the skeleton can be post-processed. Bones that are being removed aren't visible - and cannot be set, but it's also possible to just set a bone to readOnly (revealing it's value to the user, - but preventing any modification. - - .. seealso:: :func:`viewSkel`, :func:`editSkel`, :func:`~baseSkel` - - :return: Returns a Skeleton instance for editing an entry. - """ - return self.baseSkel(*args, **kwargs) + return self.skel(**kwargs) - def cloneSkel(self, *args, **kwargs) -> SkeletonInstance: + def cloneSkel(self, **kwargs) -> SkeletonInstance: """ Retrieve a new instance of a :class:`viur.core.skeleton.Skeleton` that is used by the application for cloning an existing entry from the list. @@ -89,7 +53,7 @@ def cloneSkel(self, *args, **kwargs) -> SkeletonInstance: :return: Returns a SkeletonInstance for editing an entry. """ - return self.baseSkel(*args, **kwargs) + return self.skel(**kwargs) ## External exposed functions @@ -110,7 +74,7 @@ def preview(self, *args, **kwargs) -> t.Any: if not self.canPreview(): raise errors.Unauthorized() - skel = self.viewSkel() + skel = self.viewSkel(allow_client_defined=utils.string.is_prefix(self.render.kind, "json")) skel.fromClient(kwargs) return self.render.view(skel) @@ -126,7 +90,7 @@ def structure(self, action: t.Optional[str] = "view") -> t.Any: # FIXME: In ViUR > 3.7 this could also become dynamic (ActionSkel paradigm). match action: case "view": - skel = self.viewSkel() + skel = self.viewSkel(allow_client_defined=utils.string.is_prefix(self.render.kind, "json")) if not self.canView(skel): raise errors.Unauthorized() @@ -168,7 +132,7 @@ def view(self, key: db.Key | int | str, *args, **kwargs) -> t.Any: :raises: :exc:`viur.core.errors.NotFound`, when no entry with the given *key* was found. :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions. """ - skel = self.viewSkel() + skel = self.viewSkel(allow_client_defined=utils.string.is_prefix(self.render.kind, "json")) if not skel.read(key): raise errors.NotFound() @@ -195,8 +159,10 @@ def list(self, *args, **kwargs) -> t.Any: :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions. """ + skel = self.viewSkel(allow_client_defined=utils.string.is_prefix(self.render.kind, "json")) + # The general access control is made via self.listFilter() - if not (query := self.listFilter(self.viewSkel().all().mergeExternalFilter(kwargs))): + if not (query := self.listFilter(skel.all().mergeExternalFilter(kwargs))): raise errors.Unauthorized() self._apply_default_order(query) @@ -323,10 +289,13 @@ def index(self, *args, **kwargs) -> t.Any: :return: The rendered entity or list. """ if args and args[0]: + skel = self.viewSkel( + allow_client_defined=utils.string.is_prefix(self.render.kind, "json"), + _excludeFromAccessLog=True, + ) + # We probably have a Database or SEO-Key here - seoKey = str(args[0]).lower() - skel = self.viewSkel().all(_excludeFromAccessLog=True).filter("viur.viurActiveSeoKeys =", seoKey).getSkel() - if skel: + if skel := skel.all().filter("viur.viurActiveSeoKeys =", str(args[0]).lower()).getSkel(): db.currentDbAccessLog.get(set()).add(skel["key"]) if not self.canView(skel): raise errors.Forbidden() diff --git a/src/viur/core/prototypes/singleton.py b/src/viur/core/prototypes/singleton.py index a5a77b7c1..de7077ac0 100644 --- a/src/viur/core/prototypes/singleton.py +++ b/src/viur/core/prototypes/singleton.py @@ -27,32 +27,6 @@ def getKey(self) -> str: """ return f"{self.editSkel().kindName}-modulekey" - def viewSkel(self, *args, **kwargs) -> SkeletonInstance: - """ - Retrieve a new instance of a :class:`viur.core.skeleton.Skeleton` that is used by the application - for viewing the existing entry. - - The default is a Skeleton instance returned by :func:`~baseSkel`. - - .. seealso:: :func:`addSkel`, :func:`editSkel`, :func:`~baseSkel` - - :return: Returns a Skeleton instance for viewing the singleton entry. - """ - return self.skel(bones_from_request=True, **kwargs) - - def editSkel(self, *args, **kwargs) -> SkeletonInstance: - """ - Retrieve a new instance of a :class:`viur.core.skeleton.Skeleton` that is used by the application - for editing the existing entry. - - The default is a Skeleton instance returned by :func:`~baseSkel`. - - .. seealso:: :func:`viewSkel`, :func:`editSkel`, :func:`~baseSkel` - - :return: Returns a Skeleton instance for editing the entry. - """ - return self.baseSkel(*args, **kwargs) - ## External exposed functions @exposed @@ -75,7 +49,7 @@ def preview(self, *args, **kwargs) -> t.Any: if not self.canPreview(): raise errors.Unauthorized() - skel = self.viewSkel() + skel = self.viewSkel(allow_client_defined=utils.string.is_prefix(self.render.kind, "json")) skel.fromClient(kwargs) return self.render.view(skel) @@ -91,7 +65,7 @@ def structure(self, action: t.Optional[str] = "view") -> t.Any: # FIXME: In ViUR > 3.7 this could also become dynamic (ActionSkel paradigm). match action: case "view": - skel = self.viewSkel() + skel = self.viewSkel(allow_client_defined=utils.string.is_prefix(self.render.kind, "json")) if not self.canView(): raise errors.Unauthorized() @@ -120,7 +94,7 @@ def view(self, *args, **kwargs) -> t.Any: :raises: :exc:`viur.core.errors.Unauthorized`, if the current user does not have the required permissions. """ - skel = self.viewSkel() + skel = self.viewSkel(allow_client_defined=utils.string.is_prefix(self.render.kind, "json")) if not self.canView(): raise errors.Unauthorized() diff --git a/src/viur/core/prototypes/skelmodule.py b/src/viur/core/prototypes/skelmodule.py index 74a528c3c..e3e22bfd0 100644 --- a/src/viur/core/prototypes/skelmodule.py +++ b/src/viur/core/prototypes/skelmodule.py @@ -1,7 +1,7 @@ import os import yaml import logging -from viur.core import Module, db, current +from viur.core import Module, db, current, errors from viur.core.decorators import * from viur.core.config import conf from viur.core.skeleton import skeletonByKind, Skeleton, SkeletonInstance @@ -47,6 +47,8 @@ def __load_indexes_from_file() -> dict[str, list]: DATASTORE_INDEXES = __load_indexes_from_file() +X_VIUR_BONELIST = "X-VIUR-BONELIST" +"""Defines the header parameter that might contain a client-defined bone list.""" class SkelModule(Module): """ @@ -99,7 +101,7 @@ def _resolveSkelCls(self, *args, **kwargs) -> t.Type[Skeleton]: """ return skeletonByKind(self.kindName) - def baseSkel(self, *args, **kwargs) -> SkeletonInstance: + def baseSkel(self, **kwargs) -> SkeletonInstance: """ Returns an instance of an unmodified base skeleton for this module. @@ -110,17 +112,56 @@ def baseSkel(self, *args, **kwargs) -> SkeletonInstance: """ return self._resolveSkelCls(**kwargs)() + def viewSkel(self, **kwargs) -> SkeletonInstance: + """ + Retrieve a new instance of a :class:`viur.core.skeleton.SkeletonInstance` that is used by the application + for viewing an existing entry from the list. + + The default is a Skeleton instance returned by :func:`~baseSkel`. + + This SkeletonInstance can be post-processed (just returning a subskel or manually removing single bones) - which + is the recommended way to ensure a given user cannot see certain fields. A Jinja-Template may choose not to + display certain bones, but if the json or xml render is attached (or the user can use the vi or admin render) + he could still see all values. This also prevents the user from filtering by these bones, so no binary search + is possible. + + :param client_derive: Allows to + + .. seealso:: :func:`addSkel`, :func:`editSkel`, :func:`~baseSkel` + + :return: Returns a Skeleton instance for viewing an entry. + """ + return self.skel(**kwargs) + + def editSkel(self, **kwargs) -> SkeletonInstance: + """ + Retrieve a new instance of a :class:`viur.core.skeleton.Skeleton` that is used by the application + for editing an existing entry from the list. + + The default is a Skeleton instance returned by :func:`~baseSkel`. + + Like in :func:`viewSkel`, the skeleton can be post-processed. Bones that are being removed aren't visible + and cannot be set, but it's also possible to just set a bone to readOnly (revealing it's value to the user, + but preventing any modification. + + .. seealso:: :func:`viewSkel`, :func:`editSkel`, :func:`~baseSkel` + + :return: Returns a Skeleton instance for editing an entry. + """ + return self.skel(**kwargs) + def skel( self, + *, bones: t.Iterable[str] = (), - bones_from_request: bool = False, + allow_client_defined: bool = False, **kwargs, ) -> SkeletonInstance: """ Retrieve module-specific skeleton, optionally as subskel. - :param bones: ALlows to specify a list of bones to form a subskel. - :param bones_from_request: Evaluates header X-VIUR-BONELIST to contain a comma-separated list of bones. + :param bones: Allows to specify a list of bones to form a subskel. + :param client_derive: Evaluates header X-VIUR-BONELIST to contain a comma-separated list of bones. Using this parameter enforces that the Skeleton class has a subskel named "*" for required bones that must exist. @@ -129,24 +170,24 @@ def skel( skel_cls = self._resolveSkelCls(**kwargs) bones = set(bones) if bones else set() - if ( - bones_from_request # feature generally enabled? - and skel_cls.subSkels.get("*") # a named subSkel "*"" must exist - # and (bonelist := current.request.get().kwargs.get("x-viur-bonelist")) # param must be given (DEBUG!) - and (bonelist := current.request.get().request.headers.get("x-viur-bonelist")) # header must be given - ): - bones |= {bone.strip() for bone in bonelist.split(",")} + if allow_client_defined: + if bonelist := current.request.get().kwargs.get(X_VIUR_BONELIST.lower()): # DEBUG + # if bonelist := current.request.get().request.headers.get(X_VIUR_BONELIST) + if "*" not in skel_cls.subSkels: # a named star-subskel "*"" must exist! + raise errors.BadRequest(f"Use of {X_VIUR_BONELIST!r} requires for a star-subskel") + + bones |= {bone.strip() for bone in bonelist.split(",")} # Return a subskel? if bones: - # When coming from outside of a request, "*" must always be contained. - if bones_from_request: + # When coming from outside of a request, "*" is always involved. + if allow_client_defined: return skel_cls.subskel("*", bones=bones) return skel_cls.subskel(bones=bones) # Otherwise, return full skeleton - return skel_cls() + return skel_cls() # FIXME: This is fishy, it should return a baseSkel(), but then some customer project break def _apply_default_order(self, query: db.Query): """ diff --git a/src/viur/core/prototypes/tree.py b/src/viur/core/prototypes/tree.py index cab0752f7..a51d33204 100644 --- a/src/viur/core/prototypes/tree.py +++ b/src/viur/core/prototypes/tree.py @@ -70,7 +70,7 @@ def _checkSkelType(self, skelType: t.Any) -> t.Optional[SkelType]: return None - def _resolveSkelCls(self, skelType: SkelType, *args, **kwargs) -> t.Type[Skeleton]: + def _resolveSkelCls(self, *, skelType: SkelType, **kwargs) -> t.Type[Skeleton]: if not (skelType := self._checkSkelType(skelType)): raise ValueError("Unsupported skelType") @@ -79,15 +79,17 @@ def _resolveSkelCls(self, skelType: SkelType, *args, **kwargs) -> t.Type[Skeleto return self.nodeSkelCls - def baseSkel(self, skelType: SkelType, *args, **kwargs) -> SkeletonInstance: + # FIXME: In VIUR4 skelType will become a kwargs-only-parameter + def baseSkel(self, skelType: SkelType, **kwargs) -> SkeletonInstance: """ Return unmodified base skeleton for the given skelType. .. seealso:: :func:`addSkel`, :func:`editSkel`, :func:`viewSkel`, :func:`~baseSkel` """ - return self._resolveSkelCls(skelType, *args, **kwargs)() + return self._resolveSkelCls(skelType=skelType, **kwargs)() - def viewSkel(self, skelType: SkelType, *args, **kwargs) -> SkeletonInstance: + # FIXME: In VIUR4 skelType will become a kwargs-only-parameter + def viewSkel(self, skelType: SkelType, **kwargs) -> SkeletonInstance: """ Retrieve a new instance of a :class:`viur.core.skeleton.Skeleton` that is used by the application for viewing an existing entry from the tree. @@ -98,9 +100,10 @@ def viewSkel(self, skelType: SkelType, *args, **kwargs) -> SkeletonInstance: :return: Returns a Skeleton instance for viewing an entry. """ - return self.skel(bones_from_request=True, skelType=skelType, **kwargs) + return self.skel(skelType=skelType, **kwargs) - def addSkel(self, skelType: SkelType, *args, **kwargs) -> SkeletonInstance: + # FIXME: In VIUR4 skelType will become a kwargs-only-parameter + def addSkel(self, skelType: SkelType, **kwargs) -> SkeletonInstance: """ Retrieve a new instance of a :class:`viur.core.skeleton.Skeleton` that is used by the application for adding an entry to the tree. @@ -111,9 +114,10 @@ def addSkel(self, skelType: SkelType, *args, **kwargs) -> SkeletonInstance: :return: Returns a Skeleton instance for adding an entry. """ - return self.baseSkel(skelType, *args, **kwargs) + return self.skel(skelType=skelType, **kwargs) - def editSkel(self, skelType: SkelType, *args, **kwargs) -> SkeletonInstance: + # FIXME: In VIUR4 skelType will become a kwargs-only-parameter + def editSkel(self, skelType: SkelType, **kwargs) -> SkeletonInstance: """ Retrieve a new instance of a :class:`viur.core.skeleton.Skeleton` that is used by the application for editing an existing entry from the tree. @@ -124,9 +128,10 @@ def editSkel(self, skelType: SkelType, *args, **kwargs) -> SkeletonInstance: :return: Returns a Skeleton instance for editing an entry. """ - return self.baseSkel(skelType, *args, **kwargs) + return self.skel(skelType=skelType, **kwargs) - def cloneSkel(self, skelType: SkelType, *args, **kwargs) -> SkeletonInstance: + # FIXME: In VIUR4 skelType will become a kwargs-only-parameter + def cloneSkel(self, skelType: SkelType, **kwargs) -> SkeletonInstance: """ Retrieve a new :class:`viur.core.skeleton.SkeletonInstance` that is used by the application for cloning an existing entry of the tree. @@ -137,7 +142,7 @@ def cloneSkel(self, skelType: SkelType, *args, **kwargs) -> SkeletonInstance: :return: Returns a SkeletonInstance for cloning an entry. """ - return self.baseSkel(skelType, *args, **kwargs) + return self.skel(skelType=skelType, **kwargs) def rootnodeSkel( self, @@ -246,14 +251,14 @@ def fixTxn(nodeKey, newRepoKey): db.Put(node) # Fix all nodes - q = db.Query(self.viewSkel("node").kindName).filter("parententry =", parentNode) + q = db.Query(self.viewSkel(skelType="node").kindName).filter("parententry =", parentNode) for repo in q.iter(): self.updateParentRepo(repo.key, newRepoKey, depth=depth + 1) db.RunInTransaction(fixTxn, repo.key, newRepoKey) # Fix the leafs on this level if self.leafSkelCls: - q = db.Query(self.viewSkel("leaf").kindName).filter("parententry =", parentNode) + q = db.Query(self.viewSkel(skelType="leaf").kindName).filter("parententry =", parentNode) for repo in q.iter(): db.RunInTransaction(fixTxn, repo.key, newRepoKey) @@ -269,12 +274,12 @@ def pathToKey(self, key: db.Key): """ lastLevel = [] for x in range(0, 99): - currentNodeSkel = self.viewSkel("node") + currentNodeSkel = self.viewSkel(skelType="node") if not currentNodeSkel.read(key): return [] # Either invalid key or listFilter prevented us from fetching anything if currentNodeSkel["parententry"] == currentNodeSkel["parentrepo"]: # We reached the top level break - levelQry = self.viewSkel("node").all().filter("parententry =", currentNodeSkel["parententry"]) + levelQry = self.viewSkel(skelType="node").all().filter("parententry =", currentNodeSkel["parententry"]) currentLevel = [{"skel": x, "active": x["key"] == currentNodeSkel["key"], "children": lastLevel if x["key"] == currentNodeSkel["key"] else []} @@ -330,8 +335,10 @@ def list(self, skelType: SkelType, *args, **kwargs) -> t.Any: if not (skelType := self._checkSkelType(skelType)): raise errors.NotAcceptable("Invalid skelType provided.") + skel = self.viewSkel(skelType=skelType, allow_client_defined=utils.string.is_prefix(self.render.kind, "json")) + # The general access control is made via self.listFilter() - if not (query := self.listFilter(self.viewSkel(skelType).all().mergeExternalFilter(kwargs))): + if not (query := self.listFilter(skel.all().mergeExternalFilter(kwargs))): raise errors.Unauthorized() self._apply_default_order(query) @@ -348,24 +355,27 @@ def structure(self, skelType: SkelType, action: t.Optional[str] = "view") -> t.A # FIXME: In ViUR > 3.7 this could also become dynamic (ActionSkel paradigm). match action: case "view": - skel = self.viewSkel(skelType) - if not self.canView(skelType, skel): + skel = self.viewSkel( + skelType=skelType, + allow_client_defined=utils.string.is_prefix(self.render.kind, "json") + ) + if not self.canView(skelType=skelType, skel=skel): raise errors.Unauthorized() case "edit": - skel = self.editSkel(skelType) - if not self.canEdit(skelType, skel): + skel = self.editSkel(skelType=skelType) + if not self.canEdit(skelType=skelType, skel=skel): raise errors.Unauthorized() case "add": - if not self.canAdd(skelType): + if not self.canAdd(skelType=skelType): raise errors.Unauthorized() - skel = self.addSkel(skelType) + skel = self.addSkel(skelType=skelType) case "clone": - skel = self.cloneSkel(skelType) - if not (self.canAdd(skelType) and self.canEdit(skelType, skel)): + skel = self.cloneSkel(skelType=skelType) + if not (self.canAdd(skelType) and self.canEdit(skelType=skelType, skel=skel)): raise errors.Unauthorized() case _: @@ -395,11 +405,11 @@ def view(self, skelType: SkelType, key: db.Key | int | str, *args, **kwargs) -> if not (skelType := self._checkSkelType(skelType)): raise errors.NotAcceptable(f"Invalid skelType provided.") - skel = self.viewSkel(skelType) + skel = self.viewSkel(skelType=skelType, allow_client_defined=utils.string.is_prefix(self.render.kind, "json")) if not skel.read(key): raise errors.NotFound() - if not self.canView(skelType, skel): + if not self.canView(skelType=skelType, skel=skel): raise errors.Unauthorized() self.onView(skelType, skel) @@ -430,11 +440,11 @@ def add(self, skelType: SkelType, node: db.Key | int | str, *args, **kwargs) -> if not (skelType := self._checkSkelType(skelType)): raise errors.NotAcceptable(f"Invalid skelType provided.") - skel = self.addSkel(skelType) - parentNodeSkel = self.editSkel("node") + skel = self.addSkel(skelType=skelType) + parentNodeSkel = self.editSkel(skelType="node") if not parentNodeSkel.read(node): raise errors.NotFound("The provided parent node could not be found.") - if not self.canAdd(skelType, parentNodeSkel): + if not self.canAdd(skelType=skelType, parentNodeSkel=parentNodeSkel): raise errors.Unauthorized() skel["parententry"] = parentNodeSkel["key"] @@ -474,9 +484,9 @@ def add_or_edit(self, skelType: SkelType, key: db.Key | int | str, **kwargs) -> is_add = not bool(db.Get(db_key)) if is_add: - skel = self.addSkel(skelType) + skel = self.addSkel(skelType=skelType) else: - skel = self.editSkel(skelType) + skel = self.editSkel(skelType=skelType) skel["key"] = db_key @@ -526,11 +536,11 @@ def edit(self, skelType: SkelType, key: db.Key | int | str, *args, **kwargs) -> if not (skelType := self._checkSkelType(skelType)): raise errors.NotAcceptable(f"Invalid skelType provided.") - skel = self.editSkel(skelType) + skel = self.editSkel(skelType=skelType) if not skel.read(key): raise errors.NotFound() - if not self.canEdit(skelType, skel): + if not self.canEdit(skelType=skelType, skel=skel): raise errors.Unauthorized() if ( @@ -571,11 +581,11 @@ def delete(self, skelType: SkelType, key: str, *args, **kwargs) -> t.Any: if not (skelType := self._checkSkelType(skelType)): raise errors.NotAcceptable(f"Invalid skelType provided.") - skel = self.editSkel(skelType) + skel = self.editSkel(skelType=skelType) if not skel.read(key): raise errors.NotFound() - if not self.canDelete(skelType, skel): + if not self.canDelete(skelType=skelType, skel=skel): raise errors.Unauthorized() if skelType == "node": @@ -596,16 +606,16 @@ def deleteRecursive(self, parentKey: str): :param parentKey: URL-safe key of the node which children should be deleted. """ - nodeKey = db.keyHelper(parentKey, self.viewSkel("node").kindName) + nodeKey = db.keyHelper(parentKey, self.viewSkel(skelType="node").kindName) if self.leafSkelCls: - for leaf in db.Query(self.viewSkel("leaf").kindName).filter("parententry =", nodeKey).iter(): - leafSkel = self.viewSkel("leaf") + for leaf in db.Query(self.viewSkel(skelType="leaf").kindName).filter("parententry =", nodeKey).iter(): + leafSkel = self.viewSkel(skelType="leaf") if not leafSkel.read(leaf.key): continue leafSkel.delete() - for node in db.Query(self.viewSkel("node").kindName).filter("parententry =", nodeKey).iter(): + for node in db.Query(self.viewSkel(skelType="node").kindName).filter("parententry =", nodeKey).iter(): self.deleteRecursive(node.key) - nodeSkel = self.viewSkel("node") + nodeSkel = self.viewSkel(skelType="node") if not nodeSkel.read(node.key): continue nodeSkel.delete() @@ -634,7 +644,7 @@ def move(self, skelType: SkelType, key: db.Key | int | str, parentNode: str, *ar if not (skelType := self._checkSkelType(skelType)): raise errors.NotAcceptable(f"Invalid skelType provided.") - skel = self.editSkel(skelType) # srcSkel - the skeleton to be moved + skel = self.editSkel(skelType=skelType) # srcSkel - the skeleton to be moved parentNodeSkel = self.baseSkel("node") # destSkel - the node it should be moved into if not skel.read(key): @@ -661,7 +671,8 @@ def move(self, skelType: SkelType, key: db.Key | int | str, parentNode: str, *ar for _ in range(0, 99): if currLevel.key == skel["key"]: break - if currLevel.get("rootNode") or currLevel.get("is_root_node"): + # TODO: Remove "rootNode"-fallback with VIUR4 + if currLevel.get("is_root_node") or currLevel.get("rootNode"): # We reached a rootNode, so this is okay break currLevel = db.Get(currLevel["parententry"]) @@ -717,12 +728,21 @@ def clone(self, skelType: SkelType, key: db.Key | str | int, **kwargs): if not (skelType := self._checkSkelType(skelType)): raise errors.NotAcceptable(f"Invalid skelType provided.") - skel = self.cloneSkel(skelType) + skel = self.cloneSkel(skelType=skelType) if not skel.read(key): raise errors.NotFound() + if parententry := kwargs.get("parententry"): + if not (parentNodeSkel := self.editSkel(skelType="node").read(parententry)): + raise errors.NotFound("The provided parent node could not be found.") + else: + parentNodeSkel = None + # a clone-operation is some kind of edit and add... - if not (self.canEdit(skelType, skel) and self.canAdd(skelType, kwargs.get("parententry"))): + if not ( + self.canEdit(skelType=skelType, skel=skel) + and self.canAdd(skelType=skelType, parentNodeSkel=parentNodeSkel) + ): raise errors.Unauthorized() # Remember source skel and unset the key for clone operation! @@ -788,7 +808,7 @@ def canView(self, skelType: SkelType, skel: SkeletonInstance) -> bool: :param skel: The entry we check for :return: True if the current session is authorized to view that entry, False otherwise """ - query = self.viewSkel(skelType).all() + query = self.viewSkel(skelType=skelType).all() if key := skel["key"]: query.mergeExternalFilter({"key": key}) @@ -1057,7 +1077,7 @@ def _clone_recursive( logging.debug(f"_clone_recursive {skel_type=}, {src_key=}, {target_key=}, {target_repo=}, {cursor=}") - q = self.cloneSkel(skel_type).all().filter("parententry", src_key).order("sortindex") + q = self.cloneSkel(skelType=skel_type).all().filter("parententry", src_key).order("sortindex") q.setCursor(cursor) count = 0