Coverage for tasks/gbo.py: 65%

178 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/tasks/gbo.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 

28Goal-Based Outcomes tasks. 

29 

30- By Joe Kearney, Rudolf Cardinal. 

31 

32""" 

33 

34from typing import List 

35 

36from cardinal_pythonlib.datetimefunc import format_datetime 

37from sqlalchemy import Column 

38from sqlalchemy.sql.sqltypes import Boolean, Integer, Date, UnicodeText 

39 

40from camcops_server.cc_modules.cc_constants import CssClass, DateFormat 

41from camcops_server.cc_modules.cc_html import tr_qa, answer 

42from camcops_server.cc_modules.cc_request import CamcopsRequest 

43from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

44from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

45from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

46 

47 

48# ============================================================================= 

49# Common GBO constants 

50# ============================================================================= 

51 

52AGENT_PATIENT = 1 

53AGENT_PARENT_CARER = 2 

54AGENT_CLINICIAN = 3 

55AGENT_OTHER = 4 

56 

57AGENT_STRING_MAP = { 

58 AGENT_PATIENT: "Patient/service user", # in original: "Child/young person" 

59 AGENT_PARENT_CARER: "Parent/carer", 

60 AGENT_CLINICIAN: "Practitioner/clinician", 

61 AGENT_OTHER: "Other: ", 

62} 

63UNKNOWN_AGENT = "Unknown" 

64 

65PROGRESS_COMMENT_SUFFIX = " (0 no progress - 10 reached fully)" 

66 

67 

68def agent_description(agent: int, other_detail: str) -> str: 

69 who = AGENT_STRING_MAP.get(agent, UNKNOWN_AGENT) 

70 if agent == AGENT_OTHER: 

71 who += other_detail or "?" 

72 return who 

73 

74 

75# ============================================================================= 

76# GBO-GReS 

77# ============================================================================= 

78 

79 

80class Gbogres(TaskHasPatientMixin, Task): 

81 """ 

82 Server implementation of the GBO - Goal Record Sheet task. 

83 """ 

84 

85 __tablename__ = "gbogres" 

86 shortname = "GBO-GReS" 

87 extrastring_taskname = "gbo" 

88 info_filename_stem = extrastring_taskname 

89 

90 FN_DATE = "date" # NB SQL keyword too; doesn't matter 

91 FN_GOAL_1_DESC = "goal_1_description" 

92 FN_GOAL_2_DESC = "goal_2_description" 

93 FN_GOAL_3_DESC = "goal_3_description" 

94 FN_GOAL_OTHER = "other_goals" 

95 FN_COMPLETED_BY = "completed_by" 

96 FN_COMPLETED_BY_OTHER = "completed_by_other" 

97 

98 REQUIRED_FIELDS = [FN_DATE, FN_GOAL_1_DESC, FN_COMPLETED_BY] 

99 

100 date = Column(FN_DATE, Date, comment="Date of goal-setting") 

101 goal_1_description = Column( 

102 FN_GOAL_1_DESC, UnicodeText, comment="Goal 1 description" 

103 ) 

104 goal_2_description = Column( 

105 FN_GOAL_2_DESC, UnicodeText, comment="Goal 2 description" 

106 ) 

107 goal_3_description = Column( 

108 FN_GOAL_3_DESC, UnicodeText, comment="Goal 3 description" 

109 ) 

110 other_goals = Column( 

111 FN_GOAL_OTHER, 

112 UnicodeText, 

113 comment="Other/additional goal description(s)", 

114 ) 

115 completed_by = Column( 

116 FN_COMPLETED_BY, 

117 Integer, 

118 comment="Who completed the form ({})".format( 

119 "; ".join(f"{k} = {v}" for k, v in AGENT_STRING_MAP.items()) 

120 ), 

121 ) 

122 completed_by_other = Column( 

123 FN_COMPLETED_BY_OTHER, 

124 UnicodeText, 

125 comment="If completed by 'other', who?", 

126 ) 

127 

128 @staticmethod 

129 def longname(req: "CamcopsRequest") -> str: 

130 _ = req.gettext 

131 return _("Goal-Based Outcomes – 1 – Goal Record Sheet") 

132 

133 def get_n_core_goals(self) -> int: 

134 """ 

135 Returns the number of non-blank core (1-3) goals. 

136 """ 

137 return len( 

138 list( 

139 filter( 

140 None, 

141 [ 

142 self.goal_1_description, 

143 self.goal_2_description, 

144 self.goal_3_description, 

145 ], 

146 ) 

147 ) 

148 ) 

149 

150 def goals_set_tr(self) -> str: 

151 extra = " (additional goals specified)" if self.other_goals else "" 

152 return tr_qa( 

153 "Number of goals set", f"{self.get_n_core_goals()}{extra}" 

154 ) 

155 

156 def completed_by_tr(self) -> str: 

157 who = agent_description(self.completed_by, self.completed_by_other) 

158 return tr_qa("Completed by", who) 

159 

160 def get_date_tr(self) -> str: 

161 return tr_qa( 

162 "Date", 

163 format_datetime(self.date, DateFormat.SHORT_DATE, default=None), 

164 ) 

165 

166 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: 

167 return self.standard_task_summary_fields() 

168 

169 def is_complete(self) -> bool: 

170 if self.any_fields_none(self.REQUIRED_FIELDS): 

171 return False 

172 if self.completed_by == AGENT_OTHER and not self.completed_by_other: 

173 return False 

174 return True 

175 

176 def get_task_html(self, req: CamcopsRequest) -> str: 

177 return f""" 

178 <div class="{CssClass.SUMMARY}"> 

179 <table class="{CssClass.SUMMARY}"> 

180 {self.get_is_complete_tr(req)} 

181 {self.get_date_tr()} 

182 {self.completed_by_tr()} 

183 {self.goals_set_tr()} 

184 </table> 

185 </div> 

186 <table class="{CssClass.TASKDETAIL}"> 

187 <tr> 

188 <th width="15%">Goal number</th> 

189 <th width="85%">Goal description</th> 

190 </tr> 

191 <tr><td>1</td><td>{answer(self.goal_1_description, 

192 default="")}</td></tr> 

193 <tr><td>2</td><td>{answer(self.goal_2_description, 

194 default="")}</td></tr> 

195 <tr><td>3</td><td>{answer(self.goal_3_description, 

196 default="")}</td></tr> 

197 <tr><td>Other</td><td>{answer(self.other_goals, 

198 default="")}</td></tr> 

199 </table> 

200 """ 

201 

202 

203# ============================================================================= 

204# GBO-GPC 

205# ============================================================================= 

206 

207 

208class Gbogpc(TaskHasPatientMixin, Task): 

209 """ 

210 Server implementation of the GBO-GPC task. 

211 """ 

212 

213 __tablename__ = "gbogpc" 

214 shortname = "GBO-GPC" 

215 extrastring_taskname = "gbo" 

216 info_filename_stem = extrastring_taskname 

217 provides_trackers = True 

218 

219 FN_DATE = "date" # NB SQL keyword too; doesn't matter 

220 FN_SESSION = "session" 

221 FN_GOAL_NUMBER = "goal_number" 

222 FN_GOAL_DESCRIPTION = "goal_description" 

223 FN_PROGRESS = "progress" 

224 FN_WHOSE_GOAL = "whose_goal" 

225 FN_WHOSE_GOAL_OTHER = "whose_goal_other" 

226 

227 date = Column(FN_DATE, Date, comment="Session date") 

228 session = Column(FN_SESSION, Integer, comment="Session number") 

229 goal_number = Column(FN_GOAL_NUMBER, Integer, comment="Goal number (1-3)") 

230 goal_text = Column( 

231 FN_GOAL_DESCRIPTION, 

232 UnicodeText, 

233 comment="Brief description of the goal", 

234 ) 

235 progress = Column( 

236 FN_PROGRESS, 

237 Integer, 

238 comment="Progress towards goal" + PROGRESS_COMMENT_SUFFIX, 

239 ) 

240 whose_goal = Column( 

241 FN_WHOSE_GOAL, 

242 Integer, 

243 comment="Whose goal is this ({})".format( 

244 "; ".join(f"{k} = {v}" for k, v in AGENT_STRING_MAP.items()) 

245 ), 

246 ) 

247 whose_goal_other = Column( 

248 FN_WHOSE_GOAL_OTHER, 

249 UnicodeText, 

250 comment="If 'whose goal' is 'other', who?", 

251 ) 

252 

253 REQUIRED_FIELDS = [ 

254 FN_DATE, 

255 FN_SESSION, 

256 FN_GOAL_NUMBER, 

257 FN_PROGRESS, 

258 FN_WHOSE_GOAL, 

259 ] 

260 

261 @staticmethod 

262 def longname(req: "CamcopsRequest") -> str: 

263 _ = req.gettext 

264 return _("Goal-Based Outcomes – 2 – Goal Progress Chart") 

265 

266 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: 

267 return self.standard_task_summary_fields() 

268 

269 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: 

270 axis_min = -0.5 

271 axis_max = 10.5 

272 hlines = [0, 5, 10] 

273 axis_label = "Progress towards goal (0-10)" 

274 title_start = "GBO Goal Progress Chart – Goal " 

275 return [ 

276 TrackerInfo( 

277 value=self.progress if self.goal_number == 1 else None, 

278 plot_label=title_start + "1", 

279 axis_label=axis_label, 

280 axis_min=axis_min, 

281 axis_max=axis_max, 

282 horizontal_lines=hlines, 

283 ), 

284 TrackerInfo( 

285 value=self.progress if self.goal_number == 2 else None, 

286 plot_label=title_start + "2", 

287 axis_label=axis_label, 

288 axis_min=axis_min, 

289 axis_max=axis_max, 

290 horizontal_lines=hlines, 

291 ), 

292 TrackerInfo( 

293 value=self.progress if self.goal_number == 3 else None, 

294 plot_label=title_start + "3", 

295 axis_label=axis_label, 

296 axis_min=axis_min, 

297 axis_max=axis_max, 

298 horizontal_lines=hlines, 

299 ), 

300 ] 

301 

302 def is_complete(self) -> bool: 

303 if self.any_fields_none(self.REQUIRED_FIELDS): 

304 return False 

305 if self.whose_goal == AGENT_OTHER and not self.whose_goal_other: 

306 return False 

307 return True 

308 

309 def get_task_html(self, req: CamcopsRequest) -> str: 

310 return f""" 

311 <div class="{CssClass.SUMMARY}"> 

312 <table class="{CssClass.SUMMARY}"> 

313 {self.get_is_complete_tr(req)} 

314 </table> 

315 </div> 

316 <table class="{CssClass.TASKDETAIL}"> 

317 <tr> 

318 <th width="30%">Date</th> 

319 <td width="70%">{ 

320 answer(format_datetime(self.date, DateFormat.SHORT_DATE, 

321 default=None))}</td> 

322 </tr> 

323 <tr> 

324 <th>Session number</th> 

325 <td>{answer(self.session)}</td> 

326 </tr> 

327 <tr> 

328 <th>Goal number</th> 

329 <td>{answer(self.goal_number)}</td> 

330 </tr> 

331 <tr> 

332 <th>Goal description</th> 

333 <td>{answer(self.goal_text)}</td> 

334 </tr> 

335 <tr> 

336 <th>Progress <sup>[1]</sup></th> 

337 <td>{answer(self.progress)}</td> 

338 </tr> 

339 <tr> 

340 <th>Whose goal is this?</th> 

341 <td>{answer(agent_description(self.whose_goal, 

342 self.whose_goal_other))}</td> 

343 </tr> 

344 </table> 

345 <div class="{CssClass.FOOTNOTES}"> 

346 [1] {self.wxstring(req, "progress_explanation")} 

347 </div> 

348 """ 

349 

350 

351# ============================================================================= 

352# GBO-GRaS 

353# ============================================================================= 

354 

355 

356class Gbogras(TaskHasPatientMixin, Task): 

357 """ 

358 Server implementation of the GBO-GRaS task. 

359 """ 

360 

361 __tablename__ = "gbogras" 

362 shortname = "GBO-GRaS" 

363 extrastring_taskname = "gbo" 

364 info_filename_stem = extrastring_taskname 

365 provides_trackers = True 

366 

367 FN_DATE = "date" # NB SQL keyword too; doesn't matter 

368 FN_RATE_GOAL_1 = "rate_goal_1" 

369 FN_RATE_GOAL_2 = "rate_goal_2" 

370 FN_RATE_GOAL_3 = "rate_goal_3" 

371 FN_GOAL_1_DESC = "goal_1_description" 

372 FN_GOAL_2_DESC = "goal_2_description" 

373 FN_GOAL_3_DESC = "goal_3_description" 

374 FN_GOAL_1_PROGRESS = "goal_1_progress" 

375 FN_GOAL_2_PROGRESS = "goal_2_progress" 

376 FN_GOAL_3_PROGRESS = "goal_3_progress" 

377 FN_COMPLETED_BY = "completed_by" 

378 FN_COMPLETED_BY_OTHER = "completed_by_other" 

379 

380 date = Column(FN_DATE, Date, comment="Date of ratings") 

381 # ... NB SQL keyword too; doesn't matter 

382 rate_goal_1 = Column(FN_RATE_GOAL_1, Boolean, comment="Rate goal 1?") 

383 rate_goal_2 = Column(FN_RATE_GOAL_2, Boolean, comment="Rate goal 2?") 

384 rate_goal_3 = Column(FN_RATE_GOAL_3, Boolean, comment="Rate goal 3?") 

385 goal_1_description = Column( 

386 FN_GOAL_1_DESC, UnicodeText, comment="Goal 1 description" 

387 ) 

388 goal_2_description = Column( 

389 FN_GOAL_2_DESC, UnicodeText, comment="Goal 2 description" 

390 ) 

391 goal_3_description = Column( 

392 FN_GOAL_3_DESC, UnicodeText, comment="Goal 3 description" 

393 ) 

394 goal_1_progress = Column( 

395 FN_GOAL_1_PROGRESS, 

396 Integer, 

397 comment="Goal 1 progress" + PROGRESS_COMMENT_SUFFIX, 

398 ) 

399 goal_2_progress = Column( 

400 FN_GOAL_2_PROGRESS, 

401 Integer, 

402 comment="Goal 2 progress" + PROGRESS_COMMENT_SUFFIX, 

403 ) 

404 goal_3_progress = Column( 

405 FN_GOAL_3_PROGRESS, 

406 Integer, 

407 comment="Goal 3 progress" + PROGRESS_COMMENT_SUFFIX, 

408 ) 

409 completed_by = Column( 

410 FN_COMPLETED_BY, 

411 Integer, 

412 comment="Who completed the form ({})".format( 

413 "; ".join( 

414 f"{k} = {v}" 

415 for k, v in AGENT_STRING_MAP.items() 

416 if k != AGENT_CLINICIAN 

417 ) 

418 ), 

419 ) 

420 completed_by_other = Column( 

421 FN_COMPLETED_BY_OTHER, 

422 UnicodeText, 

423 comment="If completed by 'other', who?", 

424 ) 

425 

426 REQUIRED_FIELDS = [FN_DATE, FN_COMPLETED_BY] 

427 GOAL_TUPLES = ( 

428 # goalnum, rate it?, goal description, progress 

429 (1, FN_RATE_GOAL_1, FN_GOAL_1_DESC, FN_GOAL_1_PROGRESS), 

430 (2, FN_RATE_GOAL_2, FN_GOAL_2_DESC, FN_GOAL_2_PROGRESS), 

431 (3, FN_RATE_GOAL_3, FN_GOAL_3_DESC, FN_GOAL_3_PROGRESS), 

432 ) 

433 

434 @staticmethod 

435 def longname(req: "CamcopsRequest") -> str: 

436 _ = req.gettext 

437 return _("Goal-Based Outcomes – 3 – Goal Rating Sheet") 

438 

439 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: 

440 return self.standard_task_summary_fields() 

441 

442 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: 

443 axis_min = -0.5 

444 axis_max = 10.5 

445 hlines = [0, 5, 10] 

446 axis_label = "Progress towards goal (0-10)" 

447 title_start = "GBO Goal Rating Sheet – Goal " 

448 return [ 

449 TrackerInfo( 

450 value=self.goal_1_progress if self.rate_goal_1 else None, 

451 plot_label=title_start + "1", 

452 axis_label=axis_label, 

453 axis_min=axis_min, 

454 axis_max=axis_max, 

455 horizontal_lines=hlines, 

456 ), 

457 TrackerInfo( 

458 value=self.goal_2_progress if self.rate_goal_2 else None, 

459 plot_label=title_start + "2", 

460 axis_label=axis_label, 

461 axis_min=axis_min, 

462 axis_max=axis_max, 

463 horizontal_lines=hlines, 

464 ), 

465 TrackerInfo( 

466 value=self.goal_3_progress if self.rate_goal_3 else None, 

467 plot_label=title_start + "3", 

468 axis_label=axis_label, 

469 axis_min=axis_min, 

470 axis_max=axis_max, 

471 horizontal_lines=hlines, 

472 ), 

473 ] 

474 

475 def is_complete(self) -> bool: 

476 if self.any_fields_none(self.REQUIRED_FIELDS): 

477 return False 

478 if self.completed_by == AGENT_OTHER and not self.completed_by_other: 

479 return False 

480 n_goals_completed = 0 

481 for _, rate_attr, desc_attr, prog_attr in self.GOAL_TUPLES: 

482 if getattr(self, rate_attr): 

483 n_goals_completed += 1 

484 if not getattr(self, desc_attr) or not getattr( 

485 self, prog_attr 

486 ): 

487 return False 

488 return n_goals_completed > 0 

489 

490 def completed_by_tr(self) -> str: 

491 who = agent_description(self.completed_by, self.completed_by_other) 

492 return tr_qa("Completed by", who) 

493 

494 def get_date_tr(self) -> str: 

495 return tr_qa( 

496 "Date", 

497 format_datetime(self.date, DateFormat.SHORT_DATE, default=None), 

498 ) 

499 

500 def get_task_html(self, req: CamcopsRequest) -> str: 

501 rows = [] # type: List[str] 

502 for goalnum, rate_attr, desc_attr, prog_attr in self.GOAL_TUPLES: 

503 if getattr(self, rate_attr): 

504 rows.append( 

505 f""" 

506 <tr> 

507 <td>{answer(goalnum)}</td> 

508 <td>{answer(getattr(self, desc_attr))}</td> 

509 <td>{answer(getattr(self, prog_attr))}</td> 

510 </tr> 

511 """ 

512 ) 

513 newline = "\n" 

514 return f""" 

515 <div class="{CssClass.SUMMARY}"> 

516 <table class="{CssClass.SUMMARY}"> 

517 {self.get_is_complete_tr(req)} 

518 {self.get_date_tr()} 

519 {self.completed_by_tr()} 

520 </table> 

521 </div> 

522 <table class="{CssClass.TASKDETAIL}"> 

523 <tr> 

524 <th width="15%">Goal number</th> 

525 <th width="70%">Description</th> 

526 <th width="15%">Progress <sup>[1]</sup></th> 

527 </tr> 

528 {newline.join(rows)} 

529 </table> 

530 <div class="{CssClass.FOOTNOTES}"> 

531 [1] {self.wxstring(req, "progress_explanation")} 

532 </div> 

533 """