Coverage for tasks/asdas.py : 46%

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
3"""
4camcops_server/tasks/asdas.py
6===============================================================================
8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
10 This file is part of CamCOPS.
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.
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.
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/>.
25===============================================================================
27**Ankylosing Spondylitis Disease Activity Score (ASDAS) task.**
29"""
31import math
32from typing import Any, Dict, List, Optional, Type, Tuple
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)
49import cardinal_pythonlib.rnc_web as ws
50from cardinal_pythonlib.stringfunc import strseq
51from sqlalchemy import Column, Float
52from sqlalchemy.ext.declarative import DeclarativeMeta
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:
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 )
74 setattr(
75 cls, cls.CRP_FIELD_NAME,
76 Column(cls.CRP_FIELD_NAME, Float, comment="CRP (mg/L)")
77 )
79 setattr(
80 cls, cls.ESR_FIELD_NAME,
81 Column(cls.ESR_FIELD_NAME, Float, comment="ESR (mm/h)")
82 )
84 super().__init__(name, bases, classdict)
87class Asdas(TaskHasPatientMixin,
88 Task,
89 metaclass=AsdasMetaclass):
90 __tablename__ = "asdas"
91 shortname = "ASDAS"
92 provides_trackers = True
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"
101 INACTIVE_MODERATE_CUTOFF = 1.3
102 MODERATE_HIGH_CUTOFF = 2.1
103 HIGH_VERY_HIGH_CUTOFF = 3.5
105 @staticmethod
106 def longname(req: "CamcopsRequest") -> str:
107 _ = req.gettext
108 return _("Ankylosing Spondylitis Disease Activity Score")
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 ]
130 def is_complete(self) -> bool:
131 if self.any_fields_none(self.SCALE_FIELD_NAMES):
132 return False
134 crp = getattr(self, self.CRP_FIELD_NAME)
135 esr = getattr(self, self.ESR_FIELD_NAME)
137 if crp is None and esr is None:
138 return False
140 if not self.field_contents_valid():
141 return False
143 return True
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)]
151 horizontal_lines = [
152 self.HIGH_VERY_HIGH_CUTOFF,
153 self.MODERATE_HIGH_CUTOFF,
154 self.INACTIVE_MODERATE_CUTOFF,
155 0,
156 ]
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 ]
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 ]
188 def back_pain(self) -> float:
189 return getattr(self, "q1")
191 def morning_stiffness(self) -> float:
192 return getattr(self, "q2")
194 def patient_global(self) -> float:
195 return getattr(self, "q3")
197 def peripheral_pain(self) -> float:
198 return getattr(self, "q4")
200 def asdas_crp(self) -> Optional[float]:
201 crp = getattr(self, self.CRP_FIELD_NAME)
203 if crp is None:
204 return None
206 crp = max(crp, 2.0)
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 )
216 def asdas_esr(self) -> Optional[float]:
217 esr = getattr(self, self.ESR_FIELD_NAME)
218 if esr is None:
219 return None
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 )
229 def activity_state(self, req: CamcopsRequest, measurement: Any) -> str:
230 if measurement is None:
231 return self.wxstring(req, "n_a")
233 if measurement < self.INACTIVE_MODERATE_CUTOFF:
234 return self.wxstring(req, "inactive")
236 if measurement < self.MODERATE_HIGH_CUTOFF:
237 return self.wxstring(req, "moderate")
239 if measurement > self.HIGH_VERY_HIGH_CUTOFF:
240 return self.wxstring(req, "very_high")
242 return self.wxstring(req, "high")
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)
256 rows += tr_qa(question_cell, score)
258 asdas_crp = ws.number_to_dp(self.asdas_crp(), 2, default="?")
259 asdas_esr = ws.number_to_dp(self.asdas_esr(), 2, default="?")
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] <1.3 Inactive disease,
278 <2.1 Moderate disease activity,
279 2.1-3.5 High disease activity,
280 >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<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