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# camcops_server/tasks/eq5d5l.py 

3 

4""" 

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

6 

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

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

27 

28""" 

29 

30from typing import List, Optional 

31 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy.sql.sqltypes import Integer, String 

34 

35from camcops_server.cc_modules.cc_constants import CssClass 

36from camcops_server.cc_modules.cc_html import tr_qa 

37from camcops_server.cc_modules.cc_request import CamcopsRequest 

38from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

39from camcops_server.cc_modules.cc_sqla_coltypes import ( 

40 CamcopsColumn, 

41 ONE_TO_FIVE_CHECKER, 

42 ZERO_TO_100_CHECKER, 

43) 

44from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

45from camcops_server.cc_modules.cc_task import ( 

46 get_from_dict, 

47 Task, 

48 TaskHasPatientMixin, 

49) 

50from camcops_server.cc_modules.cc_trackerhelpers import ( 

51 equally_spaced_int, 

52 regular_tracker_axis_ticks_int, 

53 TrackerInfo, 

54) 

55 

56 

57# ============================================================================= 

58# EQ-5D-5L 

59# ============================================================================= 

60 

61class Eq5d5l(TaskHasPatientMixin, Task): 

62 """ 

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

64 

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

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

67 unitary health value; see 

68 

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

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

71 """ # noqa 

72 __tablename__ = "eq5d5l" 

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

74 provides_trackers = True 

75 

76 q1 = CamcopsColumn( 

77 "q1", Integer, 

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

79 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

80 ) 

81 

82 q2 = CamcopsColumn( 

83 "q2", Integer, 

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

85 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

86 ) 

87 

88 q3 = CamcopsColumn( 

89 "q3", Integer, 

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

91 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

92 ) 

93 

94 q4 = CamcopsColumn( 

95 "q4", Integer, 

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

97 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

98 ) 

99 

100 q5 = CamcopsColumn( 

101 "q5", Integer, 

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

103 permitted_value_checker=ONE_TO_FIVE_CHECKER, 

104 ) 

105 

106 health_vas = CamcopsColumn( 

107 "health_vas", Integer, 

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

109 permitted_value_checker=ZERO_TO_100_CHECKER, 

110 ) # type: Optional[int] 

111 

112 N_QUESTIONS = 5 

113 MISSING_ANSWER_VALUE = 9 

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

115 QUESTIONS += ["health_vas"] 

116 

117 @staticmethod 

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

119 _ = req.gettext 

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

121 

122 def is_complete(self) -> bool: 

123 return self.all_fields_not_none(self.QUESTIONS) 

124 

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

126 return [TrackerInfo( 

127 value=self.health_vas, 

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

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

130 axis_min=-0.5, 

131 axis_max=100.5, 

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

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

134 )] 

135 

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

137 return self.standard_task_summary_fields() + [ 

138 SummaryElement( 

139 name="health_state", coltype=String(length=5), 

140 value=self.get_health_state_code(), 

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

142 "with 9 indicating a missing value"), 

143 SummaryElement( 

144 name="visual_task_score", coltype=Integer(), 

145 value=self.get_vis_score_or_999(), 

146 comment="Visual analogue health score " 

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

148 ] 

149 

150 def get_health_state_code(self) -> str: 

151 mcq = "" 

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

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

154 if ans is None: 

155 mcq += str(self.MISSING_ANSWER_VALUE) 

156 else: 

157 mcq += str(ans) 

158 return mcq 

159 

160 def get_vis_score_or_999(self) -> int: 

161 vis_score = self.health_vas 

162 if vis_score is None: 

163 return 999 

164 return vis_score 

165 

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

167 q_a = "" 

168 

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

170 nstr = str(i) 

171 answers = { 

172 None: None, 

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

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

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

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

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

178 } 

179 

180 q_a += tr_qa(nstr + ". " + self.wxstring(req, "q" + nstr + "_h"), 

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

182 

183 q_a += tr_qa( 

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

185 "<sup>[2]</sup>"), 

186 self.health_vas) 

187 

188 return f""" 

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

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

191 {self.get_is_complete_tr(req)} 

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

193 self.get_health_state_code())} 

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

195 self.get_vis_score_or_999())} 

196 </table> 

197 </div> 

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

199 <tr> 

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

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

202 </tr> 

203 {q_a} 

204 </table> 

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

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

207 number. Each digit corresponds to a question. 

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

209 5 indicates extreme problems. Missing values are 

210 coded as 9. 

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

212 values coded as 999. 

213 </div> 

214 """ # noqa 

215 

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

217 codes = [SnomedExpression(req.snomed(SnomedLookup.EQ5D5L_PROCEDURE_ASSESSMENT))] # noqa 

218 if self.is_complete(): 

219 codes.append(SnomedExpression( 

220 req.snomed(SnomedLookup.EQ5D5L_SCALE), 

221 { 

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

223 req.snomed(SnomedLookup.EQ5D5L_MOBILITY_SCORE): self.q1, 

224 req.snomed(SnomedLookup.EQ5D5L_SELF_CARE_SCORE): self.q2, 

225 req.snomed(SnomedLookup.EQ5D5L_USUAL_ACTIVITIES_SCORE): self.q3, # noqa 

226 req.snomed(SnomedLookup.EQ5D5L_PAIN_DISCOMFORT_SCORE): self.q4, # noqa 

227 req.snomed(SnomedLookup.EQ5D5L_ANXIETY_DEPRESSION_SCORE): self.q5, # noqa 

228 } 

229 )) 

230 return codes