Coverage for tasks/gbo.py: 65%

1"""

2 camcops_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

26 Goal-Based Outcomes tasks.

27

28- By Joe Kearney, Rudolf Cardinal.

29

30"""

31

32 import datetime

33 from typing import List, Optional

34

35 from cardinal_pythonlib.datetimefunc import format_datetime

36 from sqlalchemy.orm import Mapped, mapped_column

37 from sqlalchemy.sql.sqltypes import UnicodeText

38

39 from camcops_server.cc_modules.cc_constants import CssClass, DateFormat

40 from camcops_server.cc_modules.cc_html import tr_qa, answer

41 from camcops_server.cc_modules.cc_request import CamcopsRequest

42 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

43 from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin

44 from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo

45

46

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

48# Common GBO constants

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

50

51 AGENT_PATIENT = 1

52 AGENT_PARENT_CARER = 2

53 AGENT_CLINICIAN = 3

54 AGENT_OTHER = 4

55

56 AGENT_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}

62 UNKNOWN_AGENT = "Unknown"

63

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

65

66

67 def 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

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

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

352 class 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 """