Coverage for tasks/iesr.py: 63%

82 statements  

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

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/iesr.py 

5 

6=============================================================================== 

7 

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

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

10 

11 This file is part of CamCOPS. 

12 

13 CamCOPS is free software: you can redistribute it and/or modify 

14 it under the terms of the GNU General Public License as published by 

15 the Free Software Foundation, either version 3 of the License, or 

16 (at your option) any later version. 

17 

18 CamCOPS is distributed in the hope that it will be useful, 

19 but WITHOUT ANY WARRANTY; without even the implied warranty of 

20 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

21 GNU General Public License for more details. 

22 

23 You should have received a copy of the GNU General Public License 

24 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

25 

26=============================================================================== 

27 

28""" 

29 

30from typing import Any, Dict, List, Tuple, Type 

31 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy.ext.declarative import DeclarativeMeta 

34from sqlalchemy.sql.schema import Column 

35from sqlalchemy.sql.sqltypes import Integer, UnicodeText 

36 

37from camcops_server.cc_modules.cc_constants import ( 

38 CssClass, 

39 DATA_COLLECTION_UNLESS_UPGRADED_DIV, 

40) 

41from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

42from camcops_server.cc_modules.cc_db import add_multiple_columns 

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

44from camcops_server.cc_modules.cc_request import CamcopsRequest 

45from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

46from camcops_server.cc_modules.cc_string import AS 

47from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

48from camcops_server.cc_modules.cc_task import ( 

49 get_from_dict, 

50 Task, 

51 TaskHasPatientMixin, 

52) 

53from camcops_server.cc_modules.cc_text import SS 

54from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

55 

56 

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

58# IES-R 

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

60 

61 

62class IesrMetaclass(DeclarativeMeta): 

63 # noinspection PyInitNewSignature 

64 def __init__( 

65 cls: Type["Iesr"], 

66 name: str, 

67 bases: Tuple[Type, ...], 

68 classdict: Dict[str, Any], 

69 ) -> None: 

70 add_multiple_columns( 

71 cls, 

72 "q", 

73 1, 

74 cls.NQUESTIONS, 

75 minimum=cls.MIN_SCORE, 

76 maximum=cls.MAX_SCORE, 

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

78 comment_strings=[ 

79 "reminder feelings", # 1 

80 "sleep maintenance", 

81 "reminder thinking", 

82 "irritable", 

83 "avoided getting upset", # 5 

84 "thought unwanted", 

85 "unreal", 

86 "avoided reminder", 

87 "mental pictures", 

88 "jumpy", # 10 

89 "avoided thinking", 

90 "feelings undealt", 

91 "numb", 

92 "as if then", 

93 "sleep initiation", # 15 

94 "waves of emotion", 

95 "tried forgetting", 

96 "concentration", 

97 "reminder physical", 

98 "dreams", # 20 

99 "vigilant", 

100 "avoided talking", 

101 ], 

102 ) 

103 super().__init__(name, bases, classdict) 

104 

105 

106class Iesr(TaskHasPatientMixin, Task, metaclass=IesrMetaclass): 

107 """ 

108 Server implementation of the IES-R task. 

109 """ 

110 

111 __tablename__ = "iesr" 

112 shortname = "IES-R" 

113 provides_trackers = True 

114 

115 event = Column("event", UnicodeText, comment="Relevant event") 

116 

117 NQUESTIONS = 22 

118 MIN_SCORE = 0 # per question 

119 MAX_SCORE = 4 # per question 

120 

121 MAX_TOTAL = 88 

122 MAX_AVOIDANCE = 32 

123 MAX_INTRUSION = 28 

124 MAX_HYPERAROUSAL = 28 

125 

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

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

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

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

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

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

132 HYPERAROUSAL_FIELDS = Task.fieldnames_from_list( 

133 "q", HYPERAROUSAL_QUESTIONS 

134 ) 

135 

136 @staticmethod 

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

138 _ = req.gettext 

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

140 

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

142 return [ 

143 TrackerInfo( 

144 value=self.total_score(), 

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

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

147 axis_min=-0.5, 

148 axis_max=self.MAX_TOTAL + 0.5, 

149 ), 

150 TrackerInfo( 

151 value=self.avoidance_score(), 

152 plot_label="IES-R avoidance score", 

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

154 axis_min=-0.5, 

155 axis_max=self.MAX_AVOIDANCE + 0.5, 

156 ), 

157 TrackerInfo( 

158 value=self.intrusion_score(), 

159 plot_label="IES-R intrusion score", 

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

161 axis_min=-0.5, 

162 axis_max=self.MAX_INTRUSION + 0.5, 

163 ), 

164 TrackerInfo( 

165 value=self.hyperarousal_score(), 

166 plot_label="IES-R hyperarousal score", 

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

168 axis_min=-0.5, 

169 axis_max=self.MAX_HYPERAROUSAL + 0.5, 

170 ), 

171 ] 

172 

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

174 return self.standard_task_summary_fields() + [ 

175 SummaryElement( 

176 name="total_score", 

177 coltype=Integer(), 

178 value=self.total_score(), 

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

180 ), 

181 SummaryElement( 

182 name="avoidance_score", 

183 coltype=Integer(), 

184 value=self.avoidance_score(), 

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

186 ), 

187 SummaryElement( 

188 name="intrusion_score", 

189 coltype=Integer(), 

190 value=self.intrusion_score(), 

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

192 ), 

193 SummaryElement( 

194 name="hyperarousal_score", 

195 coltype=Integer(), 

196 value=self.hyperarousal_score(), 

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

198 ), 

199 ] 

200 

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

202 if not self.is_complete(): 

203 return CTV_INCOMPLETE 

204 t = self.total_score() 

205 a = self.avoidance_score() 

206 i = self.intrusion_score() 

207 h = self.hyperarousal_score() 

208 return [ 

209 CtvInfo( 

210 content=( 

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

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

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

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

215 ) 

216 ) 

217 ] 

218 

219 def total_score(self) -> int: 

220 return self.sum_fields(self.QUESTION_FIELDS) 

221 

222 def avoidance_score(self) -> int: 

223 return self.sum_fields(self.AVOIDANCE_FIELDS) 

224 

225 def intrusion_score(self) -> int: 

226 return self.sum_fields(self.INTRUSION_FIELDS) 

227 

228 def hyperarousal_score(self) -> int: 

229 return self.sum_fields(self.HYPERAROUSAL_FIELDS) 

230 

231 def is_complete(self) -> bool: 

232 return bool( 

233 self.field_contents_valid() 

234 and self.event 

235 and self.all_fields_not_none(self.QUESTION_FIELDS) 

236 ) 

237 

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

239 option_dict = {None: None} 

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

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

242 h = f""" 

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

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

245 {self.get_is_complete_tr(req)} 

246 <tr> 

247 <td>Total score</td> 

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

249 </td> 

250 <tr> 

251 <td>Avoidance score</td> 

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

253 </td> 

254 <tr> 

255 <td>Intrusion score</td> 

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

257 </td> 

258 <tr> 

259 <td>Hyperarousal score</td> 

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

261 </td> 

262 </table> 

263 </div> 

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

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

266 </table> 

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

268 <tr> 

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

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

271 </tr> 

272 """ # noqa 

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

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

275 fa = ( 

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

277 if a is not None 

278 else None 

279 ) 

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

281 h += ( 

282 """ 

283 </table> 

284 """ 

285 + DATA_COLLECTION_UNLESS_UPGRADED_DIV 

286 ) 

287 return h 

288 

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

290 codes = [ 

291 SnomedExpression( 

292 req.snomed(SnomedLookup.IESR_PROCEDURE_ASSESSMENT) 

293 ) 

294 ] 

295 if self.is_complete(): 

296 codes.append( 

297 SnomedExpression( 

298 req.snomed(SnomedLookup.IESR_SCALE), 

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

300 ) 

301 ) 

302 return codes