Coverage for tasks/asdas.py: 46%

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

29 

30""" 

31 

32import math 

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

34 

35from camcops_server.cc_modules.cc_constants import CssClass 

36from camcops_server.cc_modules.cc_db import add_multiple_columns 

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

38from camcops_server.cc_modules.cc_request import CamcopsRequest 

39from camcops_server.cc_modules.cc_sqla_coltypes import SummaryCategoryColType 

40from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

41from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task 

42from camcops_server.cc_modules.cc_trackerhelpers import ( 

43 TrackerAxisTick, 

44 TrackerInfo, 

45 TrackerLabel, 

46) 

47 

48import cardinal_pythonlib.rnc_web as ws 

49from cardinal_pythonlib.stringfunc import strseq 

50from sqlalchemy import Column, Float 

51from sqlalchemy.ext.declarative import DeclarativeMeta 

52 

53 

54class AsdasMetaclass(DeclarativeMeta): 

55 # noinspection PyInitNewSignature 

56 def __init__( 

57 cls: Type["Asdas"], 

58 name: str, 

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

60 classdict: Dict[str, Any], 

61 ) -> None: 

62 

63 add_multiple_columns( 

64 cls, 

65 "q", 

66 1, 

67 cls.N_SCALE_QUESTIONS, 

68 minimum=0, 

69 maximum=10, 

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

71 comment_strings=[ 

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

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

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

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

76 ], 

77 ) 

78 

79 setattr( 

80 cls, 

81 cls.CRP_FIELD_NAME, 

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

83 ) 

84 

85 setattr( 

86 cls, 

87 cls.ESR_FIELD_NAME, 

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

89 ) 

90 

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

92 

93 

94class Asdas(TaskHasPatientMixin, Task, metaclass=AsdasMetaclass): 

95 __tablename__ = "asdas" 

96 shortname = "ASDAS" 

97 provides_trackers = True 

98 

99 N_SCALE_QUESTIONS = 4 

100 MAX_SCORE_SCALE = 10 

101 N_QUESTIONS = 6 

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

103 CRP_FIELD_NAME = "q5" 

104 ESR_FIELD_NAME = "q6" 

105 

106 INACTIVE_MODERATE_CUTOFF = 1.3 

107 MODERATE_HIGH_CUTOFF = 2.1 

108 HIGH_VERY_HIGH_CUTOFF = 3.5 

109 

110 @staticmethod 

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

112 _ = req.gettext 

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

114 

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

116 return self.standard_task_summary_fields() + [ 

117 SummaryElement( 

118 name="asdas_crp", 

119 coltype=Float(), 

120 value=self.asdas_crp(), 

121 comment="ASDAS-CRP", 

122 ), 

123 SummaryElement( 

124 name="activity_state_crp", 

125 coltype=SummaryCategoryColType, 

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

127 comment="Activity state (CRP)", 

128 ), 

129 SummaryElement( 

130 name="asdas_esr", 

131 coltype=Float(), 

132 value=self.asdas_esr(), 

133 comment="ASDAS-ESR", 

134 ), 

135 SummaryElement( 

136 name="activity_state_esr", 

137 coltype=SummaryCategoryColType, 

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

139 comment="Activity state (ESR)", 

140 ), 

141 ] 

142 

143 def is_complete(self) -> bool: 

144 if self.any_fields_none(self.SCALE_FIELD_NAMES): 

145 return False 

146 

147 crp = getattr(self, self.CRP_FIELD_NAME) 

148 esr = getattr(self, self.ESR_FIELD_NAME) 

149 

150 if crp is None and esr is None: 

151 return False 

152 

153 if not self.field_contents_valid(): 

154 return False 

155 

156 return True 

157 

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

159 axis_min = -0.5 

160 axis_max = 7.5 

161 axis_ticks = [ 

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

163 ] 

164 

165 horizontal_lines = [ 

166 self.HIGH_VERY_HIGH_CUTOFF, 

167 self.MODERATE_HIGH_CUTOFF, 

168 self.INACTIVE_MODERATE_CUTOFF, 

169 0, 

170 ] 

171 

172 horizontal_labels = [ 

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

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

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

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

177 ] 

178 

179 return [ 

180 TrackerInfo( 

181 value=self.asdas_crp(), 

182 plot_label="ASDAS-CRP", 

183 axis_label="ASDAS-CRP", 

184 axis_min=axis_min, 

185 axis_max=axis_max, 

186 axis_ticks=axis_ticks, 

187 horizontal_lines=horizontal_lines, 

188 horizontal_labels=horizontal_labels, 

189 ), 

190 TrackerInfo( 

191 value=self.asdas_esr(), 

192 plot_label="ASDAS-ESR", 

193 axis_label="ASDAS-ESR", 

194 axis_min=axis_min, 

195 axis_max=axis_max, 

196 axis_ticks=axis_ticks, 

197 horizontal_lines=horizontal_lines, 

198 horizontal_labels=horizontal_labels, 

199 ), 

200 ] 

201 

202 def back_pain(self) -> float: 

203 return getattr(self, "q1") 

204 

205 def morning_stiffness(self) -> float: 

206 return getattr(self, "q2") 

207 

208 def patient_global(self) -> float: 

209 return getattr(self, "q3") 

210 

211 def peripheral_pain(self) -> float: 

212 return getattr(self, "q4") 

213 

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

215 crp = getattr(self, self.CRP_FIELD_NAME) 

216 

217 if crp is None: 

218 return None 

219 

220 crp = max(crp, 2.0) 

221 

222 return ( 

223 0.12 * self.back_pain() 

224 + 0.06 * self.morning_stiffness() 

225 + 0.11 * self.patient_global() 

226 + 0.07 * self.peripheral_pain() 

227 + 0.58 * math.log(crp + 1) 

228 ) 

229 

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

231 esr = getattr(self, self.ESR_FIELD_NAME) 

232 if esr is None: 

233 return None 

234 

235 return ( 

236 0.08 * self.back_pain() 

237 + 0.07 * self.morning_stiffness() 

238 + 0.11 * self.patient_global() 

239 + 0.09 * self.peripheral_pain() 

240 + 0.29 * math.sqrt(esr) 

241 ) 

242 

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

244 if measurement is None: 

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

246 

247 if measurement < self.INACTIVE_MODERATE_CUTOFF: 

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

249 

250 if measurement < self.MODERATE_HIGH_CUTOFF: 

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

252 

253 if measurement > self.HIGH_VERY_HIGH_CUTOFF: 

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

255 

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

257 

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

259 rows = "" 

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

261 q_field = "q" + str(q_num) 

262 qtext = self.wxstring(req, q_field) 

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

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

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

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

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

268 score = getattr(self, q_field) 

269 

270 rows += tr_qa(question_cell, score) 

271 

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

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

274 

275 html = """ 

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

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

278 {tr_is_complete} 

279 {asdas_crp} 

280 {asdas_esr} 

281 </table> 

282 </div> 

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

284 <tr> 

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

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

287 </tr> 

288 {rows} 

289 </table> 

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

291 [1] &lt;1.3 Inactive disease, 

292 &lt;2.1 Moderate disease activity, 

293 2.1-3.5 High disease activity, 

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

295 [2] 0.12 × back pain + 

296 0.06 × duration of morning stiffness + 

297 0.11 × patient global + 

298 0.07 × peripheral pain + 

299 0.58 × ln(CRP + 1). 

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

301 ASDAS-CRP.<br> 

302 [3] 0.08 x back pain + 

303 0.07 x duration of morning stiffness + 

304 0.11 x patient global + 

305 0.09 x peripheral pain + 

306 0.29 x √(ESR). 

307 ESR units: mm/h. 

308 </div> 

309 """.format( 

310 CssClass=CssClass, 

311 tr_is_complete=self.get_is_complete_tr(req), 

312 asdas_crp=tr( 

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

314 "{} ({})".format( 

315 answer(asdas_crp), 

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

317 ), 

318 ), 

319 asdas_esr=tr( 

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

321 "{} ({})".format( 

322 answer(asdas_esr), 

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

324 ), 

325 ), 

326 rows=rows, 

327 ) 

328 return html