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

34from sqlalchemy.sql.sqltypes import Float, 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 ( 

40 answer, 

41 subheading_spanning_two_columns, 

42 tr, 

43 tr_qa, 

44) 

45from camcops_server.cc_modules.cc_request import CamcopsRequest 

46from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

47from camcops_server.cc_modules.cc_sqla_coltypes import ( 

48 CamcopsColumn, 

49 MIN_ZERO_CHECKER, 

50 PermittedValueChecker, 

51 SummaryCategoryColType, 

52) 

53from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

54from camcops_server.cc_modules.cc_task import ( 

55 get_from_dict, 

56 Task, 

57 TaskHasClinicianMixin, 

58 TaskHasPatientMixin, 

59) 

60from camcops_server.cc_modules.cc_text import SS 

61from camcops_server.cc_modules.cc_trackerhelpers import ( 

62 TrackerLabel, 

63 TrackerInfo, 

64) 

65 

66 

67# ============================================================================= 

68# CIWA 

69# ============================================================================= 

70 

71class CiwaMetaclass(DeclarativeMeta): 

72 # noinspection PyInitNewSignature 

73 def __init__(cls: Type['Ciwa'], 

74 name: str, 

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

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

77 add_multiple_columns( 

78 cls, "q", 1, cls.NSCOREDQUESTIONS - 1, 

79 minimum=0, maximum=7, 

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

81 comment_strings=[ 

82 "nausea/vomiting", "tremor", "paroxysmal sweats", "anxiety", 

83 "agitation", "tactile disturbances", "auditory disturbances", 

84 "visual disturbances", "headache/fullness in head" 

85 ] 

86 ) 

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

88 

89 

90class Ciwa(TaskHasPatientMixin, TaskHasClinicianMixin, Task, 

91 metaclass=CiwaMetaclass): 

92 """ 

93 Server implementation of the CIWA-Ar task. 

94 """ 

95 __tablename__ = "ciwa" 

96 shortname = "CIWA-Ar" 

97 provides_trackers = True 

98 

99 NSCOREDQUESTIONS = 10 

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

101 

102 q10 = CamcopsColumn( 

103 "q10", Integer, 

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

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

106 ) 

107 t = Column( 

108 "t", Float, 

109 comment="Temperature (degrees C)" 

110 ) 

111 hr = CamcopsColumn( 

112 "hr", Integer, 

113 permitted_value_checker=MIN_ZERO_CHECKER, 

114 comment="Heart rate (beats/minute)" 

115 ) 

116 sbp = CamcopsColumn( 

117 "sbp", Integer, 

118 permitted_value_checker=MIN_ZERO_CHECKER, 

119 comment="Systolic blood pressure (mmHg)" 

120 ) 

121 dbp = CamcopsColumn( 

122 "dbp", Integer, 

123 permitted_value_checker=MIN_ZERO_CHECKER, 

124 comment="Diastolic blood pressure (mmHg)" 

125 ) 

126 rr = CamcopsColumn( 

127 "rr", Integer, 

128 permitted_value_checker=MIN_ZERO_CHECKER, 

129 comment="Respiratory rate (breaths/minute)" 

130 ) 

131 

132 MAX_SCORE = 67 

133 

134 @staticmethod 

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

136 _ = req.gettext 

137 return _("Clinical Institute Withdrawal Assessment for Alcohol " 

138 "Scale, Revised") 

139 

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

141 return [TrackerInfo( 

142 value=self.total_score(), 

143 plot_label="CIWA total score", 

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

145 axis_min=-0.5, 

146 axis_max=self.MAX_SCORE + 0.5, 

147 horizontal_lines=[14.5, 7.5], 

148 horizontal_labels=[ 

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

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

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

152 ] 

153 )] 

154 

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

156 if not self.is_complete(): 

157 return CTV_INCOMPLETE 

158 return [CtvInfo( 

159 content=f"CIWA total score: {self.total_score()}/{self.MAX_SCORE}" 

160 )] 

161 

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

163 return self.standard_task_summary_fields() + [ 

164 SummaryElement(name="total", 

165 coltype=Integer(), 

166 value=self.total_score(), 

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

168 SummaryElement(name="severity", 

169 coltype=SummaryCategoryColType, 

170 value=self.severity(req), 

171 comment="Likely severity"), 

172 ] 

173 

174 def is_complete(self) -> bool: 

175 return (self.all_fields_not_none(self.SCORED_QUESTIONS) and 

176 self.field_contents_valid()) 

177 

178 def total_score(self) -> int: 

179 return self.sum_fields(self.SCORED_QUESTIONS) 

180 

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

182 score = self.total_score() 

183 if score >= 15: 

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

185 elif score >= 8: 

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

187 else: 

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

189 return severity 

190 

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

192 score = self.total_score() 

193 severity = self.severity(req) 

194 answer_dicts_dict = {} 

195 for q in self.SCORED_QUESTIONS: 

196 d = {None: None} 

197 for option in range(0, 8): 

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

199 continue 

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

201 answer_dicts_dict[q] = d 

202 q_a = "" 

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

204 q_a += tr_qa( 

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

206 get_from_dict(answer_dicts_dict["q" + str(q)], 

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

208 ) 

209 tr_total_score = tr( 

210 req.sstring(SS.TOTAL_SCORE), 

211 answer(score) + f" / {self.MAX_SCORE}" 

212 ) 

213 tr_severity = tr_qa( 

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

215 severity 

216 ) 

217 return f""" 

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

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

220 {self.get_is_complete_tr(req)} 

221 {tr_total_score} 

222 {tr_severity} 

223 </table> 

224 </div> 

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

226 <tr> 

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

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

229 </tr> 

230 {q_a} 

231 {subheading_spanning_two_columns( 

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

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

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

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

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

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

238 </table> 

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

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

241 mild/minimal. 

242 </div> 

243 """ 

244 

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

246 codes = [SnomedExpression(req.snomed(SnomedLookup.CIWA_AR_PROCEDURE_ASSESSMENT))] # noqa 

247 if self.is_complete(): 

248 codes.append(SnomedExpression( 

249 req.snomed(SnomedLookup.CIWA_AR_SCALE), 

250 { 

251 req.snomed(SnomedLookup.CIWA_AR_SCORE): self.total_score(), 

252 } 

253 )) 

254 return codes