Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/prototypes/skelmodule.py: 0%

77 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-02-07 19:28 +0000

1import os 

2import yaml 

3import logging 

4from viur.core import Module, db, current 

5from viur.core.decorators import * 

6from viur.core.config import conf 

7from viur.core.skeleton import skeletonByKind, Skeleton, SkeletonInstance 

8import typing as t 

9 

10 

11SINGLE_ORDER_TYPE = str | tuple[str, db.SortOrder] 

12""" 

13Type for exactly one sort order definitions. 

14""" 

15 

16ORDER_TYPE = SINGLE_ORDER_TYPE | tuple[SINGLE_ORDER_TYPE] | list[SINGLE_ORDER_TYPE] | dict[str, str | int] | None 

17""" 

18Type for sort order definitions (any amount of single order definitions). 

19""" 

20 

21DEFAULT_ORDER_TYPE = ORDER_TYPE | t.Callable[[db.Query], ORDER_TYPE] 

22""" 

23Type for default sort order definitions. 

24""" 

25 

26 

27def __load_indexes_from_file() -> dict[str, list]: 

28 """ 

29 Loads all indexes from the index.yaml and stores it in a dictionary sorted by the module(kind) 

30 :return A dictionary of indexes per module 

31 """ 

32 indexes_dict = {} 

33 try: 

34 with open(os.path.join(conf.instance.project_base_path, "index.yaml"), "r") as file: 

35 indexes = yaml.safe_load(file) 

36 indexes = indexes.get("indexes", []) 

37 for index in indexes or (): 

38 index["properties"] = [_property["name"] for _property in index["properties"]] 

39 indexes_dict.setdefault(index["kind"], []).append(index) 

40 

41 except FileNotFoundError: 

42 logging.warning("index.yaml not found") 

43 return {} 

44 

45 return indexes_dict 

46 

47 

48DATASTORE_INDEXES = __load_indexes_from_file() 

49 

50 

51class SkelModule(Module): 

52 """ 

53 This is the extended module prototype used by any other ViUR module prototype. 

54 It a prototype which generally is bound to some database model abstracted by the ViUR skeleton system. 

55 """ 

56 

57 kindName: str = None 

58 """ 

59 Name of the datastore kind that is handled by this module. 

60 

61 This information is used to bind a specific :class:`viur.core.skeleton.Skeleton`-class to this 

62 prototype. By default, it is automatically determined from the module's class name, so a module named 

63 `Animal` refers to a Skeleton named `AnimalSkel` and its kindName is `animal`. 

64 

65 For more information, refer to the function :func:`~_resolveSkelCls`. 

66 """ 

67 

68 default_order: DEFAULT_ORDER_TYPE = None 

69 """ 

70 Allows to specify a default order for this module, which is applied when no other order is specified. 

71 

72 Setting a default_order might result in the requirement of additional indexes, which are being raised 

73 and must be specified. 

74 """ 

75 

76 def __init__(self, *args, **kwargs): 

77 super().__init__(*args, **kwargs) 

78 

79 # automatically determine kindName when not set 

80 if self.kindName is None: 

81 self.kindName = str(type(self).__name__).lower() 

82 

83 # assign index descriptions from index.yaml 

84 self.indexes = DATASTORE_INDEXES.get(self.kindName, []) 

85 

86 def _resolveSkelCls(self, *args, **kwargs) -> t.Type[Skeleton]: 

87 """ 

88 Retrieve the generally associated :class:`viur.core.skeleton.Skeleton` that is used by 

89 the application. 

90 

91 This is either be defined by the member variable *kindName* or by a Skeleton named like the 

92 application class in lower-case order. 

93 

94 If this behavior is not wanted, it can be definitely overridden by defining module-specific 

95 :func:`~viewSkel`, :func:`~addSkel`, or :func:`~editSkel` functions, or by overriding this 

96 function in general. 

97 

98 :return: Returns a Skeleton class that matches the application. 

99 """ 

100 return skeletonByKind(self.kindName) 

101 

102 def baseSkel(self, *args, **kwargs) -> SkeletonInstance: 

103 """ 

104 Returns an instance of an unmodified base skeleton for this module. 

105 

106 This function should only be used in cases where a full, unmodified skeleton of the module is required, e.g. 

107 for administrative or maintenance purposes. 

108 

109 By default, baseSkel is used by :func:`~viewSkel`, :func:`~addSkel`, and :func:`~editSkel`. 

110 """ 

111 return self._resolveSkelCls(*args, **kwargs)() 

112 

113 def _apply_default_order(self, query: db.Query): 

114 """ 

115 Apply the setting from `default_order` to a given db.Query. 

116 

117 The `default_order` will only be applied when the query has no other order, or is on a multquery. 

118 """ 

119 

120 # Apply default_order when possible! 

121 if ( 

122 self.default_order 

123 and query.queries 

124 and not isinstance(query.queries, list) 

125 and not query.queries.orders 

126 and not current.request.get().kwargs.get("search") 

127 ): 

128 if callable(default_order := self.default_order): 

129 default_order = default_order(query) 

130 

131 if isinstance(default_order, dict): 

132 logging.debug(f"Applying filter {default_order=}") 

133 query.mergeExternalFilter(default_order) 

134 

135 elif default_order: 

136 logging.debug(f"Applying {default_order=}") 

137 

138 # FIXME: This ugly test can be removed when there is type that abstracts SortOrders 

139 if ( 

140 isinstance(default_order, str) 

141 or ( 

142 isinstance(default_order, tuple) 

143 and len(default_order) == 2 

144 and isinstance(default_order[0], str) 

145 and isinstance(default_order[1], db.SortOrder) 

146 ) 

147 ): 

148 query.order(default_order) 

149 else: 

150 query.order(*default_order) 

151 

152 @force_ssl 

153 @force_post 

154 @exposed 

155 @skey 

156 @access("root") 

157 def add_or_edit(self, key: db.Key | int | str, **kwargs) -> t.Any: 

158 """ 

159 This function is intended to be used by importers. 

160 Only "root"-users are allowed to use it. 

161 """ 

162 db_key = db.keyHelper(key, targetKind=self.kindName, adjust_kind=self.kindName) 

163 is_add = not bool(db.Get(db_key)) 

164 

165 if is_add: 

166 skel = self.addSkel() 

167 else: 

168 skel = self.editSkel() 

169 

170 skel["key"] = db_key 

171 

172 if ( 

173 not kwargs # no data supplied 

174 or not skel.fromClient(kwargs) # failure on reading into the bones 

175 ): 

176 # render the skeleton in the version it could as far as it could be read. 

177 return self.render.render("add_or_edit", skel) 

178 

179 if is_add: 

180 self.onAdd(skel) 

181 else: 

182 self.onEdit(skel) 

183 

184 skel.write() 

185 

186 if is_add: 

187 self.onAdded(skel) 

188 return self.render.addSuccess(skel) 

189 

190 self.onEdited(skel) 

191 return self.render.editSuccess(skel)