Coverage for tasks/smast.py: 51%

74 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/smast.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 Integer 

35 

36from camcops_server.cc_modules.cc_constants import CssClass 

37from camcops_server.cc_modules.cc_ctvinfo import CtvInfo, CTV_INCOMPLETE 

38from camcops_server.cc_modules.cc_db import add_multiple_columns 

39from camcops_server.cc_modules.cc_html import answer, 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 ( 

43 CharColType, 

44 SummaryCategoryColType, 

45) 

46from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

47from camcops_server.cc_modules.cc_task import ( 

48 get_from_dict, 

49 Task, 

50 TaskHasPatientMixin, 

51) 

52from camcops_server.cc_modules.cc_text import SS 

53from camcops_server.cc_modules.cc_trackerhelpers import ( 

54 TrackerLabel, 

55 TrackerInfo, 

56) 

57 

58 

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

60# SMAST 

61# ============================================================================= 

62 

63 

64class SmastMetaclass(DeclarativeMeta): 

65 # noinspection PyInitNewSignature 

66 def __init__( 

67 cls: Type["Smast"], 

68 name: str, 

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

70 classdict: Dict[str, Any], 

71 ) -> None: 

72 add_multiple_columns( 

73 cls, 

74 "q", 

75 1, 

76 cls.NQUESTIONS, 

77 CharColType, 

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

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

80 comment_strings=[ 

81 "believe you are a normal drinker", 

82 "near relative worries/complains", 

83 "feel guilty", 

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

85 "stop when you want to", 

86 "ever attended Alcoholics Anonymous", 

87 "problems with close relative", 

88 "trouble at work", 

89 "neglected obligations for >=2 days", 

90 "sought help", 

91 "hospitalized", 

92 "arrested for drink-driving", 

93 "arrested for other drunken behaviour", 

94 ], 

95 ) 

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

97 

98 

99class Smast(TaskHasPatientMixin, Task, metaclass=SmastMetaclass): 

100 """ 

101 Server implementation of the SMAST task. 

102 """ 

103 

104 __tablename__ = "smast" 

105 shortname = "SMAST" 

106 info_filename_stem = "mast" 

107 provides_trackers = True 

108 

109 NQUESTIONS = 13 

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

111 

112 @staticmethod 

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

114 _ = req.gettext 

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

116 

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

118 return [ 

119 TrackerInfo( 

120 value=self.total_score(), 

121 plot_label="SMAST total score", 

122 axis_label=f"Total score (out of {self.NQUESTIONS})", 

123 axis_min=-0.5, 

124 axis_max=self.NQUESTIONS + 0.5, 

125 horizontal_lines=[2.5, 1.5], 

126 horizontal_labels=[ 

127 TrackerLabel(4, self.wxstring(req, "problem_probable")), 

128 TrackerLabel(2, self.wxstring(req, "problem_possible")), 

129 TrackerLabel(0.75, self.wxstring(req, "problem_unlikely")), 

130 ], 

131 ) 

132 ] 

133 

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

135 if not self.is_complete(): 

136 return CTV_INCOMPLETE 

137 return [ 

138 CtvInfo( 

139 content=( 

140 f"SMAST total score {self.total_score()}/{self.NQUESTIONS} " # noqa: E501 

141 f"({self.likelihood(req)})" 

142 ) 

143 ) 

144 ] 

145 

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

147 return self.standard_task_summary_fields() + [ 

148 SummaryElement( 

149 name="total", 

150 coltype=Integer(), 

151 value=self.total_score(), 

152 comment=f"Total score (/{self.NQUESTIONS})", 

153 ), 

154 SummaryElement( 

155 name="likelihood", 

156 coltype=SummaryCategoryColType, 

157 value=self.likelihood(req), 

158 comment="Likelihood of problem", 

159 ), 

160 ] 

161 

162 def is_complete(self) -> bool: 

163 return ( 

164 self.all_fields_not_none(self.TASK_FIELDS) 

165 and self.field_contents_valid() 

166 ) 

167 

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

169 yes = "Y" 

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

171 if value is None: 

172 return 0 

173 if q == 1 or q == 4 or q == 5: 

174 return 0 if value == yes else 1 

175 else: 

176 return 1 if value == yes else 0 

177 

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 

183 

184 def likelihood(self, req: CamcopsRequest) -> str: 

185 score = self.total_score() 

186 if score >= 3: 

187 return self.wxstring(req, "problem_probable") 

188 elif score >= 2: 

189 return self.wxstring(req, "problem_possible") 

190 else: 

191 return self.wxstring(req, "problem_unlikely") 

192 

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

194 score = self.total_score() 

195 likelihood = self.likelihood(req) 

196 main_dict = { 

197 None: None, 

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

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

200 } 

201 q_a = "" 

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

203 q_a += tr( 

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

205 answer(get_from_dict(main_dict, getattr(self, "q" + str(q)))) 

206 + " — " 

207 + str(self.get_score(q)), 

208 ) 

209 h = """ 

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

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

212 {tr_is_complete} 

213 {total_score} 

214 {problem_likelihood} 

215 </table> 

216 </div> 

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

218 <tr> 

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

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

221 </tr> 

222 {q_a} 

223 </table> 

224 <div class="{CssClass.FOOTNOTES}"> 

225 [1] Total score ≥3 probable, ≥2 possible, 0–1 unlikely. 

226 </div> 

227 """.format( 

228 CssClass=CssClass, 

229 tr_is_complete=self.get_is_complete_tr(req), 

230 total_score=tr( 

231 req.sstring(SS.TOTAL_SCORE), 

232 answer(score) + f" / {self.NQUESTIONS}", 

233 ), 

234 problem_likelihood=tr_qa( 

235 self.wxstring(req, "problem_likelihood") + " <sup>[1]</sup>", 

236 likelihood, 

237 ), 

238 q_a=q_a, 

239 ) 

240 return h 

241 

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

243 if not self.is_complete(): 

244 return [] 

245 return [SnomedExpression(req.snomed(SnomedLookup.SMAST_SCALE))]