Coverage for tasks/asdas.py: 46%
102 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/tasks/asdas.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
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.
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.
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/>.
26===============================================================================
28**Ankylosing Spondylitis Disease Activity Score (ASDAS) task.**
30"""
32import math
33from typing import Any, Dict, List, Optional, Type, Tuple
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)
48import cardinal_pythonlib.rnc_web as ws
49from cardinal_pythonlib.stringfunc import strseq
50from sqlalchemy import Column, Float
51from sqlalchemy.ext.declarative import DeclarativeMeta
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:
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 )
79 setattr(
80 cls,
81 cls.CRP_FIELD_NAME,
82 Column(cls.CRP_FIELD_NAME, Float, comment="CRP (mg/L)"),
83 )
85 setattr(
86 cls,
87 cls.ESR_FIELD_NAME,
88 Column(cls.ESR_FIELD_NAME, Float, comment="ESR (mm/h)"),
89 )
91 super().__init__(name, bases, classdict)
94class Asdas(TaskHasPatientMixin, Task, metaclass=AsdasMetaclass):
95 __tablename__ = "asdas"
96 shortname = "ASDAS"
97 provides_trackers = True
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"
106 INACTIVE_MODERATE_CUTOFF = 1.3
107 MODERATE_HIGH_CUTOFF = 2.1
108 HIGH_VERY_HIGH_CUTOFF = 3.5
110 @staticmethod
111 def longname(req: "CamcopsRequest") -> str:
112 _ = req.gettext
113 return _("Ankylosing Spondylitis Disease Activity Score")
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 ]
143 def is_complete(self) -> bool:
144 if self.any_fields_none(self.SCALE_FIELD_NAMES):
145 return False
147 crp = getattr(self, self.CRP_FIELD_NAME)
148 esr = getattr(self, self.ESR_FIELD_NAME)
150 if crp is None and esr is None:
151 return False
153 if not self.field_contents_valid():
154 return False
156 return True
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 ]
165 horizontal_lines = [
166 self.HIGH_VERY_HIGH_CUTOFF,
167 self.MODERATE_HIGH_CUTOFF,
168 self.INACTIVE_MODERATE_CUTOFF,
169 0,
170 ]
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 ]
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 ]
202 def back_pain(self) -> float:
203 return getattr(self, "q1")
205 def morning_stiffness(self) -> float:
206 return getattr(self, "q2")
208 def patient_global(self) -> float:
209 return getattr(self, "q3")
211 def peripheral_pain(self) -> float:
212 return getattr(self, "q4")
214 def asdas_crp(self) -> Optional[float]:
215 crp = getattr(self, self.CRP_FIELD_NAME)
217 if crp is None:
218 return None
220 crp = max(crp, 2.0)
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 )
230 def asdas_esr(self) -> Optional[float]:
231 esr = getattr(self, self.ESR_FIELD_NAME)
232 if esr is None:
233 return None
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 )
243 def activity_state(self, req: CamcopsRequest, measurement: Any) -> str:
244 if measurement is None:
245 return self.wxstring(req, "n_a")
247 if measurement < self.INACTIVE_MODERATE_CUTOFF:
248 return self.wxstring(req, "inactive")
250 if measurement < self.MODERATE_HIGH_CUTOFF:
251 return self.wxstring(req, "moderate")
253 if measurement > self.HIGH_VERY_HIGH_CUTOFF:
254 return self.wxstring(req, "very_high")
256 return self.wxstring(req, "high")
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)
270 rows += tr_qa(question_cell, score)
272 asdas_crp = ws.number_to_dp(self.asdas_crp(), 2, default="?")
273 asdas_esr = ws.number_to_dp(self.asdas_esr(), 2, default="?")
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] <1.3 Inactive disease,
292 <2.1 Moderate disease activity,
293 2.1-3.5 High disease activity,
294 >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<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