Coverage for tasks/asdas.py: 44%

1"""

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

27

28"""

29

30 import math

31 from typing import Any, List, Optional, Type

32

33 from camcops_server.cc_modules.cc_constants import CssClass

34 from camcops_server.cc_modules.cc_db import add_multiple_columns

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

36 from camcops_server.cc_modules.cc_request import CamcopsRequest

37 from camcops_server.cc_modules.cc_sqla_coltypes import SummaryCategoryColType

38 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

39 from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task

40 from camcops_server.cc_modules.cc_trackerhelpers import (

41 TrackerAxisTick,

42 TrackerInfo,

43 TrackerLabel,

44)

45

46 import cardinal_pythonlib.rnc_web as ws

47 from cardinal_pythonlib.stringfunc import strseq

48 from sqlalchemy import Column, Float

49

50

51 class Asdas( # type: ignore[misc]

52 TaskHasPatientMixin,

53 Task,

54):

55 __tablename__ = "asdas"

56 shortname = "ASDAS"

57 provides_trackers = True

58

59 N_SCALE_QUESTIONS = 4

60 MAX_SCORE_SCALE = 10

61 N_QUESTIONS = 6

62

63 @classmethod

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

65

66 add_multiple_columns(

67 cls,

68 "q",

69 1,

70 cls.N_SCALE_QUESTIONS,

71 minimum=0,

72 maximum=10,

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

74 comment_strings=[

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

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

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

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

79 ],

80 )

81

82 setattr(

83 cls,

84 cls.CRP_FIELD_NAME,

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

86 )

87

88 setattr(

89 cls,

90 cls.ESR_FIELD_NAME,

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

92 )

93

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

95 CRP_FIELD_NAME = "q5"

96 ESR_FIELD_NAME = "q6"

97

98 INACTIVE_MODERATE_CUTOFF = 1.3

99 MODERATE_HIGH_CUTOFF = 2.1

100 HIGH_VERY_HIGH_CUTOFF = 3.5

101

102 @staticmethod

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

104 _ = req.gettext

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

106

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

108 return self.standard_task_summary_fields() + [

109 SummaryElement(

110 name="asdas_crp",

111 coltype=Float(),

112 value=self.asdas_crp(),

113 comment="ASDAS-CRP",

114 ),

115 SummaryElement(

116 name="activity_state_crp",

117 coltype=SummaryCategoryColType,

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

119 comment="Activity state (CRP)",

120 ),

121 SummaryElement(

122 name="asdas_esr",

123 coltype=Float(),

124 value=self.asdas_esr(),

125 comment="ASDAS-ESR",

126 ),

127 SummaryElement(

128 name="activity_state_esr",

129 coltype=SummaryCategoryColType,

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

131 comment="Activity state (ESR)",

132 ),

133 ]

134

135 def is_complete(self) -> bool:

136 if self.any_fields_none(self.SCALE_FIELD_NAMES):

137 return False

138

139 crp = getattr(self, self.CRP_FIELD_NAME)

140 esr = getattr(self, self.ESR_FIELD_NAME)

141

142 if crp is None and esr is None:

143 return False

144

145 if not self.field_contents_valid():

146 return False

147

148 return True

149

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

151 axis_min = -0.5

152 axis_max = 7.5

153 axis_ticks = [

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

155 ]

156

157 horizontal_lines = [

158 self.HIGH_VERY_HIGH_CUTOFF,

159 self.MODERATE_HIGH_CUTOFF,

160 self.INACTIVE_MODERATE_CUTOFF,

161 0,

162 ]

163

164 horizontal_labels = [

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

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

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

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

169 ]

170

171 return [

172 TrackerInfo(

173 value=self.asdas_crp(),

174 plot_label="ASDAS-CRP",

175 axis_label="ASDAS-CRP",

176 axis_min=axis_min,

177 axis_max=axis_max,

178 axis_ticks=axis_ticks,

179 horizontal_lines=horizontal_lines,

180 horizontal_labels=horizontal_labels,

181 ),

182 TrackerInfo(

183 value=self.asdas_esr(),

184 plot_label="ASDAS-ESR",

185 axis_label="ASDAS-ESR",

186 axis_min=axis_min,

187 axis_max=axis_max,

188 axis_ticks=axis_ticks,

189 horizontal_lines=horizontal_lines,

190 horizontal_labels=horizontal_labels,

191 ),

192 ]

193

194 def back_pain(self) -> float:

195 return getattr(self, "q1")

196

197 def morning_stiffness(self) -> float:

198 return getattr(self, "q2")

199

200 def patient_global(self) -> float:

201 return getattr(self, "q3")

202

203 def peripheral_pain(self) -> float:

204 return getattr(self, "q4")

205

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

207 crp = getattr(self, self.CRP_FIELD_NAME)

208

209 if crp is None:

210 return None

211

212 crp = max(crp, 2.0)

213

214 return (

215 0.12 * self.back_pain()

216 + 0.06 * self.morning_stiffness()

217 + 0.11 * self.patient_global()

218 + 0.07 * self.peripheral_pain()

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

220 )

221

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

223 esr = getattr(self, self.ESR_FIELD_NAME)

224 if esr is None:

225 return None

226

227 return (

228 0.08 * self.back_pain()

229 + 0.07 * self.morning_stiffness()

230 + 0.11 * self.patient_global()

231 + 0.09 * self.peripheral_pain()

232 + 0.29 * math.sqrt(esr)

233 )

234

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

236 if measurement is None:

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

238

239 if measurement < self.INACTIVE_MODERATE_CUTOFF:

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

241

242 if measurement < self.MODERATE_HIGH_CUTOFF:

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

244

245 if measurement > self.HIGH_VERY_HIGH_CUTOFF:

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

247

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

249

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

251 rows = ""

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

253 q_field = "q" + str(q_num)

254 qtext = self.wxstring(req, q_field)

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

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

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

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

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

260 score = getattr(self, q_field)

261

262 rows += tr_qa(question_cell, score)

263

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

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

266

267 html = """

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

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

270 {tr_is_complete}

271 {asdas_crp}

272 {asdas_esr}

273 </table>

274 </div>

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

276 <tr>

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

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

279 </tr>

280 {rows}

281 </table>

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

283 [1] &lt;1.3 Inactive disease,

284 &lt;2.1 Moderate disease activity,

285 2.1-3.5 High disease activity,

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

287 [2] 0.12 × back pain +

288 0.06 × duration of morning stiffness +

289 0.11 × patient global +

290 0.07 × peripheral pain +

291 0.58 × ln(CRP + 1).

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

293 ASDAS-CRP.<br>

294 [3] 0.08 x back pain +

295 0.07 x duration of morning stiffness +

296 0.11 x patient global +

297 0.09 x peripheral pain +

298 0.29 x √(ESR).

299 ESR units: mm/h.

300 </div>

301 """.format(

302 CssClass=CssClass,

303 tr_is_complete=self.get_is_complete_tr(req),

304 asdas_crp=tr(

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

306 "{} ({})".format(

307 answer(asdas_crp),

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

309 ),

310 ),

311 asdas_esr=tr(

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

313 "{} ({})".format(

314 answer(asdas_esr),

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

316 ),

317 ),

318 rows=rows,

319 )

320 return html