Coverage for tasks/ciwa.py: 53%
77 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/tasks/ciwa.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
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.
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.
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/>.
24===============================================================================
26"""
28from typing import Any, cast, List, Optional, Type
30from cardinal_pythonlib.stringfunc import strseq
31from sqlalchemy.orm import Mapped, mapped_column
32from sqlalchemy.sql.sqltypes import Integer
34from camcops_server.cc_modules.cc_constants import CssClass
35from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
36from camcops_server.cc_modules.cc_db import add_multiple_columns
37from camcops_server.cc_modules.cc_html import (
38 answer,
39 subheading_spanning_two_columns,
40 tr,
41 tr_qa,
42)
43from camcops_server.cc_modules.cc_request import CamcopsRequest
44from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup
45from camcops_server.cc_modules.cc_sqla_coltypes import (
46 mapped_camcops_column,
47 MIN_ZERO_CHECKER,
48 PermittedValueChecker,
49 SummaryCategoryColType,
50)
51from camcops_server.cc_modules.cc_summaryelement import SummaryElement
52from camcops_server.cc_modules.cc_task import (
53 get_from_dict,
54 Task,
55 TaskHasClinicianMixin,
56 TaskHasPatientMixin,
57)
58from camcops_server.cc_modules.cc_text import SS
59from camcops_server.cc_modules.cc_trackerhelpers import (
60 TrackerLabel,
61 TrackerInfo,
62)
65# =============================================================================
66# CIWA
67# =============================================================================
70class Ciwa( # type: ignore[misc]
71 TaskHasPatientMixin,
72 TaskHasClinicianMixin,
73 Task,
74):
75 """
76 Server implementation of the CIWA-Ar task.
77 """
79 __tablename__ = "ciwa"
80 shortname = "CIWA-Ar"
81 provides_trackers = True
83 NSCOREDQUESTIONS = 10
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 )
108 SCORED_QUESTIONS = strseq("q", 1, NSCOREDQUESTIONS)
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 )
134 MAX_SCORE = 67
136 @staticmethod
137 def longname(req: "CamcopsRequest") -> str:
138 _ = req.gettext
139 return _(
140 "Clinical Institute Withdrawal Assessment for Alcohol "
141 "Scale, Revised"
142 )
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 ]
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 ]
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 ]
187 def is_complete(self) -> bool:
188 return (
189 self.all_fields_not_none(self.SCORED_QUESTIONS)
190 and self.field_contents_valid()
191 )
193 def total_score(self) -> int:
194 return cast(int, self.sum_fields(self.SCORED_QUESTIONS))
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
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 """
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