Coverage for tasks/mast.py: 49%

74 statements  

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

1""" 

2camcops_server/tasks/mast.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 Boolean, Integer 

32 

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) 

52 

53 

54# ============================================================================= 

55# MAST 

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

57 

58 

59class Mast( # type: ignore[misc] 

60 TaskHasPatientMixin, 

61 Task, 

62): 

63 """ 

64 Server implementation of the MAST task. 

65 """ 

66 

67 __tablename__ = "mast" 

68 shortname = "MAST" 

69 provides_trackers = True 

70 

71 NQUESTIONS = 24 

72 

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 ) 

110 

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

112 MAX_SCORE = 53 

113 ROSS_THRESHOLD = 13 

114 

115 @staticmethod 

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

117 _ = req.gettext 

118 return _("Michigan Alcohol Screening Test") 

119 

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 ] 

138 

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 ] 

148 

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 ] 

165 

166 def is_complete(self) -> bool: 

167 return ( 

168 self.all_fields_not_none(self.TASK_FIELDS) 

169 and self.field_contents_valid() 

170 ) 

171 

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 

188 

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 

194 

195 def exceeds_ross_threshold(self) -> bool: 

196 score = self.total_score() 

197 return score >= self.ROSS_THRESHOLD 

198 

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

236 

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