Coverage for cc_modules/tests/cc_report_tests.py : 29%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
3"""
4camcops_server/cc_modules/tests/cc_report_tests.py
6===============================================================================
8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
10 This file is part of CamCOPS.
12 CamCOPS is free software: you can redistribute it and/or modify
13 it under the terms of the GNU General Public License as published by
14 the Free Software Foundation, either version 3 of the License, or
15 (at your option) any later version.
17 CamCOPS is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 GNU General Public License for more details.
22 You should have received a copy of the GNU General Public License
23 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
25===============================================================================
27"""
29import logging
30from typing import Generator, Optional, TYPE_CHECKING
32from cardinal_pythonlib.classes import classproperty
33from cardinal_pythonlib.logs import BraceStyleAdapter
34from cardinal_pythonlib.pyramid.responses import (
35 OdsResponse, TsvResponse, XlsxResponse,
36)
37from deform.form import Form
38import pendulum
39from pyramid.httpexceptions import HTTPBadRequest
40from pyramid.response import Response
41from sqlalchemy.orm.query import Query
42from sqlalchemy.sql.selectable import SelectBase
44from camcops_server.cc_modules.cc_report import (
45 AverageScoreReport,
46 get_all_report_classes,
47 PlainReportType,
48 Report,
49)
50from camcops_server.cc_modules.cc_unittest import (
51 DemoDatabaseTestCase,
52 DemoRequestTestCase,
53)
54from camcops_server.cc_modules.cc_validators import (
55 validate_alphanum_underscore,
56)
58if TYPE_CHECKING:
59 from camcops_server.cc_modules.cc_forms import ( # noqa: F401
60 ReportParamForm,
61 ReportParamSchema,
62 )
63 from camcops_server.cc_modules.cc_patient import Patient
64 from camcops_server.cc_modules.cc_patientidnum import PatientIdNum
65 from camcops_server.cc_modules.cc_request import CamcopsRequest # noqa: E501,F401
67log = BraceStyleAdapter(logging.getLogger(__name__))
70# =============================================================================
71# Unit testing
72# =============================================================================
74class AllReportTests(DemoDatabaseTestCase):
75 """
76 Unit tests.
77 """
78 def test_reports(self) -> None:
79 self.announce("test_reports")
80 req = self.req
81 for cls in get_all_report_classes(req):
82 log.info("Testing report: {}", cls)
83 from camcops_server.cc_modules.cc_forms import ReportParamSchema # noqa
84 report = cls()
86 self.assertIsInstance(report.report_id, str)
87 validate_alphanum_underscore(report.report_id)
88 self.assertIsInstance(report.title(req), str)
89 self.assertIsInstance(report.superuser_only, bool)
91 querydict = report.get_test_querydict()
92 # We can't use req.params.update(querydict); we get
93 # "NestedMultiDict objects are read-only". We can't replace
94 # req.params ("can't set attribute"). Making a fresh request is
95 # also a pain, as they are difficult to initialize properly.
96 # However, achievable with some hacking to make "params" writable;
97 # see CamcopsDummyRequest.
98 # Also: we must use self.req as this has the correct database
99 # session.
100 req = self.req
101 req.clear_get_params() # as we're re-using old requests
102 req.add_get_params(querydict)
104 try:
105 q = report.get_query(req)
106 assert (q is None or
107 isinstance(q, SelectBase) or
108 isinstance(q, Query)), (
109 f"get_query() method of class {cls} returned {q} which is "
110 f"of type {type(q)}"
111 )
112 except HTTPBadRequest:
113 pass
115 try:
116 self.assertIsInstanceOrNone(
117 report.get_rows_colnames(req), PlainReportType)
118 except HTTPBadRequest:
119 pass
121 cls = report.get_paramform_schema_class()
122 assert issubclass(cls, ReportParamSchema)
124 self.assertIsInstance(report.get_form(req), Form)
126 try:
127 self.assertIsInstance(report.get_response(req), Response)
128 except HTTPBadRequest:
129 pass
132class AverageScoreReportTestCase(DemoDatabaseTestCase):
133 def __init__(self, *args, **kwargs) -> None:
134 super().__init__(*args, **kwargs)
135 self.patient_id_sequence = self.get_patient_id()
136 self.task_id_sequence = self.get_task_id()
137 self.patient_idnum_id_sequence = self.get_patient_idnum_id()
139 def setUp(self) -> None:
140 super().setUp()
142 self.report = self.create_report()
144 def create_tasks(self):
145 pass
147 def create_report(self) -> AverageScoreReport:
148 raise NotImplementedError(
149 "Report TestCase needs to implement create_report"
150 )
152 @staticmethod
153 def get_patient_id() -> Generator[int, None, None]:
154 i = 1
156 while True:
157 yield i
158 i += 1
160 @staticmethod
161 def get_task_id() -> Generator[int, None, None]:
162 i = 1
164 while True:
165 yield i
166 i += 1
168 @staticmethod
169 def get_patient_idnum_id() -> Generator[int, None, None]:
170 i = 1
172 while True:
173 yield i
174 i += 1
176 def create_patient(self, idnum_value: int = 333) -> "Patient":
177 from camcops_server.cc_modules.cc_patient import Patient
178 patient = Patient()
179 patient.id = next(self.patient_id_sequence)
180 self._apply_standard_db_fields(patient)
182 patient.forename = f"Forename {patient.id}"
183 patient.surname = f"Surname {patient.id}"
184 patient.dob = pendulum.parse("1950-01-01")
185 self.dbsession.add(patient)
187 self.create_patient_idnum(patient, idnum_value)
189 self.dbsession.commit()
191 return patient
193 def create_patient_idnum(self, patient,
194 idnum_value: int = 333) -> "PatientIdNum":
195 from camcops_server.cc_modules.cc_patient import PatientIdNum
196 patient_idnum = PatientIdNum()
197 patient_idnum.id = next(self.patient_idnum_id_sequence)
198 self._apply_standard_db_fields(patient_idnum)
199 patient_idnum.patient_id = patient.id
200 patient_idnum.which_idnum = self.nhs_iddef.which_idnum
201 patient_idnum.idnum_value = idnum_value
202 self.dbsession.add(patient_idnum)
204 return patient_idnum
207class TestReport(Report):
208 # noinspection PyMethodParameters
209 @classproperty
210 def report_id(cls) -> str:
211 return "test_report"
213 @classmethod
214 def title(cls, req: "CamcopsRequest") -> str:
215 return "Test report"
217 def get_rows_colnames(self, req: "CamcopsRequest") -> Optional[
218 PlainReportType]:
219 rows = [
220 ["one", "two", "three"],
221 ["eleven", "twelve", "thirteen"],
222 ["twenty-one", "twenty-two", "twenty-three"],
223 ]
225 column_names = ["column 1", "column 2", "column 3"]
227 return PlainReportType(rows=rows, column_names=column_names)
230class ReportSpreadsheetTests(DemoRequestTestCase):
231 def test_render_xlsx(self) -> None:
232 report = TestReport()
234 response = report.render_xlsx(self.req)
235 self.assertIsInstance(response, XlsxResponse)
237 self.assertIn(
238 "filename=CamCOPS_test_report",
239 response.content_disposition
240 )
242 self.assertIn(
243 ".xlsx", response.content_disposition
244 )
246 def test_render_ods(self) -> None:
247 report = TestReport()
249 response = report.render_ods(self.req)
250 self.assertIsInstance(response, OdsResponse)
252 self.assertIn(
253 "filename=CamCOPS_test_report",
254 response.content_disposition
255 )
257 self.assertIn(
258 ".ods", response.content_disposition
259 )
261 def test_render_tsv(self) -> None:
262 report = TestReport()
264 response = report.render_tsv(self.req)
265 self.assertIsInstance(response, TsvResponse)
267 self.assertIn(
268 "filename=CamCOPS_test_report",
269 response.content_disposition
270 )
272 self.assertIn(
273 ".tsv", response.content_disposition
274 )
276 import csv
277 import io
278 reader = csv.reader(io.StringIO(response.body.decode()),
279 dialect="excel-tab")
281 headings = next(reader)
282 row_1 = next(reader)
284 self.assertEqual(headings, ["column 1", "column 2", "column 3"])
285 self.assertEqual(row_1, ["one", "two", "three"])