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

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 

7 

8 

9MODULECONF_KINDNAME = "viur-module-conf" 

10 

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} 

14 

15 

16class ModuleConfScriptSkel(skeleton.RelSkel): 

17 

18 name = StringBone( 

19 descr="Label", 

20 required=True, 

21 params={ 

22 "tooltip": "Label for the action button displayed." 

23 }, 

24 ) 

25 

26 icon = StringBone( 

27 descr="Icon", 

28 params={ 

29 "tooltip": "Shoelace-conforming icon identifier." 

30 }, 

31 ) 

32 

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 ) 

48 

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 ) 

62 

63 

64class ModuleConfSkel(skeleton.Skeleton): 

65 kindName = MODULECONF_KINDNAME 

66 

67 name = StringBone( 

68 descr=i18n.translate("modulename"), 

69 readOnly=True, 

70 ) 

71 

72 help_text = TextBone( 

73 descr=i18n.translate("module helptext"), 

74 validHtml=_LIMITED_HTML, 

75 ) 

76 

77 help_text_add = TextBone( 

78 descr=i18n.translate("add helptext"), 

79 validHtml=_LIMITED_HTML, 

80 ) 

81 

82 help_text_edit = TextBone( 

83 descr=i18n.translate("edit helptext"), 

84 validHtml=_LIMITED_HTML, 

85 ) 

86 

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 ) 

99 

100 

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 

110 

111 def adminInfo(self): 

112 return conf.moduleconf_admin_info or {} 

113 

114 def canAdd(self): 

115 return False 

116 

117 def canDelete(self, skel): 

118 return False 

119 

120 def canEdit(self, skel): 

121 if super().canEdit(skel): 

122 return True 

123 

124 # Check for "manage"-flag on current user 

125 return (cuser := current.user.get()) and f"""{skel["name"]}-manage""" in cuser["access"] 

126 

127 def listFilter(self, query): 

128 original_query = query 

129 

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) 

135 

136 query = original_query 

137 query.filter("name IN", tuple(user_modules)) 

138 

139 return query 

140 

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 

148 

149 return skel 

150 

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() 

156 

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 

162 

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() 

182 

183 # Collect children 

184 collect_modules(module, depth=depth + 1, prefix=f"{module_name}.") 

185 

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