Coverage for tasks/basdai.py: 44%

1"""

2 camcops_server/tasks/basdai.py

3

4===============================================================================

5

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

8

9 This file is part of CamCOPS.

10

11 CamCOPS is free software: you can redistribute it and/or modify

12 it under the terms of the GNU General Public License as published by

13 the Free Software Foundation, either version 3 of the License, or

14 (at your option) any later version.

15

16 CamCOPS is distributed in the hope that it will be useful,

17 but WITHOUT ANY WARRANTY; without even the implied warranty of

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the

19 GNU General Public License for more details.

20

21 You should have received a copy of the GNU General Public License

22 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.

23

24===============================================================================

25

26**Bath Ankylosing Spondylitis Disease Activity Index (BASDAI) task.**

27

28"""

29

30 import statistics

31 from typing import Any, List, Optional, Type

32

33 import cardinal_pythonlib.rnc_web as ws

34 from cardinal_pythonlib.stringfunc import strseq

35 from sqlalchemy.sql.sqltypes import Float

36

37 from camcops_server.cc_modules.cc_constants import CssClass

38 from camcops_server.cc_modules.cc_db import add_multiple_columns

39 from camcops_server.cc_modules.cc_html import tr_qa, tr, answer

40 from camcops_server.cc_modules.cc_request import CamcopsRequest

41 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

42 from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task

43 from camcops_server.cc_modules.cc_trackerhelpers import (

44 TrackerAxisTick,

45 TrackerInfo,

46 TrackerLabel,

47)

48

49

50# =============================================================================

51# BASDAI

52# =============================================================================

53

54

55 class Basdai( # type: ignore[misc]

56 TaskHasPatientMixin,

57 Task,

58):

59 __tablename__ = "basdai"

60 shortname = "BASDAI"

61 provides_trackers = True

62

63 N_QUESTIONS = 6

64

65 @classmethod

66 def extend_columns(cls: Type["Basdai"], **kwargs: Any) -> None:

67

68 add_multiple_columns(

69 cls,

70 "q",

71 1,

72 cls.N_QUESTIONS,

73 coltype=Float,

74 minimum=0,

75 maximum=10,

76 comment_fmt="Q{n} - {s}",

77 comment_strings=[

78 "fatigue/tiredness 0-10 (none - very severe)",

79 "AS neck, back, hip pain 0-10 (none - very severe)",

80 "other joint pain/swelling 0-10 (none - very severe)",

81 "discomfort from tender areas 0-10 (none - very severe)",

82 "morning stiffness level 0-10 (none - very severe)",

83 "morning stiffness duration 0-10 (none - 2 or more hours)",

84 ],

85 )

86

87 FIELD_NAMES = strseq("q", 1, N_QUESTIONS)

88

89 MINIMUM = 0.0

90 ACTIVE_CUTOFF = 4.0

91 MAXIMUM = 10.0

92

93 @staticmethod

94 def longname(req: "CamcopsRequest") -> str:

95 _ = req.gettext

96 return _("Bath Ankylosing Spondylitis Disease Activity Index")

97

98 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:

99 return self.standard_task_summary_fields() + [

100 SummaryElement(

101 name="basdai",

102 coltype=Float(),

103 value=self.basdai(),

104 comment="BASDAI",

105 )

106 ]

107

108 def is_complete(self) -> bool:

109 if self.any_fields_none(self.FIELD_NAMES):

110 return False

111

112 if not self.field_contents_valid():

113 return False

114

115 return True

116

117 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:

118 axis_min = self.MINIMUM - 0.5

119 axis_max = self.MAXIMUM + 0.5

120 axis_ticks = [

121 TrackerAxisTick(n, str(n)) for n in range(0, int(axis_max) + 1)

122 ]

123

124 horizontal_lines = [self.MAXIMUM, self.ACTIVE_CUTOFF, self.MINIMUM]

125

126 horizontal_labels = [

127 TrackerLabel(

128 self.ACTIVE_CUTOFF + 0.5, self.wxstring(req, "active")

129 ),

130 TrackerLabel(

131 self.ACTIVE_CUTOFF - 0.5, self.wxstring(req, "inactive")

132 ),

133 ]

134

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 ]

147

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

154

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

160

161 score_a_field_names = strseq("q", 1, 4)

162 score_b_field_names = strseq("q", 5, 6)

163

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])

166

167 return (a + b) / 5

168

169 def activity_state(self, req: CamcopsRequest) -> str:

170 basdai = self.basdai()

171

172 if basdai is None:

173 return "?"

174

175 if basdai < self.ACTIVE_CUTOFF:

176 return self.wxstring(req, "inactive")

177

178 return self.wxstring(req, "active")

179

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)

190

191 rows += tr_qa(question_cell, score)

192

193 basdai = ws.number_to_dp(self.basdai(), 1, default="?")

194

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 &lt;4.0 suggests inactive disease,

215 &ge;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(answer(basdai), self.activity_state(req)),

223 ),

224 rows=rows,

225 )

226 return html