Coverage for cc_modules/cc_taskschedulereports.py: 36%

1"""

2 camcops_server/cc_modules/cc_taskschedulereports.py

3

4===============================================================================

5

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

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

8

9 This file is part of CamCOPS.

10

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.

15

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.

20

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

23

24===============================================================================

25

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

27

28"""

29

30 from typing import List, Type, TYPE_CHECKING, Union

31

32 from cardinal_pythonlib.classes import classproperty

33 from cardinal_pythonlib.sqlalchemy.core_query import (

34 get_rows_fieldnames_from_select,

35)

36 from cardinal_pythonlib.sqlalchemy.sqlfunc import extract_month, extract_year

37 from sqlalchemy import cast, Integer

38 from sqlalchemy.sql.elements import ColumnElement

39 from sqlalchemy.sql.expression import (

40 FromClause,

41 Select,

42 desc,

43 func,

44 literal,

45 select,

46 union_all,

47)

48 from sqlalchemy.sql.functions import FunctionElement

49

50 from camcops_server.cc_modules.cc_device import Device

51 from camcops_server.cc_modules.cc_sqla_coltypes import (

52 isotzdatetime_to_utcdatetime,

53)

54 from camcops_server.cc_modules.cc_email import Email

55 from camcops_server.cc_modules.cc_forms import ReportParamSchema

56 from camcops_server.cc_modules.cc_group import Group

57 from camcops_server.cc_modules.cc_patient import Patient

58 from camcops_server.cc_modules.cc_pyramid import ViewParam

59 from camcops_server.cc_modules.cc_report import Report, PlainReportType

60 from camcops_server.cc_modules.cc_reportschema import (

61 ByYearSelector,

62 ByMonthSelector,

63 DEFAULT_BY_MONTH,

64 DEFAULT_BY_YEAR,

65)

66 from camcops_server.cc_modules.cc_taskschedule import (

67 PatientTaskSchedule,

68 PatientTaskScheduleEmail,

69 TaskSchedule,

70 TaskScheduleItem,

71)

72

73 if TYPE_CHECKING:

74 from typing import Any

75

76 # noinspection PyProtectedMember

77 from sqlalchemy.sql.expression import Visitable

78 from camcops_server.cc_modules.cc_request import CamcopsRequest

79

80

81 class 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

91 class 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 _(

130 "(Server) Count server-created patients and their assigned tasks"

131 )

132

133 # noinspection PyMethodParameters

134 @classproperty

135 def superuser_only(cls) -> bool:

136 return False

137

138 @staticmethod

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

140 return TaskAssignmentReportSchema

141

142 @classmethod

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

144 return [

145 ViewParam.BY_YEAR,

146 ViewParam.BY_MONTH,

147 ]

148

149 def get_rows_colnames(self, req: "CamcopsRequest") -> PlainReportType:

150 by_year = req.get_bool_param(ViewParam.BY_YEAR, DEFAULT_BY_YEAR)

151 by_month = req.get_bool_param(ViewParam.BY_MONTH, DEFAULT_BY_MONTH)

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 # Merge the three queries.

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

175 "all_data"

176 )

177

178 if by_year:

179 selectors.append(all_data.c.year)

180 groupers.append(all_data.c.year)

181 sorters.append(desc(all_data.c.year))

182

183 if by_month:

184 selectors.append(all_data.c.month)

185 groupers.append(all_data.c.month)

186 sorters.append(desc(all_data.c.month))

187

188 sorters += [all_data.c.group_id, all_data.c.schedule_id]

189 selectors += [

190 all_data.c.group_name,

191 all_data.c.schedule_name,

192 func.sum(all_data.c.patients_created).label(

193 self.label_patients_created

194 ),

195 func.sum(all_data.c.tasks_assigned).label(self.label_tasks),

196 func.sum(all_data.c.emails_sent).label(self.label_emails_sent),

197 ]

198 query = (

199 select(*selectors) # type: ignore[arg-type]

200 .select_from(all_data)

201 .group_by(*groupers) # type: ignore[arg-type]

202 .order_by(*sorters) # type: ignore[arg-type]

203 )

204

205 rows, colnames = get_rows_fieldnames_from_select(req.dbsession, query)

206

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

208

209 def _get_tasks_query(

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

211 ) -> Select:

212 """

213 Returns a query of the number of tasks assigned to (scheduled for)

214 patients created on the server (in a way compatible with being merged

215 with other queries in this report).

216 """

217

218 pts = PatientTaskSchedule.__table__

219 ts = TaskSchedule.__table__

220 tsi = TaskScheduleItem.__table__

221 group = Group.__table__

222

223 tables = (

224 pts.join(ts, pts.c.schedule_id == ts.c.id)

225 .join(tsi, tsi.c.schedule_id == ts.c.id)

226 .join(group, ts.c.group_id == group.c.id)

227 )

228

229 date_column = isotzdatetime_to_utcdatetime(pts.c.start_datetime)

230 # Order must be consistent across queries

231 count_selectors = [

232 literal(0).label(self.label_patients_created),

233 func.count().label(self.label_tasks),

234 literal(0).label(self.label_emails_sent),

235 ]

236

237 query = self._build_query(

238 req, tables, by_year, by_month, date_column, count_selectors # type: ignore[arg-type] # noqa: E501

239 )

240

241 return query

242

243 def _get_created_patients_query(

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

245 ) -> Select:

246 """

247 Returns a query of the number of patients created on the server (in a

248 way compatible with being merged with other queries in this report).

249 """

250 server_device = Device.get_server_device(req.dbsession)

251

252 pts = PatientTaskSchedule.__table__

253 ts = TaskSchedule.__table__

254 group = Group.__table__

255 patient = Patient.__table__

256

257 # noinspection PyProtectedMember

258 tables = (

259 pts.join(ts, pts.c.schedule_id == ts.c.id)

260 .join(group, ts.c.group_id == group.c.id)

261 .join(patient, pts.c.patient_pk == patient.c._pk)

262 )

263

264 # noinspection PyProtectedMember

265 date_column = isotzdatetime_to_utcdatetime(patient.c._when_added_exact)

266 # Order must be consistent across queries

267 count_selectors = [

268 func.count().label(self.label_patients_created),

269 literal(0).label(self.label_tasks),

270 literal(0).label(self.label_emails_sent),

271 ]

272

273 # noinspection PyProtectedMember,PyTypeChecker

274 return self._build_query(

275 req, tables, by_year, by_month, date_column, count_selectors # type: ignore[arg-type] # noqa: E501

276 ).where(patient.c._device_id == server_device.id)

277

278 def _get_emails_sent_query(

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

280 ) -> Select:

281 """

282 Returns a query of the number of e-mails sent to patients created on

283 the server (in a way compatible with being merged with other queries in

284 this report).

285 """

286

287 pts = PatientTaskSchedule.__table__

288 ts = TaskSchedule.__table__

289 group = Group.__table__

290 patient = Patient.__table__

291 ptse = PatientTaskScheduleEmail.__table__

292 email = Email.__table__

293

294 # noinspection PyProtectedMember

295 tables = (

296 ptse.join(pts, ptse.c.patient_task_schedule_id == pts.c.id)

297 .join(ts, pts.c.schedule_id == ts.c.id)

298 .join(group, ts.c.group_id == group.c.id)

299 .join(patient, pts.c.patient_pk == patient.c._pk)

300 .join(email, ptse.c.email_id == email.c.id)

301 )

302

303 date_column = email.c.sent_at_utc

304 # Order must be consistent across queries

305 count_selectors = [

306 literal(0).label(self.label_patients_created),

307 literal(0).label(self.label_tasks),

308 func.count().label(self.label_emails_sent),

309 ]

310

311 # noinspection PyTypeChecker

312 return self._build_query(

313 req, tables, by_year, by_month, date_column, count_selectors # type: ignore[arg-type] # noqa: E501

314 ).where(

315 email.c.sent == True # noqa: E712

316 )

317

318 def _build_query(

319 self,

320 req: "CamcopsRequest",

321 tables: FromClause,

322 by_year: bool,

323 by_month: bool,

324 date_column: FunctionElement,

325 count_selectors: List[FunctionElement],

326 ) -> Select:

327 assert req.user is not None # For type checker

328

329 group_ids = req.user.ids_of_groups_user_may_report_on

330 superuser = req.user.superuser

331

332 ts = TaskSchedule.__table__

333 group = Group.__table__

334

335 groupers = [

336 group.c.id,

337 ts.c.id,

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

339

340 selectors = (

341 []

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

343

344 if by_year:

345 selectors.append(

346 cast( # Necessary for SQLite tests

347 extract_year(date_column),

348 Integer(),

349 ).label(self.label_year)

350 )

351 groupers.append(self.label_year)

352

353 if by_month:

354 selectors.append(

355 cast( # Necessary for SQLite tests

356 extract_month(date_column),

357 Integer(),

358 ).label(self.label_month)

359 )

360 groupers.append(self.label_month)

361 # Regardless:

362 selectors.append(group.c.id.label(self.label_group_id))

363 selectors.append(group.c.name.label(self.label_group_name))

364 selectors.append(ts.c.id.label(self.label_schedule_id))

365 selectors.append(ts.c.name.label(self.label_schedule_name))

366 selectors += count_selectors

367 # noinspection PyUnresolvedReferences

368 query = select(*selectors).select_from(tables).group_by(*groupers) # type: ignore[arg-type] # noqa: E501

369 if not superuser:

370 # Restrict to accessible groups

371 # noinspection PyProtectedMember

372 query = query.where(group.c.id.in_(group_ids))

373

374 return query