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
« 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"""
27import datetime
28import logging
29import os
30import tempfile
32import deform
34from wuttaweb.views import MasterView
36log = logging.getLogger(__name__)
39class ReportView(MasterView): # pylint: disable=abstract-method
40 """
41 Master view for :term:`reports <report>`; route prefix is
42 ``reports``.
44 Notable URLs provided by this class:
46 * ``/reports/``
47 * ``/reports/XXX``
48 """
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"
61 grid_columns = [
62 "report_title",
63 "help_text",
64 "report_key",
65 ]
67 form_fields = [
68 "help_text",
69 ]
71 def __init__(self, request, context=None):
72 super().__init__(request, context=context)
73 self.report_handler = self.app.get_report_handler()
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
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 }
92 def configure_grid(self, grid): # pylint: disable=empty-docstring
93 """ """
94 g = grid
95 super().configure_grid(g)
97 # report_key
98 g.set_link("report_key")
100 # report_title
101 g.set_link("report_title")
102 g.set_searchable("report_title")
104 # help_text
105 g.set_searchable("help_text")
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)
116 raise self.notfound()
118 def get_instance_title(self, instance): # pylint: disable=empty-docstring
119 """ """
120 report = instance
121 return report["report_title"]
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)
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 )
145 context = {
146 "instance": normal,
147 "report": report,
148 "form": form,
149 "xref_buttons": self.get_xref_buttons(report),
150 }
152 if self.request.GET:
153 form.show_button_cancel = False
154 context = self.run_report(report, context)
156 return self.render_to_response("view", context)
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)
165 # help_text
166 f.set_readonly("help_text")
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])
173 def run_report(self, report, context):
174 """
175 Run the given report and update view template context.
177 This is called automatically from :meth:`view()`.
179 :param report:
180 :class:`~wuttjamaican:wuttjamaican.reports.Report` instance
181 to run.
183 :param context: Current view template context.
185 :returns: Final view template context.
186 """
187 form = context["form"]
188 controls = list(self.request.GET.items())
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__))
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
201 data = self.report_handler.make_report_data(report, params)
203 columns = self.normalize_columns(report.get_output_columns())
204 context["report_columns"] = columns
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)
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
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
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
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)
245 @classmethod
246 def defaults(cls, config): # pylint: disable=empty-docstring
247 """ """
248 cls._defaults(config)
249 cls._report_defaults(config)
251 @classmethod
252 def _report_defaults(cls, config):
253 permission_prefix = cls.get_permission_prefix()
254 model_title = cls.get_model_title()
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 )
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 )
269def defaults(config, **kwargs): # pylint: disable=missing-function-docstring
270 base = globals()
272 ReportView = kwargs.get( # pylint: disable=invalid-name,redefined-outer-name
273 "ReportView", base["ReportView"]
274 )
275 ReportView.defaults(config)
278def includeme(config): # pylint: disable=missing-function-docstring
279 defaults(config)