Coverage for tasks/ciwa.py: 55%
80 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/tasks/ciwa.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
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.
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.
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/>.
26===============================================================================
28"""
30from typing import Any, Dict, List, Tuple, Type
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
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)
68# =============================================================================
69# CIWA
70# =============================================================================
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)
104class Ciwa(
105 TaskHasPatientMixin, TaskHasClinicianMixin, Task, metaclass=CiwaMetaclass
106):
107 """
108 Server implementation of the CIWA-Ar task.
109 """
111 __tablename__ = "ciwa"
112 shortname = "CIWA-Ar"
113 provides_trackers = True
115 NSCOREDQUESTIONS = 10
116 SCORED_QUESTIONS = strseq("q", 1, NSCOREDQUESTIONS)
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 )
150 MAX_SCORE = 67
152 @staticmethod
153 def longname(req: "CamcopsRequest") -> str:
154 _ = req.gettext
155 return _(
156 "Clinical Institute Withdrawal Assessment for Alcohol "
157 "Scale, Revised"
158 )
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 ]
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 ]
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 ]
203 def is_complete(self) -> bool:
204 return (
205 self.all_fields_not_none(self.SCORED_QUESTIONS)
206 and self.field_contents_valid()
207 )
209 def total_score(self) -> int:
210 return self.sum_fields(self.SCORED_QUESTIONS)
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
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 """
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