Hide keyboard shortcuts

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 

2 

3""" 

4camcops_server/tasks/basdai.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

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. 

16 

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. 

21 

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/>. 

24 

25=============================================================================== 

26 

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

28 

29""" 

30 

31import statistics 

32from typing import Any, Dict, List, Optional, Type, Tuple 

33 

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 

38 

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) 

50 

51 

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

53# BASDAI 

54# ============================================================================= 

55 

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: 

62 

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 ) 

76 

77 super().__init__(name, bases, classdict) 

78 

79 

80class Basdai(TaskHasPatientMixin, 

81 Task, 

82 metaclass=BasdaiMetaclass): 

83 __tablename__ = "basdai" 

84 shortname = "BASDAI" 

85 provides_trackers = True 

86 

87 N_QUESTIONS = 6 

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

89 

90 MINIMUM = 0.0 

91 ACTIVE_CUTOFF = 4.0 

92 MAXIMUM = 10.0 

93 

94 @staticmethod 

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

96 _ = req.gettext 

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

98 

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 ] 

106 

107 def is_complete(self) -> bool: 

108 if self.any_fields_none(self.FIELD_NAMES): 

109 return False 

110 

111 if not self.field_contents_valid(): 

112 return False 

113 

114 return True 

115 

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

121 

122 horizontal_lines = [ 

123 self.MAXIMUM, 

124 self.ACTIVE_CUTOFF, 

125 self.MINIMUM, 

126 ] 

127 

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 ] 

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( 

223 answer(basdai), 

224 self.activity_state(req) 

225 ) 

226 ), 

227 rows=rows, 

228 ) 

229 return html