Coverage for tasks/ciwa.py: 55%

80 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/ciwa.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.schema import Column 

35from sqlalchemy.sql.sqltypes import Float, Integer 

36 

37from camcops_server.cc_modules.cc_constants import CssClass 

38from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

39from camcops_server.cc_modules.cc_db import add_multiple_columns 

40from camcops_server.cc_modules.cc_html import ( 

41 answer, 

42 subheading_spanning_two_columns, 

43 tr, 

44 tr_qa, 

45) 

46from camcops_server.cc_modules.cc_request import CamcopsRequest 

47from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

48from camcops_server.cc_modules.cc_sqla_coltypes import ( 

49 CamcopsColumn, 

50 MIN_ZERO_CHECKER, 

51 PermittedValueChecker, 

52 SummaryCategoryColType, 

53) 

54from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

55from camcops_server.cc_modules.cc_task import ( 

56 get_from_dict, 

57 Task, 

58 TaskHasClinicianMixin, 

59 TaskHasPatientMixin, 

60) 

61from camcops_server.cc_modules.cc_text import SS 

62from camcops_server.cc_modules.cc_trackerhelpers import ( 

63 TrackerLabel, 

64 TrackerInfo, 

65) 

66 

67 

68# ============================================================================= 

69# CIWA 

70# ============================================================================= 

71 

72 

73class CiwaMetaclass(DeclarativeMeta): 

74 # noinspection PyInitNewSignature 

75 def __init__( 

76 cls: Type["Ciwa"], 

77 name: str, 

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

79 classdict: Dict[str, Any], 

80 ) -> None: 

81 add_multiple_columns( 

82 cls, 

83 "q", 

84 1, 

85 cls.NSCOREDQUESTIONS - 1, 

86 minimum=0, 

87 maximum=7, 

88 comment_fmt="Q{n}, {s} (0-7, higher worse)", 

89 comment_strings=[ 

90 "nausea/vomiting", 

91 "tremor", 

92 "paroxysmal sweats", 

93 "anxiety", 

94 "agitation", 

95 "tactile disturbances", 

96 "auditory disturbances", 

97 "visual disturbances", 

98 "headache/fullness in head", 

99 ], 

100 ) 

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

102 

103 

104class Ciwa( 

105 TaskHasPatientMixin, TaskHasClinicianMixin, Task, metaclass=CiwaMetaclass 

106): 

107 """ 

108 Server implementation of the CIWA-Ar task. 

109 """ 

110 

111 __tablename__ = "ciwa" 

112 shortname = "CIWA-Ar" 

113 provides_trackers = True 

114 

115 NSCOREDQUESTIONS = 10 

116 SCORED_QUESTIONS = strseq("q", 1, NSCOREDQUESTIONS) 

117 

118 q10 = CamcopsColumn( 

119 "q10", 

120 Integer, 

121 permitted_value_checker=PermittedValueChecker(minimum=0, maximum=4), 

122 comment="Q10, orientation/clouding of sensorium (0-4, higher worse)", 

123 ) 

124 t = Column("t", Float, comment="Temperature (degrees C)") 

125 hr = CamcopsColumn( 

126 "hr", 

127 Integer, 

128 permitted_value_checker=MIN_ZERO_CHECKER, 

129 comment="Heart rate (beats/minute)", 

130 ) 

131 sbp = CamcopsColumn( 

132 "sbp", 

133 Integer, 

134 permitted_value_checker=MIN_ZERO_CHECKER, 

135 comment="Systolic blood pressure (mmHg)", 

136 ) 

137 dbp = CamcopsColumn( 

138 "dbp", 

139 Integer, 

140 permitted_value_checker=MIN_ZERO_CHECKER, 

141 comment="Diastolic blood pressure (mmHg)", 

142 ) 

143 rr = CamcopsColumn( 

144 "rr", 

145 Integer, 

146 permitted_value_checker=MIN_ZERO_CHECKER, 

147 comment="Respiratory rate (breaths/minute)", 

148 ) 

149 

150 MAX_SCORE = 67 

151 

152 @staticmethod 

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

154 _ = req.gettext 

155 return _( 

156 "Clinical Institute Withdrawal Assessment for Alcohol " 

157 "Scale, Revised" 

158 ) 

159 

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

161 return [ 

162 TrackerInfo( 

163 value=self.total_score(), 

164 plot_label="CIWA total score", 

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

166 axis_min=-0.5, 

167 axis_max=self.MAX_SCORE + 0.5, 

168 horizontal_lines=[14.5, 7.5], 

169 horizontal_labels=[ 

170 TrackerLabel(17, req.sstring(SS.SEVERE)), 

171 TrackerLabel(11, req.sstring(SS.MODERATE)), 

172 TrackerLabel(3.75, req.sstring(SS.MILD)), 

173 ], 

174 ) 

175 ] 

176 

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

178 if not self.is_complete(): 

179 return CTV_INCOMPLETE 

180 return [ 

181 CtvInfo( 

182 content=f"CIWA total score: " 

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

184 ) 

185 ] 

186 

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

188 return self.standard_task_summary_fields() + [ 

189 SummaryElement( 

190 name="total", 

191 coltype=Integer(), 

192 value=self.total_score(), 

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

194 ), 

195 SummaryElement( 

196 name="severity", 

197 coltype=SummaryCategoryColType, 

198 value=self.severity(req), 

199 comment="Likely severity", 

200 ), 

201 ] 

202 

203 def is_complete(self) -> bool: 

204 return ( 

205 self.all_fields_not_none(self.SCORED_QUESTIONS) 

206 and self.field_contents_valid() 

207 ) 

208 

209 def total_score(self) -> int: 

210 return self.sum_fields(self.SCORED_QUESTIONS) 

211 

212 def severity(self, req: CamcopsRequest) -> str: 

213 score = self.total_score() 

214 if score >= 15: 

215 severity = self.wxstring(req, "category_severe") 

216 elif score >= 8: 

217 severity = self.wxstring(req, "category_moderate") 

218 else: 

219 severity = self.wxstring(req, "category_mild") 

220 return severity 

221 

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

223 score = self.total_score() 

224 severity = self.severity(req) 

225 answer_dicts_dict = {} 

226 for q in self.SCORED_QUESTIONS: 

227 d = {None: None} 

228 for option in range(0, 8): 

229 if option > 4 and q == "q10": 

230 continue 

231 d[option] = self.wxstring(req, q + "_option" + str(option)) 

232 answer_dicts_dict[q] = d 

233 q_a = "" 

234 for q in range(1, Ciwa.NSCOREDQUESTIONS + 1): 

235 q_a += tr_qa( 

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

237 get_from_dict( 

238 answer_dicts_dict["q" + str(q)], 

239 getattr(self, "q" + str(q)), 

240 ), 

241 ) 

242 tr_total_score = tr( 

243 req.sstring(SS.TOTAL_SCORE), answer(score) + f" / {self.MAX_SCORE}" 

244 ) 

245 tr_severity = tr_qa( 

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

247 ) 

248 return f""" 

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

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

251 {self.get_is_complete_tr(req)} 

252 {tr_total_score} 

253 {tr_severity} 

254 </table> 

255 </div> 

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

257 <tr> 

258 <th width="35%">Question</th> 

259 <th width="65%">Answer</th> 

260 </tr> 

261 {q_a} 

262 {subheading_spanning_two_columns( 

263 self.wxstring(req, "vitals_title"))} 

264 {tr_qa(self.wxstring(req, "t"), self.t)} 

265 {tr_qa(self.wxstring(req, "hr"), self.hr)} 

266 {tr(self.wxstring(req, "bp"), 

267 answer(self.sbp) + " / " + answer(self.dbp))} 

268 {tr_qa(self.wxstring(req, "rr"), self.rr)} 

269 </table> 

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

271 [1] Total score ≥15 severe, ≥8 moderate, otherwise 

272 mild/minimal. 

273 </div> 

274 """ 

275 

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

277 codes = [ 

278 SnomedExpression( 

279 req.snomed(SnomedLookup.CIWA_AR_PROCEDURE_ASSESSMENT) 

280 ) 

281 ] 

282 if self.is_complete(): 

283 codes.append( 

284 SnomedExpression( 

285 req.snomed(SnomedLookup.CIWA_AR_SCALE), 

286 { 

287 req.snomed( 

288 SnomedLookup.CIWA_AR_SCORE 

289 ): self.total_score() 

290 }, 

291 ) 

292 ) 

293 return codes