Hide keyboard shortcuts

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 

2 

3""" 

4camcops_server/tasks/rapid3.py 

5 

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

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

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. 

16 

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. 

21 

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

24 

25=============================================================================== 

26 

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

28 

29""" 

30 

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

32 

33import cardinal_pythonlib.rnc_web as ws 

34from sqlalchemy import Float, Integer 

35from sqlalchemy.ext.declarative import DeclarativeMeta 

36 

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) 

52 

53 

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

55# RAPID 3 

56# ============================================================================= 

57 

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: 

64 

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

81 

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

92 

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

94 

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

103 

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

111 

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

113 

114 

115class Rapid3(TaskHasPatientMixin, 

116 Task, 

117 metaclass=Rapid3Metaclass): 

118 __tablename__ = "rapid3" 

119 shortname = "RAPID3" 

120 provides_trackers = True 

121 

122 N_Q1_QUESTIONS = 13 

123 N_Q1_SCORING_QUESTIONS = 10 

124 

125 # > 12 = HIGH 

126 # 6.1 - 12 = MODERATE 

127 # 3.1 - 6 = LOW 

128 # <= 3 = REMISSION 

129 

130 MINIMUM = 0 

131 NEAR_REMISSION_MAX = 3 

132 LOW_SEVERITY_MAX = 6 

133 MODERATE_SEVERITY_MAX = 12 

134 MAXIMUM = 30 

135 

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

139 

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

143 

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

148 

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

153 

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

158 

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

163 

164 @classmethod 

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

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

167 

168 @staticmethod 

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

170 _ = req.gettext 

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

172 

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 ] 

180 

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

186 

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 ] 

194 

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 ] 

205 

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 ] 

218 

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

220 if not self.is_complete(): 

221 return None 

222 

223 return (self.functional_status() + 

224 self.pain_tolerance() + 

225 self.global_estimate()) 

226 

227 def functional_status(self) -> float: 

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

229 

230 def pain_tolerance(self) -> float: 

231 # noinspection PyUnresolvedReferences 

232 return self.q2 

233 

234 def global_estimate(self) -> float: 

235 # noinspection PyUnresolvedReferences 

236 return self.q3 

237 

238 def is_complete(self) -> bool: 

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

240 return False 

241 

242 if not self.field_contents_valid(): 

243 return False 

244 

245 return True 

246 

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

255 

256 qtext = self.wxstring(req, q_fieldname) 

257 score = getattr(self, q_fieldname) 

258 

259 description = "?" 

260 if score is not None: 

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

262 

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

264 

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) 

272 

273 rows += tr_qa(qtext, score) 

274 

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

276 

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

302 

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

308 

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 

323 

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

325 rapid3 = self.rapid3() 

326 

327 if rapid3 is None: 

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

329 

330 if rapid3 <= self.NEAR_REMISSION_MAX: 

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

332 

333 if rapid3 <= self.LOW_SEVERITY_MAX: 

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

335 

336 if rapid3 <= self.MODERATE_SEVERITY_MAX: 

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

338 

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