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

122 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-01-11 23:59 -0600

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

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

3# 

4# wuttaweb -- Web App for Wutta Framework 

5# Copyright © 2024 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): 

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 model_title = "Report" 

51 model_key = 'report_key' 

52 filterable = False 

53 sort_on_backend = False 

54 creatable = False 

55 editable = False 

56 deletable = False 

57 route_prefix = 'reports' 

58 template_prefix = '/reports' 

59 

60 grid_columns = [ 

61 'report_title', 

62 'help_text', 

63 'report_key', 

64 ] 

65 

66 form_fields = [ 

67 'help_text', 

68 ] 

69 

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

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

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

73 

74 def get_grid_data(self, columns=None, session=None): 

75 """ """ 

76 data = [] 

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

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

79 return data 

80 

81 def normalize_report(self, report): 

82 """ """ 

83 return { 

84 'report_key': report.report_key, 

85 'report_title': report.report_title, 

86 'help_text': report.__doc__, 

87 } 

88 

89 def configure_grid(self, g): 

90 """ """ 

91 super().configure_grid(g) 

92 

93 # report_key 

94 g.set_link('report_key') 

95 

96 # report_title 

97 g.set_link('report_title') 

98 g.set_searchable('report_title') 

99 

100 # help_text 

101 g.set_searchable('help_text') 

102 

103 def get_instance(self): 

104 """ """ 

105 key = self.request.matchdict['report_key'] 

106 report = self.report_handler.get_report(key) 

107 if report: 

108 return self.normalize_report(report) 

109 

110 raise self.notfound() 

111 

112 def get_instance_title(self, report): 

113 """ """ 

114 return report['report_title'] 

115 

116 def view(self): 

117 """ 

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

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

120 it. 

121 """ 

122 key = self.request.matchdict['report_key'] 

123 report = self.report_handler.get_report(key) 

124 normal = self.normalize_report(report) 

125 

126 report_url = self.get_action_url('view', normal) 

127 form = self.make_model_form(normal, 

128 action_method='get', 

129 action_url=report_url, 

130 cancel_url=self.get_index_url(), 

131 show_button_reset=True, 

132 reset_url=report_url, 

133 button_label_submit="Run Report", 

134 button_icon_submit='arrow-circle-right') 

135 

136 context = { 

137 'instance': normal, 

138 'report': report, 

139 'form': form, 

140 'xref_buttons': self.get_xref_buttons(report), 

141 } 

142 

143 if self.request.GET: 

144 form.show_button_cancel = False 

145 context = self.run_report(report, context) 

146 

147 return self.render_to_response('view', context) 

148 

149 def configure_form(self, f): 

150 """ """ 

151 super().configure_form(f) 

152 key = self.request.matchdict['report_key'] 

153 report = self.report_handler.get_report(key) 

154 

155 # help_text 

156 f.set_readonly('help_text') 

157 

158 # add widget fields for all report params 

159 schema = f.get_schema() 

160 report.add_params(schema) 

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

162 

163 def run_report(self, report, context): 

164 """ 

165 Run the given report and update view template context. 

166 

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

168 

169 :param report: 

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

171 to run. 

172 

173 :param context: Current view template context. 

174 

175 :returns: Final view template context. 

176 """ 

177 form = context['form'] 

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

179 

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

181 # otherwise its absence screws things up. why? 

182 controls.append(('help_text', report.__doc__)) 

183 

184 dform = form.get_deform() 

185 try: 

186 params = dform.validate(controls) 

187 except deform.ValidationFailure: 

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

189 return context 

190 

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

192 

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

194 context['report_columns'] = columns 

195 

196 format_cols = [col for col in columns if col.get('formatter')] 

197 if format_cols: 

198 for record in data['data']: 

199 for column in format_cols: 

200 if column['name'] in record: 

201 value = record[column['name']] 

202 record[column['name']] = column['formatter'](value) 

203 

204 params.pop('help_text') 

205 context['report_params'] = params 

206 context['report_data'] = data 

207 context['report_generated'] = datetime.datetime.now() 

208 return context 

209 

210 def normalize_columns(self, columns): 

211 """ """ 

212 normal = [] 

213 for column in columns: 

214 if isinstance(column, str): 

215 column = {'name': column} 

216 column.setdefault('label', column['name']) 

217 normal.append(column) 

218 return normal 

219 

220 def get_download_data(self): 

221 """ """ 

222 key = self.request.matchdict['report_key'] 

223 report = self.report_handler.get_report(key) 

224 params = dict(self.request.GET) 

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

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

227 return params, columns, data 

228 

229 def get_download_path(self, data, ext): 

230 """ """ 

231 tempdir = tempfile.mkdtemp() 

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

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

234 

235 @classmethod 

236 def defaults(cls, config): 

237 """ """ 

238 cls._defaults(config) 

239 cls._report_defaults(config) 

240 

241 @classmethod 

242 def _report_defaults(cls, config): 

243 permission_prefix = cls.get_permission_prefix() 

244 model_title = cls.get_model_title() 

245 

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

247 config.add_wutta_permission(permission_prefix, 

248 f'{permission_prefix}.view', 

249 f"View / run {model_title}") 

250 

251 

252 # separate permission to download report files 

253 config.add_wutta_permission(permission_prefix, 

254 f'{permission_prefix}.download', 

255 f"Download {model_title}") 

256 

257 

258def defaults(config, **kwargs): 

259 base = globals() 

260 

261 ReportView = kwargs.get('ReportView', base['ReportView']) 

262 ReportView.defaults(config) 

263 

264 

265def includeme(config): 

266 defaults(config)