Coverage for tasks/gbo.py: 65%
178 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/tasks/gbo.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
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.
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.
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/>.
26===============================================================================
28Goal-Based Outcomes tasks.
30- By Joe Kearney, Rudolf Cardinal.
32"""
34from typing import List
36from cardinal_pythonlib.datetimefunc import format_datetime
37from sqlalchemy import Column
38from sqlalchemy.sql.sqltypes import Boolean, Integer, Date, UnicodeText
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
48# =============================================================================
49# Common GBO constants
50# =============================================================================
52AGENT_PATIENT = 1
53AGENT_PARENT_CARER = 2
54AGENT_CLINICIAN = 3
55AGENT_OTHER = 4
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"
65PROGRESS_COMMENT_SUFFIX = " (0 no progress - 10 reached fully)"
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
75# =============================================================================
76# GBO-GReS
77# =============================================================================
80class Gbogres(TaskHasPatientMixin, Task):
81 """
82 Server implementation of the GBO - Goal Record Sheet task.
83 """
85 __tablename__ = "gbogres"
86 shortname = "GBO-GReS"
87 extrastring_taskname = "gbo"
88 info_filename_stem = extrastring_taskname
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"
98 REQUIRED_FIELDS = [FN_DATE, FN_GOAL_1_DESC, FN_COMPLETED_BY]
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 )
128 @staticmethod
129 def longname(req: "CamcopsRequest") -> str:
130 _ = req.gettext
131 return _("Goal-Based Outcomes – 1 – Goal Record Sheet")
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 )
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 )
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)
160 def get_date_tr(self) -> str:
161 return tr_qa(
162 "Date",
163 format_datetime(self.date, DateFormat.SHORT_DATE, default=None),
164 )
166 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
167 return self.standard_task_summary_fields()
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
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 """
203# =============================================================================
204# GBO-GPC
205# =============================================================================
208class Gbogpc(TaskHasPatientMixin, Task):
209 """
210 Server implementation of the GBO-GPC task.
211 """
213 __tablename__ = "gbogpc"
214 shortname = "GBO-GPC"
215 extrastring_taskname = "gbo"
216 info_filename_stem = extrastring_taskname
217 provides_trackers = True
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"
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 )
253 REQUIRED_FIELDS = [
254 FN_DATE,
255 FN_SESSION,
256 FN_GOAL_NUMBER,
257 FN_PROGRESS,
258 FN_WHOSE_GOAL,
259 ]
261 @staticmethod
262 def longname(req: "CamcopsRequest") -> str:
263 _ = req.gettext
264 return _("Goal-Based Outcomes – 2 – Goal Progress Chart")
266 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
267 return self.standard_task_summary_fields()
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 ]
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
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 """
351# =============================================================================
352# GBO-GRaS
353# =============================================================================
356class Gbogras(TaskHasPatientMixin, Task):
357 """
358 Server implementation of the GBO-GRaS task.
359 """
361 __tablename__ = "gbogras"
362 shortname = "GBO-GRaS"
363 extrastring_taskname = "gbo"
364 info_filename_stem = extrastring_taskname
365 provides_trackers = True
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"
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 )
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 )
434 @staticmethod
435 def longname(req: "CamcopsRequest") -> str:
436 _ = req.gettext
437 return _("Goal-Based Outcomes – 3 – Goal Rating Sheet")
439 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
440 return self.standard_task_summary_fields()
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 ]
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
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)
494 def get_date_tr(self) -> str:
495 return tr_qa(
496 "Date",
497 format_datetime(self.date, DateFormat.SHORT_DATE, default=None),
498 )
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 """