Coverage for tasks/rapid3.py: 53%
115 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/rapid3.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**Routine Assessment of Patient Index Data (RAPID 3) task.**
30"""
32from typing import Any, Dict, List, Optional, Type, Tuple
34import cardinal_pythonlib.rnc_web as ws
35from sqlalchemy import Float, Integer
36from sqlalchemy.ext.declarative import DeclarativeMeta
38from camcops_server.cc_modules.cc_constants import CssClass
39from camcops_server.cc_modules.cc_html import answer, tr_qa, tr, tr_span_col
40from camcops_server.cc_modules.cc_request import CamcopsRequest
41from camcops_server.cc_modules.cc_sqla_coltypes import (
42 CamcopsColumn,
43 PermittedValueChecker,
44 ZERO_TO_THREE_CHECKER,
45)
46from camcops_server.cc_modules.cc_summaryelement import SummaryElement
47from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task
48from camcops_server.cc_modules.cc_trackerhelpers import (
49 TrackerAxisTick,
50 TrackerInfo,
51 TrackerLabel,
52)
55# =============================================================================
56# RAPID 3
57# =============================================================================
60class Rapid3Metaclass(DeclarativeMeta):
61 # noinspection PyInitNewSignature
62 def __init__(
63 cls: Type["Rapid3"],
64 name: str,
65 bases: Tuple[Type, ...],
66 classdict: Dict[str, Any],
67 ) -> None:
69 comment_strings = [
70 "get dressed",
71 "get in bed",
72 "lift full cup",
73 "walk outdoors",
74 "wash body",
75 "bend down",
76 "turn taps",
77 "get in car",
78 "walk 2 miles",
79 "sports",
80 "sleep",
81 "anxiety",
82 "depression",
83 ]
84 score_comment = "(0 without any difficulty - 3 unable to do)"
86 for q_index, q_fieldname in cls.q1_all_indexed_fieldnames():
87 setattr(
88 cls,
89 q_fieldname,
90 CamcopsColumn(
91 q_fieldname,
92 Integer,
93 permitted_value_checker=ZERO_TO_THREE_CHECKER,
94 comment="{} ({}) {}".format(
95 q_fieldname.capitalize(),
96 comment_strings[q_index],
97 score_comment,
98 ),
99 ),
100 )
102 permitted_scale_values = [v / 2.0 for v in range(0, 20 + 1)]
104 setattr(
105 cls,
106 "q2",
107 CamcopsColumn(
108 "q2",
109 Float,
110 permitted_value_checker=PermittedValueChecker(
111 permitted_values=permitted_scale_values
112 ),
113 comment=(
114 "Q2 (pain tolerance) (0 no pain - 10 pain as bad as "
115 "it could be"
116 ),
117 ),
118 )
120 setattr(
121 cls,
122 "q3",
123 CamcopsColumn(
124 "q3",
125 Float,
126 permitted_value_checker=PermittedValueChecker(
127 permitted_values=permitted_scale_values
128 ),
129 comment="Q3 (patient global estimate) "
130 "(0 very well - very poorly)",
131 ),
132 )
134 super().__init__(name, bases, classdict)
137class Rapid3(TaskHasPatientMixin, Task, metaclass=Rapid3Metaclass):
138 __tablename__ = "rapid3"
139 shortname = "RAPID3"
140 provides_trackers = True
142 N_Q1_QUESTIONS = 13
143 N_Q1_SCORING_QUESTIONS = 10
145 # > 12 = HIGH
146 # 6.1 - 12 = MODERATE
147 # 3.1 - 6 = LOW
148 # <= 3 = REMISSION
150 MINIMUM = 0
151 NEAR_REMISSION_MAX = 3
152 LOW_SEVERITY_MAX = 6
153 MODERATE_SEVERITY_MAX = 12
154 MAXIMUM = 30
156 @classmethod
157 def q1_indexed_letters(cls, last: int) -> List[Tuple[int, str]]:
158 return [(i, chr(i + ord("a"))) for i in range(0, last)]
160 @classmethod
161 def q1_indexed_fieldnames(cls, last: int) -> List[Tuple[int, str]]:
162 return [(i, f"q1{c}") for (i, c) in cls.q1_indexed_letters(last)]
164 @classmethod
165 def q1_all_indexed_fieldnames(cls) -> List[Tuple[int, str]]:
166 return [
167 (i, f) for (i, f) in cls.q1_indexed_fieldnames(cls.N_Q1_QUESTIONS)
168 ]
170 @classmethod
171 def q1_all_fieldnames(cls) -> List[str]:
172 return [f for (i, f) in cls.q1_indexed_fieldnames(cls.N_Q1_QUESTIONS)]
174 @classmethod
175 def q1_all_letters(cls) -> List[str]:
176 return [c for (i, c) in cls.q1_indexed_letters(cls.N_Q1_QUESTIONS)]
178 @classmethod
179 def q1_scoring_fieldnames(cls) -> List[str]:
180 return [
181 f
182 for (i, f) in cls.q1_indexed_fieldnames(cls.N_Q1_SCORING_QUESTIONS)
183 ]
185 @classmethod
186 def all_fieldnames(cls) -> List[str]:
187 return cls.q1_all_fieldnames() + ["q2", "q3"]
189 @staticmethod
190 def longname(req: "CamcopsRequest") -> str:
191 _ = req.gettext
192 return _("Routine Assessment of Patient Index Data")
194 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
195 return self.standard_task_summary_fields() + [
196 SummaryElement(
197 name="rapid3",
198 coltype=Float(),
199 value=self.rapid3(),
200 comment="RAPID3",
201 )
202 ]
204 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
205 axis_min = self.MINIMUM - 0.5
206 axis_max = self.MAXIMUM + 0.5
207 axis_ticks = [
208 TrackerAxisTick(n, str(n)) for n in range(0, int(axis_max) + 1, 2)
209 ]
211 horizontal_lines = [
212 self.MAXIMUM,
213 self.MODERATE_SEVERITY_MAX,
214 self.LOW_SEVERITY_MAX,
215 self.NEAR_REMISSION_MAX,
216 self.MINIMUM,
217 ]
219 horizontal_labels = [
220 TrackerLabel(
221 self.MODERATE_SEVERITY_MAX + 8.0,
222 self.wxstring(req, "high_severity"),
223 ),
224 TrackerLabel(
225 self.MODERATE_SEVERITY_MAX - 3.0,
226 self.wxstring(req, "moderate_severity"),
227 ),
228 TrackerLabel(
229 self.LOW_SEVERITY_MAX - 1.5, self.wxstring(req, "low_severity")
230 ),
231 TrackerLabel(
232 self.NEAR_REMISSION_MAX - 1.5,
233 self.wxstring(req, "near_remission"),
234 ),
235 ]
237 return [
238 TrackerInfo(
239 value=self.rapid3(),
240 plot_label="RAPID3",
241 axis_label="RAPID3",
242 axis_min=axis_min,
243 axis_max=axis_max,
244 axis_ticks=axis_ticks,
245 horizontal_lines=horizontal_lines,
246 horizontal_labels=horizontal_labels,
247 )
248 ]
250 def rapid3(self) -> Optional[float]:
251 if not self.is_complete():
252 return None
254 return (
255 self.functional_status()
256 + self.pain_tolerance()
257 + self.global_estimate()
258 )
260 def functional_status(self) -> float:
261 return round(self.sum_fields(self.q1_scoring_fieldnames()) / 3, 1)
263 def pain_tolerance(self) -> float:
264 # noinspection PyUnresolvedReferences
265 return self.q2
267 def global_estimate(self) -> float:
268 # noinspection PyUnresolvedReferences
269 return self.q3
271 def is_complete(self) -> bool:
272 if self.any_fields_none(self.all_fieldnames()):
273 return False
275 if not self.field_contents_valid():
276 return False
278 return True
280 def get_task_html(self, req: CamcopsRequest) -> str:
281 rows = tr_span_col(
282 f'{self.wxstring(req, "q1")}<br>' f'{self.wxstring(req, "q1sub")}',
283 cols=2,
284 )
285 for letter in self.q1_all_letters():
286 q_fieldname = f"q1{letter}"
288 qtext = self.wxstring(req, q_fieldname)
289 score = getattr(self, q_fieldname)
291 description = "?"
292 if score is not None:
293 description = self.wxstring(req, f"q1_option{score}")
295 rows += tr_qa(qtext, f"{score} — {description}")
297 for q_num in (2, 3):
298 q_fieldname = f"q{q_num}"
299 qtext = self.wxstring(req, q_fieldname)
300 min_text = self.wxstring(req, f"{q_fieldname}_min")
301 max_text = self.wxstring(req, f"{q_fieldname}_max")
302 qtext += f" <i>(0.0 = {min_text}, 10.0 = {max_text})</i>"
303 score = getattr(self, q_fieldname)
305 rows += tr_qa(qtext, score)
307 rapid3 = ws.number_to_dp(self.rapid3(), 1, default="?")
309 html = """
310 <div class="{CssClass.SUMMARY}">
311 <table class="{CssClass.SUMMARY}">
312 {tr_is_complete}
313 {rapid3}
314 </table>
315 </div>
316 <table class="{CssClass.TASKDETAIL}">
317 <tr>
318 <th width="60%">Question</th>
319 <th width="40%">Answer</th>
320 </tr>
321 {rows}
322 </table>
323 <div class="{CssClass.FOOTNOTES}">
324 [1] Add scores for questions 1a–1j (ten questions each scored
325 0–3), divide by 3, and round to 1 decimal place (giving a
326 score for Q1 in the range 0–10). Then add this to scores
327 for Q2 and Q3 (each scored 0–10) to get the RAPID3
328 cumulative score (0–30), as shown here.
329 Interpretation of the cumulative score:
330 ≤3: Near remission (NR).
331 3.1–6: Low severity (LS).
332 6.1–12: Moderate severity (MS).
333 >12: High severity (HS).
335 Note also: questions 1k–1m are each scored 0, 1.1, 2.2, or
336 3.3 in the PDF/paper version of the RAPID3, but do not
337 contribute to the formal score. They are shown here with
338 values 0, 1, 2, 3 (and, similarly, do not contribute to
339 the overall score).
341 </div>
342 """.format(
343 CssClass=CssClass,
344 tr_is_complete=self.get_is_complete_tr(req),
345 rapid3=tr(
346 self.wxstring(req, "rapid3") + " (0–30) <sup>[1]</sup>",
347 "{} ({})".format(answer(rapid3), self.disease_severity(req)),
348 ),
349 rows=rows,
350 )
351 return html
353 def disease_severity(self, req: CamcopsRequest) -> str:
354 rapid3 = self.rapid3()
356 if rapid3 is None:
357 return self.wxstring(req, "n_a")
359 if rapid3 <= self.NEAR_REMISSION_MAX:
360 return self.wxstring(req, "near_remission")
362 if rapid3 <= self.LOW_SEVERITY_MAX:
363 return self.wxstring(req, "low_severity")
365 if rapid3 <= self.MODERATE_SEVERITY_MAX:
366 return self.wxstring(req, "moderate_severity")
368 return self.wxstring(req, "high_severity")