Coverage for tasks/hama.py : 55%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python
3"""
4camcops_server/tasks/hama.py
6===============================================================================
8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
10 This file is part of CamCOPS.
12 CamCOPS is free software: you can redistribute it and/or modify
13 it under the terms of the GNU General Public License as published by
14 the Free Software Foundation, either version 3 of the License, or
15 (at your option) any later version.
17 CamCOPS is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 GNU General Public License for more details.
22 You should have received a copy of the GNU General Public License
23 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
25===============================================================================
27"""
29from typing import Any, Dict, List, Tuple, Type
31from cardinal_pythonlib.stringfunc import strseq
32from sqlalchemy.ext.declarative import DeclarativeMeta
33from sqlalchemy.sql.sqltypes import Integer
35from camcops_server.cc_modules.cc_constants import CssClass
36from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE
37from camcops_server.cc_modules.cc_db import add_multiple_columns
38from camcops_server.cc_modules.cc_html import answer, tr, tr_qa
39from camcops_server.cc_modules.cc_request import CamcopsRequest
40from camcops_server.cc_modules.cc_sqla_coltypes import SummaryCategoryColType
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# HAM-A
57# =============================================================================
59class HamaMetaclass(DeclarativeMeta):
60 # noinspection PyInitNewSignature
61 def __init__(cls: Type['Hama'],
62 name: str,
63 bases: Tuple[Type, ...],
64 classdict: Dict[str, Any]) -> None:
65 add_multiple_columns(
66 cls, "q", 1, cls.NQUESTIONS,
67 comment_fmt="Q{n}, {s} (0-4, higher worse)",
68 minimum=0, maximum=4,
69 comment_strings=[
70 "anxious mood", "tension", "fears", "insomnia",
71 "concentration/memory", "depressed mood", "somatic, muscular",
72 "somatic, sensory", "cardiovascular", "respiratory",
73 "gastrointestinal", "genitourinary", "other autonomic",
74 "behaviour in interview"
75 ]
76 )
77 super().__init__(name, bases, classdict)
80class Hama(TaskHasPatientMixin, TaskHasClinicianMixin, Task,
81 metaclass=HamaMetaclass):
82 """
83 Server implementation of the HAM-A task.
84 """
85 __tablename__ = "hama"
86 shortname = "HAM-A"
87 provides_trackers = True
89 NQUESTIONS = 14
90 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
91 MAX_SCORE = 56
93 @staticmethod
94 def longname(req: "CamcopsRequest") -> str:
95 _ = req.gettext
96 return _("Hamilton Rating Scale for Anxiety")
98 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
99 return [TrackerInfo(
100 value=self.total_score(),
101 plot_label="HAM-A total score",
102 axis_label=f"Total score (out of {self.MAX_SCORE})",
103 axis_min=-0.5,
104 axis_max=self.MAX_SCORE + 0.5,
105 horizontal_lines=[30.5, 24.5, 17.5],
106 horizontal_labels=[
107 TrackerLabel(33, req.sstring(SS.VERY_SEVERE)),
108 TrackerLabel(27.5, req.sstring(SS.MODERATE_TO_SEVERE)),
109 TrackerLabel(21, req.sstring(SS.MILD_TO_MODERATE)),
110 TrackerLabel(8.75, req.sstring(SS.MILD)),
111 ]
112 )]
114 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
115 if not self.is_complete():
116 return CTV_INCOMPLETE
117 return [CtvInfo(content=(
118 f"HAM-A total score {self.total_score()}/{self.MAX_SCORE} "
119 f"({self.severity(req)})"
120 ))]
122 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
123 return self.standard_task_summary_fields() + [
124 SummaryElement(name="total",
125 coltype=Integer(),
126 value=self.total_score(),
127 comment=f"Total score (/{self.MAX_SCORE})"),
128 SummaryElement(name="severity",
129 coltype=SummaryCategoryColType,
130 value=self.severity(req),
131 comment="Severity"),
132 ]
134 def is_complete(self) -> bool:
135 return (
136 self.all_fields_not_none(self.TASK_FIELDS) and
137 self.field_contents_valid()
138 )
140 def total_score(self) -> int:
141 return self.sum_fields(self.TASK_FIELDS)
143 def severity(self, req: CamcopsRequest) -> str:
144 score = self.total_score()
145 if score >= 31:
146 return req.sstring(SS.VERY_SEVERE)
147 elif score >= 25:
148 return req.sstring(SS.MODERATE_TO_SEVERE)
149 elif score >= 18:
150 return req.sstring(SS.MILD_TO_MODERATE)
151 else:
152 return req.sstring(SS.MILD)
154 def get_task_html(self, req: CamcopsRequest) -> str:
155 score = self.total_score()
156 severity = self.severity(req)
157 answer_dicts = []
158 for q in range(1, self.NQUESTIONS + 1):
159 d = {None: None}
160 for option in range(0, 4 + 1):
161 d[option] = self.wxstring(req, "q" + str(q) + "_option" +
162 str(option))
163 answer_dicts.append(d)
164 q_a = ""
165 for q in range(1, self.NQUESTIONS + 1):
166 q_a += tr_qa(
167 self.wxstring(req, "q" + str(q) + "_s") + " " +
168 self.wxstring(req, "q" + str(q) + "_question"),
169 get_from_dict(answer_dicts[q - 1], getattr(self, "q" + str(q)))
170 )
171 return """
172 <div class="{CssClass.SUMMARY}">
173 <table class="{CssClass.SUMMARY}">
174 {tr_is_complete}
175 {total_score}
176 {symptom_severity}
177 </table>
178 </div>
179 <table class="{CssClass.TASKDETAIL}">
180 <tr>
181 <th width="50%">Question</th>
182 <th width="50%">Answer</th>
183 </tr>
184 {q_a}
185 </table>
186 <div class="{CssClass.FOOTNOTES}">
187 [1] ≥31 very severe, ≥25 moderate to severe,
188 ≥18 mild to moderate, otherwise mild.
189 </div>
190 """.format(
191 CssClass=CssClass,
192 tr_is_complete=self.get_is_complete_tr(req),
193 total_score=tr(
194 req.sstring(SS.TOTAL_SCORE),
195 answer(score) + " / {}".format(self.MAX_SCORE)
196 ),
197 symptom_severity=tr_qa(
198 self.wxstring(req, "symptom_severity") + " <sup>[1]</sup>",
199 severity
200 ),
201 q_a=q_a,
202 )