Coverage for tasks/smast.py: 49%

71 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 14:23 +0100

1""" 

2camcops_server/tasks/smast.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CamCOPS. 

10 

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. 

15 

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. 

20 

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/>. 

23 

24=============================================================================== 

25 

26""" 

27 

28from typing import Any, List, Type 

29 

30from cardinal_pythonlib.stringfunc import strseq 

31from sqlalchemy.sql.sqltypes import Integer 

32 

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) 

54 

55 

56# ============================================================================= 

57# SMAST 

58# ============================================================================= 

59 

60 

61class Smast( # type: ignore[misc] 

62 TaskHasPatientMixin, 

63 Task, 

64): 

65 """ 

66 Server implementation of the SMAST task. 

67 """ 

68 

69 __tablename__ = "smast" 

70 shortname = "SMAST" 

71 info_filename_stem = "mast" 

72 provides_trackers = True 

73 

74 NQUESTIONS = 13 

75 

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 ) 

102 

103 TASK_FIELDS = strseq("q", 1, NQUESTIONS) 

104 

105 @staticmethod 

106 def longname(req: "CamcopsRequest") -> str: 

107 _ = req.gettext 

108 return _("Short Michigan Alcohol Screening Test") 

109 

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 ] 

126 

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 ] 

139 

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 ] 

155 

156 def is_complete(self) -> bool: 

157 return ( 

158 self.all_fields_not_none(self.TASK_FIELDS) 

159 and self.field_contents_valid() 

160 ) 

161 

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 

171 

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 

177 

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") 

186 

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 

235 

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))]