Coverage for tasks/basdai.py: 47%

75 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/basdai.py 

5 

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

7 

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

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

10 

11 This file is part of CamCOPS. 

12 

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. 

17 

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. 

22 

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

25 

26=============================================================================== 

27 

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

29 

30""" 

31 

32import statistics 

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

34 

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 

39 

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) 

51 

52 

53# ============================================================================= 

54# BASDAI 

55# ============================================================================= 

56 

57 

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: 

66 

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 ) 

85 

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

87 

88 

89class Basdai(TaskHasPatientMixin, Task, metaclass=BasdaiMetaclass): 

90 __tablename__ = "basdai" 

91 shortname = "BASDAI" 

92 provides_trackers = True 

93 

94 N_QUESTIONS = 6 

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

96 

97 MINIMUM = 0.0 

98 ACTIVE_CUTOFF = 4.0 

99 MAXIMUM = 10.0 

100 

101 @staticmethod 

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

103 _ = req.gettext 

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

105 

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 ] 

115 

116 def is_complete(self) -> bool: 

117 if self.any_fields_none(self.FIELD_NAMES): 

118 return False 

119 

120 if not self.field_contents_valid(): 

121 return False 

122 

123 return True 

124 

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 ] 

131 

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

133 

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 ] 

142 

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 ] 

155 

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 

162 

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 

168 

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

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

171 

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

174 

175 return (a + b) / 5 

176 

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

178 basdai = self.basdai() 

179 

180 if basdai is None: 

181 return "?" 

182 

183 if basdai < self.ACTIVE_CUTOFF: 

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

185 

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

187 

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) 

198 

199 rows += tr_qa(question_cell, score) 

200 

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

202 

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

223 &ge;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