Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/modules/moduleconf.py: 63%
78 statements
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-01 22:44 +0000
« prev ^ index » next coverage.py v7.16.0, created at 2026-09-01 22:44 +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.core.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 # Must be a set, not a generator: the membership test below runs once per module
155 # and a generator would be consumed by it (see #1762).
156 db_module_names = {m["name"] for m in db.Query(MODULECONF_KINDNAME).run(999)}
157 visited_modules = set()
159 def collect_modules(parent, depth: int = 0, prefix: str = "") -> None:
160 """Recursively collects all routable modules for the vi renderer"""
161 if depth > 10: 161 ↛ 162line 161 didn't jump to line 162 because the condition on line 161 was never true
162 logging.warning(f"Reached maximum recursion limit of {depth} at {parent=}")
163 return
165 for module_name in dir(parent):
166 module = getattr(parent, module_name, None)
167 if not isinstance(module, Module):
168 continue
169 if module in visited_modules: 169 ↛ 173line 169 didn't jump to line 173 because the condition on line 169 was never true
170 # Some modules reference other modules as parents, this will
171 # lead to infinite recursion. We can avoid reaching the
172 # maximum recursion limit by remembering already seen modules.
173 if conf.debug.trace:
174 logging.debug(f"Already visited and added {module=}")
175 continue
176 module_name = f"{prefix}{module_name}"
177 visited_modules.add(module)
178 ModuleConf.MODULES.add(module_name)
179 if module_name not in db_module_names:
180 skel = conf.main_app.vi._moduleconf.addSkel()
181 skel["key"] = db.Key(MODULECONF_KINDNAME, module_name)
182 skel["name"] = module_name
183 skel.write()
185 # Collect children
186 collect_modules(module, depth=depth + 1, prefix=f"{module_name}.")
188 collect_modules(conf.main_app.vi)
189 # TODO: Remove entries from MODULECONF_KINDNAME which are in db_module_names but not in ModuleConf.MODULES