Coverage for tasks/smast.py: 49%
71 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/smast.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, List, 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_snomed import SnomedExpression, SnomedLookup
39from camcops_server.cc_modules.cc_sqla_coltypes import (
40 CharColType,
41 SummaryCategoryColType,
42)
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 TrackerLabel,
52 TrackerInfo,
53)
56# =============================================================================
57# SMAST
58# =============================================================================
61class Smast( # type: ignore[misc]
62 TaskHasPatientMixin,
63 Task,
64):
65 """
66 Server implementation of the SMAST task.
67 """
69 __tablename__ = "smast"
70 shortname = "SMAST"
71 info_filename_stem = "mast"
72 provides_trackers = True
74 NQUESTIONS = 13
76 @classmethod
77 def extend_columns(cls: Type["Smast"], **kwargs: Any) -> None:
78 add_multiple_columns(
79 cls,
80 "q",
81 1,
82 cls.NQUESTIONS,
83 CharColType,
84 pv=["Y", "N"],
85 comment_fmt="Q{n}: {s} (Y or N)",
86 comment_strings=[
87 "believe you are a normal drinker",
88 "near relative worries/complains",
89 "feel guilty",
90 "friends/relative think you are a normal drinker",
91 "stop when you want to",
92 "ever attended Alcoholics Anonymous",
93 "problems with close relative",
94 "trouble at work",
95 "neglected obligations for >=2 days",
96 "sought help",
97 "hospitalized",
98 "arrested for drink-driving",
99 "arrested for other drunken behaviour",
100 ],
101 )
103 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
105 @staticmethod
106 def longname(req: "CamcopsRequest") -> str:
107 _ = req.gettext
108 return _("Short Michigan Alcohol Screening Test")
110 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
111 return [
112 TrackerInfo(
113 value=self.total_score(),
114 plot_label="SMAST total score",
115 axis_label=f"Total score (out of {self.NQUESTIONS})",
116 axis_min=-0.5,
117 axis_max=self.NQUESTIONS + 0.5,
118 horizontal_lines=[2.5, 1.5],
119 horizontal_labels=[
120 TrackerLabel(4, self.wxstring(req, "problem_probable")),
121 TrackerLabel(2, self.wxstring(req, "problem_possible")),
122 TrackerLabel(0.75, self.wxstring(req, "problem_unlikely")),
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"SMAST total score "
134 f"{self.total_score()}/{self.NQUESTIONS} "
135 f"({self.likelihood(req)})"
136 )
137 )
138 ]
140 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
141 return self.standard_task_summary_fields() + [
142 SummaryElement(
143 name="total",
144 coltype=Integer(),
145 value=self.total_score(),
146 comment=f"Total score (/{self.NQUESTIONS})",
147 ),
148 SummaryElement(
149 name="likelihood",
150 coltype=SummaryCategoryColType,
151 value=self.likelihood(req),
152 comment="Likelihood of problem",
153 ),
154 ]
156 def is_complete(self) -> bool:
157 return (
158 self.all_fields_not_none(self.TASK_FIELDS)
159 and self.field_contents_valid()
160 )
162 def get_score(self, q: int) -> int:
163 yes = "Y"
164 value = getattr(self, "q" + str(q))
165 if value is None:
166 return 0
167 if q == 1 or q == 4 or q == 5:
168 return 0 if value == yes else 1
169 else:
170 return 1 if value == yes else 0
172 def total_score(self) -> int:
173 total = 0
174 for q in range(1, self.NQUESTIONS + 1):
175 total += self.get_score(q)
176 return total
178 def likelihood(self, req: CamcopsRequest) -> str:
179 score = self.total_score()
180 if score >= 3:
181 return self.wxstring(req, "problem_probable")
182 elif score >= 2:
183 return self.wxstring(req, "problem_possible")
184 else:
185 return self.wxstring(req, "problem_unlikely")
187 def get_task_html(self, req: CamcopsRequest) -> str:
188 score = self.total_score()
189 likelihood = self.likelihood(req)
190 main_dict = {
191 None: None,
192 "Y": req.sstring(SS.YES),
193 "N": req.sstring(SS.NO),
194 }
195 q_a = ""
196 for q in range(1, self.NQUESTIONS + 1):
197 q_a += tr(
198 self.wxstring(req, "q" + str(q)),
199 answer(get_from_dict(main_dict, getattr(self, "q" + str(q))))
200 + " — "
201 + str(self.get_score(q)),
202 )
203 h = """
204 <div class="{CssClass.SUMMARY}">
205 <table class="{CssClass.SUMMARY}">
206 {tr_is_complete}
207 {total_score}
208 {problem_likelihood}
209 </table>
210 </div>
211 <table class="{CssClass.TASKDETAIL}">
212 <tr>
213 <th width="80%">Question</th>
214 <th width="20%">Answer</th>
215 </tr>
216 {q_a}
217 </table>
218 <div class="{CssClass.FOOTNOTES}">
219 [1] Total score ≥3 probable, ≥2 possible, 0–1 unlikely.
220 </div>
221 """.format(
222 CssClass=CssClass,
223 tr_is_complete=self.get_is_complete_tr(req),
224 total_score=tr(
225 req.sstring(SS.TOTAL_SCORE),
226 answer(score) + f" / {self.NQUESTIONS}",
227 ),
228 problem_likelihood=tr_qa(
229 self.wxstring(req, "problem_likelihood") + " <sup>[1]</sup>",
230 likelihood,
231 ),
232 q_a=q_a,
233 )
234 return h
236 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
237 if not self.is_complete():
238 return []
239 return [SnomedExpression(req.snomed(SnomedLookup.SMAST_SCALE))]