Coverage for tasks/caps.py: 43%

1"""

2 camcops_server/tasks/caps.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

28 from typing import Any, cast, List, Type

29

30 from cardinal_pythonlib.stringfunc import strseq

31 from sqlalchemy.sql.sqltypes import Integer

32

33 from camcops_server.cc_modules.cc_constants import CssClass, PV

34 from camcops_server.cc_modules.cc_db import add_multiple_columns

35 from camcops_server.cc_modules.cc_html import (

36 answer,

37 get_yes_no_none,

38 tr,

39 tr_qa,

40)

41 from camcops_server.cc_modules.cc_request import CamcopsRequest

42 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

43 from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin

44 from camcops_server.cc_modules.cc_text import SS

45 from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo

46

47

48# =============================================================================

49# CAPS

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

51

52 QUESTION_SNIPPETS = [

53 "sounds loud",

54 "presence of another",

55 "heard thoughts echoed",

56 "see shapes/lights/colours",

57 "burning or other bodily sensations",

58 "hear noises/sounds",

59 "thoughts spoken aloud",

60 "unexplained smells",

61 "body changing shape",

62 "limbs not own",

63 "voices commenting",

64 "feeling a touch",

65 "hearing words or sentences",

66 "unexplained tastes",

67 "sensations flooding",

68 "sounds distorted",

69 "hard to distinguish sensations",

70 "odours strong",

71 "shapes/people distorted",

72 "hypersensitive to touch/temperature",

73 "tastes stronger than normal",

74 "face looks different",

75 "lights/colours more intense",

76 "feeling of being uplifted",

77 "common smells seem different",

78 "everyday things look abnormal",

79 "altered perception of time",

80 "hear voices conversing",

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

82 "food/drink tastes unusual",

83 "see things that others cannot",

84 "hear sounds/music that others cannot",

85]

86

87

88 class Caps( # type: ignore[misc]

89 TaskHasPatientMixin,

90 Task,

91):

92 """

93 Server implementation of the CAPS task.

94 """

95

96 __tablename__ = "caps"

97 shortname = "CAPS"

98 provides_trackers = True

99

100 prohibits_commercial = True

101

102 NQUESTIONS = 32

103

104 @classmethod

105 def extend_columns(cls: Type["Caps"], **kwargs: Any) -> None:

106 add_multiple_columns(

107 cls,

108 "endorse",

109 1,

110 cls.NQUESTIONS,

111 pv=PV.BIT,

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

113 comment_strings=QUESTION_SNIPPETS,

114 )

115 add_multiple_columns(

116 cls,

117 "distress",

118 1,

119 cls.NQUESTIONS,

120 minimum=1,

121 maximum=5,

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

123 comment_strings=QUESTION_SNIPPETS,

124 )

125 add_multiple_columns(

126 cls,

127 "intrusiveness",

128 1,

129 cls.NQUESTIONS,

130 minimum=1,

131 maximum=5,

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

133 "if endorsed",

134 comment_strings=QUESTION_SNIPPETS,

135 )

136 add_multiple_columns(

137 cls,

138 "frequency",

139 1,

140 cls.NQUESTIONS,

141 minimum=1,

142 maximum=5,

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

144 comment_strings=QUESTION_SNIPPETS,

145 )

146

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

148

149 @staticmethod

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

151 _ = req.gettext

152 return _("Cardiff Anomalous Perceptions Scale")

153

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

155 return [

156 TrackerInfo(

157 value=self.total_score(),

158 plot_label="CAPS total score",

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

160 axis_min=-0.5,

161 axis_max=32.5,

162 )

163 ]

164

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

166 return self.standard_task_summary_fields() + [

167 SummaryElement(

168 name="total",

169 coltype=Integer(),

170 value=self.total_score(),

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

172 ),

173 SummaryElement(

174 name="distress",

175 coltype=Integer(),

176 value=self.distress_score(),

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

178 ),

179 SummaryElement(

180 name="intrusiveness",

181 coltype=Integer(),

182 value=self.intrusiveness_score(),

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

184 ),

185 SummaryElement(

186 name="frequency",

187 coltype=Integer(),

188 value=self.frequency_score(),

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

190 ),

191 ]

192

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

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

195 return False

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

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

198 return False

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

200 return False

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

202 return False

203 return True

204

205 def is_complete(self) -> bool:

206 if not self.field_contents_valid():

207 return False

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

209 if not self.is_question_complete(i):

210 return False

211 return True

212

213 def total_score(self) -> int:

214 return self.count_booleans(self.ENDORSE_FIELDS)

215

216 def distress_score(self) -> int:

217 score = 0

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

219 if (

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

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

222 ):

223 score += cast(int, self.sum_fields(["distress" + str(q)]))

224 return score

225

226 def intrusiveness_score(self) -> int:

227 score = 0

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

229 if (

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

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

232 ):

233 score += cast(int, self.sum_fields(["intrusiveness" + str(q)]))

234 return score

235

236 def frequency_score(self) -> int:

237 score = 0

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

239 if (

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

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

242 ):

243 score += cast(int, self.sum_fields(["frequency" + str(q)]))

244 return score

245

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

247 total = self.total_score()

248 distress = self.distress_score()

249 intrusiveness = self.intrusiveness_score()

250 frequency = self.frequency_score()

251

252 q_a = ""

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

254 q_a += tr(

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

256 answer(

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

258 ),

259 answer(

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

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

262 else ""

263 ),

264 answer(

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

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

267 else ""

268 ),

269 answer(

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

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

272 else ""

273 ),

274 )

275

276 tr_total_score = tr_qa(

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

278 )

279 tr_distress = tr_qa(

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

281 )

282 tr_intrusiveness = tr_qa(

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

284 intrusiveness,

285 )

286 tr_frequency = tr_qa(

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

288 )

289 return f"""

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

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

292 {self.get_is_complete_tr(req)}

293 {tr_total_score}

294 {tr_distress}

295 {tr_intrusiveness}

296 {tr_frequency}

297 </table>

298 </div>

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

300 Anchor points:

301 DISTRESS

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

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

304 INTRUSIVENESS

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

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

307 FREQUENCY

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

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

310 </div>

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

312 <tr>

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

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

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

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

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

318 </tr>

319 </table>

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

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

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

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

324 </div>

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

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

327 Original article:

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

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

330 validated measure of anomalous perceptual experience.

331 Schizophrenia Bulletin 32: 366–377.

332 Published by Oxford University Press on behalf of the Maryland

333 Psychiatric Research Center. All rights reserved. The online

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

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

336 display the open access version of this article for

337 non-commercial purposes provided that: the original authorship

338 is properly and fully attributed; the Journal and Oxford

339 University Press are attributed as the original place of

340 publication with the correct citation details given; if an

341 article is subsequently reproduced or disseminated not in its

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

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

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

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

346 scale text).</b>

347 </div>

348 """