Coverage for tasks/asdas.py: 44%
99 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/tasks/asdas.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
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.
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.
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/>.
24===============================================================================
26**Ankylosing Spondylitis Disease Activity Score (ASDAS) task.**
28"""
30import math
31from typing import Any, List, Optional, Type
33from camcops_server.cc_modules.cc_constants import CssClass
34from camcops_server.cc_modules.cc_db import add_multiple_columns
35from camcops_server.cc_modules.cc_html import tr_qa, tr, answer
36from camcops_server.cc_modules.cc_request import CamcopsRequest
37from camcops_server.cc_modules.cc_sqla_coltypes import SummaryCategoryColType
38from camcops_server.cc_modules.cc_summaryelement import SummaryElement
39from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task
40from camcops_server.cc_modules.cc_trackerhelpers import (
41 TrackerAxisTick,
42 TrackerInfo,
43 TrackerLabel,
44)
46import cardinal_pythonlib.rnc_web as ws
47from cardinal_pythonlib.stringfunc import strseq
48from sqlalchemy import Column, Float
51class Asdas( # type: ignore[misc]
52 TaskHasPatientMixin,
53 Task,
54):
55 __tablename__ = "asdas"
56 shortname = "ASDAS"
57 provides_trackers = True
59 N_SCALE_QUESTIONS = 4
60 MAX_SCORE_SCALE = 10
61 N_QUESTIONS = 6
63 @classmethod
64 def extend_columns(cls: Type["Asdas"], **kwargs: Any) -> None:
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 )
82 setattr(
83 cls,
84 cls.CRP_FIELD_NAME,
85 Column(cls.CRP_FIELD_NAME, Float, comment="CRP (mg/L)"),
86 )
88 setattr(
89 cls,
90 cls.ESR_FIELD_NAME,
91 Column(cls.ESR_FIELD_NAME, Float, comment="ESR (mm/h)"),
92 )
94 SCALE_FIELD_NAMES = strseq("q", 1, N_SCALE_QUESTIONS)
95 CRP_FIELD_NAME = "q5"
96 ESR_FIELD_NAME = "q6"
98 INACTIVE_MODERATE_CUTOFF = 1.3
99 MODERATE_HIGH_CUTOFF = 2.1
100 HIGH_VERY_HIGH_CUTOFF = 3.5
102 @staticmethod
103 def longname(req: "CamcopsRequest") -> str:
104 _ = req.gettext
105 return _("Ankylosing Spondylitis Disease Activity Score")
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 ]
135 def is_complete(self) -> bool:
136 if self.any_fields_none(self.SCALE_FIELD_NAMES):
137 return False
139 crp = getattr(self, self.CRP_FIELD_NAME)
140 esr = getattr(self, self.ESR_FIELD_NAME)
142 if crp is None and esr is None:
143 return False
145 if not self.field_contents_valid():
146 return False
148 return True
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 ]
157 horizontal_lines = [
158 self.HIGH_VERY_HIGH_CUTOFF,
159 self.MODERATE_HIGH_CUTOFF,
160 self.INACTIVE_MODERATE_CUTOFF,
161 0,
162 ]
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 ]
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 ]
194 def back_pain(self) -> float:
195 return getattr(self, "q1")
197 def morning_stiffness(self) -> float:
198 return getattr(self, "q2")
200 def patient_global(self) -> float:
201 return getattr(self, "q3")
203 def peripheral_pain(self) -> float:
204 return getattr(self, "q4")
206 def asdas_crp(self) -> Optional[float]:
207 crp = getattr(self, self.CRP_FIELD_NAME)
209 if crp is None:
210 return None
212 crp = max(crp, 2.0)
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 )
222 def asdas_esr(self) -> Optional[float]:
223 esr = getattr(self, self.ESR_FIELD_NAME)
224 if esr is None:
225 return None
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 )
235 def activity_state(self, req: CamcopsRequest, measurement: Any) -> str:
236 if measurement is None:
237 return self.wxstring(req, "n_a")
239 if measurement < self.INACTIVE_MODERATE_CUTOFF:
240 return self.wxstring(req, "inactive")
242 if measurement < self.MODERATE_HIGH_CUTOFF:
243 return self.wxstring(req, "moderate")
245 if measurement > self.HIGH_VERY_HIGH_CUTOFF:
246 return self.wxstring(req, "very_high")
248 return self.wxstring(req, "high")
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)
262 rows += tr_qa(question_cell, score)
264 asdas_crp = ws.number_to_dp(self.asdas_crp(), 2, default="?")
265 asdas_esr = ws.number_to_dp(self.asdas_esr(), 2, default="?")
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] <1.3 Inactive disease,
284 <2.1 Moderate disease activity,
285 2.1-3.5 High disease activity,
286 >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<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