Coverage for tasks/gbo.py : 64%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
3"""
4camcops_server/tasks/gbo.py
6===============================================================================
8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
10 This file is part of CamCOPS.
12 CamCOPS is free software: you can redistribute it and/or modify
13 it under the terms of the GNU General Public License as published by
14 the Free Software Foundation, either version 3 of the License, or
15 (at your option) any later version.
17 CamCOPS is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 GNU General Public License for more details.
22 You should have received a copy of the GNU General Public License
23 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
25===============================================================================
27Goal-Based Outcomes tasks.
29- By Joe Kearney, Rudolf Cardinal.
31"""
33from typing import List
35from cardinal_pythonlib.datetimefunc import format_datetime
36from sqlalchemy import Column
37from sqlalchemy.sql.sqltypes import Boolean, Integer, Date, 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 (
44 Task,
45 TaskHasPatientMixin,
46)
47from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
50# =============================================================================
51# Common GBO constants
52# =============================================================================
54AGENT_PATIENT = 1
55AGENT_PARENT_CARER = 2
56AGENT_CLINICIAN = 3
57AGENT_OTHER = 4
59AGENT_STRING_MAP = {
60 AGENT_PATIENT: "Patient/service user", # in original: "Child/young person"
61 AGENT_PARENT_CARER: "Parent/carer",
62 AGENT_CLINICIAN: "Practitioner/clinician",
63 AGENT_OTHER: "Other: "
64}
65UNKNOWN_AGENT = "Unknown"
67PROGRESS_COMMENT_SUFFIX = " (0 no progress - 10 reached fully)"
70def agent_description(agent: int, other_detail: str) -> str:
71 who = AGENT_STRING_MAP.get(agent, UNKNOWN_AGENT)
72 if agent == AGENT_OTHER:
73 who += other_detail or "?"
74 return who
77# =============================================================================
78# GBO-GReS
79# =============================================================================
81class Gbogres(TaskHasPatientMixin, Task):
82 """
83 Server implementation of the GBO - Goal Record Sheet task.
84 """
85 __tablename__ = "gbogres"
86 shortname = "GBO-GReS"
87 extrastring_taskname = "gbo"
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 = Column(FN_DATE, Date, comment="Date of goal-setting")
100 goal_1_description = Column(
101 FN_GOAL_1_DESC, UnicodeText,
102 comment="Goal 1 description")
103 goal_2_description = Column(
104 FN_GOAL_2_DESC, UnicodeText,
105 comment="Goal 2 description")
106 goal_3_description = Column(
107 FN_GOAL_3_DESC, UnicodeText,
108 comment="Goal 3 description")
109 other_goals = Column(
110 FN_GOAL_OTHER, UnicodeText,
111 comment="Other/additional goal description(s)")
112 completed_by = Column(
113 FN_COMPLETED_BY, Integer,
114 comment="Who completed the form ({})".format(
115 "; ".join(f"{k} = {v}"
116 for k, v in AGENT_STRING_MAP.items())
117 )
118 )
119 completed_by_other = Column(
120 FN_COMPLETED_BY_OTHER, UnicodeText,
121 comment="If completed by 'other', who?")
123 @staticmethod
124 def longname(req: "CamcopsRequest") -> str:
125 _ = req.gettext
126 return _("Goal-Based Outcomes – 1 – Goal Record Sheet")
128 def get_n_core_goals(self) -> int:
129 """
130 Returns the number of non-blank core (1-3) goals.
131 """
132 return len(list(filter(
133 None,
134 [self.goal_1_description, self.goal_2_description,
135 self.goal_3_description])))
137 def goals_set_tr(self) -> str:
138 extra = " (additional goals specified)" if self.other_goals else ""
139 return tr_qa("Number of goals set",
140 f"{self.get_n_core_goals()}{extra}")
142 def completed_by_tr(self) -> str:
143 who = agent_description(self.completed_by, self.completed_by_other)
144 return tr_qa("Completed by", who)
146 def get_date_tr(self) -> str:
147 return tr_qa("Date", format_datetime(self.date,
148 DateFormat.SHORT_DATE,
149 default=None))
151 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
152 return self.standard_task_summary_fields()
154 def is_complete(self) -> bool:
155 if self.any_fields_none(self.REQUIRED_FIELDS):
156 return False
157 if self.completed_by == AGENT_OTHER and not self.completed_by_other:
158 return False
159 return True
161 def get_task_html(self, req: CamcopsRequest) -> str:
162 return f"""
163 <div class="{CssClass.SUMMARY}">
164 <table class="{CssClass.SUMMARY}">
165 {self.get_is_complete_tr(req)}
166 {self.get_date_tr()}
167 {self.completed_by_tr()}
168 {self.goals_set_tr()}
169 </table>
170 </div>
171 <table class="{CssClass.TASKDETAIL}">
172 <tr>
173 <th width="15%">Goal number</th>
174 <th width="85%">Goal description</th>
175 </tr>
176 <tr><td>1</td><td>{answer(self.goal_1_description,
177 default="")}</td></tr>
178 <tr><td>2</td><td>{answer(self.goal_2_description,
179 default="")}</td></tr>
180 <tr><td>3</td><td>{answer(self.goal_3_description,
181 default="")}</td></tr>
182 <tr><td>Other</td><td>{answer(self.other_goals,
183 default="")}</td></tr>
184 </table>
185 """
188# =============================================================================
189# GBO-GPC
190# =============================================================================
192class Gbogpc(TaskHasPatientMixin, Task):
193 """
194 Server implementation of the GBO-GPC task.
195 """
196 __tablename__ = "gbogpc"
197 shortname = "GBO-GPC"
198 extrastring_taskname = "gbo"
199 provides_trackers = True
201 FN_DATE = "date" # NB SQL keyword too; doesn't matter
202 FN_SESSION = "session"
203 FN_GOAL_NUMBER = "goal_number"
204 FN_GOAL_DESCRIPTION = "goal_description"
205 FN_PROGRESS = "progress"
206 FN_WHOSE_GOAL = "whose_goal"
207 FN_WHOSE_GOAL_OTHER = "whose_goal_other"
209 date = Column(FN_DATE, Date, comment="Session date")
210 session = Column(FN_SESSION, Integer, comment="Session number")
211 goal_number = Column(FN_GOAL_NUMBER, Integer, comment="Goal number (1-3)")
212 goal_text = Column(
213 FN_GOAL_DESCRIPTION, UnicodeText,
214 comment="Brief description of the goal")
215 progress = Column(
216 FN_PROGRESS, Integer,
217 comment="Progress towards goal" + PROGRESS_COMMENT_SUFFIX
218 )
219 whose_goal = Column(
220 FN_WHOSE_GOAL, Integer,
221 comment="Whose goal is this ({})".format(
222 "; ".join(f"{k} = {v}"
223 for k, v in AGENT_STRING_MAP.items())
224 )
225 )
226 whose_goal_other = Column(
227 FN_WHOSE_GOAL_OTHER, UnicodeText,
228 comment="If 'whose goal' is 'other', who?")
230 REQUIRED_FIELDS = [
231 FN_DATE, FN_SESSION, FN_GOAL_NUMBER, FN_PROGRESS, FN_WHOSE_GOAL
232 ]
234 @staticmethod
235 def longname(req: "CamcopsRequest") -> str:
236 _ = req.gettext
237 return _("Goal-Based Outcomes – 2 – Goal Progress Chart")
239 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
240 return self.standard_task_summary_fields()
242 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
243 axis_min = -0.5
244 axis_max = 10.5
245 hlines = [0, 5, 10]
246 axis_label = "Progress towards goal (0-10)"
247 title_start = "GBO Goal Progress Chart – Goal "
248 return [
249 TrackerInfo(
250 value=self.progress if self.goal_number == 1 else None,
251 plot_label=title_start + "1",
252 axis_label=axis_label,
253 axis_min=axis_min,
254 axis_max=axis_max,
255 horizontal_lines=hlines
256 ),
257 TrackerInfo(
258 value=self.progress if self.goal_number == 2 else None,
259 plot_label=title_start + "2",
260 axis_label=axis_label,
261 axis_min=axis_min,
262 axis_max=axis_max,
263 horizontal_lines=hlines
264 ),
265 TrackerInfo(
266 value=self.progress if self.goal_number == 3 else None,
267 plot_label=title_start + "3",
268 axis_label=axis_label,
269 axis_min=axis_min,
270 axis_max=axis_max,
271 horizontal_lines=hlines
272 ),
273 ]
275 def is_complete(self) -> bool:
276 if self.any_fields_none(self.REQUIRED_FIELDS):
277 return False
278 if self.whose_goal == AGENT_OTHER and not self.whose_goal_other:
279 return False
280 return True
282 def get_task_html(self, req: CamcopsRequest) -> str:
283 return f"""
284 <div class="{CssClass.SUMMARY}">
285 <table class="{CssClass.SUMMARY}">
286 {self.get_is_complete_tr(req)}
287 </table>
288 </div>
289 <table class="{CssClass.TASKDETAIL}">
290 <tr>
291 <th width="30%">Date</th>
292 <td width="70%">{
293 answer(format_datetime(self.date, DateFormat.SHORT_DATE,
294 default=None))}</td>
295 </tr>
296 <tr>
297 <th>Session number</th>
298 <td>{answer(self.session)}</td>
299 </tr>
300 <tr>
301 <th>Goal number</th>
302 <td>{answer(self.goal_number)}</td>
303 </tr>
304 <tr>
305 <th>Goal description</th>
306 <td>{answer(self.goal_text)}</td>
307 </tr>
308 <tr>
309 <th>Progress <sup>[1]</sup></th>
310 <td>{answer(self.progress)}</td>
311 </tr>
312 <tr>
313 <th>Whose goal is this?</th>
314 <td>{answer(agent_description(self.whose_goal,
315 self.whose_goal_other))}</td>
316 </tr>
317 </table>
318 <div class="{CssClass.FOOTNOTES}">
319 [1] {self.wxstring(req, "progress_explanation")}
320 </div>
321 """
324# =============================================================================
325# GBO-GRaS
326# =============================================================================
328class Gbogras(TaskHasPatientMixin, Task):
329 """
330 Server implementation of the GBO-GRaS task.
331 """
332 __tablename__ = "gbogras"
333 shortname = "GBO-GRaS"
334 extrastring_taskname = "gbo"
335 provides_trackers = True
337 FN_DATE = "date" # NB SQL keyword too; doesn't matter
338 FN_RATE_GOAL_1 = "rate_goal_1"
339 FN_RATE_GOAL_2 = "rate_goal_2"
340 FN_RATE_GOAL_3 = "rate_goal_3"
341 FN_GOAL_1_DESC = "goal_1_description"
342 FN_GOAL_2_DESC = "goal_2_description"
343 FN_GOAL_3_DESC = "goal_3_description"
344 FN_GOAL_1_PROGRESS = "goal_1_progress"
345 FN_GOAL_2_PROGRESS = "goal_2_progress"
346 FN_GOAL_3_PROGRESS = "goal_3_progress"
347 FN_COMPLETED_BY = "completed_by"
348 FN_COMPLETED_BY_OTHER = "completed_by_other"
350 date = Column(FN_DATE, Date, comment="Date of ratings")
351 # ... NB SQL keyword too; doesn't matter
352 rate_goal_1 = Column(FN_RATE_GOAL_1, Boolean, comment="Rate goal 1?")
353 rate_goal_2 = Column(FN_RATE_GOAL_2, Boolean, comment="Rate goal 2?")
354 rate_goal_3 = Column(FN_RATE_GOAL_3, Boolean, comment="Rate goal 3?")
355 goal_1_description = Column(
356 FN_GOAL_1_DESC, UnicodeText,
357 comment="Goal 1 description")
358 goal_2_description = Column(
359 FN_GOAL_2_DESC, UnicodeText,
360 comment="Goal 2 description")
361 goal_3_description = Column(
362 FN_GOAL_3_DESC, UnicodeText,
363 comment="Goal 3 description")
364 goal_1_progress = Column(
365 FN_GOAL_1_PROGRESS, Integer,
366 comment="Goal 1 progress" + PROGRESS_COMMENT_SUFFIX)
367 goal_2_progress = Column(
368 FN_GOAL_2_PROGRESS, Integer,
369 comment="Goal 2 progress" + PROGRESS_COMMENT_SUFFIX)
370 goal_3_progress = Column(
371 FN_GOAL_3_PROGRESS, Integer,
372 comment="Goal 3 progress" + PROGRESS_COMMENT_SUFFIX)
373 completed_by = Column(
374 FN_COMPLETED_BY, Integer,
375 comment="Who completed the form ({})".format(
376 "; ".join(f"{k} = {v}"
377 for k, v in AGENT_STRING_MAP.items()
378 if k != AGENT_CLINICIAN)
379 )
380 )
381 completed_by_other = Column(
382 FN_COMPLETED_BY_OTHER, UnicodeText,
383 comment="If completed by 'other', who?")
385 REQUIRED_FIELDS = [FN_DATE, FN_COMPLETED_BY]
386 GOAL_TUPLES = (
387 # goalnum, rate it?, goal description, progress
388 (1, FN_RATE_GOAL_1, FN_GOAL_1_DESC, FN_GOAL_1_PROGRESS),
389 (2, FN_RATE_GOAL_2, FN_GOAL_2_DESC, FN_GOAL_2_PROGRESS),
390 (3, FN_RATE_GOAL_3, FN_GOAL_3_DESC, FN_GOAL_3_PROGRESS),
391 )
393 @staticmethod
394 def longname(req: "CamcopsRequest") -> str:
395 _ = req.gettext
396 return _("Goal-Based Outcomes – 3 – Goal Rating Sheet")
398 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
399 return self.standard_task_summary_fields()
401 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
402 axis_min = -0.5
403 axis_max = 10.5
404 hlines = [0, 5, 10]
405 axis_label = "Progress towards goal (0-10)"
406 title_start = "GBO Goal Rating Sheet – Goal "
407 return [
408 TrackerInfo(
409 value=self.goal_1_progress if self.rate_goal_1 else None,
410 plot_label=title_start + "1",
411 axis_label=axis_label,
412 axis_min=axis_min,
413 axis_max=axis_max,
414 horizontal_lines=hlines
415 ),
416 TrackerInfo(
417 value=self.goal_2_progress if self.rate_goal_2 else None,
418 plot_label=title_start + "2",
419 axis_label=axis_label,
420 axis_min=axis_min,
421 axis_max=axis_max,
422 horizontal_lines=hlines
423 ),
424 TrackerInfo(
425 value=self.goal_3_progress if self.rate_goal_3 else None,
426 plot_label=title_start + "3",
427 axis_label=axis_label,
428 axis_min=axis_min,
429 axis_max=axis_max,
430 horizontal_lines=hlines
431 ),
432 ]
434 def is_complete(self) -> bool:
435 if self.any_fields_none(self.REQUIRED_FIELDS):
436 return False
437 if self.completed_by == AGENT_OTHER and not self.completed_by_other:
438 return False
439 n_goals_completed = 0
440 for _, rate_attr, desc_attr, prog_attr in self.GOAL_TUPLES:
441 if getattr(self, rate_attr):
442 n_goals_completed += 1
443 if not getattr(self, desc_attr) or not getattr(self, prog_attr): # noqa
444 return False
445 return n_goals_completed > 0
447 def completed_by_tr(self) -> str:
448 who = agent_description(self.completed_by, self.completed_by_other)
449 return tr_qa("Completed by", who)
451 def get_date_tr(self) -> str:
452 return tr_qa("Date", format_datetime(self.date,
453 DateFormat.SHORT_DATE,
454 default=None))
456 def get_task_html(self, req: CamcopsRequest) -> str:
457 rows = [] # type: List[str]
458 for goalnum, rate_attr, desc_attr, prog_attr in self.GOAL_TUPLES:
459 if getattr(self, rate_attr):
460 rows.append(f"""
461 <tr>
462 <td>{answer(goalnum)}</td>
463 <td>{answer(getattr(self, desc_attr))}</td>
464 <td>{answer(getattr(self, prog_attr))}</td>
465 </tr>
466 """)
467 newline = "\n"
468 return f"""
469 <div class="{CssClass.SUMMARY}">
470 <table class="{CssClass.SUMMARY}">
471 {self.get_is_complete_tr(req)}
472 {self.get_date_tr()}
473 {self.completed_by_tr()}
474 </table>
475 </div>
476 <table class="{CssClass.TASKDETAIL}">
477 <tr>
478 <th width="15%">Goal number</th>
479 <th width="70%">Description</th>
480 <th width="15%">Progress <sup>[1]</sup></th>
481 </tr>
482 {newline.join(rows)}
483 </table>
484 <div class="{CssClass.FOOTNOTES}">
485 [1] {self.wxstring(req, "progress_explanation")}
486 </div>
487 """