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

34 

35from camcops_server.cc_modules.cc_constants import CssClass, PV 

36from camcops_server.cc_modules.cc_db import add_multiple_columns 

37from camcops_server.cc_modules.cc_html import ( 

38 answer, 

39 get_yes_no_none, 

40 tr, 

41 tr_qa, 

42) 

43from camcops_server.cc_modules.cc_request import CamcopsRequest 

44from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

45from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

46from camcops_server.cc_modules.cc_text import SS 

47from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

48 

49 

50# ============================================================================= 

51# CAPS 

52# ============================================================================= 

53 

54QUESTION_SNIPPETS = [ 

55 "sounds loud", "presence of another", "heard thoughts echoed", 

56 "see shapes/lights/colours", "burning or other bodily sensations", 

57 "hear noises/sounds", "thoughts spoken aloud", "unexplained smells", 

58 "body changing shape", "limbs not own", "voices commenting", 

59 "feeling a touch", "hearing words or sentences", "unexplained tastes", 

60 "sensations flooding", "sounds distorted", 

61 "hard to distinguish sensations", "odours strong", 

62 "shapes/people distorted", "hypersensitive to touch/temperature", 

63 "tastes stronger than normal", "face looks different", 

64 "lights/colours more intense", "feeling of being uplifted", 

65 "common smells seem different", "everyday things look abnormal", 

66 "altered perception of time", "hear voices conversing", 

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

68 "food/drink tastes unusual", "see things that others cannot", 

69 "hear sounds/music that others cannot" 

70] 

71 

72 

73class CapsMetaclass(DeclarativeMeta): 

74 # noinspection PyInitNewSignature 

75 def __init__(cls: Type['Caps'], 

76 name: str, 

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

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

79 add_multiple_columns( 

80 cls, "endorse", 1, cls.NQUESTIONS, 

81 pv=PV.BIT, 

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

83 comment_strings=QUESTION_SNIPPETS 

84 ) 

85 add_multiple_columns( 

86 cls, "distress", 1, cls.NQUESTIONS, 

87 minimum=1, maximum=5, 

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

89 comment_strings=QUESTION_SNIPPETS 

90 ) 

91 add_multiple_columns( 

92 cls, "intrusiveness", 1, cls.NQUESTIONS, 

93 minimum=1, maximum=5, 

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

95 "if endorsed", 

96 comment_strings=QUESTION_SNIPPETS 

97 ) 

98 add_multiple_columns( 

99 cls, "frequency", 1, cls.NQUESTIONS, 

100 minimum=1, maximum=5, 

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

102 comment_strings=QUESTION_SNIPPETS 

103 ) 

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

105 

106 

107class Caps(TaskHasPatientMixin, Task, 

108 metaclass=CapsMetaclass): 

109 """ 

110 Server implementation of the CAPS task. 

111 """ 

112 __tablename__ = "caps" 

113 shortname = "CAPS" 

114 provides_trackers = True 

115 

116 prohibits_commercial = True 

117 

118 NQUESTIONS = 32 

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

120 

121 @staticmethod 

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

123 _ = req.gettext 

124 return _("Cardiff Anomalous Perceptions Scale") 

125 

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

127 return [TrackerInfo( 

128 value=self.total_score(), 

129 plot_label="CAPS total score", 

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

131 axis_min=-0.5, 

132 axis_max=32.5 

133 )] 

134 

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

136 return self.standard_task_summary_fields() + [ 

137 SummaryElement( 

138 name="total", coltype=Integer(), 

139 value=self.total_score(), 

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

141 SummaryElement( 

142 name="distress", coltype=Integer(), 

143 value=self.distress_score(), 

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

145 SummaryElement( 

146 name="intrusiveness", coltype=Integer(), 

147 value=self.intrusiveness_score(), 

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

149 SummaryElement( 

150 name="frequency", coltype=Integer(), 

151 value=self.frequency_score(), 

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

153 ] 

154 

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

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

157 return False 

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

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

160 return False 

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

162 return False 

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

164 return False 

165 return True 

166 

167 def is_complete(self) -> bool: 

168 if not self.field_contents_valid(): 

169 return False 

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

171 if not self.is_question_complete(i): 

172 return False 

173 return True 

174 

175 def total_score(self) -> int: 

176 return self.count_booleans(self.ENDORSE_FIELDS) 

177 

178 def distress_score(self) -> int: 

179 score = 0 

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

181 if getattr(self, "endorse" + str(q)) \ 

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

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

184 return score 

185 

186 def intrusiveness_score(self) -> int: 

187 score = 0 

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

189 if getattr(self, "endorse" + str(q)) \ 

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

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

192 return score 

193 

194 def frequency_score(self) -> int: 

195 score = 0 

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

197 if getattr(self, "endorse" + str(q)) \ 

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

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

200 return score 

201 

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

203 total = self.total_score() 

204 distress = self.distress_score() 

205 intrusiveness = self.intrusiveness_score() 

206 frequency = self.frequency_score() 

207 

208 q_a = "" 

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

210 q_a += tr( 

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

212 answer(get_yes_no_none(req, 

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

214 answer(getattr(self, "distress" + str(q)) 

215 if getattr(self, "endorse" + str(q)) else ""), 

216 answer(getattr(self, "intrusiveness" + str(q)) 

217 if getattr(self, "endorse" + str(q)) else ""), 

218 answer(getattr(self, "frequency" + str(q)) 

219 if getattr(self, "endorse" + str(q)) else "") 

220 ) 

221 

222 tr_total_score = tr_qa( 

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

224 total 

225 ) 

226 tr_distress = tr_qa( 

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

228 distress 

229 ), 

230 tr_intrusiveness = tr_qa( 

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

232 intrusiveness 

233 ), 

234 tr_frequency = tr_qa( 

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

236 frequency 

237 ) 

238 return f""" 

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

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

241 {self.get_is_complete_tr(req)} 

242 {tr_total_score} 

243 {tr_distress} 

244 {tr_intrusiveness} 

245 {tr_frequency} 

246 </table> 

247 </div> 

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

249 Anchor points: 

250 DISTRESS 

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

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

253 INTRUSIVENESS 

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

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

256 FREQUENCY 

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

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

259 </div> 

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

261 <tr> 

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

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

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

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

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

267 </tr> 

268 </table> 

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

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

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

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

273 </div> 

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

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

276 Original article: 

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

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

279 validated measure of anomalous perceptual experience. 

280 Schizophrenia Bulletin 32: 366–377. 

281 Published by Oxford University Press on behalf of the Maryland 

282 Psychiatric Research Center. All rights reserved. The online 

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

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

285 display the open access version of this article for 

286 non-commercial purposes provided that: the original authorship 

287 is properly and fully attributed; the Journal and Oxford 

288 University Press are attributed as the original place of 

289 publication with the correct citation details given; if an 

290 article is subsequently reproduced or disseminated not in its 

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

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

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

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

295 scale text).</b> 

296 </div> 

297 """