Coverage for tasks/ciwa.py: 53%

1"""

2 camcops_server/tasks/ciwa.py

3

4===============================================================================

5

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

8

9 This file is part of CamCOPS.

10

11 CamCOPS is free software: you can redistribute it and/or modify

12 it under the terms of the GNU General Public License as published by

13 the Free Software Foundation, either version 3 of the License, or

14 (at your option) any later version.

15

16 CamCOPS is distributed in the hope that it will be useful,

17 but WITHOUT ANY WARRANTY; without even the implied warranty of

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

19 GNU General Public License for more details.

20

21 You should have received a copy of the GNU General Public License

22 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.

23

24===============================================================================

25

26"""

27

28 from typing import Any, cast, List, Optional, Type

29

30 from cardinal_pythonlib.stringfunc import strseq

31 from sqlalchemy.orm import Mapped, mapped_column

32 from sqlalchemy.sql.sqltypes import Integer

33

34 from camcops_server.cc_modules.cc_constants import CssClass

35 from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo

36 from camcops_server.cc_modules.cc_db import add_multiple_columns

37 from camcops_server.cc_modules.cc_html import (

38 answer,

39 subheading_spanning_two_columns,

40 tr,

41 tr_qa,

42)

43 from camcops_server.cc_modules.cc_request import CamcopsRequest

44 from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup

45 from camcops_server.cc_modules.cc_sqla_coltypes import (

46 mapped_camcops_column,

47 MIN_ZERO_CHECKER,

48 PermittedValueChecker,

49 SummaryCategoryColType,

50)

51 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

52 from camcops_server.cc_modules.cc_task import (

53 get_from_dict,

54 Task,

55 TaskHasClinicianMixin,

56 TaskHasPatientMixin,

57)

58 from camcops_server.cc_modules.cc_text import SS

59 from camcops_server.cc_modules.cc_trackerhelpers import (

60 TrackerLabel,

61 TrackerInfo,

62)

63

64

65# =============================================================================

66# CIWA

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

68

69

70 class Ciwa( # type: ignore[misc]

71 TaskHasPatientMixin,

72 TaskHasClinicianMixin,

73 Task,

74):

75 """

76 Server implementation of the CIWA-Ar task.

77 """

78

79 __tablename__ = "ciwa"

80 shortname = "CIWA-Ar"

81 provides_trackers = True

82

83 NSCOREDQUESTIONS = 10

84

85 @classmethod

86 def extend_columns(cls: Type["Ciwa"], **kwargs: Any) -> None:

87 add_multiple_columns(

88 cls,

89 "q",

90 1,

91 cls.NSCOREDQUESTIONS - 1,

92 minimum=0,

93 maximum=7,

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

95 comment_strings=[

96 "nausea/vomiting",

97 "tremor",

98 "paroxysmal sweats",

99 "anxiety",

100 "agitation",

101 "tactile disturbances",

102 "auditory disturbances",

103 "visual disturbances",

104 "headache/fullness in head",

105 ],

106 )

107

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

109

110 q10: Mapped[Optional[int]] = mapped_camcops_column(

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

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

113 )

114 t: Mapped[Optional[float]] = mapped_column(

115 comment="Temperature (degrees C)"

116 )

117 hr: Mapped[Optional[int]] = mapped_camcops_column(

118 permitted_value_checker=MIN_ZERO_CHECKER,

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

120 )

121 sbp: Mapped[Optional[int]] = mapped_camcops_column(

122 permitted_value_checker=MIN_ZERO_CHECKER,

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

124 )

125 dbp: Mapped[Optional[int]] = mapped_camcops_column(

126 permitted_value_checker=MIN_ZERO_CHECKER,

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

128 )

129 rr: Mapped[Optional[int]] = mapped_camcops_column(

130 permitted_value_checker=MIN_ZERO_CHECKER,

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

132 )

133

134 MAX_SCORE = 67

135

136 @staticmethod

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

138 _ = req.gettext

139 return _(

140 "Clinical Institute Withdrawal Assessment for Alcohol "

141 "Scale, Revised"

142 )

143

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

145 return [

146 TrackerInfo(

147 value=self.total_score(),

148 plot_label="CIWA total score",

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

150 axis_min=-0.5,

151 axis_max=self.MAX_SCORE + 0.5,

152 horizontal_lines=[14.5, 7.5],

153 horizontal_labels=[

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

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

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

157 ],

158 )

159 ]

160

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

162 if not self.is_complete():

163 return CTV_INCOMPLETE

164 return [

165 CtvInfo(

166 content=f"CIWA total score: "

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

168 )

169 ]

170

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

172 return self.standard_task_summary_fields() + [

173 SummaryElement(

174 name="total",

175 coltype=Integer(),

176 value=self.total_score(),

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

178 ),

179 SummaryElement(

180 name="severity",

181 coltype=SummaryCategoryColType,

182 value=self.severity(req),

183 comment="Likely severity",

184 ),

185 ]

186

187 def is_complete(self) -> bool:

188 return (

189 self.all_fields_not_none(self.SCORED_QUESTIONS)

190 and self.field_contents_valid()

191 )

192

193 def total_score(self) -> int:

194 return cast(int, self.sum_fields(self.SCORED_QUESTIONS))

195

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

197 score = self.total_score()

198 if score >= 15:

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

200 elif score >= 8:

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

202 else:

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

204 return severity

205

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

207 score = self.total_score()

208 severity = self.severity(req)

209 answer_dicts_dict = {}

210 for q in self.SCORED_QUESTIONS:

211 d: dict[Optional[int], Optional[str]] = {None: None}

212 for option in range(0, 8):

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

214 continue

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

216 answer_dicts_dict[q] = d

217 q_a = ""

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

219 q_a += tr_qa(

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

221 get_from_dict(

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

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

224 ),

225 )

226 tr_total_score = tr(

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

228 )

229 tr_severity = tr_qa(

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

231 )

232 return f"""

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

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

235 {self.get_is_complete_tr(req)}

236 {tr_total_score}

237 {tr_severity}

238 </table>

239 </div>

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

241 <tr>

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

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

244 </tr>

245 {q_a}

246 {subheading_spanning_two_columns(

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

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

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

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

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

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

253 </table>

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

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

256 mild/minimal.

257 </div>

258 """

259

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

261 codes = [

262 SnomedExpression(

263 req.snomed(SnomedLookup.CIWA_AR_PROCEDURE_ASSESSMENT)

264 )

265 ]

266 if self.is_complete():

267 codes.append(

268 SnomedExpression(

269 req.snomed(SnomedLookup.CIWA_AR_SCALE),

270 {

271 req.snomed(

272 SnomedLookup.CIWA_AR_SCORE

273 ): self.total_score()

274 },

275 )

276 )

277 return codes