Coverage for tasks/hama.py: 52%
61 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/hama.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 CtvInfo, CTV_INCOMPLETE
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 SummaryCategoryColType
39from camcops_server.cc_modules.cc_summaryelement import SummaryElement
40from camcops_server.cc_modules.cc_task import (
41 get_from_dict,
42 Task,
43 TaskHasClinicianMixin,
44 TaskHasPatientMixin,
45)
46from camcops_server.cc_modules.cc_text import SS
47from camcops_server.cc_modules.cc_trackerhelpers import (
48 TrackerInfo,
49 TrackerLabel,
50)
53# =============================================================================
54# HAM-A
55# =============================================================================
58class Hama( # type: ignore[misc]
59 TaskHasPatientMixin,
60 TaskHasClinicianMixin,
61 Task,
62):
63 """
64 Server implementation of the HAM-A task.
65 """
67 __tablename__ = "hama"
68 shortname = "HAM-A"
69 provides_trackers = True
71 NQUESTIONS = 14
73 @classmethod
74 def extend_columns(cls: Type["Hama"], **kwargs: Any) -> None:
75 add_multiple_columns(
76 cls,
77 "q",
78 1,
79 cls.NQUESTIONS,
80 comment_fmt="Q{n}, {s} (0-4, higher worse)",
81 minimum=0,
82 maximum=4,
83 comment_strings=[
84 "anxious mood",
85 "tension",
86 "fears",
87 "insomnia",
88 "concentration/memory",
89 "depressed mood",
90 "somatic, muscular",
91 "somatic, sensory",
92 "cardiovascular",
93 "respiratory",
94 "gastrointestinal",
95 "genitourinary",
96 "other autonomic",
97 "behaviour in interview",
98 ],
99 )
101 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
102 MAX_SCORE = 56
104 @staticmethod
105 def longname(req: "CamcopsRequest") -> str:
106 _ = req.gettext
107 return _("Hamilton Rating Scale for Anxiety")
109 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
110 return [
111 TrackerInfo(
112 value=self.total_score(),
113 plot_label="HAM-A total score",
114 axis_label=f"Total score (out of {self.MAX_SCORE})",
115 axis_min=-0.5,
116 axis_max=self.MAX_SCORE + 0.5,
117 horizontal_lines=[30.5, 24.5, 17.5],
118 horizontal_labels=[
119 TrackerLabel(33, req.sstring(SS.VERY_SEVERE)),
120 TrackerLabel(27.5, req.sstring(SS.MODERATE_TO_SEVERE)),
121 TrackerLabel(21, req.sstring(SS.MILD_TO_MODERATE)),
122 TrackerLabel(8.75, req.sstring(SS.MILD)),
123 ],
124 )
125 ]
127 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
128 if not self.is_complete():
129 return CTV_INCOMPLETE
130 return [
131 CtvInfo(
132 content=(
133 f"HAM-A total score {self.total_score()}/{self.MAX_SCORE} "
134 f"({self.severity(req)})"
135 )
136 )
137 ]
139 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
140 return self.standard_task_summary_fields() + [
141 SummaryElement(
142 name="total",
143 coltype=Integer(),
144 value=self.total_score(),
145 comment=f"Total score (/{self.MAX_SCORE})",
146 ),
147 SummaryElement(
148 name="severity",
149 coltype=SummaryCategoryColType,
150 value=self.severity(req),
151 comment="Severity",
152 ),
153 ]
155 def is_complete(self) -> bool:
156 return (
157 self.all_fields_not_none(self.TASK_FIELDS)
158 and self.field_contents_valid()
159 )
161 def total_score(self) -> int:
162 return cast(int, self.sum_fields(self.TASK_FIELDS))
164 def severity(self, req: CamcopsRequest) -> str:
165 score = self.total_score()
166 if score >= 31:
167 return req.sstring(SS.VERY_SEVERE)
168 elif score >= 25:
169 return req.sstring(SS.MODERATE_TO_SEVERE)
170 elif score >= 18:
171 return req.sstring(SS.MILD_TO_MODERATE)
172 else:
173 return req.sstring(SS.MILD)
175 def get_task_html(self, req: CamcopsRequest) -> str:
176 score = self.total_score()
177 severity = self.severity(req)
178 answer_dicts = []
179 for q in range(1, self.NQUESTIONS + 1):
180 d: dict[Optional[int], Optional[str]] = {None: None}
181 for option in range(0, 4 + 1):
182 d[option] = self.wxstring(
183 req, "q" + str(q) + "_option" + str(option)
184 )
185 answer_dicts.append(d)
186 q_a = ""
187 for q in range(1, self.NQUESTIONS + 1):
188 q_a += tr_qa(
189 self.wxstring(req, "q" + str(q) + "_s")
190 + " "
191 + self.wxstring(req, "q" + str(q) + "_question"),
192 get_from_dict(
193 answer_dicts[q - 1], getattr(self, "q" + str(q))
194 ),
195 )
196 return """
197 <div class="{CssClass.SUMMARY}">
198 <table class="{CssClass.SUMMARY}">
199 {tr_is_complete}
200 {total_score}
201 {symptom_severity}
202 </table>
203 </div>
204 <table class="{CssClass.TASKDETAIL}">
205 <tr>
206 <th width="50%">Question</th>
207 <th width="50%">Answer</th>
208 </tr>
209 {q_a}
210 </table>
211 <div class="{CssClass.FOOTNOTES}">
212 [1] ≥31 very severe, ≥25 moderate to severe,
213 ≥18 mild to moderate, otherwise mild.
214 </div>
215 """.format(
216 CssClass=CssClass,
217 tr_is_complete=self.get_is_complete_tr(req),
218 total_score=tr(
219 req.sstring(SS.TOTAL_SCORE),
220 answer(score) + " / {}".format(self.MAX_SCORE),
221 ),
222 symptom_severity=tr_qa(
223 self.wxstring(req, "symptom_severity") + " <sup>[1]</sup>",
224 severity,
225 ),
226 q_a=q_a,
227 )