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 

3""" 

4camcops_server/tasks/iesr.py 

5 

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

7 

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

9 

10 This file is part of CamCOPS. 

11 

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

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

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

15 (at your option) any later version. 

16 

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

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

19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

20 GNU General Public License for more details. 

21 

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

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

24 

25=============================================================================== 

26 

27""" 

28 

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

30 

31from cardinal_pythonlib.stringfunc import strseq 

32from sqlalchemy.ext.declarative import DeclarativeMeta 

33from sqlalchemy.sql.schema import Column 

34from sqlalchemy.sql.sqltypes import Integer, UnicodeText 

35 

36from camcops_server.cc_modules.cc_constants import ( 

37 CssClass, 

38 DATA_COLLECTION_UNLESS_UPGRADED_DIV, 

39) 

40from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

41from camcops_server.cc_modules.cc_db import add_multiple_columns 

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

43from camcops_server.cc_modules.cc_request import CamcopsRequest 

44from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup 

45from camcops_server.cc_modules.cc_string import AS 

46from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

47from camcops_server.cc_modules.cc_task import ( 

48 get_from_dict, 

49 Task, 

50 TaskHasPatientMixin, 

51) 

52from camcops_server.cc_modules.cc_text import SS 

53from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

54 

55 

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

57# IES-R 

58# ============================================================================= 

59 

60class IesrMetaclass(DeclarativeMeta): 

61 # noinspection PyInitNewSignature 

62 def __init__(cls: Type['Iesr'], 

63 name: str, 

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

65 classdict: Dict[str, Any]) -> None: 

66 add_multiple_columns( 

67 cls, "q", 1, cls.NQUESTIONS, 

68 minimum=cls.MIN_SCORE, maximum=cls.MAX_SCORE, 

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

70 comment_strings=[ 

71 "reminder feelings", # 1 

72 "sleep maintenance", 

73 "reminder thinking", 

74 "irritable", 

75 "avoided getting upset", # 5 

76 "thought unwanted", 

77 "unreal", 

78 "avoided reminder", 

79 "mental pictures", 

80 "jumpy", # 10 

81 "avoided thinking", 

82 "feelings undealt", 

83 "numb", 

84 "as if then", 

85 "sleep initiation", # 15 

86 "waves of emotion", 

87 "tried forgetting", 

88 "concentration", 

89 "reminder physical", 

90 "dreams", # 20 

91 "vigilant", 

92 "avoided talking", 

93 ] 

94 ) 

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

96 

97 

98class Iesr(TaskHasPatientMixin, Task, 

99 metaclass=IesrMetaclass): 

100 """ 

101 Server implementation of the IES-R task. 

102 """ 

103 __tablename__ = "iesr" 

104 shortname = "IES-R" 

105 provides_trackers = True 

106 

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

108 

109 NQUESTIONS = 22 

110 MIN_SCORE = 0 # per question 

111 MAX_SCORE = 4 # per question 

112 

113 MAX_TOTAL = 88 

114 MAX_AVOIDANCE = 32 

115 MAX_INTRUSION = 28 

116 MAX_HYPERAROUSAL = 28 

117 

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

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

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

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

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

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

124 HYPERAROUSAL_FIELDS = Task.fieldnames_from_list( 

125 "q", HYPERAROUSAL_QUESTIONS) 

126 

127 @staticmethod 

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

129 _ = req.gettext 

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

131 

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

133 return [ 

134 TrackerInfo( 

135 value=self.total_score(), 

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

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

138 axis_min=-0.5, 

139 axis_max=self.MAX_TOTAL + 0.5 

140 ), 

141 TrackerInfo( 

142 value=self.avoidance_score(), 

143 plot_label="IES-R avoidance score", 

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

145 axis_min=-0.5, 

146 axis_max=self.MAX_AVOIDANCE + 0.5 

147 ), 

148 TrackerInfo( 

149 value=self.intrusion_score(), 

150 plot_label="IES-R intrusion score", 

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

152 axis_min=-0.5, 

153 axis_max=self.MAX_INTRUSION + 0.5 

154 ), 

155 TrackerInfo( 

156 value=self.hyperarousal_score(), 

157 plot_label="IES-R hyperarousal score", 

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

159 axis_min=-0.5, 

160 axis_max=self.MAX_HYPERAROUSAL + 0.5 

161 ), 

162 ] 

163 

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

165 return self.standard_task_summary_fields() + [ 

166 SummaryElement( 

167 name="total_score", 

168 coltype=Integer(), 

169 value=self.total_score(), 

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

171 SummaryElement( 

172 name="avoidance_score", 

173 coltype=Integer(), 

174 value=self.avoidance_score(), 

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

176 SummaryElement( 

177 name="intrusion_score", 

178 coltype=Integer(), 

179 value=self.intrusion_score(), 

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

181 SummaryElement( 

182 name="hyperarousal_score", 

183 coltype=Integer(), 

184 value=self.hyperarousal_score(), 

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

186 ] 

187 

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

189 if not self.is_complete(): 

190 return CTV_INCOMPLETE 

191 t = self.total_score() 

192 a = self.avoidance_score() 

193 i = self.intrusion_score() 

194 h = self.hyperarousal_score() 

195 return [CtvInfo( 

196 content=( 

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

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

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

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

201 ) 

202 )] 

203 

204 def total_score(self) -> int: 

205 return self.sum_fields(self.QUESTION_FIELDS) 

206 

207 def avoidance_score(self) -> int: 

208 return self.sum_fields(self.AVOIDANCE_FIELDS) 

209 

210 def intrusion_score(self) -> int: 

211 return self.sum_fields(self.INTRUSION_FIELDS) 

212 

213 def hyperarousal_score(self) -> int: 

214 return self.sum_fields(self.HYPERAROUSAL_FIELDS) 

215 

216 def is_complete(self) -> bool: 

217 return bool( 

218 self.field_contents_valid() and 

219 self.event and 

220 self.all_fields_not_none(self.QUESTION_FIELDS) 

221 ) 

222 

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

224 option_dict = {None: None} 

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

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

227 h = f""" 

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

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

230 {self.get_is_complete_tr(req)} 

231 <tr> 

232 <td>Total score</td> 

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

234 </td> 

235 <tr> 

236 <td>Avoidance score</td> 

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

238 </td> 

239 <tr> 

240 <td>Intrusion score</td> 

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

242 </td> 

243 <tr> 

244 <td>Hyperarousal score</td> 

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

246 </td> 

247 </table> 

248 </div> 

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

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

251 </table> 

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

253 <tr> 

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

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

256 </tr> 

257 """ # noqa 

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

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

260 fa = (f"{a}: {get_from_dict(option_dict, a)}" 

261 if a is not None else None) 

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

263 h += """ 

264 </table> 

265 """ + DATA_COLLECTION_UNLESS_UPGRADED_DIV 

266 return h 

267 

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

269 codes = [SnomedExpression(req.snomed(SnomedLookup.IESR_PROCEDURE_ASSESSMENT))] # noqa 

270 if self.is_complete(): 

271 codes.append(SnomedExpression( 

272 req.snomed(SnomedLookup.IESR_SCALE), 

273 { 

274 req.snomed(SnomedLookup.IESR_SCORE): self.total_score(), 

275 } 

276 )) 

277 return codes