Coverage for tasks/caps.py: 45%

87 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/caps.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.sqltypes import Integer 

35 

36from camcops_server.cc_modules.cc_constants import CssClass, PV 

37from camcops_server.cc_modules.cc_db import add_multiple_columns 

38from camcops_server.cc_modules.cc_html import ( 

39 answer, 

40 get_yes_no_none, 

41 tr, 

42 tr_qa, 

43) 

44from camcops_server.cc_modules.cc_request import CamcopsRequest 

45from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

46from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

47from camcops_server.cc_modules.cc_text import SS 

48from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

49 

50 

51# ============================================================================= 

52# CAPS 

53# ============================================================================= 

54 

55QUESTION_SNIPPETS = [ 

56 "sounds loud", 

57 "presence of another", 

58 "heard thoughts echoed", 

59 "see shapes/lights/colours", 

60 "burning or other bodily sensations", 

61 "hear noises/sounds", 

62 "thoughts spoken aloud", 

63 "unexplained smells", 

64 "body changing shape", 

65 "limbs not own", 

66 "voices commenting", 

67 "feeling a touch", 

68 "hearing words or sentences", 

69 "unexplained tastes", 

70 "sensations flooding", 

71 "sounds distorted", 

72 "hard to distinguish sensations", 

73 "odours strong", 

74 "shapes/people distorted", 

75 "hypersensitive to touch/temperature", 

76 "tastes stronger than normal", 

77 "face looks different", 

78 "lights/colours more intense", 

79 "feeling of being uplifted", 

80 "common smells seem different", 

81 "everyday things look abnormal", 

82 "altered perception of time", 

83 "hear voices conversing", 

84 "smells or odours that others are unaware of", 

85 "food/drink tastes unusual", 

86 "see things that others cannot", 

87 "hear sounds/music that others cannot", 

88] 

89 

90 

91class CapsMetaclass(DeclarativeMeta): 

92 # noinspection PyInitNewSignature 

93 def __init__( 

94 cls: Type["Caps"], 

95 name: str, 

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

97 classdict: Dict[str, Any], 

98 ) -> None: 

99 add_multiple_columns( 

100 cls, 

101 "endorse", 

102 1, 

103 cls.NQUESTIONS, 

104 pv=PV.BIT, 

105 comment_fmt="Q{n} ({s}): endorsed? (0 no, 1 yes)", 

106 comment_strings=QUESTION_SNIPPETS, 

107 ) 

108 add_multiple_columns( 

109 cls, 

110 "distress", 

111 1, 

112 cls.NQUESTIONS, 

113 minimum=1, 

114 maximum=5, 

115 comment_fmt="Q{n} ({s}): distress (1 low - 5 high), if endorsed", 

116 comment_strings=QUESTION_SNIPPETS, 

117 ) 

118 add_multiple_columns( 

119 cls, 

120 "intrusiveness", 

121 1, 

122 cls.NQUESTIONS, 

123 minimum=1, 

124 maximum=5, 

125 comment_fmt="Q{n} ({s}): intrusiveness (1 low - 5 high), " 

126 "if endorsed", 

127 comment_strings=QUESTION_SNIPPETS, 

128 ) 

129 add_multiple_columns( 

130 cls, 

131 "frequency", 

132 1, 

133 cls.NQUESTIONS, 

134 minimum=1, 

135 maximum=5, 

136 comment_fmt="Q{n} ({s}): frequency (1 low - 5 high), if endorsed", 

137 comment_strings=QUESTION_SNIPPETS, 

138 ) 

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

140 

141 

142class Caps(TaskHasPatientMixin, Task, metaclass=CapsMetaclass): 

143 """ 

144 Server implementation of the CAPS task. 

145 """ 

146 

147 __tablename__ = "caps" 

148 shortname = "CAPS" 

149 provides_trackers = True 

150 

151 prohibits_commercial = True 

152 

153 NQUESTIONS = 32 

154 ENDORSE_FIELDS = strseq("endorse", 1, NQUESTIONS) 

155 

156 @staticmethod 

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

158 _ = req.gettext 

159 return _("Cardiff Anomalous Perceptions Scale") 

160 

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

162 return [ 

163 TrackerInfo( 

164 value=self.total_score(), 

165 plot_label="CAPS total score", 

166 axis_label="Total score (out of 32)", 

167 axis_min=-0.5, 

168 axis_max=32.5, 

169 ) 

170 ] 

171 

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

173 return self.standard_task_summary_fields() + [ 

174 SummaryElement( 

175 name="total", 

176 coltype=Integer(), 

177 value=self.total_score(), 

178 comment="Total score (/32)", 

179 ), 

180 SummaryElement( 

181 name="distress", 

182 coltype=Integer(), 

183 value=self.distress_score(), 

184 comment="Distress score (/160)", 

185 ), 

186 SummaryElement( 

187 name="intrusiveness", 

188 coltype=Integer(), 

189 value=self.intrusiveness_score(), 

190 comment="Intrusiveness score (/160)", 

191 ), 

192 SummaryElement( 

193 name="frequency", 

194 coltype=Integer(), 

195 value=self.frequency_score(), 

196 comment="Frequency score (/160)", 

197 ), 

198 ] 

199 

200 def is_question_complete(self, q: int) -> bool: 

201 if getattr(self, "endorse" + str(q)) is None: 

202 return False 

203 if getattr(self, "endorse" + str(q)): 

204 if getattr(self, "distress" + str(q)) is None: 

205 return False 

206 if getattr(self, "intrusiveness" + str(q)) is None: 

207 return False 

208 if getattr(self, "frequency" + str(q)) is None: 

209 return False 

210 return True 

211 

212 def is_complete(self) -> bool: 

213 if not self.field_contents_valid(): 

214 return False 

215 for i in range(1, Caps.NQUESTIONS + 1): 

216 if not self.is_question_complete(i): 

217 return False 

218 return True 

219 

220 def total_score(self) -> int: 

221 return self.count_booleans(self.ENDORSE_FIELDS) 

222 

223 def distress_score(self) -> int: 

224 score = 0 

225 for q in range(1, Caps.NQUESTIONS + 1): 

226 if ( 

227 getattr(self, "endorse" + str(q)) 

228 and getattr(self, "distress" + str(q)) is not None 

229 ): 

230 score += self.sum_fields(["distress" + str(q)]) 

231 return score 

232 

233 def intrusiveness_score(self) -> int: 

234 score = 0 

235 for q in range(1, Caps.NQUESTIONS + 1): 

236 if ( 

237 getattr(self, "endorse" + str(q)) 

238 and getattr(self, "intrusiveness" + str(q)) is not None 

239 ): 

240 score += self.sum_fields(["intrusiveness" + str(q)]) 

241 return score 

242 

243 def frequency_score(self) -> int: 

244 score = 0 

245 for q in range(1, Caps.NQUESTIONS + 1): 

246 if ( 

247 getattr(self, "endorse" + str(q)) 

248 and getattr(self, "frequency" + str(q)) is not None 

249 ): 

250 score += self.sum_fields(["frequency" + str(q)]) 

251 return score 

252 

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

254 total = self.total_score() 

255 distress = self.distress_score() 

256 intrusiveness = self.intrusiveness_score() 

257 frequency = self.frequency_score() 

258 

259 q_a = "" 

260 for q in range(1, Caps.NQUESTIONS + 1): 

261 q_a += tr( 

262 self.wxstring(req, "q" + str(q)), 

263 answer( 

264 get_yes_no_none(req, getattr(self, "endorse" + str(q))) 

265 ), 

266 answer( 

267 getattr(self, "distress" + str(q)) 

268 if getattr(self, "endorse" + str(q)) 

269 else "" 

270 ), 

271 answer( 

272 getattr(self, "intrusiveness" + str(q)) 

273 if getattr(self, "endorse" + str(q)) 

274 else "" 

275 ), 

276 answer( 

277 getattr(self, "frequency" + str(q)) 

278 if getattr(self, "endorse" + str(q)) 

279 else "" 

280 ), 

281 ) 

282 

283 tr_total_score = tr_qa( 

284 f"{req.sstring(SS.TOTAL_SCORE)} <sup>[1]</sup> (0–32)", total 

285 ) 

286 tr_distress = ( 

287 tr_qa( 

288 "{} (0–160)".format(self.wxstring(req, "distress")), distress 

289 ), 

290 ) 

291 tr_intrusiveness = ( 

292 tr_qa( 

293 "{} (0–160)".format(self.wxstring(req, "intrusiveness")), 

294 intrusiveness, 

295 ), 

296 ) 

297 tr_frequency = tr_qa( 

298 "{} (0–160)".format(self.wxstring(req, "frequency")), frequency 

299 ) 

300 return f""" 

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

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

303 {self.get_is_complete_tr(req)} 

304 {tr_total_score} 

305 {tr_distress} 

306 {tr_intrusiveness} 

307 {tr_frequency} 

308 </table> 

309 </div> 

310 <div class="{CssClass.EXPLANATION}"> 

311 Anchor points: 

312 DISTRESS 

313 {self.wxstring(req, "distress_option1")}, 

314 {self.wxstring(req, "distress_option5")}. 

315 INTRUSIVENESS 

316 {self.wxstring(req, "intrusiveness_option1")}, 

317 {self.wxstring(req, "intrusiveness_option5")}. 

318 FREQUENCY 

319 {self.wxstring(req, "frequency_option1")}, 

320 {self.wxstring(req, "frequency_option5")}. 

321 </div> 

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

323 <tr> 

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

325 <th width="10%">Endorsed?</th> 

326 <th width="10%">Distress (1–5)</th> 

327 <th width="10%">Intrusiveness (1–5)</th> 

328 <th width="10%">Frequency (1–5)</th> 

329 </tr> 

330 </table> 

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

332 [1] Total score: sum of endorsements (yes = 1, no = 0). 

333 Dimension scores: sum of ratings (0 if not endorsed). 

334 (Bell et al. 2006, PubMed ID 16237200) 

335 </div> 

336 <div class="{CssClass.COPYRIGHT}"> 

337 CAPS: Copyright © 2005, Bell, Halligan & Ellis. 

338 Original article: 

339 Bell V, Halligan PW, Ellis HD (2006). 

340 The Cardiff Anomalous Perceptions Scale (CAPS): a new 

341 validated measure of anomalous perceptual experience. 

342 Schizophrenia Bulletin 32: 366–377. 

343 Published by Oxford University Press on behalf of the Maryland 

344 Psychiatric Research Center. All rights reserved. The online 

345 version of this article has been published under an open access 

346 model. Users are entitled to use, reproduce, disseminate, or 

347 display the open access version of this article for 

348 non-commercial purposes provided that: the original authorship 

349 is properly and fully attributed; the Journal and Oxford 

350 University Press are attributed as the original place of 

351 publication with the correct citation details given; if an 

352 article is subsequently reproduced or disseminated not in its 

353 entirety but only in part or as a derivative work this must be 

354 clearly indicated. For commercial re-use, please contact 

355 journals.permissions@oxfordjournals.org.<br> 

356 <b>This is a derivative work (partial reproduction, viz. the 

357 scale text).</b> 

358 </div> 

359 """