Coverage for tasks/iesr.py: 62%

79 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 14:23 +0100

1""" 

2camcops_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 

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

29 

30from cardinal_pythonlib.stringfunc import strseq 

31from sqlalchemy.orm import Mapped, mapped_column 

32from sqlalchemy.sql.sqltypes import Integer, UnicodeText 

33 

34from camcops_server.cc_modules.cc_constants import ( 

35 CssClass, 

36 DATA_COLLECTION_UNLESS_UPGRADED_DIV, 

37) 

38from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

39from camcops_server.cc_modules.cc_db import add_multiple_columns 

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

41from camcops_server.cc_modules.cc_request import CamcopsRequest 

42from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

43from camcops_server.cc_modules.cc_string import AS 

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_text import SS 

51from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

52 

53 

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

55# IES-R 

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

57 

58 

59class 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