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