Coverage for tasks/audit.py: 45%

120 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/audit.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 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_summaryelement import SummaryElement 

43from camcops_server.cc_modules.cc_task import ( 

44 get_from_dict, 

45 Task, 

46 TaskHasPatientMixin, 

47) 

48from camcops_server.cc_modules.cc_text import SS 

49from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

50 

51 

52# ============================================================================= 

53# AUDIT 

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

55 

56 

57class AuditMetaclass(DeclarativeMeta): 

58 # noinspection PyInitNewSignature 

59 def __init__( 

60 cls: Type["Audit"], 

61 name: str, 

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

63 classdict: Dict[str, Any], 

64 ) -> None: 

65 add_multiple_columns( 

66 cls, 

67 "q", 

68 1, 

69 cls.NQUESTIONS, 

70 minimum=0, 

71 maximum=4, 

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

73 comment_strings=[ 

74 "how often drink", 

75 "drinks per day", 

76 "how often six drinks", 

77 "unable to stop", 

78 "unable to do what was expected", 

79 "eye opener", 

80 "guilt", 

81 "unable to remember", 

82 "injuries", 

83 "others concerned", 

84 ], 

85 ) 

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

87 

88 

89class Audit(TaskHasPatientMixin, Task, metaclass=AuditMetaclass): 

90 """ 

91 Server implementation of the AUDIT task. 

92 """ 

93 

94 __tablename__ = "audit" 

95 shortname = "AUDIT" 

96 provides_trackers = True 

97 

98 prohibits_commercial = True 

99 

100 NQUESTIONS = 10 

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

102 

103 @staticmethod 

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

105 _ = req.gettext 

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

107 

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

109 return [ 

110 TrackerInfo( 

111 value=self.total_score(), 

112 plot_label="AUDIT total score", 

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

114 axis_min=-0.5, 

115 axis_max=40.5, 

116 horizontal_lines=[7.5], 

117 ) 

118 ] 

119 

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

121 if not self.is_complete(): 

122 return CTV_INCOMPLETE 

123 return [CtvInfo(content=f"AUDIT total score {self.total_score()}/40")] 

124 

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

126 return self.standard_task_summary_fields() + [ 

127 SummaryElement( 

128 name="total", 

129 coltype=Integer(), 

130 value=self.total_score(), 

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

132 ) 

133 ] 

134 

135 # noinspection PyUnresolvedReferences 

136 def is_complete(self) -> bool: 

137 if not self.field_contents_valid(): 

138 return False 

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

140 return False 

141 if self.q1 == 0: 

142 # Special limited-information completeness 

143 return True 

144 if ( 

145 self.q2 is not None 

146 and self.q3 is not None 

147 and (self.q2 + self.q3 == 0) 

148 ): 

149 # Special limited-information completeness 

150 return True 

151 # Otherwise, any null values cause problems 

152 return self.all_fields_not_none(self.TASK_FIELDS) 

153 

154 def total_score(self) -> int: 

155 return self.sum_fields(self.TASK_FIELDS) 

156 

157 # noinspection PyUnresolvedReferences 

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

159 score = self.total_score() 

160 exceeds_cutoff = score >= 8 

161 q1_dict = {None: None} 

162 q2_dict = {None: None} 

163 q3_to_8_dict = {None: None} 

164 q9_to_10_dict = {None: None} 

165 for option in range(0, 5): 

166 q1_dict[option] = ( 

167 str(option) 

168 + " – " 

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

170 ) 

171 q2_dict[option] = ( 

172 str(option) 

173 + " – " 

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

175 ) 

176 q3_to_8_dict[option] = ( 

177 str(option) 

178 + " – " 

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

180 ) 

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

182 q9_to_10_dict[option] = ( 

183 str(option) 

184 + " – " 

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

186 ) 

187 

188 q_a = tr_qa( 

189 self.wxstring(req, "q1_s"), get_from_dict(q1_dict, self.q1) 

190 ) 

191 q_a += tr_qa( 

192 self.wxstring(req, "q2_s"), get_from_dict(q2_dict, self.q2) 

193 ) 

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

195 q_a += tr_qa( 

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

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

198 ) 

199 q_a += tr_qa( 

200 self.wxstring(req, "q9_s"), get_from_dict(q9_to_10_dict, self.q9) 

201 ) 

202 q_a += tr_qa( 

203 self.wxstring(req, "q10_s"), get_from_dict(q9_to_10_dict, self.q10) 

204 ) 

205 

206 return f""" 

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

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

209 {self.get_is_complete_tr(req)} 

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

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

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

213 get_yes_no(req, exceeds_cutoff))} 

214 </table> 

215 </div> 

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

217 <tr> 

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

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

220 </tr> 

221 {q_a} 

222 </table> 

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

224 AUDIT: Copyright © World Health Organization. 

225 Reproduced here under the permissions granted for 

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

227 copyright holder for any other use. 

228 </div> 

229 """ 

230 

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

232 codes = [ 

233 SnomedExpression( 

234 req.snomed(SnomedLookup.AUDIT_PROCEDURE_ASSESSMENT) 

235 ) 

236 ] 

237 if self.is_complete(): 

238 codes.append( 

239 SnomedExpression( 

240 req.snomed(SnomedLookup.AUDIT_SCALE), 

241 {req.snomed(SnomedLookup.AUDIT_SCORE): self.total_score()}, 

242 ) 

243 ) 

244 return codes 

245 

246 

247# ============================================================================= 

248# AUDIT-C 

249# ============================================================================= 

250 

251 

252class AuditCMetaclass(DeclarativeMeta): 

253 # noinspection PyInitNewSignature 

254 def __init__( 

255 cls: Type["AuditC"], 

256 name: str, 

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

258 classdict: Dict[str, Any], 

259 ) -> None: 

260 add_multiple_columns( 

261 cls, 

262 "q", 

263 1, 

264 cls.NQUESTIONS, 

265 minimum=0, 

266 maximum=4, 

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

268 comment_strings=[ 

269 "how often drink", 

270 "drinks per day", 

271 "how often six drinks", 

272 ], 

273 ) 

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

275 

276 

277class AuditC(TaskHasPatientMixin, Task, metaclass=AuditMetaclass): 

278 __tablename__ = "audit_c" 

279 shortname = "AUDIT-C" 

280 extrastring_taskname = "audit" # shares strings with AUDIT 

281 info_filename_stem = extrastring_taskname 

282 

283 prohibits_commercial = True 

284 

285 NQUESTIONS = 3 

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

287 

288 @staticmethod 

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

290 _ = req.gettext 

291 return _("AUDIT Alcohol Consumption Questions") 

292 

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

294 return [ 

295 TrackerInfo( 

296 value=self.total_score(), 

297 plot_label="AUDIT-C total score", 

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

299 axis_min=-0.5, 

300 axis_max=12.5, 

301 ) 

302 ] 

303 

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

305 if not self.is_complete(): 

306 return CTV_INCOMPLETE 

307 return [ 

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

309 ] 

310 

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

312 return self.standard_task_summary_fields() + [ 

313 SummaryElement( 

314 name="total", 

315 coltype=Integer(), 

316 value=self.total_score(), 

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

318 ) 

319 ] 

320 

321 def is_complete(self) -> bool: 

322 return self.all_fields_not_none(self.TASK_FIELDS) 

323 

324 def total_score(self) -> int: 

325 return self.sum_fields(self.TASK_FIELDS) 

326 

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

328 score = self.total_score() 

329 q1_dict = {None: None} 

330 q2_dict = {None: None} 

331 q3_dict = {None: None} 

332 for option in range(0, 5): 

333 q1_dict[option] = ( 

334 str(option) 

335 + " – " 

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

337 ) 

338 if option == 0: # special! 

339 q2_dict[option] = ( 

340 str(option) + " – " + self.wxstring(req, "c_q2_option0") 

341 ) 

342 else: 

343 q2_dict[option] = ( 

344 str(option) 

345 + " – " 

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

347 ) 

348 q3_dict[option] = ( 

349 str(option) 

350 + " – " 

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

352 ) 

353 

354 # noinspection PyUnresolvedReferences 

355 return f""" 

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

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

358 {self.get_is_complete_tr(req)} 

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

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

361 </table> 

362 </div> 

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

364 <tr> 

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

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

367 </tr> 

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

369 get_from_dict(q1_dict, self.q1))} 

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

371 get_from_dict(q2_dict, self.q2))} 

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

373 get_from_dict(q3_dict, self.q3))} 

374 </table> 

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

376 AUDIT: Copyright © World Health Organization. 

377 Reproduced here under the permissions granted for 

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

379 copyright holder for any other use. 

380 

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

382 </div> 

383 """ 

384 

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

386 codes = [ 

387 SnomedExpression( 

388 req.snomed(SnomedLookup.AUDITC_PROCEDURE_ASSESSMENT) 

389 ) 

390 ] 

391 if self.is_complete(): 

392 codes.append( 

393 SnomedExpression( 

394 req.snomed(SnomedLookup.AUDITC_SCALE), 

395 { 

396 req.snomed( 

397 SnomedLookup.AUDITC_SCORE 

398 ): self.total_score() 

399 }, 

400 ) 

401 ) 

402 return codes