Coverage for tasks/basdai.py: 47%
75 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/basdai.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**Bath Ankylosing Spondylitis Disease Activity Index (BASDAI) task.**
30"""
32import statistics
33from typing import Any, Dict, List, Optional, Type, Tuple
35import cardinal_pythonlib.rnc_web as ws
36from cardinal_pythonlib.stringfunc import strseq
37from sqlalchemy.ext.declarative import DeclarativeMeta
38from sqlalchemy.sql.sqltypes import Float
40from camcops_server.cc_modules.cc_constants import CssClass
41from camcops_server.cc_modules.cc_db import add_multiple_columns
42from camcops_server.cc_modules.cc_html import tr_qa, tr, answer
43from camcops_server.cc_modules.cc_request import CamcopsRequest
44from camcops_server.cc_modules.cc_summaryelement import SummaryElement
45from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task
46from camcops_server.cc_modules.cc_trackerhelpers import (
47 TrackerAxisTick,
48 TrackerInfo,
49 TrackerLabel,
50)
53# =============================================================================
54# BASDAI
55# =============================================================================
58class BasdaiMetaclass(DeclarativeMeta):
59 # noinspection PyInitNewSignature
60 def __init__(
61 cls: Type["Basdai"],
62 name: str,
63 bases: Tuple[Type, ...],
64 classdict: Dict[str, Any],
65 ) -> None:
67 add_multiple_columns(
68 cls,
69 "q",
70 1,
71 cls.N_QUESTIONS,
72 coltype=Float,
73 minimum=0,
74 maximum=10,
75 comment_fmt="Q{n} - {s}",
76 comment_strings=[
77 "fatigue/tiredness 0-10 (none - very severe)",
78 "AS neck, back, hip pain 0-10 (none - very severe)",
79 "other joint pain/swelling 0-10 (none - very severe)",
80 "discomfort from tender areas 0-10 (none - very severe)",
81 "morning stiffness level 0-10 (none - very severe)",
82 "morning stiffness duration 0-10 (none - 2 or more hours)",
83 ],
84 )
86 super().__init__(name, bases, classdict)
89class Basdai(TaskHasPatientMixin, Task, metaclass=BasdaiMetaclass):
90 __tablename__ = "basdai"
91 shortname = "BASDAI"
92 provides_trackers = True
94 N_QUESTIONS = 6
95 FIELD_NAMES = strseq("q", 1, N_QUESTIONS)
97 MINIMUM = 0.0
98 ACTIVE_CUTOFF = 4.0
99 MAXIMUM = 10.0
101 @staticmethod
102 def longname(req: "CamcopsRequest") -> str:
103 _ = req.gettext
104 return _("Bath Ankylosing Spondylitis Disease Activity Index")
106 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
107 return self.standard_task_summary_fields() + [
108 SummaryElement(
109 name="basdai",
110 coltype=Float(),
111 value=self.basdai(),
112 comment="BASDAI",
113 )
114 ]
116 def is_complete(self) -> bool:
117 if self.any_fields_none(self.FIELD_NAMES):
118 return False
120 if not self.field_contents_valid():
121 return False
123 return True
125 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
126 axis_min = self.MINIMUM - 0.5
127 axis_max = self.MAXIMUM + 0.5
128 axis_ticks = [
129 TrackerAxisTick(n, str(n)) for n in range(0, int(axis_max) + 1)
130 ]
132 horizontal_lines = [self.MAXIMUM, self.ACTIVE_CUTOFF, self.MINIMUM]
134 horizontal_labels = [
135 TrackerLabel(
136 self.ACTIVE_CUTOFF + 0.5, self.wxstring(req, "active")
137 ),
138 TrackerLabel(
139 self.ACTIVE_CUTOFF - 0.5, self.wxstring(req, "inactive")
140 ),
141 ]
143 return [
144 TrackerInfo(
145 value=self.basdai(),
146 plot_label="BASDAI",
147 axis_label="BASDAI",
148 axis_min=axis_min,
149 axis_max=axis_max,
150 axis_ticks=axis_ticks,
151 horizontal_lines=horizontal_lines,
152 horizontal_labels=horizontal_labels,
153 )
154 ]
156 def basdai(self) -> Optional[float]:
157 """
158 Calculating the BASDAI
159 A. Add scores for questions 1 – 4
160 B. Calculate the mean for questions 5 and 6
161 C. Add A and B and divide by 5
163 The higher the BASDAI score, the more severe the patient’s disability
164 due to their AS.
165 """
166 if not self.is_complete():
167 return None
169 score_a_field_names = strseq("q", 1, 4)
170 score_b_field_names = strseq("q", 5, 6)
172 a = sum([getattr(self, q) for q in score_a_field_names])
173 b = statistics.mean([getattr(self, q) for q in score_b_field_names])
175 return (a + b) / 5
177 def activity_state(self, req: CamcopsRequest) -> str:
178 basdai = self.basdai()
180 if basdai is None:
181 return "?"
183 if basdai < self.ACTIVE_CUTOFF:
184 return self.wxstring(req, "inactive")
186 return self.wxstring(req, "active")
188 def get_task_html(self, req: CamcopsRequest) -> str:
189 rows = ""
190 for q_num in range(1, self.N_QUESTIONS + 1):
191 q_field = "q" + str(q_num)
192 qtext = self.xstring(req, q_field) # includes HTML
193 min_text = self.wxstring(req, q_field + "_min")
194 max_text = self.wxstring(req, q_field + "_max")
195 qtext += f" <i>(0 = {min_text}, 10 = {max_text})</i>"
196 question_cell = f"{q_num}. {qtext}"
197 score = getattr(self, q_field)
199 rows += tr_qa(question_cell, score)
201 basdai = ws.number_to_dp(self.basdai(), 1, default="?")
203 html = """
204 <div class="{CssClass.SUMMARY}">
205 <table class="{CssClass.SUMMARY}">
206 {tr_is_complete}
207 {basdai}
208 </table>
209 </div>
210 <table class="{CssClass.TASKDETAIL}">
211 <tr>
212 <th width="60%">Question</th>
213 <th width="40%">Answer</th>
214 </tr>
215 {rows}
216 </table>
217 <div class="{CssClass.FOOTNOTES}">
218 [1] (A) Add scores for questions 1–4.
219 (B) Calculate the mean for questions 5 and 6.
220 (C) Add A and B and divide by 5, giving a total in the
221 range 0–10.
222 <4.0 suggests inactive disease,
223 ≥4.0 suggests active disease.
224 </div>
225 """.format(
226 CssClass=CssClass,
227 tr_is_complete=self.get_is_complete_tr(req),
228 basdai=tr(
229 self.wxstring(req, "basdai") + " <sup>[1]</sup>",
230 "{} ({})".format(answer(basdai), self.activity_state(req)),
231 ),
232 rows=rows,
233 )
234 return html