Coverage for tasks/hamd7.py: 53%
66 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/tasks/hamd7.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"""
28from typing import Any, cast, List, Optional, Type
30from cardinal_pythonlib.stringfunc import strseq
31from sqlalchemy.sql.sqltypes import Integer
33from camcops_server.cc_modules.cc_constants import CssClass
34from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
35from camcops_server.cc_modules.cc_db import add_multiple_columns
36from camcops_server.cc_modules.cc_html import answer, tr, tr_qa
37from camcops_server.cc_modules.cc_request import CamcopsRequest
38from camcops_server.cc_modules.cc_sqla_coltypes import (
39 SummaryCategoryColType,
40)
41from camcops_server.cc_modules.cc_summaryelement import SummaryElement
42from camcops_server.cc_modules.cc_task import (
43 get_from_dict,
44 Task,
45 TaskHasClinicianMixin,
46 TaskHasPatientMixin,
47)
48from camcops_server.cc_modules.cc_text import SS
49from camcops_server.cc_modules.cc_trackerhelpers import (
50 TrackerInfo,
51 TrackerLabel,
52)
55# =============================================================================
56# HAMD-7
57# =============================================================================
60class Hamd7( # type: ignore[misc]
61 TaskHasPatientMixin,
62 TaskHasClinicianMixin,
63 Task,
64):
65 """
66 Server implementation of the HAMD-7 task.
67 """
69 __tablename__ = "hamd7"
70 shortname = "HAMD-7"
71 info_filename_stem = "hamd"
72 provides_trackers = True
74 NQUESTIONS = 7
76 @classmethod
77 def extend_columns(cls: Type["Hamd7"], **kwargs: Any) -> None:
78 add_multiple_columns(
79 cls,
80 "q",
81 1,
82 5,
83 minimum=0,
84 maximum=4,
85 comment_fmt="Q{n}, {s} (0-4, higher worse)",
86 comment_strings=[
87 "depressed mood",
88 "guilt",
89 "interest/pleasure/level of activities",
90 "psychological anxiety",
91 "somatic anxiety",
92 ],
93 )
94 add_multiple_columns(
95 cls,
96 "q",
97 6,
98 6,
99 minimum=0,
100 maximum=2,
101 comment_fmt="Q{n}, {s} (0-2, higher worse)",
102 comment_strings=[
103 "energy/somatic symptoms",
104 ],
105 )
106 add_multiple_columns(
107 cls,
108 "q",
109 7,
110 7,
111 minimum=0,
112 maximum=4,
113 comment_fmt="Q{n}, {s} (0-4, higher worse)",
114 comment_strings=[
115 "suicide",
116 ],
117 )
119 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
120 MAX_SCORE = 26
122 @staticmethod
123 def longname(req: "CamcopsRequest") -> str:
124 _ = req.gettext
125 return _("Hamilton Rating Scale for Depression (7-item scale)")
127 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
128 return [
129 TrackerInfo(
130 value=self.total_score(),
131 plot_label="HAM-D-7 total score",
132 axis_label=f"Total score (out of {self.MAX_SCORE})",
133 axis_min=-0.5,
134 axis_max=self.MAX_SCORE + 0.5,
135 horizontal_lines=[19.5, 11.5, 3.5],
136 horizontal_labels=[
137 TrackerLabel(23, self.wxstring(req, "severity_severe")),
138 TrackerLabel(
139 15.5, self.wxstring(req, "severity_moderate")
140 ),
141 TrackerLabel(7.5, self.wxstring(req, "severity_mild")),
142 TrackerLabel(1.75, self.wxstring(req, "severity_none")),
143 ],
144 )
145 ]
147 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
148 if not self.is_complete():
149 return CTV_INCOMPLETE
150 return [
151 CtvInfo(
152 content=(
153 f"HAM-D-7 total score "
154 f"{self.total_score()}/{self.MAX_SCORE} "
155 f"({self.severity(req)})"
156 )
157 )
158 ]
160 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
161 return self.standard_task_summary_fields() + [
162 SummaryElement(
163 name="total",
164 coltype=Integer(),
165 value=self.total_score(),
166 comment=f"Total score (/{self.MAX_SCORE})",
167 ),
168 SummaryElement(
169 name="severity",
170 coltype=SummaryCategoryColType,
171 value=self.severity(req),
172 comment="Severity",
173 ),
174 ]
176 def is_complete(self) -> bool:
177 return (
178 self.all_fields_not_none(self.TASK_FIELDS)
179 and self.field_contents_valid()
180 )
182 def total_score(self) -> int:
183 return cast(int, self.sum_fields(self.TASK_FIELDS))
185 def severity(self, req: CamcopsRequest) -> str:
186 score = self.total_score()
187 if score >= 20:
188 return self.wxstring(req, "severity_severe")
189 elif score >= 12:
190 return self.wxstring(req, "severity_moderate")
191 elif score >= 4:
192 return self.wxstring(req, "severity_mild")
193 else:
194 return self.wxstring(req, "severity_none")
196 def get_task_html(self, req: CamcopsRequest) -> str:
197 score = self.total_score()
198 severity = self.severity(req)
199 answer_dicts = []
200 for q in range(1, self.NQUESTIONS + 1):
201 d: dict[Optional[int], Optional[str]] = {None: None}
202 for option in range(0, 5):
203 if q == 6 and option > 2:
204 continue
205 d[option] = self.wxstring(
206 req, "q" + str(q) + "_option" + str(option)
207 )
208 answer_dicts.append(d)
210 q_a = ""
211 for q in range(1, self.NQUESTIONS + 1):
212 q_a += tr_qa(
213 self.wxstring(req, "q" + str(q) + "_s"),
214 get_from_dict(
215 answer_dicts[q - 1], getattr(self, "q" + str(q))
216 ),
217 )
219 return """
220 <div class="{CssClass.SUMMARY}">
221 <table class="{CssClass.SUMMARY}">
222 {tr_is_complete}
223 {total_score}
224 {severity}
225 </table>
226 </div>
227 <table class="{CssClass.TASKDETAIL}">
228 <tr>
229 <th width="30%">Question</th>
230 <th width="70%">Answer</th>
231 </tr>
232 {q_a}
233 </table>
234 <div class="{CssClass.FOOTNOTES}">
235 [1] ≥20 severe, ≥12 moderate, ≥4 mild, <4 none.
236 </div>
237 """.format(
238 CssClass=CssClass,
239 tr_is_complete=self.get_is_complete_tr(req),
240 total_score=tr(
241 req.sstring(SS.TOTAL_SCORE),
242 answer(score) + " / {}".format(self.MAX_SCORE),
243 ),
244 severity=tr_qa(
245 self.wxstring(req, "severity") + " <sup>[1]</sup>", severity
246 ),
247 q_a=q_a,
248 )