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

126 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-24 16:36 -0500

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 

36log = logging.getLogger(__name__) 

37 

38 

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

40 """ 

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

42 ``reports``. 

43 

44 Notable URLs provided by this class: 

45 

46 * ``/reports/`` 

47 * ``/reports/XXX`` 

48 """ 

49 

50 model_name = "report" 

51 model_title = "Report" 

52 model_key = "report_key" 

53 filterable = False 

54 sort_on_backend = False 

55 creatable = False 

56 editable = False 

57 deletable = False 

58 route_prefix = "reports" 

59 template_prefix = "/reports" 

60 

61 grid_columns = [ 

62 "report_title", 

63 "help_text", 

64 "report_key", 

65 ] 

66 

67 form_fields = [ 

68 "help_text", 

69 ] 

70 

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

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

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

74 

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

76 self, columns=None, session=None 

77 ): 

78 """ """ 

79 data = [] 

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

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

82 return data 

83 

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

85 """ """ 

86 return { 

87 "report_key": report.report_key, 

88 "report_title": report.report_title, 

89 "help_text": report.__doc__, 

90 } 

91 

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

93 """ """ 

94 g = grid 

95 super().configure_grid(g) 

96 

97 # report_key 

98 g.set_link("report_key") 

99 

100 # report_title 

101 g.set_link("report_title") 

102 g.set_searchable("report_title") 

103 

104 # help_text 

105 g.set_searchable("help_text") 

106 

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

108 self, **kwargs 

109 ): 

110 """ """ 

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

112 report = self.report_handler.get_report(key) 

113 if report: 

114 return self.normalize_report(report) 

115 

116 raise self.notfound() 

117 

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

119 """ """ 

120 report = instance 

121 return report["report_title"] 

122 

123 def view(self): 

124 """ 

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

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

127 it. 

128 """ 

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

130 report = self.report_handler.get_report(key) 

131 normal = self.normalize_report(report) 

132 

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

134 form = self.make_model_form( 

135 normal, 

136 action_method="get", 

137 action_url=report_url, 

138 cancel_url=self.get_index_url(), 

139 show_button_reset=True, 

140 reset_url=report_url, 

141 button_label_submit="Run Report", 

142 button_icon_submit="arrow-circle-right", 

143 ) 

144 

145 context = { 

146 "instance": normal, 

147 "report": report, 

148 "form": form, 

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

150 } 

151 

152 if self.request.GET: 

153 form.show_button_cancel = False 

154 context = self.run_report(report, context) 

155 

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

157 

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

159 """ """ 

160 f = form 

161 super().configure_form(f) 

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

163 report = self.report_handler.get_report(key) 

164 

165 # help_text 

166 f.set_readonly("help_text") 

167 

168 # add widget fields for all report params 

169 schema = f.get_schema() 

170 report.add_params(schema) 

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

172 

173 def run_report(self, report, context): 

174 """ 

175 Run the given report and update view template context. 

176 

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

178 

179 :param report: 

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

181 to run. 

182 

183 :param context: Current view template context. 

184 

185 :returns: Final view template context. 

186 """ 

187 form = context["form"] 

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

189 

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

191 # otherwise its absence screws things up. why? 

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

193 

194 dform = form.get_deform() 

195 try: 

196 params = dform.validate(controls) 

197 except deform.ValidationFailure: 

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

199 return context 

200 

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

202 

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

204 context["report_columns"] = columns 

205 

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

207 if format_cols: 

208 for record in data["data"]: 

209 for column in format_cols: 

210 if column["name"] in record: 

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

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

213 

214 params.pop("help_text") 

215 context["report_params"] = params 

216 context["report_data"] = data 

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

218 return context 

219 

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

221 """ """ 

222 normal = [] 

223 for column in columns: 

224 if isinstance(column, str): 

225 column = {"name": column} 

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

227 normal.append(column) 

228 return normal 

229 

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

231 """ """ 

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

233 report = self.report_handler.get_report(key) 

234 params = dict(self.request.GET) 

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

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

237 return params, columns, data 

238 

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

240 """ """ 

241 tempdir = tempfile.mkdtemp() 

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

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

244 

245 @classmethod 

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

247 """ """ 

248 cls._defaults(config) 

249 cls._report_defaults(config) 

250 

251 @classmethod 

252 def _report_defaults(cls, config): 

253 permission_prefix = cls.get_permission_prefix() 

254 model_title = cls.get_model_title() 

255 

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

257 config.add_wutta_permission( 

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

259 ) 

260 

261 # separate permission to download report files 

262 config.add_wutta_permission( 

263 permission_prefix, 

264 f"{permission_prefix}.download", 

265 f"Download {model_title}", 

266 ) 

267 

268 

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

270 base = globals() 

271 

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

273 "ReportView", base["ReportView"] 

274 ) 

275 ReportView.defaults(config) 

276 

277 

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

279 defaults(config)