Coverage for tasks/cesd.py: 59%
75 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/cesd.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===============================================================================
28- By Joe Kearney, Rudolf Cardinal.
30"""
32from typing import Any, Dict, List, Tuple, Type
34from cardinal_pythonlib.classes import classproperty
35from cardinal_pythonlib.stringfunc import strseq
36from semantic_version import Version
37from sqlalchemy.ext.declarative import DeclarativeMeta
38from sqlalchemy.sql.sqltypes import Boolean
40from camcops_server.cc_modules.cc_constants import CssClass
41from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE
42from camcops_server.cc_modules.cc_db import add_multiple_columns
43from camcops_server.cc_modules.cc_html import get_yes_no, tr_qa
44from camcops_server.cc_modules.cc_request import CamcopsRequest
46from camcops_server.cc_modules.cc_summaryelement import SummaryElement
47from camcops_server.cc_modules.cc_task import (
48 get_from_dict,
49 Task,
50 TaskHasPatientMixin,
51)
52from camcops_server.cc_modules.cc_text import SS
53from camcops_server.cc_modules.cc_trackerhelpers import (
54 equally_spaced_int,
55 regular_tracker_axis_ticks_int,
56 TrackerInfo,
57 TrackerLabel,
58)
61# =============================================================================
62# CESD
63# =============================================================================
66class CesdMetaclass(DeclarativeMeta):
67 """
68 There is a multilayer metaclass problem; see hads.py for discussion.
69 """
71 # noinspection PyInitNewSignature
72 def __init__(
73 cls: Type["Cesd"],
74 name: str,
75 bases: Tuple[Type, ...],
76 classdict: Dict[str, Any],
77 ) -> None:
78 add_multiple_columns(
79 cls,
80 "q",
81 1,
82 cls.N_QUESTIONS,
83 minimum=0,
84 maximum=4,
85 comment_fmt=(
86 "Q{n} ({s}) (0 rarely/none of the time - 4 all of the time)"
87 ),
88 comment_strings=[
89 "sensitivity/irritability",
90 "poor appetite",
91 "unshakeable blues",
92 "low self-esteem",
93 "poor concentration",
94 "depressed",
95 "everything effortful",
96 "hopeful",
97 "feelings of failure",
98 "fearful",
99 "sleep restless",
100 "happy",
101 "uncommunicative",
102 "lonely",
103 "perceived unfriendliness",
104 "enjoyment",
105 "crying spells",
106 "sadness",
107 "feeling disliked",
108 "could not get going",
109 ],
110 )
111 super().__init__(name, bases, classdict)
114class Cesd(TaskHasPatientMixin, Task, metaclass=CesdMetaclass):
115 """
116 Server implementation of the CESD task.
117 """
119 __tablename__ = "cesd"
120 shortname = "CESD"
121 provides_trackers = True
122 extrastring_taskname = "cesd"
123 N_QUESTIONS = 20
124 N_ANSWERS = 4
125 DEPRESSION_RISK_THRESHOLD = 16
126 SCORED_FIELDS = strseq("q", 1, N_QUESTIONS)
127 TASK_FIELDS = SCORED_FIELDS
128 MIN_SCORE = 0
129 MAX_SCORE = 3 * N_QUESTIONS
130 REVERSE_SCORED_QUESTIONS = [4, 8, 12, 16]
132 @staticmethod
133 def longname(req: "CamcopsRequest") -> str:
134 _ = req.gettext
135 return _("Center for Epidemiologic Studies Depression Scale")
137 # noinspection PyMethodParameters
138 @classproperty
139 def minimum_client_version(cls) -> Version:
140 return Version("2.2.8")
142 def is_complete(self) -> bool:
143 return (
144 self.all_fields_not_none(self.TASK_FIELDS)
145 and self.field_contents_valid()
146 )
148 def total_score(self) -> int:
149 # Need to store values as per original then flip here
150 total = 0
151 for qnum, fieldname in enumerate(self.SCORED_FIELDS, start=1):
152 score = getattr(self, fieldname)
153 if score is None:
154 continue
155 if qnum in self.REVERSE_SCORED_QUESTIONS:
156 total += 3 - score
157 else:
158 total += score
159 return total
161 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
162 line_step = 20
163 threshold_line = self.DEPRESSION_RISK_THRESHOLD - 0.5
164 # noinspection PyTypeChecker
165 return [
166 TrackerInfo(
167 value=self.total_score(),
168 plot_label="CESD total score",
169 axis_label=f"Total score ({self.MIN_SCORE}-{self.MAX_SCORE})",
170 axis_min=self.MIN_SCORE - 0.5,
171 axis_max=self.MAX_SCORE + 0.5,
172 axis_ticks=regular_tracker_axis_ticks_int(
173 self.MIN_SCORE, self.MAX_SCORE, step=line_step
174 ),
175 horizontal_lines=equally_spaced_int(
176 self.MIN_SCORE + line_step,
177 self.MAX_SCORE - line_step,
178 step=line_step,
179 )
180 + [threshold_line],
181 horizontal_labels=[
182 TrackerLabel(
183 threshold_line,
184 self.wxstring(req, "depression_or_risk_of"),
185 )
186 ],
187 )
188 ]
190 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
191 if not self.is_complete():
192 return CTV_INCOMPLETE
193 return [CtvInfo(content=f"CESD total score {self.total_score()}")]
195 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
196 return self.standard_task_summary_fields() + [
197 SummaryElement(
198 name="depression_risk",
199 coltype=Boolean(),
200 value=self.has_depression_risk(),
201 comment="Has depression or at risk of depression",
202 )
203 ]
205 def has_depression_risk(self) -> bool:
206 return self.total_score() >= self.DEPRESSION_RISK_THRESHOLD
208 def get_task_html(self, req: CamcopsRequest) -> str:
209 score = self.total_score()
210 answer_dict = {None: None}
211 for option in range(self.N_ANSWERS):
212 answer_dict[option] = (
213 str(option) + " – " + self.wxstring(req, "a" + str(option))
214 )
215 q_a = ""
216 for q in range(1, self.N_QUESTIONS):
217 q_a += tr_qa(
218 self.wxstring(req, "q" + str(q) + "_s"),
219 get_from_dict(answer_dict, getattr(self, "q" + str(q))),
220 )
222 tr_total_score = (
223 tr_qa(f"{req.sstring(SS.TOTAL_SCORE)} (0–60)", score),
224 )
225 tr_depression_or_risk_of = (
226 tr_qa(
227 self.wxstring(req, "depression_or_risk_of")
228 + "? <sup>[1]</sup>",
229 get_yes_no(req, self.has_depression_risk()),
230 ),
231 )
232 return f"""
233 <div class="{CssClass.SUMMARY}">
234 <table class="{CssClass.SUMMARY}">
235 {self.get_is_complete_tr(req)}
236 {tr_total_score}
237 {tr_depression_or_risk_of}
238 </table>
239 </div>
240 <table class="{CssClass.TASKDETAIL}">
241 <tr>
242 <th width="70%">Question</th>
243 <th width="30%">Answer</th>
244 </tr>
245 {q_a}
246 </table>
247 <div class="{CssClass.FOOTNOTES}">
248 [1] Presence of depression (or depression risk) is indicated by a
249 score ≥ 16
250 </div>
251 """