Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/modules/moduleconf.py: 0%
78 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 Module, conf, db, current, i18n, tasks, skeleton
4from viur.core.bones import StringBone, TextBone, SelectBone, TreeLeafBone
5from viur.core.bones.text import HtmlBoneConfiguration
6from viur.core.prototypes import List
9MODULECONF_KINDNAME = "viur-module-conf"
11_LIMITED_HTML: t.Final[HtmlBoneConfiguration] = conf.bone_html_default_allow | {
12 "validTags": "a abbr b blockquote br div em h1 h2 h3 h4 h5 h6 hr i li ol p span strong sub sup u ul".split(),
13}
16class ModuleConfScriptSkel(skeleton.RelSkel):
18 name = StringBone(
19 descr="Label",
20 required=True,
21 params={
22 "tooltip": "Label for the action button displayed."
23 },
24 )
26 icon = StringBone(
27 descr="Icon",
28 params={
29 "tooltip": "Shoelace-conforming icon identifier."
30 },
31 )
33 capable = SelectBone(
34 descr="Arguments",
35 required=True,
36 defaultValue="none",
37 values={
38 "none": "none: No arguments, always executable",
39 "single": "single: Run script with single entry key as argument",
40 "multiple": "multiple: Run script with list of entity keys as argument",
41 },
42 params={
43 "tooltip":
44 "Describes the behavior in the admin, "
45 "if and how selected entries from the module are being processed."
46 },
47 )
49 access = SelectBone(
50 descr="Required access rights",
51 values=lambda: {
52 right: i18n.translate(f"viur.modules.user.accessright.{right}", defaultText=right)
53 for right in sorted(conf.user.access_rights)
54 },
55 multiple=True,
56 params={
57 "tooltip":
58 "To whom the button should be displayed in the admin. "
59 "In addition, the admin checks whether all rights of the script are also fulfilled.",
60 },
61 )
64class ModuleConfSkel(skeleton.Skeleton):
65 kindName = MODULECONF_KINDNAME
67 name = StringBone(
68 descr=i18n.translate("modulename"),
69 readOnly=True,
70 )
72 help_text = TextBone(
73 descr=i18n.translate("module helptext"),
74 validHtml=_LIMITED_HTML,
75 )
77 help_text_add = TextBone(
78 descr=i18n.translate("add helptext"),
79 validHtml=_LIMITED_HTML,
80 )
82 help_text_edit = TextBone(
83 descr=i18n.translate("edit helptext"),
84 validHtml=_LIMITED_HTML,
85 )
87 scripts = TreeLeafBone(
88 descr=i18n.translate("scriptor scripts"),
89 module="script",
90 kind="viur-script-leaf",
91 using=ModuleConfScriptSkel,
92 refKeys=[
93 "key",
94 "name",
95 "access",
96 ],
97 multiple=True,
98 )
101class ModuleConf(List):
102 """
103 This module is for ViUR internal purposes only.
104 It lists all other modules to be able to provide them with help texts.
105 """
106 MODULES = set() # will be filled by read_all_modules
107 kindName = MODULECONF_KINDNAME
108 accessRights = ["edit"]
109 default_order = None # disable default ordering for ModuleConf
111 def adminInfo(self):
112 return conf.moduleconf_admin_info or {}
114 def canAdd(self):
115 return False
117 def canDelete(self, skel):
118 return False
120 def canEdit(self, skel):
121 if super().canEdit(skel):
122 return True
124 # Check for "manage"-flag on current user
125 return (cuser := current.user.get()) and f"""{skel["name"]}-manage""" in cuser["access"]
127 def listFilter(self, query):
128 original_query = query
130 # when super-call does not satisfy...
131 if not (query := super().listFilter(query)):
132 if cuser := current.user.get():
133 # ... then, list modules the user is allowed to use!
134 user_modules = set(right.split("-", 1)[0] for right in cuser["access"] if "-" in right)
136 query = original_query
137 query.filter("name IN", tuple(user_modules))
139 return query
141 @classmethod
142 def get_by_module_name(cls, module_name: str) -> None | skeleton.SkeletonInstance:
143 db_key = db.Key(MODULECONF_KINDNAME, module_name)
144 skel = conf.main_app.vi._moduleconf.viewSkel()
145 if not skel.read(db_key):
146 logging.error(f"module({module_name}) not found")
147 return None
149 return skel
151 @tasks.StartupTask
152 @staticmethod
153 def read_all_modules():
154 db_module_names = (m["name"] for m in db.Query(MODULECONF_KINDNAME).run(999))
155 visited_modules = set()
157 def collect_modules(parent, depth: int = 0, prefix: str = "") -> None:
158 """Recursively collects all routable modules for the vi renderer"""
159 if depth > 10:
160 logging.warning(f"Reached maximum recursion limit of {depth} at {parent=}")
161 return
163 for module_name in dir(parent):
164 module = getattr(parent, module_name, None)
165 if not isinstance(module, Module):
166 continue
167 if module in visited_modules:
168 # Some modules reference other modules as parents, this will
169 # lead to infinite recursion. We can avoid reaching the
170 # maximum recursion limit by remembering already seen modules.
171 if conf.debug.trace:
172 logging.debug(f"Already visited and added {module=}")
173 continue
174 module_name = f"{prefix}{module_name}"
175 visited_modules.add(module)
176 ModuleConf.MODULES.add(module_name)
177 if module_name not in db_module_names:
178 skel = conf.main_app.vi._moduleconf.addSkel()
179 skel["key"] = db.Key(MODULECONF_KINDNAME, module_name)
180 skel["name"] = module_name
181 skel.write()
183 # Collect children
184 collect_modules(module, depth=depth + 1, prefix=f"{module_name}.")
186 collect_modules(conf.main_app.vi)
187 # TODO: Remove entries from MODULECONF_KINDNAME which are in db_module_names but not in ModuleConf.MODULES