Coverage for cc_modules/tests/cc_report_tests.py: 34%
86 statements
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-30 13:48 +0000
« prev ^ index » next coverage.py v7.6.10, created at 2025-01-30 13:48 +0000
1"""
2camcops_server/cc_modules/tests/cc_report_tests.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
11 CamCOPS is free software: you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation, either version 3 of the License, or
14 (at your option) any later version.
16 CamCOPS is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
21 You should have received a copy of the GNU General Public License
22 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
24===============================================================================
26"""
28import csv
29import io
30import logging
31from typing import Optional, TYPE_CHECKING
33from cardinal_pythonlib.classes import classproperty
34from cardinal_pythonlib.logs import BraceStyleAdapter
35from cardinal_pythonlib.pyramid.responses import (
36 OdsResponse,
37 TsvResponse,
38 XlsxResponse,
39)
40from deform.form import Form
41from pyramid.httpexceptions import HTTPBadRequest
42from pyramid.response import Response
43from sqlalchemy.orm.query import Query
44from sqlalchemy.sql.selectable import SelectBase
46from camcops_server.cc_modules.cc_report import (
47 get_all_report_classes,
48 PlainReportType,
49 Report,
50)
51from camcops_server.cc_modules.cc_unittest import (
52 DemoDatabaseTestCase,
53 DemoRequestTestCase,
54)
55from camcops_server.cc_modules.cc_validators import (
56 validate_alphanum_underscore,
57)
59if TYPE_CHECKING:
60 from camcops_server.cc_modules.cc_forms import ( # noqa: F401
61 ReportParamForm,
62 ReportParamSchema,
63 )
64 from camcops_server.cc_modules.cc_request import (
65 CamcopsRequest,
66 )
68log = BraceStyleAdapter(logging.getLogger(__name__))
71# =============================================================================
72# Unit testing
73# =============================================================================
76class AllReportTests(DemoDatabaseTestCase):
77 """
78 Unit tests.
79 """
81 def test_reports(self) -> None:
82 self.announce("test_reports")
83 from camcops_server.cc_modules.cc_forms import ( # noqa: F811
84 ReportParamSchema,
85 )
87 req = self.req
88 for cls in get_all_report_classes(req):
89 log.info("Testing report: {}", cls)
91 report = cls()
93 self.assertIsInstance(report.report_id, str)
94 validate_alphanum_underscore(report.report_id)
95 self.assertIsInstance(report.title(req), str)
96 self.assertIsInstance(report.superuser_only, bool)
98 querydict = report.get_test_querydict()
99 # We can't use req.params.update(querydict); we get
100 # "NestedMultiDict objects are read-only". We can't replace
101 # req.params ("can't set attribute"). Making a fresh request is
102 # also a pain, as they are difficult to initialize properly.
103 # However, achievable with some hacking to make "params" writable;
104 # see CamcopsDummyRequest.
105 # Also: we must use self.req as this has the correct database
106 # session.
107 req = self.req
108 req.clear_get_params() # as we're re-using old requests
109 req.add_get_params(querydict)
111 try:
112 q = report.get_query(req)
113 assert (
114 q is None
115 or isinstance(q, SelectBase)
116 or isinstance(q, Query)
117 ), (
118 f"get_query() method of class {cls} returned {q} which is "
119 f"of type {type(q)}"
120 )
121 except HTTPBadRequest:
122 pass
124 try:
125 self.assertIsInstanceOrNone(
126 report.get_rows_colnames(req), PlainReportType
127 )
128 except HTTPBadRequest:
129 pass
131 cls = report.get_paramform_schema_class()
132 assert issubclass(cls, ReportParamSchema)
134 self.assertIsInstance(report.get_form(req), Form)
136 try:
137 self.assertIsInstance(report.get_response(req), Response)
138 except HTTPBadRequest:
139 pass
142class TestReport(Report):
143 # noinspection PyMethodParameters
144 @classproperty
145 def report_id(cls) -> str:
146 return "test_report"
148 @classmethod
149 def title(cls, req: "CamcopsRequest") -> str:
150 return "Test report"
152 def get_rows_colnames(
153 self, req: "CamcopsRequest"
154 ) -> Optional[PlainReportType]:
155 rows = [
156 ["one", "two", "three"],
157 ["eleven", "twelve", "thirteen"],
158 ["twenty-one", "twenty-two", "twenty-three"],
159 ]
161 column_names = ["column 1", "column 2", "column 3"]
163 return PlainReportType(rows=rows, column_names=column_names)
166class ReportSpreadsheetTests(DemoRequestTestCase):
167 def test_render_xlsx(self) -> None:
168 report = TestReport()
170 response = report.render_xlsx(self.req)
171 self.assertIsInstance(response, XlsxResponse)
173 self.assertIn(
174 "filename=CamCOPS_test_report", response.content_disposition
175 )
177 self.assertIn(".xlsx", response.content_disposition)
179 def test_render_ods(self) -> None:
180 report = TestReport()
182 response = report.render_ods(self.req)
183 self.assertIsInstance(response, OdsResponse)
185 self.assertIn(
186 "filename=CamCOPS_test_report", response.content_disposition
187 )
189 self.assertIn(".ods", response.content_disposition)
191 def test_render_tsv(self) -> None:
192 report = TestReport()
194 response = report.render_tsv(self.req)
195 self.assertIsInstance(response, TsvResponse)
197 self.assertIn(
198 "filename=CamCOPS_test_report", response.content_disposition
199 )
201 self.assertIn(".tsv", response.content_disposition)
203 reader = csv.reader(
204 io.StringIO(response.body.decode()), dialect="excel-tab"
205 )
207 headings = next(reader)
208 row_1 = next(reader)
210 self.assertEqual(headings, ["column 1", "column 2", "column 3"])
211 self.assertEqual(row_1, ["one", "two", "three"])