Coverage for tasks/mast.py : 51%

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