Coverage for tasks/mast.py: 51%

77 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/mast.py 

5 

6=============================================================================== 

7 

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

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

10 

11 This file is part of CamCOPS. 

12 

13 CamCOPS is free software: you can redistribute it and/or modify 

14 it under the terms of the GNU General Public License as published by 

15 the Free Software Foundation, either version 3 of the License, or 

16 (at your option) any later version. 

17 

18 CamCOPS is distributed in the hope that it will be useful, 

19 but WITHOUT ANY WARRANTY; without even the implied warranty of 

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

23 You should have received a copy of the GNU General Public License 

24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

25 

26=============================================================================== 

27 

28""" 

29 

30from typing import Any, Dict, List, Tuple, Type 

31 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy.ext.declarative import DeclarativeMeta 

34from sqlalchemy.sql.sqltypes import Boolean, Integer 

35 

36from camcops_server.cc_modules.cc_constants import CssClass 

37from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

38from camcops_server.cc_modules.cc_db import add_multiple_columns 

39from camcops_server.cc_modules.cc_html import answer, get_yes_no, tr, tr_qa 

40from camcops_server.cc_modules.cc_request import CamcopsRequest 

41from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

42from camcops_server.cc_modules.cc_sqla_coltypes import CharColType 

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 LabelAlignment, 

52 TrackerInfo, 

53 TrackerLabel, 

54) 

55 

56 

57# ============================================================================= 

58# MAST 

59# ============================================================================= 

60 

61 

62class MastMetaclass(DeclarativeMeta): 

63 # noinspection PyInitNewSignature 

64 def __init__( 

65 cls: Type["Mast"], 

66 name: str, 

67 bases: Tuple[Type, ...], 

68 classdict: Dict[str, Any], 

69 ) -> None: 

70 add_multiple_columns( 

71 cls, 

72 "q", 

73 1, 

74 cls.NQUESTIONS, 

75 CharColType, 

76 pv=["Y", "N"], 

77 comment_fmt="Q{n}: {s} (Y or N)", 

78 comment_strings=[ 

79 "feel you are a normal drinker", 

80 "couldn't remember evening before", 

81 "relative worries/complains", 

82 "stop drinking after 1-2 drinks", 

83 "feel guilty", 

84 "friends/relatives think you are a normal drinker", 

85 "can stop drinking when you want", 

86 "attended Alcoholics Anonymous", 

87 "physical fights", 

88 "drinking caused problems with relatives", 

89 "family have sought help", 

90 "lost friends", 

91 "trouble at work/school", 

92 "lost job", 

93 "neglected obligations for >=2 days", 

94 "drink before noon often", 

95 "liver trouble", 

96 "delirium tremens", 

97 "sought help", 

98 "hospitalized", 

99 "psychiatry admission", 

100 "clinic visit or professional help", 

101 "arrested for drink-driving", 

102 "arrested for other drunk behaviour", 

103 ], 

104 ) 

105 super().__init__(name, bases, classdict) 

106 

107 

108class Mast(TaskHasPatientMixin, Task, metaclass=MastMetaclass): 

109 """ 

110 Server implementation of the MAST task. 

111 """ 

112 

113 __tablename__ = "mast" 

114 shortname = "MAST" 

115 provides_trackers = True 

116 

117 NQUESTIONS = 24 

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

119 MAX_SCORE = 53 

120 ROSS_THRESHOLD = 13 

121 

122 @staticmethod 

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

124 _ = req.gettext 

125 return _("Michigan Alcohol Screening Test") 

126 

127 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: 

128 return [ 

129 TrackerInfo( 

130 value=self.total_score(), 

131 plot_label="MAST total score", 

132 axis_label=f"Total score (out of {self.MAX_SCORE})", 

133 axis_min=-0.5, 

134 axis_max=self.MAX_SCORE + 0.5, 

135 horizontal_lines=[self.ROSS_THRESHOLD - 0.5], 

136 horizontal_labels=[ 

137 TrackerLabel( 

138 self.ROSS_THRESHOLD, 

139 "Ross threshold", 

140 LabelAlignment.bottom, 

141 ) 

142 ], 

143 ) 

144 ] 

145 

146 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: 

147 if not self.is_complete(): 

148 return CTV_INCOMPLETE 

149 return [ 

150 CtvInfo( 

151 content=f"MAST total score " 

152 f"{self.total_score()}/{self.MAX_SCORE}" 

153 ) 

154 ] 

155 

156 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: 

157 return self.standard_task_summary_fields() + [ 

158 SummaryElement( 

159 name="total", 

160 coltype=Integer(), 

161 value=self.total_score(), 

162 comment=f"Total score (/{self.MAX_SCORE})", 

163 ), 

164 SummaryElement( 

165 name="exceeds_threshold", 

166 coltype=Boolean(), 

167 value=self.exceeds_ross_threshold(), 

168 comment=f"Exceeds Ross threshold " 

169 f"(total score >= {self.ROSS_THRESHOLD})", 

170 ), 

171 ] 

172 

173 def is_complete(self) -> bool: 

174 return ( 

175 self.all_fields_not_none(self.TASK_FIELDS) 

176 and self.field_contents_valid() 

177 ) 

178 

179 def get_score(self, q: int) -> int: 

180 yes = "Y" 

181 value = getattr(self, "q" + str(q)) 

182 if value is None: 

183 return 0 

184 if q == 1 or q == 4 or q == 6 or q == 7: 

185 presence = 0 if value == yes else 1 

186 else: 

187 presence = 1 if value == yes else 0 

188 if q == 3 or q == 5 or q == 9 or q == 16: 

189 points = 1 

190 elif q == 8 or q == 19 or q == 20: 

191 points = 5 

192 else: 

193 points = 2 

194 return points * presence 

195 

196 def total_score(self) -> int: 

197 total = 0 

198 for q in range(1, self.NQUESTIONS + 1): 

199 total += self.get_score(q) 

200 return total 

201 

202 def exceeds_ross_threshold(self) -> bool: 

203 score = self.total_score() 

204 return score >= self.ROSS_THRESHOLD 

205 

206 def get_task_html(self, req: CamcopsRequest) -> str: 

207 score = self.total_score() 

208 exceeds_threshold = self.exceeds_ross_threshold() 

209 main_dict = { 

210 None: None, 

211 "Y": req.sstring(SS.YES), 

212 "N": req.sstring(SS.NO), 

213 } 

214 q_a = "" 

215 for q in range(1, self.NQUESTIONS + 1): 

216 q_a += tr( 

217 self.wxstring(req, "q" + str(q)), 

218 ( 

219 answer( 

220 get_from_dict(main_dict, getattr(self, "q" + str(q))) 

221 ) 

222 + answer(" — " + str(self.get_score(q))) 

223 ), 

224 ) 

225 return f""" 

226 <div class="{CssClass.SUMMARY}"> 

227 <table class="{CssClass.SUMMARY}"> 

228 {self.get_is_complete_tr(req)} 

229 {tr(req.sstring(SS.TOTAL_SCORE), 

230 answer(score) + " / {}".format(self.MAX_SCORE))} 

231 {tr_qa(self.wxstring(req, "exceeds_threshold"), 

232 get_yes_no(req, exceeds_threshold))} 

233 </table> 

234 </div> 

235 <table class="{CssClass.TASKDETAIL}"> 

236 <tr> 

237 <th width="80%">Question</th> 

238 <th width="20%">Answer</th> 

239 </tr> 

240 {q_a} 

241 </table> 

242 """ 

243 

244 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: 

245 codes = [ 

246 SnomedExpression( 

247 req.snomed(SnomedLookup.MAST_PROCEDURE_ASSESSMENT) 

248 ) 

249 ] 

250 if self.is_complete(): 

251 codes.append( 

252 SnomedExpression( 

253 req.snomed(SnomedLookup.MAST_SCALE), 

254 {req.snomed(SnomedLookup.MAST_SCORE): self.total_score()}, 

255 ) 

256 ) 

257 return codes