Coverage for tasks/eq5d5l.py: 56%

62 statements  

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

1#!/usr/bin/env python 

2# camcops_server/tasks/eq5d5l.py 

3 

4""" 

5=============================================================================== 

6 

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

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

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- By Joe Kearney, Rudolf Cardinal. 

28 

29""" 

30 

31from typing import List, Optional 

32 

33from cardinal_pythonlib.stringfunc import strseq 

34from sqlalchemy.sql.sqltypes import Integer, String 

35 

36from camcops_server.cc_modules.cc_constants import CssClass 

37from camcops_server.cc_modules.cc_html import tr_qa 

38from camcops_server.cc_modules.cc_request import CamcopsRequest 

39from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

40from camcops_server.cc_modules.cc_sqla_coltypes import ( 

41 CamcopsColumn, 

42 ONE_TO_FIVE_CHECKER, 

43 ZERO_TO_100_CHECKER, 

44) 

45from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

46from camcops_server.cc_modules.cc_task import ( 

47 get_from_dict, 

48 Task, 

49 TaskHasPatientMixin, 

50) 

51from camcops_server.cc_modules.cc_trackerhelpers import ( 

52 equally_spaced_int, 

53 regular_tracker_axis_ticks_int, 

54 TrackerInfo, 

55) 

56 

57 

58# ============================================================================= 

59# EQ-5D-5L 

60# ============================================================================= 

61 

62 

63class Eq5d5l(TaskHasPatientMixin, Task): 

64 """ 

65 Server implementation of the EQ-5D-5L task. 

66 

67 Note that the "index value" summary (e.g. SNOMED-CT code 736534008) is not 

68 implemented. This is a country-specific conversion of the raw values to a 

69 unitary health value; see 

70 

71 - https://euroqol.org/publications/key-euroqol-references/value-sets/ 

72 - https://euroqol.org/eq-5d-instruments/eq-5d-3l-about/valuation/choosing-a-value-set/ 

73 """ # noqa 

74 

75 __tablename__ = "eq5d5l" 

76 shortname = "EQ-5D-5L" 

77 provides_trackers = True 

78 

79 q1 = CamcopsColumn( 

80 "q1", 

81 Integer, 

82 comment="Q1 (mobility) (1 no problems - 5 unable)", 

83 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

84 ) 

85 

86 q2 = CamcopsColumn( 

87 "q2", 

88 Integer, 

89 comment="Q2 (self-care) (1 no problems - 5 unable)", 

90 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

91 ) 

92 

93 q3 = CamcopsColumn( 

94 "q3", 

95 Integer, 

96 comment="Q3 (usual activities) (1 no problems - 5 unable)", 

97 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

98 ) 

99 

100 q4 = CamcopsColumn( 

101 "q4", 

102 Integer, 

103 comment="Q4 (pain/discomfort) (1 none - 5 extreme)", 

104 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

105 ) 

106 

107 q5 = CamcopsColumn( 

108 "q5", 

109 Integer, 

110 comment="Q5 (anxiety/depression) (1 not - 5 extremely)", 

111 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

112 ) 

113 

114 health_vas = CamcopsColumn( 

115 "health_vas", 

116 Integer, 

117 comment="Visual analogue scale for overall health (0 worst - 100 best)", # noqa 

118 permitted_value_checker=ZERO_TO_100_CHECKER, 

119 ) # type: Optional[int] 

120 

121 N_QUESTIONS = 5 

122 MISSING_ANSWER_VALUE = 9 

123 QUESTIONS = strseq("q", 1, N_QUESTIONS) 

124 QUESTIONS += ["health_vas"] 

125 

126 @staticmethod 

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

128 _ = req.gettext 

129 return _("EuroQol 5-Dimension, 5-Level Health Scale") 

130 

131 def is_complete(self) -> bool: 

132 return self.all_fields_not_none(self.QUESTIONS) 

133 

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

135 return [ 

136 TrackerInfo( 

137 value=self.health_vas, 

138 plot_label="EQ-5D-5L health visual analogue scale", 

139 axis_label="Self-rated health today (out of 100)", 

140 axis_min=-0.5, 

141 axis_max=100.5, 

142 axis_ticks=regular_tracker_axis_ticks_int(0, 100, 25), 

143 horizontal_lines=equally_spaced_int(0, 100, 25), 

144 ) 

145 ] 

146 

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

148 return self.standard_task_summary_fields() + [ 

149 SummaryElement( 

150 name="health_state", 

151 coltype=String(length=5), 

152 value=self.get_health_state_code(), 

153 comment="Health state as a 5-character string of numbers, " 

154 "with 9 indicating a missing value", 

155 ), 

156 SummaryElement( 

157 name="visual_task_score", 

158 coltype=Integer(), 

159 value=self.get_vis_score_or_999(), 

160 comment="Visual analogue health score " 

161 "(0-100, with 999 replacing None)", 

162 ), 

163 ] 

164 

165 def get_health_state_code(self) -> str: 

166 mcq = "" 

167 for i in range(1, self.N_QUESTIONS + 1): 

168 ans = getattr(self, "q" + str(i)) 

169 if ans is None: 

170 mcq += str(self.MISSING_ANSWER_VALUE) 

171 else: 

172 mcq += str(ans) 

173 return mcq 

174 

175 def get_vis_score_or_999(self) -> int: 

176 vis_score = self.health_vas 

177 if vis_score is None: 

178 return 999 

179 return vis_score 

180 

181 def get_task_html(self, req: CamcopsRequest) -> str: 

182 q_a = "" 

183 

184 for i in range(1, self.N_QUESTIONS + 1): 

185 nstr = str(i) 

186 answers = { 

187 None: None, 

188 1: "1 – " + self.wxstring(req, "q" + nstr + "_o1"), 

189 2: "2 – " + self.wxstring(req, "q" + nstr + "_o2"), 

190 3: "3 – " + self.wxstring(req, "q" + nstr + "_o3"), 

191 4: "4 – " + self.wxstring(req, "q" + nstr + "_o4"), 

192 5: "5 – " + self.wxstring(req, "q" + nstr + "_o5"), 

193 } 

194 

195 q_a += tr_qa( 

196 nstr + ". " + self.wxstring(req, "q" + nstr + "_h"), 

197 get_from_dict(answers, getattr(self, "q" + str(i))), 

198 ) 

199 

200 q_a += tr_qa( 

201 ( 

202 "Self-rated health on a visual analogue scale (0–100) " 

203 "<sup>[2]</sup>" 

204 ), 

205 self.health_vas, 

206 ) 

207 

208 return f""" 

209 <div class="{CssClass.SUMMARY}"> 

210 <table class="{CssClass.SUMMARY}"> 

211 {self.get_is_complete_tr(req)} 

212 {tr_qa("Health state code <sup>[1]</sup>", 

213 self.get_health_state_code())} 

214 {tr_qa("Visual analogue scale summary number <sup>[2]</sup>", 

215 self.get_vis_score_or_999())} 

216 </table> 

217 </div> 

218 <table class="{CssClass.TASKDETAIL}"> 

219 <tr> 

220 <th width="60%">Question</th> 

221 <th width="40%">Answer</th> 

222 </tr> 

223 {q_a} 

224 </table> 

225 <div class="{CssClass.FOOTNOTES}"> 

226 [1] This is a string of digits, not a directly meaningful 

227 number. Each digit corresponds to a question. 

228 A score of 1 indicates no problems in any given dimension. 

229 5 indicates extreme problems. Missing values are 

230 coded as 9. 

231 [2] This is the visual analogue health score, with missing 

232 values coded as 999. 

233 </div> 

234 """ # noqa 

235 

236 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]: 

237 codes = [ 

238 SnomedExpression( 

239 req.snomed(SnomedLookup.EQ5D5L_PROCEDURE_ASSESSMENT) 

240 ) 

241 ] 

242 if self.is_complete(): 

243 codes.append( 

244 SnomedExpression( 

245 req.snomed(SnomedLookup.EQ5D5L_SCALE), 

246 { 

247 # SnomedLookup.EQ5D5L_INDEX_VALUE: not used; see docstring above # noqa 

248 req.snomed( 

249 SnomedLookup.EQ5D5L_MOBILITY_SCORE 

250 ): self.q1, 

251 req.snomed( 

252 SnomedLookup.EQ5D5L_SELF_CARE_SCORE 

253 ): self.q2, 

254 req.snomed( 

255 SnomedLookup.EQ5D5L_USUAL_ACTIVITIES_SCORE 

256 ): self.q3, # noqa 

257 req.snomed( 

258 SnomedLookup.EQ5D5L_PAIN_DISCOMFORT_SCORE 

259 ): self.q4, # noqa 

260 req.snomed( 

261 SnomedLookup.EQ5D5L_ANXIETY_DEPRESSION_SCORE 

262 ): self.q5, # noqa 

263 }, 

264 ) 

265 ) 

266 return codes