Coverage for tasks/basdai.py : 47%

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