Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/render/json/default.py: 0%

125 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-27 07:59 +0000

1import json 

2import typing as t 

3from enum import Enum 

4 

5from viur.core import bones, db, current 

6from viur.core.render.abstract import AbstractRenderer 

7from viur.core.skeleton import SkeletonInstance 

8from viur.core.i18n import translate 

9from viur.core.config import conf 

10from datetime import datetime 

11 

12 

13class CustomJsonEncoder(json.JSONEncoder): 

14 """ 

15 This custom JSON-Encoder for this json-render ensures that translations are evaluated and can be dumped. 

16 """ 

17 

18 def default(self, o: t.Any) -> t.Any: 

19 

20 if isinstance(o, translate): 

21 return str(o) 

22 elif isinstance(o, datetime): 

23 return o.isoformat() 

24 elif isinstance(o, db.Key): 

25 return str(o) 

26 elif isinstance(o, Enum): 

27 return o.value 

28 elif isinstance(o, set): 

29 return tuple(o) 

30 elif isinstance(o, SkeletonInstance): 

31 return {bone_name: o[bone_name] for bone_name in o} 

32 return json.JSONEncoder.default(self, o) 

33 

34 

35class DefaultRender(AbstractRenderer): 

36 kind = "json" 

37 

38 @staticmethod 

39 def render_structure(structure: dict): 

40 """ 

41 Performs structure rewriting according to VIUR2/3 compatibility flags. 

42 # fixme: Remove this entire function with VIUR4 

43 """ 

44 for struct in structure.values(): 

45 # Optionally replace new-key by a copy of the value under the old-key 

46 if "json.bone.structure.camelcasenames" in conf.compatibility: 

47 for find, replace in { 

48 "boundslat": "boundsLat", 

49 "boundslng": "boundsLng", 

50 "emptyvalue": "emptyValue", 

51 "max": "maxAmount", 

52 "maxlength": "maxLength", 

53 "min": "minAmount", 

54 "preventduplicates": "preventDuplicates", 

55 "readonly": "readOnly", 

56 "valid_html": "validHtml", 

57 "valid_mime_types": "validMimeTypes", 

58 }.items(): 

59 if find in struct: 

60 struct[replace] = struct[find] 

61 

62 # Call render_structure() recursively on "using" and "relskel" members. 

63 for substruct in ("using", "relskel"): 

64 if substruct in struct and struct[substruct]: 

65 struct[substruct] = DefaultRender.render_structure(struct[substruct]) 

66 

67 # Optionally return list of tuples instead of dict 

68 if "json.bone.structure.keytuples" in conf.compatibility: 

69 return [(key, struct) for key, struct in structure.items()] 

70 

71 return structure 

72 

73 def renderSingleBoneValue(self, value: t.Any, 

74 bone: bones.BaseBone, 

75 skel: SkeletonInstance, 

76 key 

77 ) -> dict | str | None: 

78 """ 

79 Renders the value of a bone. 

80 

81 It can be overridden and super-called from a custom renderer. 

82 

83 :param bone: The bone which value should be rendered. 

84 :type bone: Any bone that inherits from :class:`server.bones.base.BaseBone`. 

85 

86 :return: A dict containing the rendered attributes. 

87 """ 

88 if isinstance(bone, bones.RelationalBone): 

89 if isinstance(value, dict): 

90 return { 

91 "dest": self.renderSkelValues(value["dest"], injectDownloadURL=isinstance(bone, bones.FileBone)), 

92 "rel": (self.renderSkelValues(value["rel"], injectDownloadURL=isinstance(bone, bones.FileBone)) 

93 if value["rel"] else None), 

94 } 

95 elif isinstance(bone, bones.RecordBone): 

96 return self.renderSkelValues(value) 

97 elif isinstance(bone, bones.PasswordBone): 

98 return "" 

99 else: 

100 return value 

101 return None 

102 

103 def renderBoneValue(self, bone: bones.BaseBone, skel: SkeletonInstance, key: str) -> list | dict | None: 

104 boneVal = skel[key] 

105 if bone.languages and bone.multiple: 

106 res = {} 

107 for language in bone.languages: 

108 if boneVal and language in boneVal and boneVal[language]: 

109 res[language] = [self.renderSingleBoneValue(v, bone, skel, key) for v in boneVal[language]] 

110 else: 

111 res[language] = [] 

112 elif bone.languages: 

113 res = {} 

114 for language in bone.languages: 

115 if boneVal and language in boneVal and boneVal[language] is not None: 

116 res[language] = self.renderSingleBoneValue(boneVal[language], bone, skel, key) 

117 else: 

118 res[language] = None 

119 elif bone.multiple: 

120 res = [self.renderSingleBoneValue(v, bone, skel, key) for v in boneVal] if boneVal else None 

121 else: 

122 res = self.renderSingleBoneValue(boneVal, bone, skel, key) 

123 return res 

124 

125 def renderSkelValues(self, skel: SkeletonInstance, injectDownloadURL: bool = False) -> t.Optional[dict]: 

126 """ 

127 Prepares values of one :class:`viur.core.skeleton.Skeleton` or a list of skeletons for output. 

128 

129 :param skel: Skeleton which contents will be processed. 

130 """ 

131 if skel is None: 

132 return None 

133 elif isinstance(skel, dict): 

134 return skel 

135 

136 res = {} 

137 

138 for key, bone in skel.items(): 

139 res[key] = self.renderBoneValue(bone, skel, key) 

140 

141 if ( 

142 injectDownloadURL 

143 and (file := getattr(conf.main_app, "file", None)) 

144 and "dlkey" in skel 

145 and "name" in skel 

146 ): 

147 res["downloadUrl"] = file.create_download_url( 

148 skel["dlkey"], 

149 skel["name"], 

150 expires=conf.render_json_download_url_expiration 

151 ) 

152 return res 

153 

154 def renderEntry(self, skel: SkeletonInstance, actionName, params=None): 

155 structure = None 

156 errors = None 

157 

158 if isinstance(skel, list): 

159 vals = [self.renderSkelValues(x) for x in skel] 

160 if isinstance(skel[0], SkeletonInstance): 

161 structure = DefaultRender.render_structure(skel[0].structure()) 

162 

163 elif isinstance(skel, SkeletonInstance): 

164 vals = self.renderSkelValues(skel) 

165 structure = DefaultRender.render_structure(skel.structure()) 

166 errors = [{"severity": x.severity.value, "fieldPath": x.fieldPath, "errorMessage": x.errorMessage, 

167 "invalidatedFields": x.invalidatedFields} for x in skel.errors] 

168 

169 else: # Hopefully we can pass it directly... 

170 vals = skel 

171 

172 res = { 

173 "action": actionName, 

174 "errors": errors, 

175 "params": params, 

176 "structure": structure, 

177 "values": vals, 

178 } 

179 

180 current.request.get().response.headers["Content-Type"] = "application/json" 

181 return json.dumps(res, cls=CustomJsonEncoder) 

182 

183 def view(self, skel: SkeletonInstance, action: str = "view", params=None, **kwargs): 

184 return self.renderEntry(skel, action, params) 

185 

186 def list(self, skellist, action: str = "list", params=None, **kwargs): 

187 # Rendering the structure in lists is flagged as deprecated 

188 structure = None 

189 cursor = None 

190 orders = None 

191 

192 if skellist: 

193 if isinstance(skellist[0], SkeletonInstance): 

194 if "json.bone.structure.inlists" in conf.compatibility: 

195 structure = DefaultRender.render_structure(skellist[0].structure()) 

196 

197 cursor = skellist.getCursor() 

198 orders = skellist.get_orders() 

199 

200 skellist = [self.renderSkelValues(skel) for skel in skellist] 

201 else: 

202 skellist = [] 

203 

204 # VIUR4 ;-) 

205 # loc = locals() 

206 # res = {k: loc[k] for k in ("action", "cursor", "params", "skellist", "structure", "orders") if loc[k]} 

207 

208 res = { 

209 "action": action, 

210 "cursor": cursor, 

211 "params": params, 

212 "skellist": skellist, 

213 "structure": structure, 

214 "orders": orders 

215 } 

216 

217 current.request.get().response.headers["Content-Type"] = "application/json" 

218 return json.dumps(res, cls=CustomJsonEncoder) 

219 

220 def add(self, skel: SkeletonInstance, action: str = "add", params=None, **kwargs): 

221 return self.renderEntry(skel, action, params) 

222 

223 def edit(self, skel: SkeletonInstance, action: str = "edit", params=None, **kwargs): 

224 return self.renderEntry(skel, action, params) 

225 

226 def editSuccess(self, skel: SkeletonInstance, action: str = "editSuccess", params=None, **kwargs): 

227 return self.renderEntry(skel, action, params) 

228 

229 def addSuccess(self, skel: SkeletonInstance, action: str = "addSuccess", params=None, **kwargs): 

230 return self.renderEntry(skel, action, params) 

231 

232 def deleteSuccess(self, skel: SkeletonInstance, params=None, *args, **kwargs): 

233 return json.dumps("OKAY") 

234 

235 def listRootNodes(self, rootNodes, *args, **kwargs): 

236 current.request.get().response.headers["Content-Type"] = "application/json" 

237 return json.dumps(rootNodes, cls=CustomJsonEncoder) 

238 

239 def render(self, action: str, skel: t.Optional[SkeletonInstance] = None, **kwargs): 

240 """ 

241 Universal rendering function. 

242 

243 Handles an action and a skeleton. It shall be used by any action, in future. 

244 """ 

245 return self.renderEntry(skel, action, params=kwargs)