Coverage for tasks/gbo.py: 65%
178 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/tasks/gbo.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
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.
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.
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/>.
24===============================================================================
26Goal-Based Outcomes tasks.
28- By Joe Kearney, Rudolf Cardinal.
30"""
32import datetime
33from typing import List, Optional
35from cardinal_pythonlib.datetimefunc import format_datetime
36from sqlalchemy.orm import Mapped, mapped_column
37from sqlalchemy.sql.sqltypes import UnicodeText
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
47# =============================================================================
48# Common GBO constants
49# =============================================================================
51AGENT_PATIENT = 1
52AGENT_PARENT_CARER = 2
53AGENT_CLINICIAN = 3
54AGENT_OTHER = 4
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"
64PROGRESS_COMMENT_SUFFIX = " (0 no progress - 10 reached fully)"
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
74# =============================================================================
75# GBO-GReS
76# =============================================================================
79class Gbogres(TaskHasPatientMixin, Task): # type: ignore[misc]
80 """
81 Server implementation of the GBO - Goal Record Sheet task.
82 """
84 __tablename__ = "gbogres"
85 shortname = "GBO-GReS"
86 extrastring_taskname = "gbo"
87 info_filename_stem = extrastring_taskname
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"
97 REQUIRED_FIELDS = [FN_DATE, FN_GOAL_1_DESC, FN_COMPLETED_BY]
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 )
125 @staticmethod
126 def longname(req: "CamcopsRequest") -> str:
127 _ = req.gettext
128 return _("Goal-Based Outcomes – 1 – Goal Record Sheet")
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 )
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 )
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)
157 def get_date_tr(self) -> str:
158 return tr_qa(
159 "Date",
160 format_datetime(self.date, DateFormat.SHORT_DATE, default=None),
161 )
163 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
164 return self.standard_task_summary_fields()
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
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 """
200# =============================================================================
201# GBO-GPC
202# =============================================================================
205class Gbogpc(TaskHasPatientMixin, Task): # type: ignore[misc]
206 """
207 Server implementation of the GBO-GPC task.
208 """
210 __tablename__ = "gbogpc"
211 shortname = "GBO-GPC"
212 extrastring_taskname = "gbo"
213 info_filename_stem = extrastring_taskname
214 provides_trackers = True
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"
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 )
249 REQUIRED_FIELDS = [
250 FN_DATE,
251 FN_SESSION,
252 FN_GOAL_NUMBER,
253 FN_PROGRESS,
254 FN_WHOSE_GOAL,
255 ]
257 @staticmethod
258 def longname(req: "CamcopsRequest") -> str:
259 _ = req.gettext
260 return _("Goal-Based Outcomes – 2 – Goal Progress Chart")
262 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
263 return self.standard_task_summary_fields()
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 ]
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
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 """
347# =============================================================================
348# GBO-GRaS
349# =============================================================================
352class Gbogras(TaskHasPatientMixin, Task): # type: ignore[misc]
353 """
354 Server implementation of the GBO-GRaS task.
355 """
357 __tablename__ = "gbogras"
358 shortname = "GBO-GRaS"
359 extrastring_taskname = "gbo"
360 info_filename_stem = extrastring_taskname
361 provides_trackers = True
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"
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 )
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 )
423 @staticmethod
424 def longname(req: "CamcopsRequest") -> str:
425 _ = req.gettext
426 return _("Goal-Based Outcomes – 3 – Goal Rating Sheet")
428 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
429 return self.standard_task_summary_fields()
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 ]
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
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)
483 def get_date_tr(self) -> str:
484 return tr_qa(
485 "Date",
486 format_datetime(self.date, DateFormat.SHORT_DATE, default=None),
487 )
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 """