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
« 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"""
27import datetime
28import logging
29import os
30import tempfile
32import deform
34from wuttaweb.views import MasterView
37log = logging.getLogger(__name__)
40class ReportView(MasterView): # pylint: disable=abstract-method
41 """
42 Master view for :term:`reports <report>`; route prefix is
43 ``reports``.
45 Notable URLs provided by this class:
47 * ``/reports/``
48 * ``/reports/XXX``
49 """
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"
62 grid_columns = [
63 "report_title",
64 "help_text",
65 "report_key",
66 ]
68 form_fields = [
69 "help_text",
70 ]
72 def __init__(self, request, context=None):
73 super().__init__(request, context=context)
74 self.report_handler = self.app.get_report_handler()
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
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 }
93 def configure_grid(self, grid): # pylint: disable=empty-docstring
94 """ """
95 g = grid
96 super().configure_grid(g)
98 # report_key
99 g.set_link("report_key")
101 # report_title
102 g.set_link("report_title")
103 g.set_searchable("report_title")
105 # help_text
106 g.set_searchable("help_text")
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)
117 raise self.notfound()
119 def get_instance_title(self, instance): # pylint: disable=empty-docstring
120 """ """
121 report = instance
122 return report["report_title"]
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)
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 )
146 context = {
147 "instance": normal,
148 "report": report,
149 "form": form,
150 "xref_buttons": self.get_xref_buttons(report),
151 }
153 if self.request.GET:
154 form.show_button_cancel = False
155 context = self.run_report(report, context)
157 return self.render_to_response("view", context)
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)
166 # help_text
167 f.set_readonly("help_text")
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])
174 def run_report(self, report, context):
175 """
176 Run the given report and update view template context.
178 This is called automatically from :meth:`view()`.
180 :param report:
181 :class:`~wuttjamaican:wuttjamaican.reports.Report` instance
182 to run.
184 :param context: Current view template context.
186 :returns: Final view template context.
187 """
188 form = context["form"]
189 controls = list(self.request.GET.items())
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__))
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
202 data = self.report_handler.make_report_data(report, params)
204 columns = self.normalize_columns(report.get_output_columns())
205 context["report_columns"] = columns
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)
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
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
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
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)
246 @classmethod
247 def defaults(cls, config): # pylint: disable=empty-docstring
248 """ """
249 cls._defaults(config)
250 cls._report_defaults(config)
252 @classmethod
253 def _report_defaults(cls, config):
254 permission_prefix = cls.get_permission_prefix()
255 model_title = cls.get_model_title()
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 )
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 )
270def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
271 base = globals()
273 ReportView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
274 "ReportView", base["ReportView"]
275 )
276 ReportView.defaults(config)
279def includeme(config): # pylint: disable=missing-function-docstring
280 defaults(config)