Coverage for .tox / coverage / lib / python3.11 / site-packages / wuttaweb / views / reports.py: 100%

126 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2025-12-31 19:25 -0600

1# -*- coding: utf-8; -*- 

2################################################################################ 

3# 

4# wuttaweb -- Web App for Wutta Framework 

5# Copyright © 2024-2025 Lance Edgar 

6# 

7# This file is part of Wutta Framework. 

8# 

9# Wutta Framework is free software: you can redistribute it and/or modify it 

10# under the terms of the GNU General Public License as published by the Free 

11# Software Foundation, either version 3 of the License, or (at your option) any 

12# later version. 

13# 

14# Wutta Framework is distributed in the hope that it will be useful, but 

15# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 

16# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 

17# more details. 

18# 

19# You should have received a copy of the GNU General Public License along with 

20# Wutta Framework. If not, see <http://www.gnu.org/licenses/>. 

21# 

22################################################################################ 

23""" 

24Report Views 

25""" 

26 

27import datetime 

28import logging 

29import os 

30import tempfile 

31 

32import deform 

33 

34from wuttaweb.views import MasterView 

35 

36 

37log = logging.getLogger(__name__) 

38 

39 

40class ReportView(MasterView): # pylint: disable=abstract-method 

41 """ 

42 Master view for :term:`reports <report>`; route prefix is 

43 ``reports``. 

44 

45 Notable URLs provided by this class: 

46 

47 * ``/reports/`` 

48 * ``/reports/XXX`` 

49 """ 

50 

51 model_name = "report" 

52 model_title = "Report" 

53 model_key = "report_key" 

54 filterable = False 

55 sort_on_backend = False 

56 creatable = False 

57 editable = False 

58 deletable = False 

59 route_prefix = "reports" 

60 template_prefix = "/reports" 

61 

62 grid_columns = [ 

63 "report_title", 

64 "help_text", 

65 "report_key", 

66 ] 

67 

68 form_fields = [ 

69 "help_text", 

70 ] 

71 

72 def __init__(self, request, context=None): 

73 super().__init__(request, context=context) 

74 self.report_handler = self.app.get_report_handler() 

75 

76 def get_grid_data( # pylint: disable=empty-docstring 

77 self, columns=None, session=None 

78 ): 

79 """ """ 

80 data = [] 

81 for report in self.report_handler.get_reports().values(): 

82 data.append(self.normalize_report(report)) 

83 return data 

84 

85 def normalize_report(self, report): # pylint: disable=empty-docstring 

86 """ """ 

87 return { 

88 "report_key": report.report_key, 

89 "report_title": report.report_title, 

90 "help_text": report.__doc__, 

91 } 

92 

93 def configure_grid(self, grid): # pylint: disable=empty-docstring 

94 """ """ 

95 g = grid 

96 super().configure_grid(g) 

97 

98 # report_key 

99 g.set_link("report_key") 

100 

101 # report_title 

102 g.set_link("report_title") 

103 g.set_searchable("report_title") 

104 

105 # help_text 

106 g.set_searchable("help_text") 

107 

108 def get_instance( # pylint: disable=empty-docstring,arguments-differ,unused-argument 

109 self, **kwargs 

110 ): 

111 """ """ 

112 key = self.request.matchdict["report_key"] 

113 report = self.report_handler.get_report(key) 

114 if report: 

115 return self.normalize_report(report) 

116 

117 raise self.notfound() 

118 

119 def get_instance_title(self, instance): # pylint: disable=empty-docstring 

120 """ """ 

121 report = instance 

122 return report["report_title"] 

123 

124 def view(self): 

125 """ 

126 This lets user "view" the report but in this context that 

127 means showing them a form with report params, so they can run 

128 it. 

129 """ 

130 key = self.request.matchdict["report_key"] 

131 report = self.report_handler.get_report(key) 

132 normal = self.normalize_report(report) 

133 

134 report_url = self.get_action_url("view", normal) 

135 form = self.make_model_form( 

136 normal, 

137 action_method="get", 

138 action_url=report_url, 

139 cancel_url=self.get_index_url(), 

140 show_button_reset=True, 

141 reset_url=report_url, 

142 button_label_submit="Run Report", 

143 button_icon_submit="arrow-circle-right", 

144 ) 

145 

146 context = { 

147 "instance": normal, 

148 "report": report, 

149 "form": form, 

150 "xref_buttons": self.get_xref_buttons(report), 

151 } 

152 

153 if self.request.GET: 

154 form.show_button_cancel = False 

155 context = self.run_report(report, context) 

156 

157 return self.render_to_response("view", context) 

158 

159 def configure_form(self, form): # pylint: disable=empty-docstring 

160 """ """ 

161 f = form 

162 super().configure_form(f) 

163 key = self.request.matchdict["report_key"] 

164 report = self.report_handler.get_report(key) 

165 

166 # help_text 

167 f.set_readonly("help_text") 

168 

169 # add widget fields for all report params 

170 schema = f.get_schema() 

171 report.add_params(schema) 

172 f.set_fields([node.name for node in schema.children]) 

173 

174 def run_report(self, report, context): 

175 """ 

176 Run the given report and update view template context. 

177 

178 This is called automatically from :meth:`view()`. 

179 

180 :param report: 

181 :class:`~wuttjamaican:wuttjamaican.reports.Report` instance 

182 to run. 

183 

184 :param context: Current view template context. 

185 

186 :returns: Final view template context. 

187 """ 

188 form = context["form"] 

189 controls = list(self.request.GET.items()) 

190 

191 # TODO: must re-inject help_text value for some reason, 

192 # otherwise its absence screws things up. why? 

193 controls.append(("help_text", report.__doc__)) 

194 

195 dform = form.get_deform() 

196 try: 

197 params = dform.validate(controls) 

198 except deform.ValidationFailure: 

199 log.debug("form not valid: %s", dform.error) 

200 return context 

201 

202 data = self.report_handler.make_report_data(report, params) 

203 

204 columns = self.normalize_columns(report.get_output_columns()) 

205 context["report_columns"] = columns 

206 

207 format_cols = [col for col in columns if col.get("formatter")] 

208 if format_cols: 

209 for record in data["data"]: 

210 for column in format_cols: 

211 if column["name"] in record: 

212 value = record[column["name"]] 

213 record[column["name"]] = column["formatter"](value) 

214 

215 params.pop("help_text") 

216 context["report_params"] = params 

217 context["report_data"] = data 

218 context["report_generated"] = datetime.datetime.now() 

219 return context 

220 

221 def normalize_columns(self, columns): # pylint: disable=empty-docstring 

222 """ """ 

223 normal = [] 

224 for column in columns: 

225 if isinstance(column, str): 

226 column = {"name": column} 

227 column.setdefault("label", column["name"]) 

228 normal.append(column) 

229 return normal 

230 

231 def get_download_data(self): # pylint: disable=empty-docstring 

232 """ """ 

233 key = self.request.matchdict["report_key"] 

234 report = self.report_handler.get_report(key) 

235 params = dict(self.request.GET) 

236 columns = self.normalize_columns(report.get_output_columns()) 

237 data = self.report_handler.make_report_data(report, params) 

238 return params, columns, data 

239 

240 def get_download_path(self, data, ext): # pylint: disable=empty-docstring 

241 """ """ 

242 tempdir = tempfile.mkdtemp() 

243 filename = f"{data['output_title']}.{ext}" 

244 return os.path.join(tempdir, filename) 

245 

246 @classmethod 

247 def defaults(cls, config): # pylint: disable=empty-docstring 

248 """ """ 

249 cls._defaults(config) 

250 cls._report_defaults(config) 

251 

252 @classmethod 

253 def _report_defaults(cls, config): 

254 permission_prefix = cls.get_permission_prefix() 

255 model_title = cls.get_model_title() 

256 

257 # overwrite title for "view" perm since it also implies "run" 

258 config.add_wutta_permission( 

259 permission_prefix, f"{permission_prefix}.view", f"View / run {model_title}" 

260 ) 

261 

262 # separate permission to download report files 

263 config.add_wutta_permission( 

264 permission_prefix, 

265 f"{permission_prefix}.download", 

266 f"Download {model_title}", 

267 ) 

268 

269 

270def defaults(config, **kwargs): # pylint: disable=missing-function-docstring 

271 base = globals() 

272 

273 ReportView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name 

274 "ReportView", base["ReportView"] 

275 ) 

276 ReportView.defaults(config) 

277 

278 

279def includeme(config): # pylint: disable=missing-function-docstring 

280 defaults(config)