Coverage for tasks/eq5d5l.py: 56%

1# camcops_server/tasks/eq5d5l.py

2

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

27

28"""

29

30 from typing import List, Optional

31

32 from cardinal_pythonlib.stringfunc import strseq

33 from sqlalchemy.orm import Mapped

34 from sqlalchemy.sql.sqltypes import Integer, String

35

36 from camcops_server.cc_modules.cc_constants import CssClass

37 from camcops_server.cc_modules.cc_html import tr_qa

38 from camcops_server.cc_modules.cc_request import CamcopsRequest

39 from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup

40 from camcops_server.cc_modules.cc_sqla_coltypes import (

41 mapped_camcops_column,

42 ONE_TO_FIVE_CHECKER,

43 ZERO_TO_100_CHECKER,

44)

45 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

46 from camcops_server.cc_modules.cc_task import (

47 get_from_dict,

48 Task,

49 TaskHasPatientMixin,

50)

51 from 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

63 class Eq5d5l(TaskHasPatientMixin, Task): # type: ignore[misc]

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: Mapped[Optional[int]] = mapped_camcops_column(

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

81 permitted_value_checker=ONE_TO_FIVE_CHECKER,

82 )

83

84 q2: Mapped[Optional[int]] = mapped_camcops_column(

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

86 permitted_value_checker=ONE_TO_FIVE_CHECKER,

87 )

88

89 q3: Mapped[Optional[int]] = mapped_camcops_column(

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

91 permitted_value_checker=ONE_TO_FIVE_CHECKER,

92 )

93

94 q4: Mapped[Optional[int]] = mapped_camcops_column(

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

96 permitted_value_checker=ONE_TO_FIVE_CHECKER,

97 )

98

99 q5: Mapped[Optional[int]] = mapped_camcops_column(

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

101 permitted_value_checker=ONE_TO_FIVE_CHECKER,

102 )

103

104 health_vas: Mapped[Optional[int]] = mapped_camcops_column(

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

106 permitted_value_checker=ZERO_TO_100_CHECKER,

107 )

108

109 N_QUESTIONS = 5

110 MISSING_ANSWER_VALUE = 9

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

112 QUESTIONS += ["health_vas"]

113

114 @staticmethod

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

116 _ = req.gettext

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

118

119 def is_complete(self) -> bool:

120 return self.all_fields_not_none(self.QUESTIONS)

121

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

123 return [

124 TrackerInfo(

125 value=self.health_vas,

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

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

128 axis_min=-0.5,

129 axis_max=100.5,

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

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

132 )

133 ]

134

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

136 return self.standard_task_summary_fields() + [

137 SummaryElement(

138 name="health_state",

139 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 ),

144 SummaryElement(

145 name="visual_task_score",

146 coltype=Integer(),

147 value=self.get_vis_score_or_999(),

148 comment="Visual analogue health score "

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

150 ),

151 ]

152

153 def get_health_state_code(self) -> str:

154 mcq = ""

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

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

157 if ans is None:

158 mcq += str(self.MISSING_ANSWER_VALUE)

159 else:

160 mcq += str(ans)

161 return mcq

162

163 def get_vis_score_or_999(self) -> int:

164 vis_score = self.health_vas

165 if vis_score is None:

166 return 999

167 return vis_score

168

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

170 q_a = ""

171

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

173 nstr = str(i)

174 answers = {

175 None: None,

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

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

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

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

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

181 }

182

183 q_a += tr_qa(

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

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

186 )

187

188 q_a += tr_qa(

189 (

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

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

192 ),

193 self.health_vas,

194 )

195

196 return f"""

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

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

199 {self.get_is_complete_tr(req)}

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

201 self.get_health_state_code())}

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

203 self.get_vis_score_or_999())}

204 </table>

205 </div>

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

207 <tr>

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

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

210 </tr>

211 {q_a}

212 </table>

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

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

215 number. Each digit corresponds to a question.

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

217 5 indicates extreme problems. Missing values are

218 coded as 9.

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

220 values coded as 999.

221 </div>

222 """ # noqa

223

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

225 codes = [

226 SnomedExpression(

227 req.snomed(SnomedLookup.EQ5D5L_PROCEDURE_ASSESSMENT)

228 )

229 ]

230 if self.is_complete():

231 codes.append(

232 SnomedExpression(

233 req.snomed(SnomedLookup.EQ5D5L_SCALE),

234 {

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

236 req.snomed(

237 SnomedLookup.EQ5D5L_MOBILITY_SCORE

238 ): self.q1,

239 req.snomed(

240 SnomedLookup.EQ5D5L_SELF_CARE_SCORE

241 ): self.q2,

242 req.snomed(

243 SnomedLookup.EQ5D5L_USUAL_ACTIVITIES_SCORE

244 ): self.q3,

245 req.snomed(

246 SnomedLookup.EQ5D5L_PAIN_DISCOMFORT_SCORE

247 ): self.q4,

248 req.snomed(

249 SnomedLookup.EQ5D5L_ANXIETY_DEPRESSION_SCORE

250 ): self.q5,

251 },

252 )

253 )

254 return codes