Coverage for tasks/iesr.py: 62%

1"""

2 camcops_server/tasks/iesr.py

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"""

27

28 from typing import Any, cast, List, Optional, Type

29

30 from cardinal_pythonlib.stringfunc import strseq

31 from sqlalchemy.orm import Mapped, mapped_column

32 from sqlalchemy.sql.sqltypes import Integer, UnicodeText

33

34 from camcops_server.cc_modules.cc_constants import (

35 CssClass,

36 DATA_COLLECTION_UNLESS_UPGRADED_DIV,

37)

38 from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo

39 from camcops_server.cc_modules.cc_db import add_multiple_columns

40 from camcops_server.cc_modules.cc_html import answer, tr, tr_qa

41 from camcops_server.cc_modules.cc_request import CamcopsRequest

42 from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup

43 from camcops_server.cc_modules.cc_string import AS

44 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

45 from camcops_server.cc_modules.cc_task import (

46 get_from_dict,

47 Task,

48 TaskHasPatientMixin,

49)

50 from camcops_server.cc_modules.cc_text import SS

51 from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo

52

53

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

55# IES-R

56# =============================================================================

57

58

59 class Iesr( # type: ignore[misc]

60 TaskHasPatientMixin,

61 Task,

62):

63 """

64 Server implementation of the IES-R task.

65 """

66

67 __tablename__ = "iesr"

68 shortname = "IES-R"

69 provides_trackers = True

70

71 @classmethod

72 def extend_columns(cls: Type["Iesr"], **kwargs: Any) -> None:

73 add_multiple_columns(

74 cls,

75 "q",

76 1,

77 cls.NQUESTIONS,

78 minimum=cls.MIN_SCORE,

79 maximum=cls.MAX_SCORE,

80 comment_fmt="Q{n}, {s} (0-4, higher worse)",

81 comment_strings=[

82 "reminder feelings", # 1

83 "sleep maintenance",

84 "reminder thinking",

85 "irritable",

86 "avoided getting upset", # 5

87 "thought unwanted",

88 "unreal",

89 "avoided reminder",

90 "mental pictures",

91 "jumpy", # 10

92 "avoided thinking",

93 "feelings undealt",

94 "numb",

95 "as if then",

96 "sleep initiation", # 15

97 "waves of emotion",

98 "tried forgetting",

99 "concentration",

100 "reminder physical",

101 "dreams", # 20

102 "vigilant",

103 "avoided talking",

104 ],

105 )

106

107 event: Mapped[Optional[str]] = mapped_column(

108 UnicodeText, comment="Relevant event"

109 )

110

111 NQUESTIONS = 22

112 MIN_SCORE = 0 # per question

113 MAX_SCORE = 4 # per question

114

115 MAX_TOTAL = 88

116 MAX_AVOIDANCE = 32

117 MAX_INTRUSION = 28

118 MAX_HYPERAROUSAL = 28

119

120 QUESTION_FIELDS = strseq("q", 1, NQUESTIONS)

121 AVOIDANCE_QUESTIONS = [5, 7, 8, 11, 12, 13, 17, 22]

122 AVOIDANCE_FIELDS = Task.fieldnames_from_list("q", AVOIDANCE_QUESTIONS)

123 INTRUSION_QUESTIONS = [1, 2, 3, 6, 9, 16, 20]

124 INTRUSION_FIELDS = Task.fieldnames_from_list("q", INTRUSION_QUESTIONS)

125 HYPERAROUSAL_QUESTIONS = [4, 10, 14, 15, 18, 19, 21]

126 HYPERAROUSAL_FIELDS = Task.fieldnames_from_list(

127 "q", HYPERAROUSAL_QUESTIONS

128 )

129

130 @staticmethod

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

132 _ = req.gettext

133 return _("Impact of Events Scale – Revised")

134

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

136 return [

137 TrackerInfo(

138 value=self.total_score(),

139 plot_label="IES-R total score (lower is better)",

140 axis_label=f"Total score (out of {self.MAX_TOTAL})",

141 axis_min=-0.5,

142 axis_max=self.MAX_TOTAL + 0.5,

143 ),

144 TrackerInfo(

145 value=self.avoidance_score(),

146 plot_label="IES-R avoidance score",

147 axis_label=f"Avoidance score (out of {self.MAX_AVOIDANCE})",

148 axis_min=-0.5,

149 axis_max=self.MAX_AVOIDANCE + 0.5,

150 ),

151 TrackerInfo(

152 value=self.intrusion_score(),

153 plot_label="IES-R intrusion score",

154 axis_label=f"Intrusion score (out of {self.MAX_INTRUSION})",

155 axis_min=-0.5,

156 axis_max=self.MAX_INTRUSION + 0.5,

157 ),

158 TrackerInfo(

159 value=self.hyperarousal_score(),

160 plot_label="IES-R hyperarousal score",

161 axis_label=f"Hyperarousal score (out of {self.MAX_HYPERAROUSAL})", # noqa

162 axis_min=-0.5,

163 axis_max=self.MAX_HYPERAROUSAL + 0.5,

164 ),

165 ]

166

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

168 return self.standard_task_summary_fields() + [

169 SummaryElement(

170 name="total_score",

171 coltype=Integer(),

172 value=self.total_score(),

173 comment=f"Total score (/ {self.MAX_TOTAL})",

174 ),

175 SummaryElement(

176 name="avoidance_score",

177 coltype=Integer(),

178 value=self.avoidance_score(),

179 comment=f"Avoidance score (/ {self.MAX_AVOIDANCE})",

180 ),

181 SummaryElement(

182 name="intrusion_score",

183 coltype=Integer(),

184 value=self.intrusion_score(),

185 comment=f"Intrusion score (/ {self.MAX_INTRUSION})",

186 ),

187 SummaryElement(

188 name="hyperarousal_score",

189 coltype=Integer(),

190 value=self.hyperarousal_score(),

191 comment=f"Hyperarousal score (/ {self.MAX_HYPERAROUSAL})",

192 ),

193 ]

194

195 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:

196 if not self.is_complete():

197 return CTV_INCOMPLETE

198 t = self.total_score()

199 a = self.avoidance_score()

200 i = self.intrusion_score()

201 h = self.hyperarousal_score()

202 return [

203 CtvInfo(

204 content=(

205 f"IES-R total score {t}/{self.MAX_TOTAL} "

206 f"(avoidance {a}/{self.MAX_AVOIDANCE} "

207 f"intrusion {i}/{self.MAX_INTRUSION}, "

208 f"hyperarousal {h}/{self.MAX_HYPERAROUSAL})"

209 )

210 )

211 ]

212

213 def total_score(self) -> int:

214 return cast(int, self.sum_fields(self.QUESTION_FIELDS))

215

216 def avoidance_score(self) -> int:

217 return cast(int, self.sum_fields(self.AVOIDANCE_FIELDS))

218

219 def intrusion_score(self) -> int:

220 return cast(int, self.sum_fields(self.INTRUSION_FIELDS))

221

222 def hyperarousal_score(self) -> int:

223 return cast(int, self.sum_fields(self.HYPERAROUSAL_FIELDS))

224

225 def is_complete(self) -> bool:

226 return bool(

227 self.field_contents_valid()

228 and self.event

229 and self.all_fields_not_none(self.QUESTION_FIELDS)

230 )

231

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

233 option_dict: dict[Optional[int], Optional[str]] = {None: None}

234 for a in range(self.MIN_SCORE, self.MAX_SCORE + 1):

235 option_dict[a] = req.wappstring(AS.IESR_A_PREFIX + str(a))

236 h = f"""

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

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

239 {self.get_is_complete_tr(req)}

240 <tr>

241 <td>Total score</td>

242 <td>{answer(self.total_score())} / {self.MAX_TOTAL}</td>

243 </td>

244 <tr>

245 <td>Avoidance score</td>

246 <td>{answer(self.avoidance_score())} / {self.MAX_AVOIDANCE}</td>

247 </td>

248 <tr>

249 <td>Intrusion score</td>

250 <td>{answer(self.intrusion_score())} / {self.MAX_INTRUSION}</td>

251 </td>

252 <tr>

253 <td>Hyperarousal score</td>

254 <td>{answer(self.hyperarousal_score())} / {self.MAX_HYPERAROUSAL}</td>

255 </td>

256 </table>

257 </div>

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

259 {tr_qa(req.sstring(SS.EVENT), self.event)}

260 </table>

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

262 <tr>

263 <th width="75%">Question</th>

264 <th width="25%">Answer (0–4)</th>

265 </tr>

266 """ # noqa

267 for q in range(1, self.NQUESTIONS + 1):

268 a = getattr(self, "q" + str(q))

269 fa = (

270 f"{a}: {get_from_dict(option_dict, a)}"

271 if a is not None

272 else None

273 )

274 h += tr(self.wxstring(req, "q" + str(q)), answer(fa))

275 h += (

276 """

277 </table>

278 """

279 + DATA_COLLECTION_UNLESS_UPGRADED_DIV

280 )

281 return h

282

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

284 codes = [

285 SnomedExpression(

286 req.snomed(SnomedLookup.IESR_PROCEDURE_ASSESSMENT)

287 )

288 ]

289 if self.is_complete():

290 codes.append(

291 SnomedExpression(

292 req.snomed(SnomedLookup.IESR_SCALE),

293 {req.snomed(SnomedLookup.IESR_SCORE): self.total_score()},

294 )

295 )

296 return codes