"""
camcops_server/tasks/asdas.py
===============================================================================
Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
This file is part of CamCOPS.
CamCOPS is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CamCOPS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
===============================================================================
**Ankylosing Spondylitis Disease Activity Score (ASDAS) task.**
"""
import math
from typing import Any, Dict, List, Optional, Type, Tuple
from camcops_server.cc_modules.cc_constants import CssClass
from camcops_server.cc_modules.cc_db import add_multiple_columns
from camcops_server.cc_modules.cc_html import tr_qa, tr, answer
from camcops_server.cc_modules.cc_request import CamcopsRequest
from camcops_server.cc_modules.cc_sqla_coltypes import SummaryCategoryColType
from camcops_server.cc_modules.cc_summaryelement import SummaryElement
from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task
from camcops_server.cc_modules.cc_trackerhelpers import (
TrackerAxisTick,
TrackerInfo,
TrackerLabel,
)
import cardinal_pythonlib.rnc_web as ws
from cardinal_pythonlib.stringfunc import strseq
from sqlalchemy import Column, Float
from sqlalchemy.ext.declarative import DeclarativeMeta
class AsdasMetaclass(DeclarativeMeta):
# noinspection PyInitNewSignature
def __init__(
cls: Type["Asdas"],
name: str,
bases: Tuple[Type, ...],
classdict: Dict[str, Any],
) -> None:
add_multiple_columns(
cls,
"q",
1,
cls.N_SCALE_QUESTIONS,
minimum=0,
maximum=10,
comment_fmt="Q{n} - {s}",
comment_strings=[
"back pain 0-10 (None - very severe)",
"morning stiffness 0-10 (None - 2+ hours)",
"patient global 0-10 (Not active - very active)",
"peripheral pain 0-10 (None - very severe)",
],
)
setattr(
cls,
cls.CRP_FIELD_NAME,
Column(cls.CRP_FIELD_NAME, Float, comment="CRP (mg/L)"),
)
setattr(
cls,
cls.ESR_FIELD_NAME,
Column(cls.ESR_FIELD_NAME, Float, comment="ESR (mm/h)"),
)
super().__init__(name, bases, classdict)
[docs]class Asdas(TaskHasPatientMixin, Task, metaclass=AsdasMetaclass):
__tablename__ = "asdas"
shortname = "ASDAS"
provides_trackers = True
N_SCALE_QUESTIONS = 4
MAX_SCORE_SCALE = 10
N_QUESTIONS = 6
SCALE_FIELD_NAMES = strseq("q", 1, N_SCALE_QUESTIONS)
CRP_FIELD_NAME = "q5"
ESR_FIELD_NAME = "q6"
INACTIVE_MODERATE_CUTOFF = 1.3
MODERATE_HIGH_CUTOFF = 2.1
HIGH_VERY_HIGH_CUTOFF = 3.5
[docs] @staticmethod
def longname(req: "CamcopsRequest") -> str:
_ = req.gettext
return _("Ankylosing Spondylitis Disease Activity Score")
[docs] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
return self.standard_task_summary_fields() + [
SummaryElement(
name="asdas_crp",
coltype=Float(),
value=self.asdas_crp(),
comment="ASDAS-CRP",
),
SummaryElement(
name="activity_state_crp",
coltype=SummaryCategoryColType,
value=self.activity_state(req, self.asdas_crp()),
comment="Activity state (CRP)",
),
SummaryElement(
name="asdas_esr",
coltype=Float(),
value=self.asdas_esr(),
comment="ASDAS-ESR",
),
SummaryElement(
name="activity_state_esr",
coltype=SummaryCategoryColType,
value=self.activity_state(req, self.asdas_esr()),
comment="Activity state (ESR)",
),
]
[docs] def is_complete(self) -> bool:
if self.any_fields_none(self.SCALE_FIELD_NAMES):
return False
crp = getattr(self, self.CRP_FIELD_NAME)
esr = getattr(self, self.ESR_FIELD_NAME)
if crp is None and esr is None:
return False
if not self.field_contents_valid():
return False
return True
[docs] def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
axis_min = -0.5
axis_max = 7.5
axis_ticks = [
TrackerAxisTick(n, str(n)) for n in range(0, int(axis_max) + 1)
]
horizontal_lines = [
self.HIGH_VERY_HIGH_CUTOFF,
self.MODERATE_HIGH_CUTOFF,
self.INACTIVE_MODERATE_CUTOFF,
0,
]
horizontal_labels = [
TrackerLabel(5.25, self.wxstring(req, "very_high")),
TrackerLabel(2.8, self.wxstring(req, "high")),
TrackerLabel(1.7, self.wxstring(req, "moderate")),
TrackerLabel(0.65, self.wxstring(req, "inactive")),
]
return [
TrackerInfo(
value=self.asdas_crp(),
plot_label="ASDAS-CRP",
axis_label="ASDAS-CRP",
axis_min=axis_min,
axis_max=axis_max,
axis_ticks=axis_ticks,
horizontal_lines=horizontal_lines,
horizontal_labels=horizontal_labels,
),
TrackerInfo(
value=self.asdas_esr(),
plot_label="ASDAS-ESR",
axis_label="ASDAS-ESR",
axis_min=axis_min,
axis_max=axis_max,
axis_ticks=axis_ticks,
horizontal_lines=horizontal_lines,
horizontal_labels=horizontal_labels,
),
]
def back_pain(self) -> float:
return getattr(self, "q1")
def morning_stiffness(self) -> float:
return getattr(self, "q2")
def patient_global(self) -> float:
return getattr(self, "q3")
def peripheral_pain(self) -> float:
return getattr(self, "q4")
def asdas_crp(self) -> Optional[float]:
crp = getattr(self, self.CRP_FIELD_NAME)
if crp is None:
return None
crp = max(crp, 2.0)
return (
0.12 * self.back_pain()
+ 0.06 * self.morning_stiffness()
+ 0.11 * self.patient_global()
+ 0.07 * self.peripheral_pain()
+ 0.58 * math.log(crp + 1)
)
def asdas_esr(self) -> Optional[float]:
esr = getattr(self, self.ESR_FIELD_NAME)
if esr is None:
return None
return (
0.08 * self.back_pain()
+ 0.07 * self.morning_stiffness()
+ 0.11 * self.patient_global()
+ 0.09 * self.peripheral_pain()
+ 0.29 * math.sqrt(esr)
)
def activity_state(self, req: CamcopsRequest, measurement: Any) -> str:
if measurement is None:
return self.wxstring(req, "n_a")
if measurement < self.INACTIVE_MODERATE_CUTOFF:
return self.wxstring(req, "inactive")
if measurement < self.MODERATE_HIGH_CUTOFF:
return self.wxstring(req, "moderate")
if measurement > self.HIGH_VERY_HIGH_CUTOFF:
return self.wxstring(req, "very_high")
return self.wxstring(req, "high")
[docs] def get_task_html(self, req: CamcopsRequest) -> str:
rows = ""
for q_num in range(1, self.N_QUESTIONS + 1):
q_field = "q" + str(q_num)
qtext = self.wxstring(req, q_field)
if q_num <= 4: # not for ESR, CRP
min_text = self.wxstring(req, q_field + "_min")
max_text = self.wxstring(req, q_field + "_max")
qtext += f" <i>(0 = {min_text}, 10 = {max_text})</i>"
question_cell = f"{q_num}. {qtext}"
score = getattr(self, q_field)
rows += tr_qa(question_cell, score)
asdas_crp = ws.number_to_dp(self.asdas_crp(), 2, default="?")
asdas_esr = ws.number_to_dp(self.asdas_esr(), 2, default="?")
html = """
<div class="{CssClass.SUMMARY}">
<table class="{CssClass.SUMMARY}">
{tr_is_complete}
{asdas_crp}
{asdas_esr}
</table>
</div>
<table class="{CssClass.TASKDETAIL}">
<tr>
<th width="60%">Question</th>
<th width="40%">Answer</th>
</tr>
{rows}
</table>
<div class="{CssClass.FOOTNOTES}">
[1] <1.3 Inactive disease,
<2.1 Moderate disease activity,
2.1-3.5 High disease activity,
>3.5 Very high disease activity.<br>
[2] 0.12 × back pain +
0.06 × duration of morning stiffness +
0.11 × patient global +
0.07 × peripheral pain +
0.58 × ln(CRP + 1).
CRP units: mg/L. When CRP<2mg/L, use 2mg/L to calculate
ASDAS-CRP.<br>
[3] 0.08 x back pain +
0.07 x duration of morning stiffness +
0.11 x patient global +
0.09 x peripheral pain +
0.29 x √(ESR).
ESR units: mm/h.
</div>
""".format(
CssClass=CssClass,
tr_is_complete=self.get_is_complete_tr(req),
asdas_crp=tr(
self.wxstring(req, "asdas_crp") + " <sup>[1][2]</sup>",
"{} ({})".format(
answer(asdas_crp),
self.activity_state(req, self.asdas_crp()),
),
),
asdas_esr=tr(
self.wxstring(req, "asdas_esr") + " <sup>[1][3]</sup>",
"{} ({})".format(
answer(asdas_esr),
self.activity_state(req, self.asdas_esr()),
),
),
rows=rows,
)
return html