Coverage for cc_modules/cc_taskschedulereports.py: 93%
136 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/cc_modules/cc_taskschedulereports.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
13 CamCOPS is free software: you can redistribute it and/or modify
14 it under the terms of the GNU General Public License as published by
15 the Free Software Foundation, either version 3 of the License, or
16 (at your option) any later version.
18 CamCOPS is distributed in the hope that it will be useful,
19 but WITHOUT ANY WARRANTY; without even the implied warranty of
20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 GNU General Public License for more details.
23 You should have received a copy of the GNU General Public License
24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
26===============================================================================
28**Server reports on CamCOPS scheduled tasks.**
30"""
32from typing import List, Type, TYPE_CHECKING, Union
34from cardinal_pythonlib.classes import classproperty
35from cardinal_pythonlib.sqlalchemy.orm_query import (
36 get_rows_fieldnames_from_query,
37)
38from cardinal_pythonlib.sqlalchemy.sqlfunc import extract_month, extract_year
39from sqlalchemy import cast, Integer
40from sqlalchemy.sql.elements import ColumnElement
41from sqlalchemy.sql.expression import (
42 FromClause,
43 Select,
44 desc,
45 func,
46 literal,
47 select,
48 union_all,
49)
50from sqlalchemy.sql.functions import FunctionElement
52from camcops_server.cc_modules.cc_device import Device
53from camcops_server.cc_modules.cc_sqla_coltypes import (
54 isotzdatetime_to_utcdatetime,
55)
56from camcops_server.cc_modules.cc_email import Email
57from camcops_server.cc_modules.cc_forms import ReportParamSchema
58from camcops_server.cc_modules.cc_group import Group
59from camcops_server.cc_modules.cc_patient import Patient
60from camcops_server.cc_modules.cc_pyramid import ViewParam
61from camcops_server.cc_modules.cc_report import Report, PlainReportType
62from camcops_server.cc_modules.cc_reportschema import (
63 ByYearSelector,
64 ByMonthSelector,
65 DEFAULT_BY_MONTH,
66 DEFAULT_BY_YEAR,
67)
68from camcops_server.cc_modules.cc_taskschedule import (
69 PatientTaskSchedule,
70 PatientTaskScheduleEmail,
71 TaskSchedule,
72 TaskScheduleItem,
73)
75if TYPE_CHECKING:
76 from typing import Any
77 from sqlalchemy.sql.expression import Visitable
78 from camcops_server.cc_modules.cc_request import CamcopsRequest
81class TaskAssignmentReportSchema(ReportParamSchema):
82 by_year = ByYearSelector() # must match ViewParam.BY_YEAR
83 by_month = ByMonthSelector() # must match ViewParam.BY_MONTH
86# =============================================================================
87# Reports
88# =============================================================================
91class TaskAssignmentReport(Report):
92 """
93 Report to count server-side patients and their assigned tasks.
95 We don't currently record when a patient was assigned to a task schedule;
96 we only record when the patient registered themselves on the app, along
97 with any tasks they completed. This report provides:
99 - Number of server-side patients created (by month or year)
100 - Number of tasks assigned to registered patients (by month or year)
101 - Number of tasks assigned to unregistered patients (all time)
102 - Number of emails sent to patients (by month or year)
104 This along with the task count report should give good data on completed
105 and outstanding tasks.
107 """
109 template_name = "task_assignment_report.mako"
111 label_year = "year"
112 label_month = "month"
113 label_group_id = "group_id"
114 label_group_name = "group_name"
115 label_schedule_id = "schedule_id"
116 label_schedule_name = "schedule_name"
117 label_tasks = "tasks_assigned"
118 label_patients_created = "patients_created"
119 label_emails_sent = "emails_sent"
121 # noinspection PyMethodParameters
122 @classproperty
123 def report_id(cls) -> str:
124 return "taskassignment"
126 @classmethod
127 def title(cls, req: "CamcopsRequest") -> str:
128 _ = req.gettext
129 return _("(Server) Count of patients and their assigned tasks")
131 # noinspection PyMethodParameters
132 @classproperty
133 def superuser_only(cls) -> bool:
134 return False
136 @staticmethod
137 def get_paramform_schema_class() -> Type["ReportParamSchema"]:
138 return TaskAssignmentReportSchema
140 @classmethod
141 def get_specific_http_query_keys(cls) -> List[str]:
142 return [
143 ViewParam.BY_YEAR,
144 ViewParam.BY_MONTH,
145 ]
147 def get_rows_colnames(self, req: "CamcopsRequest") -> PlainReportType:
148 by_year = req.get_bool_param(ViewParam.BY_YEAR, DEFAULT_BY_YEAR)
149 by_month = req.get_bool_param(ViewParam.BY_MONTH, DEFAULT_BY_MONTH)
151 colnames = [] # type: List[str] # for type checker
153 tasks_query = self._get_tasks_query(req, by_year, by_month)
154 tasks_query.alias("tasks_data")
155 patients_query = self._get_created_patients_query(
156 req, by_year, by_month
157 )
158 patients_query.alias("patients_data")
159 emails_query = self._get_emails_sent_query(req, by_year, by_month)
160 emails_query.alias("emails_data")
162 selectors = (
163 []
164 ) # type: List[Union[ColumnElement[Any], FromClause, int]]
165 sorters = [] # type: List[Union[str, bool, Visitable, None]]
166 groupers = [
167 self.label_group_id,
168 self.label_schedule_id,
169 self.label_group_name,
170 self.label_schedule_name,
171 ] # type: List[Union[str, bool, Visitable, None]]
173 all_data = union_all(tasks_query, patients_query, emails_query).alias(
174 "all_data"
175 )
177 if by_year:
178 selectors.append(all_data.c.year)
179 groupers.append(all_data.c.year)
180 sorters.append(desc(all_data.c.year))
182 if by_month:
183 selectors.append(all_data.c.month)
184 groupers.append(all_data.c.month)
185 sorters.append(desc(all_data.c.month))
187 sorters += [all_data.c.group_id, all_data.c.schedule_id]
188 selectors += [
189 all_data.c.group_name,
190 all_data.c.schedule_name,
191 func.sum(all_data.c.patients_created).label(
192 self.label_patients_created
193 ),
194 func.sum(all_data.c.tasks_assigned).label(self.label_tasks),
195 func.sum(all_data.c.emails_sent).label(self.label_emails_sent),
196 ]
197 query = (
198 select(selectors)
199 .select_from(all_data)
200 .group_by(*groupers)
201 .order_by(*sorters)
202 )
204 rows, colnames = get_rows_fieldnames_from_query(req.dbsession, query)
206 return PlainReportType(rows=rows, column_names=colnames)
208 def _get_tasks_query(
209 self, req: "CamcopsRequest", by_year: bool, by_month: bool
210 ) -> Select:
212 pts = PatientTaskSchedule.__table__
213 ts = TaskSchedule.__table__
214 tsi = TaskScheduleItem.__table__
215 group = Group.__table__
217 tables = (
218 pts.join(ts, pts.c.schedule_id == ts.c.id)
219 .join(tsi, tsi.c.schedule_id == ts.c.id)
220 .join(group, ts.c.group_id == group.c.id)
221 )
223 date_column = isotzdatetime_to_utcdatetime(pts.c.start_datetime)
224 # Order must be consistent across queries
225 count_selectors = [
226 literal(0).label(self.label_patients_created),
227 func.count().label(self.label_tasks),
228 literal(0).label(self.label_emails_sent),
229 ]
231 query = self._build_query(
232 req, tables, by_year, by_month, date_column, count_selectors
233 )
235 return query
237 def _get_created_patients_query(
238 self, req: "CamcopsRequest", by_year: bool, by_month: bool
239 ) -> Select:
240 server_device = Device.get_server_device(req.dbsession)
242 pts = PatientTaskSchedule.__table__
243 ts = TaskSchedule.__table__
244 group = Group.__table__
245 patient = Patient.__table__
247 tables = (
248 pts.join(ts, pts.c.schedule_id == ts.c.id)
249 .join(group, ts.c.group_id == group.c.id)
250 .join(patient, pts.c.patient_pk == patient.c._pk)
251 )
253 date_column = isotzdatetime_to_utcdatetime(patient.c._when_added_exact)
254 # Order must be consistent across queries
255 count_selectors = [
256 func.count().label(self.label_patients_created),
257 literal(0).label(self.label_tasks),
258 literal(0).label(self.label_emails_sent),
259 ]
261 query = self._build_query(
262 req, tables, by_year, by_month, date_column, count_selectors
263 ).where(patient.c._device_id == server_device.id)
265 return query
267 def _get_emails_sent_query(
268 self, req: "CamcopsRequest", by_year: bool, by_month: bool
269 ) -> Select:
270 pts = PatientTaskSchedule.__table__
271 ts = TaskSchedule.__table__
272 group = Group.__table__
273 patient = Patient.__table__
274 ptse = PatientTaskScheduleEmail.__table__
275 email = Email.__table__
277 tables = (
278 ptse.join(pts, ptse.c.patient_task_schedule_id == pts.c.id)
279 .join(ts, pts.c.schedule_id == ts.c.id)
280 .join(group, ts.c.group_id == group.c.id)
281 .join(patient, pts.c.patient_pk == patient.c._pk)
282 .join(email, ptse.c.email_id == email.c.id)
283 )
285 date_column = email.c.sent_at_utc
286 # Order must be consistent across queries
287 count_selectors = [
288 literal(0).label(self.label_patients_created),
289 literal(0).label(self.label_tasks),
290 func.count().label(self.label_emails_sent),
291 ]
293 query = self._build_query(
294 req, tables, by_year, by_month, date_column, count_selectors
295 ).where(
296 email.c.sent == True # noqa: E712
297 )
299 return query
301 def _build_query(
302 self,
303 req: "CamcopsRequest",
304 tables: FromClause,
305 by_year: bool,
306 by_month: bool,
307 date_column: FunctionElement,
308 count_selectors: List[FunctionElement],
309 ) -> Select:
310 assert req.user is not None # For type checker
312 group_ids = req.user.ids_of_groups_user_may_report_on
313 superuser = req.user.superuser
315 ts = TaskSchedule.__table__
316 group = Group.__table__
318 groupers = [
319 group.c.id,
320 ts.c.id,
321 ] # type: List[Union[str, bool, Visitable, None]]
323 selectors = (
324 []
325 ) # type: List[Union[ColumnElement[Any], FromClause, int]]
327 if by_year:
328 selectors.append(
329 cast( # Necessary for SQLite tests
330 extract_year(date_column),
331 Integer(),
332 ).label(self.label_year)
333 )
334 groupers.append(self.label_year)
336 if by_month:
337 selectors.append(
338 cast( # Necessary for SQLite tests
339 extract_month(date_column),
340 Integer(),
341 ).label(self.label_month)
342 )
343 groupers.append(self.label_month)
344 # Regardless:
345 selectors.append(group.c.id.label(self.label_group_id))
346 selectors.append(group.c.name.label(self.label_group_name))
347 selectors.append(ts.c.id.label(self.label_schedule_id))
348 selectors.append(ts.c.name.label(self.label_schedule_name))
349 selectors += count_selectors
350 # noinspection PyUnresolvedReferences
351 query = select(selectors).select_from(tables).group_by(*groupers)
352 if not superuser:
353 # Restrict to accessible groups
354 # noinspection PyProtectedMember
355 query = query.where(group.c.id.in_(group_ids))
357 return query