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

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/cc_modules/cc_taskschedulereports.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

10 

11 This file is part of CamCOPS. 

12 

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. 

17 

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. 

22 

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/>. 

25 

26=============================================================================== 

27 

28**Server reports on CamCOPS scheduled tasks.** 

29 

30""" 

31 

32from typing import List, Type, TYPE_CHECKING, Union 

33 

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 

51 

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) 

74 

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 

79 

80 

81class TaskAssignmentReportSchema(ReportParamSchema): 

82 by_year = ByYearSelector() # must match ViewParam.BY_YEAR 

83 by_month = ByMonthSelector() # must match ViewParam.BY_MONTH 

84 

85 

86# ============================================================================= 

87# Reports 

88# ============================================================================= 

89 

90 

91class TaskAssignmentReport(Report): 

92 """ 

93 Report to count server-side patients and their assigned tasks. 

94 

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: 

98 

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) 

103 

104 This along with the task count report should give good data on completed 

105 and outstanding tasks. 

106 

107 """ 

108 

109 template_name = "task_assignment_report.mako" 

110 

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" 

120 

121 # noinspection PyMethodParameters 

122 @classproperty 

123 def report_id(cls) -> str: 

124 return "taskassignment" 

125 

126 @classmethod 

127 def title(cls, req: "CamcopsRequest") -> str: 

128 _ = req.gettext 

129 return _("(Server) Count of patients and their assigned tasks") 

130 

131 # noinspection PyMethodParameters 

132 @classproperty 

133 def superuser_only(cls) -> bool: 

134 return False 

135 

136 @staticmethod 

137 def get_paramform_schema_class() -> Type["ReportParamSchema"]: 

138 return TaskAssignmentReportSchema 

139 

140 @classmethod 

141 def get_specific_http_query_keys(cls) -> List[str]: 

142 return [ 

143 ViewParam.BY_YEAR, 

144 ViewParam.BY_MONTH, 

145 ] 

146 

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) 

150 

151 colnames = [] # type: List[str] # for type checker 

152 

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") 

161 

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]] 

172 

173 all_data = union_all(tasks_query, patients_query, emails_query).alias( 

174 "all_data" 

175 ) 

176 

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)) 

181 

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)) 

186 

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 ) 

203 

204 rows, colnames = get_rows_fieldnames_from_query(req.dbsession, query) 

205 

206 return PlainReportType(rows=rows, column_names=colnames) 

207 

208 def _get_tasks_query( 

209 self, req: "CamcopsRequest", by_year: bool, by_month: bool 

210 ) -> Select: 

211 

212 pts = PatientTaskSchedule.__table__ 

213 ts = TaskSchedule.__table__ 

214 tsi = TaskScheduleItem.__table__ 

215 group = Group.__table__ 

216 

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 ) 

222 

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 ] 

230 

231 query = self._build_query( 

232 req, tables, by_year, by_month, date_column, count_selectors 

233 ) 

234 

235 return query 

236 

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) 

241 

242 pts = PatientTaskSchedule.__table__ 

243 ts = TaskSchedule.__table__ 

244 group = Group.__table__ 

245 patient = Patient.__table__ 

246 

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 ) 

252 

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 ] 

260 

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) 

264 

265 return query 

266 

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__ 

276 

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 ) 

284 

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 ] 

292 

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 ) 

298 

299 return query 

300 

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 

311 

312 group_ids = req.user.ids_of_groups_user_may_report_on 

313 superuser = req.user.superuser 

314 

315 ts = TaskSchedule.__table__ 

316 group = Group.__table__ 

317 

318 groupers = [ 

319 group.c.id, 

320 ts.c.id, 

321 ] # type: List[Union[str, bool, Visitable, None]] 

322 

323 selectors = ( 

324 [] 

325 ) # type: List[Union[ColumnElement[Any], FromClause, int]] 

326 

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) 

335 

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)) 

356 

357 return query