Coverage for tasks/rapid3.py : 53%

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