Coverage for tasks/rapid3.py: 52%

1"""

2 camcops_server/tasks/rapid3.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**Routine Assessment of Patient Index Data (RAPID 3) task.**

27

28"""

29

30 from typing import Any, List, Optional, Type, Tuple

31

32 import cardinal_pythonlib.rnc_web as ws

33 from sqlalchemy import Float, Integer

34

35 from camcops_server.cc_modules.cc_constants import CssClass

36 from camcops_server.cc_modules.cc_html import answer, tr_qa, tr, tr_span_col

37 from camcops_server.cc_modules.cc_request import CamcopsRequest

38 from camcops_server.cc_modules.cc_sqla_coltypes import (

39 camcops_column,

40 PermittedValueChecker,

41 ZERO_TO_THREE_CHECKER,

42)

43 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

44 from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task

45 from camcops_server.cc_modules.cc_trackerhelpers import (

46 TrackerAxisTick,

47 TrackerInfo,

48 TrackerLabel,

49)

50

51

52# =============================================================================

53# RAPID 3

54# =============================================================================

55

56

57 class Rapid3( # type: ignore[misc]

58 TaskHasPatientMixin,

59 Task,

60):

61 __tablename__ = "rapid3"

62 shortname = "RAPID3"

63 provides_trackers = True

64

65 N_Q1_QUESTIONS = 13

66 N_Q1_SCORING_QUESTIONS = 10

67

68 # > 12 = HIGH

69 # 6.1 - 12 = MODERATE

70 # 3.1 - 6 = LOW

71 # <= 3 = REMISSION

72

73 MINIMUM = 0

74 NEAR_REMISSION_MAX = 3

75 LOW_SEVERITY_MAX = 6

76 MODERATE_SEVERITY_MAX = 12

77 MAXIMUM = 30

78

79 @classmethod

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

81

82 comment_strings = [

83 "get dressed",

84 "get in bed",

85 "lift full cup",

86 "walk outdoors",

87 "wash body",

88 "bend down",

89 "turn taps",

90 "get in car",

91 "walk 2 miles",

92 "sports",

93 "sleep",

94 "anxiety",

95 "depression",

96 ]

97 score_comment = "(0 without any difficulty - 3 unable to do)"

98

99 for q_index, q_fieldname in cls.q1_all_indexed_fieldnames():

100 setattr(

101 cls,

102 q_fieldname,

103 camcops_column(

104 q_fieldname,

105 Integer,

106 permitted_value_checker=ZERO_TO_THREE_CHECKER,

107 comment="{} ({}) {}".format(

108 q_fieldname.capitalize(),

109 comment_strings[q_index],

110 score_comment,

111 ),

112 ),

113 )

114

115 permitted_scale_values = [v / 2.0 for v in range(0, 20 + 1)]

116

117 setattr(

118 cls,

119 "q2",

120 camcops_column(

121 "q2",

122 Float,

123 permitted_value_checker=PermittedValueChecker(

124 permitted_values=permitted_scale_values

125 ),

126 comment=(

127 "Q2 (pain tolerance) (0 no pain - 10 pain as bad as "

128 "it could be"

129 ),

130 ),

131 )

132

133 setattr(

134 cls,

135 "q3",

136 camcops_column(

137 "q3",

138 Float,

139 permitted_value_checker=PermittedValueChecker(

140 permitted_values=permitted_scale_values

141 ),

142 comment="Q3 (patient global estimate) "

143 "(0 very well - very poorly)",

144 ),

145 )

146

147 @classmethod

148 def q1_indexed_letters(cls, last: int) -> List[Tuple[int, str]]:

149 return [(i, chr(i + ord("a"))) for i in range(0, last)]

150

151 @classmethod

152 def q1_indexed_fieldnames(cls, last: int) -> List[Tuple[int, str]]:

153 return [(i, f"q1{c}") for (i, c) in cls.q1_indexed_letters(last)]

154

155 @classmethod

156 def q1_all_indexed_fieldnames(cls) -> List[Tuple[int, str]]:

157 return [

158 (i, f) for (i, f) in cls.q1_indexed_fieldnames(cls.N_Q1_QUESTIONS)

159 ]

160

161 @classmethod

162 def q1_all_fieldnames(cls) -> List[str]:

163 return [f for (i, f) in cls.q1_indexed_fieldnames(cls.N_Q1_QUESTIONS)]

164

165 @classmethod

166 def q1_all_letters(cls) -> List[str]:

167 return [c for (i, c) in cls.q1_indexed_letters(cls.N_Q1_QUESTIONS)]

168

169 @classmethod

170 def q1_scoring_fieldnames(cls) -> List[str]:

171 return [

172 f

173 for (i, f) in cls.q1_indexed_fieldnames(cls.N_Q1_SCORING_QUESTIONS)

174 ]

175

176 @classmethod

177 def all_fieldnames(cls) -> List[str]:

178 return cls.q1_all_fieldnames() + ["q2", "q3"]

179

180 @staticmethod

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

182 _ = req.gettext

183 return _("Routine Assessment of Patient Index Data")

184

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

186 return self.standard_task_summary_fields() + [

187 SummaryElement(

188 name="rapid3",

189 coltype=Float(),

190 value=self.rapid3(),

191 comment="RAPID3",

192 )

193 ]

194

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

196 axis_min = self.MINIMUM - 0.5

197 axis_max = self.MAXIMUM + 0.5

198 axis_ticks = [

199 TrackerAxisTick(n, str(n)) for n in range(0, int(axis_max) + 1, 2)

200 ]

201

202 horizontal_lines: list[float] = [

203 self.MAXIMUM,

204 self.MODERATE_SEVERITY_MAX,

205 self.LOW_SEVERITY_MAX,

206 self.NEAR_REMISSION_MAX,

207 self.MINIMUM,

208 ]

209

210 horizontal_labels = [

211 TrackerLabel(

212 self.MODERATE_SEVERITY_MAX + 8.0,

213 self.wxstring(req, "high_severity"),

214 ),

215 TrackerLabel(

216 self.MODERATE_SEVERITY_MAX - 3.0,

217 self.wxstring(req, "moderate_severity"),

218 ),

219 TrackerLabel(

220 self.LOW_SEVERITY_MAX - 1.5, self.wxstring(req, "low_severity")

221 ),

222 TrackerLabel(

223 self.NEAR_REMISSION_MAX - 1.5,

224 self.wxstring(req, "near_remission"),

225 ),

226 ]

227

228 return [

229 TrackerInfo(

230 value=self.rapid3(),

231 plot_label="RAPID3",

232 axis_label="RAPID3",

233 axis_min=axis_min,

234 axis_max=axis_max,

235 axis_ticks=axis_ticks,

236 horizontal_lines=horizontal_lines,

237 horizontal_labels=horizontal_labels,

238 )

239 ]

240

241 def rapid3(self) -> Optional[float]:

242 if not self.is_complete():

243 return None

244

245 return (

246 self.functional_status()

247 + self.pain_tolerance()

248 + self.global_estimate()

249 )

250

251 def functional_status(self) -> float:

252 return round(self.sum_fields(self.q1_scoring_fieldnames()) / 3, 1)

253

254 def pain_tolerance(self) -> float:

255 # noinspection PyUnresolvedReferences

256 return self.q2 # type: ignore[attr-defined]

257

258 def global_estimate(self) -> float:

259 # noinspection PyUnresolvedReferences

260 return self.q3 # type: ignore[attr-defined]

261

262 def is_complete(self) -> bool:

263 if self.any_fields_none(self.all_fieldnames()):

264 return False

265

266 if not self.field_contents_valid():

267 return False

268

269 return True

270

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

272 rows = tr_span_col(

273 f'{self.wxstring(req, "q1")}<br>' f'{self.wxstring(req, "q1sub")}',

274 cols=2,

275 )

276 for letter in self.q1_all_letters():

277 q_fieldname = f"q1{letter}"

278

279 qtext = self.wxstring(req, q_fieldname)

280 score = getattr(self, q_fieldname)

281

282 description = "?"

283 if score is not None:

284 description = self.wxstring(req, f"q1_option{score}")

285

286 rows += tr_qa(qtext, f"{score} — {description}")

287

288 for q_num in (2, 3):

289 q_fieldname = f"q{q_num}"

290 qtext = self.wxstring(req, q_fieldname)

291 min_text = self.wxstring(req, f"{q_fieldname}_min")

292 max_text = self.wxstring(req, f"{q_fieldname}_max")

293 qtext += f" <i>(0.0 = {min_text}, 10.0 = {max_text})</i>"

294 score = getattr(self, q_fieldname)

295

296 rows += tr_qa(qtext, score)

297

298 rapid3 = ws.number_to_dp(self.rapid3(), 1, default="?")

299

300 html = """

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

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

303 {tr_is_complete}

304 {rapid3}

305 </table>

306 </div>

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

308 <tr>

309 <th width="60%">Question</th>

310 <th width="40%">Answer</th>

311 </tr>

312 {rows}

313 </table>

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

315 [1] Add scores for questions 1a–1j (ten questions each scored

316 0–3), divide by 3, and round to 1 decimal place (giving a

317 score for Q1 in the range 0–10). Then add this to scores

318 for Q2 and Q3 (each scored 0–10) to get the RAPID3

319 cumulative score (0–30), as shown here.

320 Interpretation of the cumulative score:

321 ≤3: Near remission (NR).

322 3.1–6: Low severity (LS).

323 6.1–12: Moderate severity (MS).

324 >12: High severity (HS).

325

326 Note also: questions 1k–1m are each scored 0, 1.1, 2.2, or

327 3.3 in the PDF/paper version of the RAPID3, but do not

328 contribute to the formal score. They are shown here with

329 values 0, 1, 2, 3 (and, similarly, do not contribute to

330 the overall score).

331

332 </div>

333 """.format(

334 CssClass=CssClass,

335 tr_is_complete=self.get_is_complete_tr(req),

336 rapid3=tr(

337 self.wxstring(req, "rapid3") + " (0–30) <sup>[1]</sup>",

338 "{} ({})".format(answer(rapid3), self.disease_severity(req)),

339 ),

340 rows=rows,

341 )

342 return html

343

344 def disease_severity(self, req: CamcopsRequest) -> str:

345 rapid3 = self.rapid3()

346

347 if rapid3 is None:

348 return self.wxstring(req, "n_a")

349

350 if rapid3 <= self.NEAR_REMISSION_MAX:

351 return self.wxstring(req, "near_remission")

352

353 if rapid3 <= self.LOW_SEVERITY_MAX:

354 return self.wxstring(req, "low_severity")

355

356 if rapid3 <= self.MODERATE_SEVERITY_MAX:

357 return self.wxstring(req, "moderate_severity")

358

359 return self.wxstring(req, "high_severity")