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/asdas.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**Ankylosing Spondylitis Disease Activity Score (ASDAS) task.** 

28 

29""" 

30 

31import math 

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

33 

34from camcops_server.cc_modules.cc_constants import CssClass 

35from camcops_server.cc_modules.cc_db import add_multiple_columns 

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

37from camcops_server.cc_modules.cc_request import CamcopsRequest 

38from camcops_server.cc_modules.cc_sqla_coltypes import ( 

39 SummaryCategoryColType, 

40) 

41from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

42from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task 

43from camcops_server.cc_modules.cc_trackerhelpers import ( 

44 TrackerAxisTick, 

45 TrackerInfo, 

46 TrackerLabel, 

47) 

48 

49import cardinal_pythonlib.rnc_web as ws 

50from cardinal_pythonlib.stringfunc import strseq 

51from sqlalchemy import Column, Float 

52from sqlalchemy.ext.declarative import DeclarativeMeta 

53 

54 

55class AsdasMetaclass(DeclarativeMeta): 

56 # noinspection PyInitNewSignature 

57 def __init__(cls: Type['Asdas'], 

58 name: str, 

59 bases: Tuple[Type, ...], 

60 classdict: Dict[str, Any]) -> None: 

61 

62 add_multiple_columns( 

63 cls, "q", 1, cls.N_SCALE_QUESTIONS, 

64 minimum=0, maximum=10, 

65 comment_fmt="Q{n} - {s}", 

66 comment_strings=[ 

67 "back pain 0-10 (None - very severe)", 

68 "morning stiffness 0-10 (None - 2+ hours)", 

69 "patient global 0-10 (Not active - very active)", 

70 "peripheral pain 0-10 (None - very severe)", 

71 ] 

72 ) 

73 

74 setattr( 

75 cls, cls.CRP_FIELD_NAME, 

76 Column(cls.CRP_FIELD_NAME, Float, comment="CRP (mg/L)") 

77 ) 

78 

79 setattr( 

80 cls, cls.ESR_FIELD_NAME, 

81 Column(cls.ESR_FIELD_NAME, Float, comment="ESR (mm/h)") 

82 ) 

83 

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

85 

86 

87class Asdas(TaskHasPatientMixin, 

88 Task, 

89 metaclass=AsdasMetaclass): 

90 __tablename__ = "asdas" 

91 shortname = "ASDAS" 

92 provides_trackers = True 

93 

94 N_SCALE_QUESTIONS = 4 

95 MAX_SCORE_SCALE = 10 

96 N_QUESTIONS = 6 

97 SCALE_FIELD_NAMES = strseq("q", 1, N_SCALE_QUESTIONS) 

98 CRP_FIELD_NAME = "q5" 

99 ESR_FIELD_NAME = "q6" 

100 

101 INACTIVE_MODERATE_CUTOFF = 1.3 

102 MODERATE_HIGH_CUTOFF = 2.1 

103 HIGH_VERY_HIGH_CUTOFF = 3.5 

104 

105 @staticmethod 

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

107 _ = req.gettext 

108 return _("Ankylosing Spondylitis Disease Activity Score") 

109 

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

111 return self.standard_task_summary_fields() + [ 

112 SummaryElement( 

113 name="asdas_crp", coltype=Float(), 

114 value=self.asdas_crp(), 

115 comment="ASDAS-CRP"), 

116 SummaryElement( 

117 name="activity_state_crp", coltype=SummaryCategoryColType, 

118 value=self.activity_state(req, self.asdas_crp()), 

119 comment="Activity state (CRP)"), 

120 SummaryElement( 

121 name="asdas_esr", coltype=Float(), 

122 value=self.asdas_esr(), 

123 comment="ASDAS-ESR"), 

124 SummaryElement( 

125 name="activity_state_esr", coltype=SummaryCategoryColType, 

126 value=self.activity_state(req, self.asdas_esr()), 

127 comment="Activity state (ESR)"), 

128 ] 

129 

130 def is_complete(self) -> bool: 

131 if self.any_fields_none(self.SCALE_FIELD_NAMES): 

132 return False 

133 

134 crp = getattr(self, self.CRP_FIELD_NAME) 

135 esr = getattr(self, self.ESR_FIELD_NAME) 

136 

137 if crp is None and esr is None: 

138 return False 

139 

140 if not self.field_contents_valid(): 

141 return False 

142 

143 return True 

144 

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

146 axis_min = -0.5 

147 axis_max = 7.5 

148 axis_ticks = [TrackerAxisTick(n, str(n)) 

149 for n in range(0, int(axis_max) + 1)] 

150 

151 horizontal_lines = [ 

152 self.HIGH_VERY_HIGH_CUTOFF, 

153 self.MODERATE_HIGH_CUTOFF, 

154 self.INACTIVE_MODERATE_CUTOFF, 

155 0, 

156 ] 

157 

158 horizontal_labels = [ 

159 TrackerLabel(5.25, self.wxstring(req, "very_high")), 

160 TrackerLabel(2.8, self.wxstring(req, "high")), 

161 TrackerLabel(1.7, self.wxstring(req, "moderate")), 

162 TrackerLabel(0.65, self.wxstring(req, "inactive")), 

163 ] 

164 

165 return [ 

166 TrackerInfo( 

167 value=self.asdas_crp(), 

168 plot_label="ASDAS-CRP", 

169 axis_label="ASDAS-CRP", 

170 axis_min=axis_min, 

171 axis_max=axis_max, 

172 axis_ticks=axis_ticks, 

173 horizontal_lines=horizontal_lines, 

174 horizontal_labels=horizontal_labels, 

175 ), 

176 TrackerInfo( 

177 value=self.asdas_esr(), 

178 plot_label="ASDAS-ESR", 

179 axis_label="ASDAS-ESR", 

180 axis_min=axis_min, 

181 axis_max=axis_max, 

182 axis_ticks=axis_ticks, 

183 horizontal_lines=horizontal_lines, 

184 horizontal_labels=horizontal_labels, 

185 ), 

186 ] 

187 

188 def back_pain(self) -> float: 

189 return getattr(self, "q1") 

190 

191 def morning_stiffness(self) -> float: 

192 return getattr(self, "q2") 

193 

194 def patient_global(self) -> float: 

195 return getattr(self, "q3") 

196 

197 def peripheral_pain(self) -> float: 

198 return getattr(self, "q4") 

199 

200 def asdas_crp(self) -> Optional[float]: 

201 crp = getattr(self, self.CRP_FIELD_NAME) 

202 

203 if crp is None: 

204 return None 

205 

206 crp = max(crp, 2.0) 

207 

208 return ( 

209 0.12 * self.back_pain() + 

210 0.06 * self.morning_stiffness() + 

211 0.11 * self.patient_global() + 

212 0.07 * self.peripheral_pain() + 

213 0.58 * math.log(crp + 1) 

214 ) 

215 

216 def asdas_esr(self) -> Optional[float]: 

217 esr = getattr(self, self.ESR_FIELD_NAME) 

218 if esr is None: 

219 return None 

220 

221 return ( 

222 0.08 * self.back_pain() + 

223 0.07 * self.morning_stiffness() + 

224 0.11 * self.patient_global() + 

225 0.09 * self.peripheral_pain() + 

226 0.29 * math.sqrt(esr) 

227 ) 

228 

229 def activity_state(self, req: CamcopsRequest, measurement: Any) -> str: 

230 if measurement is None: 

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

232 

233 if measurement < self.INACTIVE_MODERATE_CUTOFF: 

234 return self.wxstring(req, "inactive") 

235 

236 if measurement < self.MODERATE_HIGH_CUTOFF: 

237 return self.wxstring(req, "moderate") 

238 

239 if measurement > self.HIGH_VERY_HIGH_CUTOFF: 

240 return self.wxstring(req, "very_high") 

241 

242 return self.wxstring(req, "high") 

243 

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

245 rows = "" 

246 for q_num in range(1, self.N_QUESTIONS + 1): 

247 q_field = "q" + str(q_num) 

248 qtext = self.wxstring(req, q_field) 

249 if q_num <= 4: # not for ESR, CRP 

250 min_text = self.wxstring(req, q_field + "_min") 

251 max_text = self.wxstring(req, q_field + "_max") 

252 qtext += f" <i>(0 = {min_text}, 10 = {max_text})</i>" 

253 question_cell = f"{q_num}. {qtext}" 

254 score = getattr(self, q_field) 

255 

256 rows += tr_qa(question_cell, score) 

257 

258 asdas_crp = ws.number_to_dp(self.asdas_crp(), 2, default="?") 

259 asdas_esr = ws.number_to_dp(self.asdas_esr(), 2, default="?") 

260 

261 html = """ 

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

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

264 {tr_is_complete} 

265 {asdas_crp} 

266 {asdas_esr} 

267 </table> 

268 </div> 

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

270 <tr> 

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

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

273 </tr> 

274 {rows} 

275 </table> 

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

277 [1] &lt;1.3 Inactive disease, 

278 &lt;2.1 Moderate disease activity, 

279 2.1-3.5 High disease activity, 

280 &gt;3.5 Very high disease activity.<br> 

281 [2] 0.12 × back pain + 

282 0.06 × duration of morning stiffness + 

283 0.11 × patient global + 

284 0.07 × peripheral pain + 

285 0.58 × ln(CRP + 1). 

286 CRP units: mg/L. When CRP&lt;2mg/L, use 2mg/L to calculate 

287 ASDAS-CRP.<br> 

288 [3] 0.08 x back pain + 

289 0.07 x duration of morning stiffness + 

290 0.11 x patient global + 

291 0.09 x peripheral pain + 

292 0.29 x √(ESR). 

293 ESR units: mm/h. 

294 </div> 

295 """.format( 

296 CssClass=CssClass, 

297 tr_is_complete=self.get_is_complete_tr(req), 

298 asdas_crp=tr( 

299 self.wxstring(req, "asdas_crp") + " <sup>[1][2]</sup>", 

300 "{} ({})".format( 

301 answer(asdas_crp), 

302 self.activity_state(req, self.asdas_crp()) 

303 ) 

304 ), 

305 asdas_esr=tr( 

306 self.wxstring(req, "asdas_esr") + " <sup>[1][3]</sup>", 

307 "{} ({})".format( 

308 answer(asdas_esr), 

309 self.activity_state(req, self.asdas_esr()) 

310 ) 

311 ), 

312 rows=rows, 

313 ) 

314 return html