Coverage for tasks/gbo.py: 65%

178 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 14:23 +0100

1""" 

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

26Goal-Based Outcomes tasks. 

27 

28- By Joe Kearney, Rudolf Cardinal. 

29 

30""" 

31 

32import datetime 

33from typing import List, Optional 

34 

35from cardinal_pythonlib.datetimefunc import format_datetime 

36from sqlalchemy.orm import Mapped, mapped_column 

37from sqlalchemy.sql.sqltypes import UnicodeText 

38 

39from camcops_server.cc_modules.cc_constants import CssClass, DateFormat 

40from camcops_server.cc_modules.cc_html import tr_qa, answer 

41from camcops_server.cc_modules.cc_request import CamcopsRequest 

42from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

43from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

44from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

45 

46 

47# ============================================================================= 

48# Common GBO constants 

49# ============================================================================= 

50 

51AGENT_PATIENT = 1 

52AGENT_PARENT_CARER = 2 

53AGENT_CLINICIAN = 3 

54AGENT_OTHER = 4 

55 

56AGENT_STRING_MAP = { 

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

58 AGENT_PARENT_CARER: "Parent/carer", 

59 AGENT_CLINICIAN: "Practitioner/clinician", 

60 AGENT_OTHER: "Other: ", 

61} 

62UNKNOWN_AGENT = "Unknown" 

63 

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

65 

66 

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

68 who = AGENT_STRING_MAP.get(agent, UNKNOWN_AGENT) 

69 if agent == AGENT_OTHER: 

70 who += other_detail or "?" 

71 return who 

72 

73 

74# ============================================================================= 

75# GBO-GReS 

76# ============================================================================= 

77 

78 

79class Gbogres(TaskHasPatientMixin, Task): # type: ignore[misc] 

80 """ 

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

82 """ 

83 

84 __tablename__ = "gbogres" 

85 shortname = "GBO-GReS" 

86 extrastring_taskname = "gbo" 

87 info_filename_stem = extrastring_taskname 

88 

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

90 FN_GOAL_1_DESC = "goal_1_description" 

91 FN_GOAL_2_DESC = "goal_2_description" 

92 FN_GOAL_3_DESC = "goal_3_description" 

93 FN_GOAL_OTHER = "other_goals" 

94 FN_COMPLETED_BY = "completed_by" 

95 FN_COMPLETED_BY_OTHER = "completed_by_other" 

96 

97 REQUIRED_FIELDS = [FN_DATE, FN_GOAL_1_DESC, FN_COMPLETED_BY] 

98 

99 date: Mapped[Optional[datetime.date]] = mapped_column( 

100 comment="Date of goal-setting" 

101 ) 

102 goal_1_description: Mapped[Optional[str]] = mapped_column( 

103 UnicodeText, comment="Goal 1 description" 

104 ) 

105 goal_2_description: Mapped[Optional[str]] = mapped_column( 

106 UnicodeText, comment="Goal 2 description" 

107 ) 

108 goal_3_description: Mapped[Optional[str]] = mapped_column( 

109 UnicodeText, comment="Goal 3 description" 

110 ) 

111 other_goals: Mapped[Optional[str]] = mapped_column( 

112 UnicodeText, 

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

114 ) 

115 completed_by: Mapped[Optional[int]] = mapped_column( 

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

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

118 ), 

119 ) 

120 completed_by_other: Mapped[Optional[str]] = mapped_column( 

121 UnicodeText, 

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

123 ) 

124 

125 @staticmethod 

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

127 _ = req.gettext 

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

129 

130 def get_n_core_goals(self) -> int: 

131 """ 

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

133 """ 

134 return len( 

135 list( 

136 filter( 

137 None, 

138 [ 

139 self.goal_1_description, 

140 self.goal_2_description, 

141 self.goal_3_description, 

142 ], 

143 ) 

144 ) 

145 ) 

146 

147 def goals_set_tr(self) -> str: 

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

149 return tr_qa( 

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

151 ) 

152 

153 def completed_by_tr(self) -> str: 

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

155 return tr_qa("Completed by", who) 

156 

157 def get_date_tr(self) -> str: 

158 return tr_qa( 

159 "Date", 

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

161 ) 

162 

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

164 return self.standard_task_summary_fields() 

165 

166 def is_complete(self) -> bool: 

167 if self.any_fields_none(self.REQUIRED_FIELDS): 

168 return False 

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

170 return False 

171 return True 

172 

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

174 return f""" 

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

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

177 {self.get_is_complete_tr(req)} 

178 {self.get_date_tr()} 

179 {self.completed_by_tr()} 

180 {self.goals_set_tr()} 

181 </table> 

182 </div> 

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

184 <tr> 

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

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

187 </tr> 

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

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

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

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

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

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

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

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

196 </table> 

197 """ 

198 

199 

200# ============================================================================= 

201# GBO-GPC 

202# ============================================================================= 

203 

204 

205class Gbogpc(TaskHasPatientMixin, Task): # type: ignore[misc] 

206 """ 

207 Server implementation of the GBO-GPC task. 

208 """ 

209 

210 __tablename__ = "gbogpc" 

211 shortname = "GBO-GPC" 

212 extrastring_taskname = "gbo" 

213 info_filename_stem = extrastring_taskname 

214 provides_trackers = True 

215 

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

217 FN_SESSION = "session" 

218 FN_GOAL_NUMBER = "goal_number" 

219 FN_GOAL_DESCRIPTION = "goal_description" 

220 FN_PROGRESS = "progress" 

221 FN_WHOSE_GOAL = "whose_goal" 

222 FN_WHOSE_GOAL_OTHER = "whose_goal_other" 

223 

224 date: Mapped[Optional[datetime.date]] = mapped_column( 

225 comment="Session date" 

226 ) 

227 session: Mapped[Optional[int]] = mapped_column(comment="Session number") 

228 goal_number: Mapped[Optional[int]] = mapped_column( 

229 comment="Goal number (1-3)" 

230 ) 

231 goal_text: Mapped[Optional[str]] = mapped_column( 

232 FN_GOAL_DESCRIPTION, 

233 UnicodeText, 

234 comment="Brief description of the goal", 

235 ) 

236 progress: Mapped[Optional[int]] = mapped_column( 

237 comment="Progress towards goal" + PROGRESS_COMMENT_SUFFIX, 

238 ) 

239 whose_goal: Mapped[Optional[int]] = mapped_column( 

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

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

242 ), 

243 ) 

244 whose_goal_other: Mapped[Optional[str]] = mapped_column( 

245 UnicodeText, 

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

247 ) 

248 

249 REQUIRED_FIELDS = [ 

250 FN_DATE, 

251 FN_SESSION, 

252 FN_GOAL_NUMBER, 

253 FN_PROGRESS, 

254 FN_WHOSE_GOAL, 

255 ] 

256 

257 @staticmethod 

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

259 _ = req.gettext 

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

261 

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

263 return self.standard_task_summary_fields() 

264 

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

266 axis_min = -0.5 

267 axis_max = 10.5 

268 hlines = [0, 5, 10] 

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

270 title_start = "GBO Goal Progress Chart – Goal " 

271 return [ 

272 TrackerInfo( 

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

274 plot_label=title_start + "1", 

275 axis_label=axis_label, 

276 axis_min=axis_min, 

277 axis_max=axis_max, 

278 horizontal_lines=hlines, 

279 ), 

280 TrackerInfo( 

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

282 plot_label=title_start + "2", 

283 axis_label=axis_label, 

284 axis_min=axis_min, 

285 axis_max=axis_max, 

286 horizontal_lines=hlines, 

287 ), 

288 TrackerInfo( 

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

290 plot_label=title_start + "3", 

291 axis_label=axis_label, 

292 axis_min=axis_min, 

293 axis_max=axis_max, 

294 horizontal_lines=hlines, 

295 ), 

296 ] 

297 

298 def is_complete(self) -> bool: 

299 if self.any_fields_none(self.REQUIRED_FIELDS): 

300 return False 

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

302 return False 

303 return True 

304 

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

306 return f""" 

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

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

309 {self.get_is_complete_tr(req)} 

310 </table> 

311 </div> 

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

313 <tr> 

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

315 <td width="70%">{ 

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

317 default=None))}</td> 

318 </tr> 

319 <tr> 

320 <th>Session number</th> 

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

322 </tr> 

323 <tr> 

324 <th>Goal number</th> 

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

326 </tr> 

327 <tr> 

328 <th>Goal description</th> 

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

330 </tr> 

331 <tr> 

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

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

334 </tr> 

335 <tr> 

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

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

338 self.whose_goal_other))}</td> 

339 </tr> 

340 </table> 

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

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

343 </div> 

344 """ 

345 

346 

347# ============================================================================= 

348# GBO-GRaS 

349# ============================================================================= 

350 

351 

352class Gbogras(TaskHasPatientMixin, Task): # type: ignore[misc] 

353 """ 

354 Server implementation of the GBO-GRaS task. 

355 """ 

356 

357 __tablename__ = "gbogras" 

358 shortname = "GBO-GRaS" 

359 extrastring_taskname = "gbo" 

360 info_filename_stem = extrastring_taskname 

361 provides_trackers = True 

362 

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

364 FN_RATE_GOAL_1 = "rate_goal_1" 

365 FN_RATE_GOAL_2 = "rate_goal_2" 

366 FN_RATE_GOAL_3 = "rate_goal_3" 

367 FN_GOAL_1_DESC = "goal_1_description" 

368 FN_GOAL_2_DESC = "goal_2_description" 

369 FN_GOAL_3_DESC = "goal_3_description" 

370 FN_GOAL_1_PROGRESS = "goal_1_progress" 

371 FN_GOAL_2_PROGRESS = "goal_2_progress" 

372 FN_GOAL_3_PROGRESS = "goal_3_progress" 

373 FN_COMPLETED_BY = "completed_by" 

374 FN_COMPLETED_BY_OTHER = "completed_by_other" 

375 

376 date: Mapped[Optional[datetime.date]] = mapped_column( 

377 comment="Date of ratings" 

378 ) 

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

380 rate_goal_1: Mapped[Optional[bool]] = mapped_column(comment="Rate goal 1?") 

381 rate_goal_2: Mapped[Optional[bool]] = mapped_column(comment="Rate goal 2?") 

382 rate_goal_3: Mapped[Optional[bool]] = mapped_column(comment="Rate goal 3?") 

383 goal_1_description: Mapped[Optional[str]] = mapped_column( 

384 UnicodeText, comment="Goal 1 description" 

385 ) 

386 goal_2_description: Mapped[Optional[str]] = mapped_column( 

387 UnicodeText, comment="Goal 2 description" 

388 ) 

389 goal_3_description: Mapped[Optional[str]] = mapped_column( 

390 UnicodeText, comment="Goal 3 description" 

391 ) 

392 goal_1_progress: Mapped[Optional[int]] = mapped_column( 

393 comment="Goal 1 progress" + PROGRESS_COMMENT_SUFFIX, 

394 ) 

395 goal_2_progress: Mapped[Optional[int]] = mapped_column( 

396 comment="Goal 2 progress" + PROGRESS_COMMENT_SUFFIX, 

397 ) 

398 goal_3_progress: Mapped[Optional[int]] = mapped_column( 

399 comment="Goal 3 progress" + PROGRESS_COMMENT_SUFFIX, 

400 ) 

401 completed_by: Mapped[Optional[int]] = mapped_column( 

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

403 "; ".join( 

404 f"{k} = {v}" 

405 for k, v in AGENT_STRING_MAP.items() 

406 if k != AGENT_CLINICIAN 

407 ) 

408 ), 

409 ) 

410 completed_by_other: Mapped[Optional[str]] = mapped_column( 

411 UnicodeText, 

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

413 ) 

414 

415 REQUIRED_FIELDS = [FN_DATE, FN_COMPLETED_BY] 

416 GOAL_TUPLES = ( 

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

418 (1, FN_RATE_GOAL_1, FN_GOAL_1_DESC, FN_GOAL_1_PROGRESS), 

419 (2, FN_RATE_GOAL_2, FN_GOAL_2_DESC, FN_GOAL_2_PROGRESS), 

420 (3, FN_RATE_GOAL_3, FN_GOAL_3_DESC, FN_GOAL_3_PROGRESS), 

421 ) 

422 

423 @staticmethod 

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

425 _ = req.gettext 

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

427 

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

429 return self.standard_task_summary_fields() 

430 

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

432 axis_min = -0.5 

433 axis_max = 10.5 

434 hlines = [0, 5, 10] 

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

436 title_start = "GBO Goal Rating Sheet – Goal " 

437 return [ 

438 TrackerInfo( 

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

440 plot_label=title_start + "1", 

441 axis_label=axis_label, 

442 axis_min=axis_min, 

443 axis_max=axis_max, 

444 horizontal_lines=hlines, 

445 ), 

446 TrackerInfo( 

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

448 plot_label=title_start + "2", 

449 axis_label=axis_label, 

450 axis_min=axis_min, 

451 axis_max=axis_max, 

452 horizontal_lines=hlines, 

453 ), 

454 TrackerInfo( 

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

456 plot_label=title_start + "3", 

457 axis_label=axis_label, 

458 axis_min=axis_min, 

459 axis_max=axis_max, 

460 horizontal_lines=hlines, 

461 ), 

462 ] 

463 

464 def is_complete(self) -> bool: 

465 if self.any_fields_none(self.REQUIRED_FIELDS): 

466 return False 

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

468 return False 

469 n_goals_completed = 0 

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

471 if getattr(self, rate_attr): 

472 n_goals_completed += 1 

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

474 self, prog_attr 

475 ): 

476 return False 

477 return n_goals_completed > 0 

478 

479 def completed_by_tr(self) -> str: 

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

481 return tr_qa("Completed by", who) 

482 

483 def get_date_tr(self) -> str: 

484 return tr_qa( 

485 "Date", 

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

487 ) 

488 

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

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

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

492 if getattr(self, rate_attr): 

493 rows.append( 

494 f""" 

495 <tr> 

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

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

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

499 </tr> 

500 """ 

501 ) 

502 newline = "\n" 

503 return f""" 

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

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

506 {self.get_is_complete_tr(req)} 

507 {self.get_date_tr()} 

508 {self.completed_by_tr()} 

509 </table> 

510 </div> 

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

512 <tr> 

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

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

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

516 </tr> 

517 {newline.join(rows)} 

518 </table> 

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

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

521 </div> 

522 """