Hide keyboard shortcuts

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 

2 

3""" 

4camcops_server/tasks/audit.py 

5 

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

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

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. 

16 

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. 

21 

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

24 

25=============================================================================== 

26 

27""" 

28 

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

30 

31from cardinal_pythonlib.stringfunc import strseq 

32from sqlalchemy.ext.declarative import DeclarativeMeta 

33from sqlalchemy.sql.sqltypes import Integer 

34 

35from camcops_server.cc_modules.cc_constants import CssClass 

36from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

37from camcops_server.cc_modules.cc_db import add_multiple_columns 

38from camcops_server.cc_modules.cc_html import ( 

39 answer, 

40 get_yes_no, 

41 tr, 

42 tr_qa, 

43) 

44from camcops_server.cc_modules.cc_request import CamcopsRequest 

45from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

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 TrackerInfo 

54 

55 

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

57# AUDIT 

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

59 

60class AuditMetaclass(DeclarativeMeta): 

61 # noinspection PyInitNewSignature 

62 def __init__(cls: Type['Audit'], 

63 name: str, 

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

65 classdict: Dict[str, Any]) -> None: 

66 add_multiple_columns( 

67 cls, "q", 1, cls.NQUESTIONS, 

68 minimum=0, maximum=4, 

69 comment_fmt="Q{n}, {s} (0-4, higher worse)", 

70 comment_strings=[ 

71 "how often drink", "drinks per day", "how often six drinks", 

72 "unable to stop", "unable to do what was expected", 

73 "eye opener", "guilt", "unable to remember", "injuries", 

74 "others concerned"] 

75 ) 

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

77 

78 

79class Audit(TaskHasPatientMixin, Task, 

80 metaclass=AuditMetaclass): 

81 """ 

82 Server implementation of the AUDIT task. 

83 """ 

84 __tablename__ = "audit" 

85 shortname = "AUDIT" 

86 provides_trackers = True 

87 

88 prohibits_commercial = True 

89 

90 NQUESTIONS = 10 

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

92 

93 @staticmethod 

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

95 _ = req.gettext 

96 return _("WHO Alcohol Use Disorders Identification Test") 

97 

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

99 return [TrackerInfo( 

100 value=self.total_score(), 

101 plot_label="AUDIT total score", 

102 axis_label="Total score (out of 40)", 

103 axis_min=-0.5, 

104 axis_max=40.5, 

105 horizontal_lines=[7.5] 

106 )] 

107 

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

109 if not self.is_complete(): 

110 return CTV_INCOMPLETE 

111 return [CtvInfo( 

112 content=f"AUDIT total score {self.total_score()}/40" 

113 )] 

114 

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

116 return self.standard_task_summary_fields() + [ 

117 SummaryElement(name="total", 

118 coltype=Integer(), 

119 value=self.total_score(), 

120 comment="Total score (/40)"), 

121 ] 

122 

123 # noinspection PyUnresolvedReferences 

124 def is_complete(self) -> bool: 

125 if not self.field_contents_valid(): 

126 return False 

127 if self.q1 is None or self.q9 is None or self.q10 is None: 

128 return False 

129 if self.q1 == 0: 

130 # Special limited-information completeness 

131 return True 

132 if self.q2 is not None \ 

133 and self.q3 is not None \ 

134 and (self.q2 + self.q3 == 0): 

135 # Special limited-information completeness 

136 return True 

137 # Otherwise, any null values cause problems 

138 return self.all_fields_not_none(self.TASK_FIELDS) 

139 

140 def total_score(self) -> int: 

141 return self.sum_fields(self.TASK_FIELDS) 

142 

143 # noinspection PyUnresolvedReferences 

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

145 score = self.total_score() 

146 exceeds_cutoff = score >= 8 

147 q1_dict = {None: None} 

148 q2_dict = {None: None} 

149 q3_to_8_dict = {None: None} 

150 q9_to_10_dict = {None: None} 

151 for option in range(0, 5): 

152 q1_dict[option] = str(option) + " – " + \ 

153 self.wxstring(req, "q1_option" + str(option)) 

154 q2_dict[option] = str(option) + " – " + \ 

155 self.wxstring(req, "q2_option" + str(option)) 

156 q3_to_8_dict[option] = str(option) + " – " + \ 

157 self.wxstring(req, "q3to8_option" + str(option)) 

158 if option != 1 and option != 3: 

159 q9_to_10_dict[option] = str(option) + " – " + \ 

160 self.wxstring(req, "q9to10_option" + str(option)) 

161 

162 q_a = tr_qa(self.wxstring(req, "q1_s"), 

163 get_from_dict(q1_dict, self.q1)) 

164 q_a += tr_qa(self.wxstring(req, "q2_s"), 

165 get_from_dict(q2_dict, self.q2)) 

166 for q in range(3, 8 + 1): 

167 q_a += tr_qa( 

168 self.wxstring(req, "q" + str(q) + "_s"), 

169 get_from_dict(q3_to_8_dict, getattr(self, "q" + str(q))) 

170 ) 

171 q_a += tr_qa(self.wxstring(req, "q9_s"), 

172 get_from_dict(q9_to_10_dict, self.q9)) 

173 q_a += tr_qa(self.wxstring(req, "q10_s"), 

174 get_from_dict(q9_to_10_dict, self.q10)) 

175 

176 return f""" 

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

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

179 {self.get_is_complete_tr(req)} 

180 {tr(req.wsstring(SS.TOTAL_SCORE), 

181 answer(score) + " / 40")} 

182 {tr_qa(self.wxstring(req, "exceeds_standard_cutoff"), 

183 get_yes_no(req, exceeds_cutoff))} 

184 </table> 

185 </div> 

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

187 <tr> 

188 <th width="50%">Question</th> 

189 <th width="50%">Answer</th> 

190 </tr> 

191 {q_a} 

192 </table> 

193 <div class="{CssClass.COPYRIGHT}"> 

194 AUDIT: Copyright © World Health Organization. 

195 Reproduced here under the permissions granted for 

196 NON-COMMERCIAL use only. You must obtain permission from the 

197 copyright holder for any other use. 

198 </div> 

199 """ 

200 

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

202 codes = [SnomedExpression(req.snomed(SnomedLookup.AUDIT_PROCEDURE_ASSESSMENT))] # noqa 

203 if self.is_complete(): 

204 codes.append(SnomedExpression( 

205 req.snomed(SnomedLookup.AUDIT_SCALE), 

206 { 

207 req.snomed(SnomedLookup.AUDIT_SCORE): self.total_score(), 

208 } 

209 )) 

210 return codes 

211 

212 

213# ============================================================================= 

214# AUDIT-C 

215# ============================================================================= 

216 

217class AuditCMetaclass(DeclarativeMeta): 

218 # noinspection PyInitNewSignature 

219 def __init__(cls: Type['AuditC'], 

220 name: str, 

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

222 classdict: Dict[str, Any]) -> None: 

223 add_multiple_columns( 

224 cls, "q", 1, cls.NQUESTIONS, 

225 minimum=0, maximum=4, 

226 comment_fmt="Q{n}, {s} (0-4, higher worse)", 

227 comment_strings=[ 

228 "how often drink", "drinks per day", "how often six drinks" 

229 ] 

230 ) 

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

232 

233 

234class AuditC(TaskHasPatientMixin, Task, 

235 metaclass=AuditMetaclass): 

236 __tablename__ = "audit_c" 

237 shortname = "AUDIT-C" 

238 extrastring_taskname = "audit" # shares strings with AUDIT 

239 

240 prohibits_commercial = True 

241 

242 NQUESTIONS = 3 

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

244 

245 @staticmethod 

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

247 _ = req.gettext 

248 return _("AUDIT Alcohol Consumption Questions") 

249 

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

251 return [TrackerInfo( 

252 value=self.total_score(), 

253 plot_label="AUDIT-C total score", 

254 axis_label="Total score (out of 12)", 

255 axis_min=-0.5, 

256 axis_max=12.5, 

257 )] 

258 

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

260 if not self.is_complete(): 

261 return CTV_INCOMPLETE 

262 return [CtvInfo( 

263 content=f"AUDIT-C total score {self.total_score()}/12" 

264 )] 

265 

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

267 return self.standard_task_summary_fields() + [ 

268 SummaryElement(name="total", 

269 coltype=Integer(), 

270 value=self.total_score(), 

271 comment="Total score (/12)"), 

272 ] 

273 

274 def is_complete(self) -> bool: 

275 return self.all_fields_not_none(self.TASK_FIELDS) 

276 

277 def total_score(self) -> int: 

278 return self.sum_fields(self.TASK_FIELDS) 

279 

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

281 score = self.total_score() 

282 q1_dict = {None: None} 

283 q2_dict = {None: None} 

284 q3_dict = {None: None} 

285 for option in range(0, 5): 

286 q1_dict[option] = str(option) + " – " + \ 

287 self.wxstring(req, "q1_option" + str(option)) 

288 if option == 0: # special! 

289 q2_dict[option] = str(option) + " – " + \ 

290 self.wxstring(req, "c_q2_option0") 

291 else: 

292 q2_dict[option] = str(option) + " – " + \ 

293 self.wxstring(req, "q2_option" + str(option)) 

294 q3_dict[option] = str(option) + " – " + \ 

295 self.wxstring(req, "q3to8_option" + str(option)) 

296 

297 # noinspection PyUnresolvedReferences 

298 return f""" 

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

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

301 {self.get_is_complete_tr(req)} 

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

303 answer(score) + " / 12")} 

304 </table> 

305 </div> 

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

307 <tr> 

308 <th width="50%">Question</th> 

309 <th width="50%">Answer</th> 

310 </tr> 

311 {tr_qa(self.wxstring(req, "c_q1_question"), 

312 get_from_dict(q1_dict, self.q1))} 

313 {tr_qa(self.wxstring(req, "c_q2_question"), 

314 get_from_dict(q2_dict, self.q2))} 

315 {tr_qa(self.wxstring(req, "c_q3_question"), 

316 get_from_dict(q3_dict, self.q3))} 

317 </table> 

318 <div class="{CssClass.COPYRIGHT}"> 

319 AUDIT: Copyright © World Health Organization. 

320 Reproduced here under the permissions granted for 

321 NON-COMMERCIAL use only. You must obtain permission from the 

322 copyright holder for any other use. 

323 

324 AUDIT-C: presumed to have the same restrictions. 

325 </div> 

326 """ 

327 

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

329 codes = [SnomedExpression(req.snomed(SnomedLookup.AUDITC_PROCEDURE_ASSESSMENT))] # noqa 

330 if self.is_complete(): 

331 codes.append(SnomedExpression( 

332 req.snomed(SnomedLookup.AUDITC_SCALE), 

333 { 

334 req.snomed(SnomedLookup.AUDITC_SCORE): self.total_score(), 

335 } 

336 )) 

337 return codes