Source code for camcops_server.tasks.gbo

#!/usr/bin/env python

"""
camcops_server/tasks/gbo.py

===============================================================================

    Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
    Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

    This file is part of CamCOPS.

    CamCOPS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    CamCOPS is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.

===============================================================================

Goal-Based Outcomes tasks.

- By Joe Kearney, Rudolf Cardinal.

"""

from typing import List

from cardinal_pythonlib.datetimefunc import format_datetime
from sqlalchemy import Column
from sqlalchemy.sql.sqltypes import Boolean, Integer, Date, UnicodeText

from camcops_server.cc_modules.cc_constants import CssClass, DateFormat
from camcops_server.cc_modules.cc_html import tr_qa, answer
from camcops_server.cc_modules.cc_request import CamcopsRequest
from camcops_server.cc_modules.cc_summaryelement import SummaryElement
from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin
from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo


# =============================================================================
# Common GBO constants
# =============================================================================

AGENT_PATIENT = 1
AGENT_PARENT_CARER = 2
AGENT_CLINICIAN = 3
AGENT_OTHER = 4

AGENT_STRING_MAP = {
    AGENT_PATIENT: "Patient/service user",  # in original: "Child/young person"
    AGENT_PARENT_CARER: "Parent/carer",
    AGENT_CLINICIAN: "Practitioner/clinician",
    AGENT_OTHER: "Other: ",
}
UNKNOWN_AGENT = "Unknown"

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


def agent_description(agent: int, other_detail: str) -> str:
    who = AGENT_STRING_MAP.get(agent, UNKNOWN_AGENT)
    if agent == AGENT_OTHER:
        who += other_detail or "?"
    return who


# =============================================================================
# GBO-GReS
# =============================================================================


[docs]class Gbogres(TaskHasPatientMixin, Task): """ Server implementation of the GBO - Goal Record Sheet task. """ __tablename__ = "gbogres" shortname = "GBO-GReS" extrastring_taskname = "gbo" info_filename_stem = extrastring_taskname FN_DATE = "date" # NB SQL keyword too; doesn't matter FN_GOAL_1_DESC = "goal_1_description" FN_GOAL_2_DESC = "goal_2_description" FN_GOAL_3_DESC = "goal_3_description" FN_GOAL_OTHER = "other_goals" FN_COMPLETED_BY = "completed_by" FN_COMPLETED_BY_OTHER = "completed_by_other" REQUIRED_FIELDS = [FN_DATE, FN_GOAL_1_DESC, FN_COMPLETED_BY] date = Column(FN_DATE, Date, comment="Date of goal-setting") goal_1_description = Column( FN_GOAL_1_DESC, UnicodeText, comment="Goal 1 description" ) goal_2_description = Column( FN_GOAL_2_DESC, UnicodeText, comment="Goal 2 description" ) goal_3_description = Column( FN_GOAL_3_DESC, UnicodeText, comment="Goal 3 description" ) other_goals = Column( FN_GOAL_OTHER, UnicodeText, comment="Other/additional goal description(s)", ) completed_by = Column( FN_COMPLETED_BY, Integer, comment="Who completed the form ({})".format( "; ".join(f"{k} = {v}" for k, v in AGENT_STRING_MAP.items()) ), ) completed_by_other = Column( FN_COMPLETED_BY_OTHER, UnicodeText, comment="If completed by 'other', who?", )
[docs] @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Goal-Based Outcomes – 1 – Goal Record Sheet")
[docs] def get_n_core_goals(self) -> int: """ Returns the number of non-blank core (1-3) goals. """ return len( list( filter( None, [ self.goal_1_description, self.goal_2_description, self.goal_3_description, ], ) ) )
def goals_set_tr(self) -> str: extra = " (additional goals specified)" if self.other_goals else "" return tr_qa( "Number of goals set", f"{self.get_n_core_goals()}{extra}" ) def completed_by_tr(self) -> str: who = agent_description(self.completed_by, self.completed_by_other) return tr_qa("Completed by", who) def get_date_tr(self) -> str: return tr_qa( "Date", format_datetime(self.date, DateFormat.SHORT_DATE, default=None), )
[docs] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields()
[docs] def is_complete(self) -> bool: if self.any_fields_none(self.REQUIRED_FIELDS): return False if self.completed_by == AGENT_OTHER and not self.completed_by_other: return False return True
[docs] def get_task_html(self, req: CamcopsRequest) -> str: return f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} {self.get_date_tr()} {self.completed_by_tr()} {self.goals_set_tr()} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="15%">Goal number</th> <th width="85%">Goal description</th> </tr> <tr><td>1</td><td>{answer(self.goal_1_description, default="")}</td></tr> <tr><td>2</td><td>{answer(self.goal_2_description, default="")}</td></tr> <tr><td>3</td><td>{answer(self.goal_3_description, default="")}</td></tr> <tr><td>Other</td><td>{answer(self.other_goals, default="")}</td></tr> </table> """
# ============================================================================= # GBO-GPC # =============================================================================
[docs]class Gbogpc(TaskHasPatientMixin, Task): """ Server implementation of the GBO-GPC task. """ __tablename__ = "gbogpc" shortname = "GBO-GPC" extrastring_taskname = "gbo" info_filename_stem = extrastring_taskname provides_trackers = True FN_DATE = "date" # NB SQL keyword too; doesn't matter FN_SESSION = "session" FN_GOAL_NUMBER = "goal_number" FN_GOAL_DESCRIPTION = "goal_description" FN_PROGRESS = "progress" FN_WHOSE_GOAL = "whose_goal" FN_WHOSE_GOAL_OTHER = "whose_goal_other" date = Column(FN_DATE, Date, comment="Session date") session = Column(FN_SESSION, Integer, comment="Session number") goal_number = Column(FN_GOAL_NUMBER, Integer, comment="Goal number (1-3)") goal_text = Column( FN_GOAL_DESCRIPTION, UnicodeText, comment="Brief description of the goal", ) progress = Column( FN_PROGRESS, Integer, comment="Progress towards goal" + PROGRESS_COMMENT_SUFFIX, ) whose_goal = Column( FN_WHOSE_GOAL, Integer, comment="Whose goal is this ({})".format( "; ".join(f"{k} = {v}" for k, v in AGENT_STRING_MAP.items()) ), ) whose_goal_other = Column( FN_WHOSE_GOAL_OTHER, UnicodeText, comment="If 'whose goal' is 'other', who?", ) REQUIRED_FIELDS = [ FN_DATE, FN_SESSION, FN_GOAL_NUMBER, FN_PROGRESS, FN_WHOSE_GOAL, ]
[docs] @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Goal-Based Outcomes – 2 – Goal Progress Chart")
[docs] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields()
[docs] def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: axis_min = -0.5 axis_max = 10.5 hlines = [0, 5, 10] axis_label = "Progress towards goal (0-10)" title_start = "GBO Goal Progress Chart – Goal " return [ TrackerInfo( value=self.progress if self.goal_number == 1 else None, plot_label=title_start + "1", axis_label=axis_label, axis_min=axis_min, axis_max=axis_max, horizontal_lines=hlines, ), TrackerInfo( value=self.progress if self.goal_number == 2 else None, plot_label=title_start + "2", axis_label=axis_label, axis_min=axis_min, axis_max=axis_max, horizontal_lines=hlines, ), TrackerInfo( value=self.progress if self.goal_number == 3 else None, plot_label=title_start + "3", axis_label=axis_label, axis_min=axis_min, axis_max=axis_max, horizontal_lines=hlines, ), ]
[docs] def is_complete(self) -> bool: if self.any_fields_none(self.REQUIRED_FIELDS): return False if self.whose_goal == AGENT_OTHER and not self.whose_goal_other: return False return True
[docs] def get_task_html(self, req: CamcopsRequest) -> str: return f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="30%">Date</th> <td width="70%">{ answer(format_datetime(self.date, DateFormat.SHORT_DATE, default=None))}</td> </tr> <tr> <th>Session number</th> <td>{answer(self.session)}</td> </tr> <tr> <th>Goal number</th> <td>{answer(self.goal_number)}</td> </tr> <tr> <th>Goal description</th> <td>{answer(self.goal_text)}</td> </tr> <tr> <th>Progress <sup>[1]</sup></th> <td>{answer(self.progress)}</td> </tr> <tr> <th>Whose goal is this?</th> <td>{answer(agent_description(self.whose_goal, self.whose_goal_other))}</td> </tr> </table> <div class="{CssClass.FOOTNOTES}"> [1] {self.wxstring(req, "progress_explanation")} </div> """
# ============================================================================= # GBO-GRaS # =============================================================================
[docs]class Gbogras(TaskHasPatientMixin, Task): """ Server implementation of the GBO-GRaS task. """ __tablename__ = "gbogras" shortname = "GBO-GRaS" extrastring_taskname = "gbo" info_filename_stem = extrastring_taskname provides_trackers = True FN_DATE = "date" # NB SQL keyword too; doesn't matter FN_RATE_GOAL_1 = "rate_goal_1" FN_RATE_GOAL_2 = "rate_goal_2" FN_RATE_GOAL_3 = "rate_goal_3" FN_GOAL_1_DESC = "goal_1_description" FN_GOAL_2_DESC = "goal_2_description" FN_GOAL_3_DESC = "goal_3_description" FN_GOAL_1_PROGRESS = "goal_1_progress" FN_GOAL_2_PROGRESS = "goal_2_progress" FN_GOAL_3_PROGRESS = "goal_3_progress" FN_COMPLETED_BY = "completed_by" FN_COMPLETED_BY_OTHER = "completed_by_other" date = Column(FN_DATE, Date, comment="Date of ratings") # ... NB SQL keyword too; doesn't matter rate_goal_1 = Column(FN_RATE_GOAL_1, Boolean, comment="Rate goal 1?") rate_goal_2 = Column(FN_RATE_GOAL_2, Boolean, comment="Rate goal 2?") rate_goal_3 = Column(FN_RATE_GOAL_3, Boolean, comment="Rate goal 3?") goal_1_description = Column( FN_GOAL_1_DESC, UnicodeText, comment="Goal 1 description" ) goal_2_description = Column( FN_GOAL_2_DESC, UnicodeText, comment="Goal 2 description" ) goal_3_description = Column( FN_GOAL_3_DESC, UnicodeText, comment="Goal 3 description" ) goal_1_progress = Column( FN_GOAL_1_PROGRESS, Integer, comment="Goal 1 progress" + PROGRESS_COMMENT_SUFFIX, ) goal_2_progress = Column( FN_GOAL_2_PROGRESS, Integer, comment="Goal 2 progress" + PROGRESS_COMMENT_SUFFIX, ) goal_3_progress = Column( FN_GOAL_3_PROGRESS, Integer, comment="Goal 3 progress" + PROGRESS_COMMENT_SUFFIX, ) completed_by = Column( FN_COMPLETED_BY, Integer, comment="Who completed the form ({})".format( "; ".join( f"{k} = {v}" for k, v in AGENT_STRING_MAP.items() if k != AGENT_CLINICIAN ) ), ) completed_by_other = Column( FN_COMPLETED_BY_OTHER, UnicodeText, comment="If completed by 'other', who?", ) REQUIRED_FIELDS = [FN_DATE, FN_COMPLETED_BY] GOAL_TUPLES = ( # goalnum, rate it?, goal description, progress (1, FN_RATE_GOAL_1, FN_GOAL_1_DESC, FN_GOAL_1_PROGRESS), (2, FN_RATE_GOAL_2, FN_GOAL_2_DESC, FN_GOAL_2_PROGRESS), (3, FN_RATE_GOAL_3, FN_GOAL_3_DESC, FN_GOAL_3_PROGRESS), )
[docs] @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("Goal-Based Outcomes – 3 – Goal Rating Sheet")
[docs] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields()
[docs] def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: axis_min = -0.5 axis_max = 10.5 hlines = [0, 5, 10] axis_label = "Progress towards goal (0-10)" title_start = "GBO Goal Rating Sheet – Goal " return [ TrackerInfo( value=self.goal_1_progress if self.rate_goal_1 else None, plot_label=title_start + "1", axis_label=axis_label, axis_min=axis_min, axis_max=axis_max, horizontal_lines=hlines, ), TrackerInfo( value=self.goal_2_progress if self.rate_goal_2 else None, plot_label=title_start + "2", axis_label=axis_label, axis_min=axis_min, axis_max=axis_max, horizontal_lines=hlines, ), TrackerInfo( value=self.goal_3_progress if self.rate_goal_3 else None, plot_label=title_start + "3", axis_label=axis_label, axis_min=axis_min, axis_max=axis_max, horizontal_lines=hlines, ), ]
[docs] def is_complete(self) -> bool: if self.any_fields_none(self.REQUIRED_FIELDS): return False if self.completed_by == AGENT_OTHER and not self.completed_by_other: return False n_goals_completed = 0 for _, rate_attr, desc_attr, prog_attr in self.GOAL_TUPLES: if getattr(self, rate_attr): n_goals_completed += 1 if not getattr(self, desc_attr) or not getattr( self, prog_attr ): return False return n_goals_completed > 0
def completed_by_tr(self) -> str: who = agent_description(self.completed_by, self.completed_by_other) return tr_qa("Completed by", who) def get_date_tr(self) -> str: return tr_qa( "Date", format_datetime(self.date, DateFormat.SHORT_DATE, default=None), )
[docs] def get_task_html(self, req: CamcopsRequest) -> str: rows = [] # type: List[str] for goalnum, rate_attr, desc_attr, prog_attr in self.GOAL_TUPLES: if getattr(self, rate_attr): rows.append( f""" <tr> <td>{answer(goalnum)}</td> <td>{answer(getattr(self, desc_attr))}</td> <td>{answer(getattr(self, prog_attr))}</td> </tr> """ ) newline = "\n" return f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} {self.get_date_tr()} {self.completed_by_tr()} </table> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="15%">Goal number</th> <th width="70%">Description</th> <th width="15%">Progress <sup>[1]</sup></th> </tr> {newline.join(rows)} </table> <div class="{CssClass.FOOTNOTES}"> [1] {self.wxstring(req, "progress_explanation")} </div> """