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