Coverage for tasks/mast.py: 49%
74 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/mast.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 Boolean, 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, get_yes_no, 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 CharColType
40from camcops_server.cc_modules.cc_summaryelement import SummaryElement
41from camcops_server.cc_modules.cc_task import (
42 get_from_dict,
43 Task,
44 TaskHasPatientMixin,
45)
46from camcops_server.cc_modules.cc_text import SS
47from camcops_server.cc_modules.cc_trackerhelpers import (
48 LabelAlignment,
49 TrackerInfo,
50 TrackerLabel,
51)
54# =============================================================================
55# MAST
56# =============================================================================
59class Mast( # type: ignore[misc]
60 TaskHasPatientMixin,
61 Task,
62):
63 """
64 Server implementation of the MAST task.
65 """
67 __tablename__ = "mast"
68 shortname = "MAST"
69 provides_trackers = True
71 NQUESTIONS = 24
73 @classmethod
74 def extend_columns(cls: Type["Mast"], **kwargs: Any) -> None:
75 add_multiple_columns(
76 cls,
77 "q",
78 1,
79 cls.NQUESTIONS,
80 CharColType,
81 pv=["Y", "N"],
82 comment_fmt="Q{n}: {s} (Y or N)",
83 comment_strings=[
84 "feel you are a normal drinker",
85 "couldn't remember evening before",
86 "relative worries/complains",
87 "stop drinking after 1-2 drinks",
88 "feel guilty",
89 "friends/relatives think you are a normal drinker",
90 "can stop drinking when you want",
91 "attended Alcoholics Anonymous",
92 "physical fights",
93 "drinking caused problems with relatives",
94 "family have sought help",
95 "lost friends",
96 "trouble at work/school",
97 "lost job",
98 "neglected obligations for >=2 days",
99 "drink before noon often",
100 "liver trouble",
101 "delirium tremens",
102 "sought help",
103 "hospitalized",
104 "psychiatry admission",
105 "clinic visit or professional help",
106 "arrested for drink-driving",
107 "arrested for other drunk behaviour",
108 ],
109 )
111 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
112 MAX_SCORE = 53
113 ROSS_THRESHOLD = 13
115 @staticmethod
116 def longname(req: "CamcopsRequest") -> str:
117 _ = req.gettext
118 return _("Michigan Alcohol Screening Test")
120 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
121 return [
122 TrackerInfo(
123 value=self.total_score(),
124 plot_label="MAST total score",
125 axis_label=f"Total score (out of {self.MAX_SCORE})",
126 axis_min=-0.5,
127 axis_max=self.MAX_SCORE + 0.5,
128 horizontal_lines=[self.ROSS_THRESHOLD - 0.5],
129 horizontal_labels=[
130 TrackerLabel(
131 self.ROSS_THRESHOLD,
132 "Ross threshold",
133 LabelAlignment.bottom,
134 )
135 ],
136 )
137 ]
139 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
140 if not self.is_complete():
141 return CTV_INCOMPLETE
142 return [
143 CtvInfo(
144 content=f"MAST total score "
145 f"{self.total_score()}/{self.MAX_SCORE}"
146 )
147 ]
149 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
150 return self.standard_task_summary_fields() + [
151 SummaryElement(
152 name="total",
153 coltype=Integer(),
154 value=self.total_score(),
155 comment=f"Total score (/{self.MAX_SCORE})",
156 ),
157 SummaryElement(
158 name="exceeds_threshold",
159 coltype=Boolean(),
160 value=self.exceeds_ross_threshold(),
161 comment=f"Exceeds Ross threshold "
162 f"(total score >= {self.ROSS_THRESHOLD})",
163 ),
164 ]
166 def is_complete(self) -> bool:
167 return (
168 self.all_fields_not_none(self.TASK_FIELDS)
169 and self.field_contents_valid()
170 )
172 def get_score(self, q: int) -> int:
173 yes = "Y"
174 value = getattr(self, "q" + str(q))
175 if value is None:
176 return 0
177 if q == 1 or q == 4 or q == 6 or q == 7:
178 presence = 0 if value == yes else 1
179 else:
180 presence = 1 if value == yes else 0
181 if q == 3 or q == 5 or q == 9 or q == 16:
182 points = 1
183 elif q == 8 or q == 19 or q == 20:
184 points = 5
185 else:
186 points = 2
187 return points * presence
189 def total_score(self) -> int:
190 total = 0
191 for q in range(1, self.NQUESTIONS + 1):
192 total += self.get_score(q)
193 return total
195 def exceeds_ross_threshold(self) -> bool:
196 score = self.total_score()
197 return score >= self.ROSS_THRESHOLD
199 def get_task_html(self, req: CamcopsRequest) -> str:
200 score = self.total_score()
201 exceeds_threshold = self.exceeds_ross_threshold()
202 main_dict = {
203 None: None,
204 "Y": req.sstring(SS.YES),
205 "N": req.sstring(SS.NO),
206 }
207 q_a = ""
208 for q in range(1, self.NQUESTIONS + 1):
209 q_a += tr(
210 self.wxstring(req, "q" + str(q)),
211 (
212 answer(
213 get_from_dict(main_dict, getattr(self, "q" + str(q)))
214 )
215 + answer(" — " + str(self.get_score(q)))
216 ),
217 )
218 return f"""
219 <div class="{CssClass.SUMMARY}">
220 <table class="{CssClass.SUMMARY}">
221 {self.get_is_complete_tr(req)}
222 {tr(req.sstring(SS.TOTAL_SCORE),
223 answer(score) + " / {}".format(self.MAX_SCORE))}
224 {tr_qa(self.wxstring(req, "exceeds_threshold"),
225 get_yes_no(req, exceeds_threshold))}
226 </table>
227 </div>
228 <table class="{CssClass.TASKDETAIL}">
229 <tr>
230 <th width="80%">Question</th>
231 <th width="20%">Answer</th>
232 </tr>
233 {q_a}
234 </table>
235 """
237 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
238 codes = [
239 SnomedExpression(
240 req.snomed(SnomedLookup.MAST_PROCEDURE_ASSESSMENT)
241 )
242 ]
243 if self.is_complete():
244 codes.append(
245 SnomedExpression(
246 req.snomed(SnomedLookup.MAST_SCALE),
247 {req.snomed(SnomedLookup.MAST_SCORE): self.total_score()},
248 )
249 )
250 return codes