Coverage for tasks/rapid3.py: 53%

115 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/rapid3.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

10 

11 This file is part of CamCOPS. 

12 

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. 

17 

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. 

22 

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/>. 

25 

26=============================================================================== 

27 

28**Routine Assessment of Patient Index Data (RAPID 3) task.** 

29 

30""" 

31 

32from typing import Any, Dict, List, Optional, Type, Tuple 

33 

34import cardinal_pythonlib.rnc_web as ws 

35from sqlalchemy import Float, Integer 

36from sqlalchemy.ext.declarative import DeclarativeMeta 

37 

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) 

53 

54 

55# ============================================================================= 

56# RAPID 3 

57# ============================================================================= 

58 

59 

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: 

68 

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)" 

85 

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 ) 

101 

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

103 

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 ) 

119 

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 ) 

133 

134 super().__init__(name, bases, classdict) 

135 

136 

137class Rapid3(TaskHasPatientMixin, Task, metaclass=Rapid3Metaclass): 

138 __tablename__ = "rapid3" 

139 shortname = "RAPID3" 

140 provides_trackers = True 

141 

142 N_Q1_QUESTIONS = 13 

143 N_Q1_SCORING_QUESTIONS = 10 

144 

145 # > 12 = HIGH 

146 # 6.1 - 12 = MODERATE 

147 # 3.1 - 6 = LOW 

148 # <= 3 = REMISSION 

149 

150 MINIMUM = 0 

151 NEAR_REMISSION_MAX = 3 

152 LOW_SEVERITY_MAX = 6 

153 MODERATE_SEVERITY_MAX = 12 

154 MAXIMUM = 30 

155 

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)] 

159 

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)] 

163 

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 ] 

169 

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)] 

173 

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)] 

177 

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 ] 

184 

185 @classmethod 

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

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

188 

189 @staticmethod 

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

191 _ = req.gettext 

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

193 

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 ] 

203 

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 ] 

210 

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 ] 

218 

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 ] 

236 

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 ] 

249 

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

251 if not self.is_complete(): 

252 return None 

253 

254 return ( 

255 self.functional_status() 

256 + self.pain_tolerance() 

257 + self.global_estimate() 

258 ) 

259 

260 def functional_status(self) -> float: 

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

262 

263 def pain_tolerance(self) -> float: 

264 # noinspection PyUnresolvedReferences 

265 return self.q2 

266 

267 def global_estimate(self) -> float: 

268 # noinspection PyUnresolvedReferences 

269 return self.q3 

270 

271 def is_complete(self) -> bool: 

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

273 return False 

274 

275 if not self.field_contents_valid(): 

276 return False 

277 

278 return True 

279 

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}" 

287 

288 qtext = self.wxstring(req, q_fieldname) 

289 score = getattr(self, q_fieldname) 

290 

291 description = "?" 

292 if score is not None: 

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

294 

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

296 

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) 

304 

305 rows += tr_qa(qtext, score) 

306 

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

308 

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). 

334 

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). 

340 

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 

352 

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

354 rapid3 = self.rapid3() 

355 

356 if rapid3 is None: 

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

358 

359 if rapid3 <= self.NEAR_REMISSION_MAX: 

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

361 

362 if rapid3 <= self.LOW_SEVERITY_MAX: 

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

364 

365 if rapid3 <= self.MODERATE_SEVERITY_MAX: 

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

367 

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