Coverage for tasks/caps.py: 45%
87 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/tasks/caps.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
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.
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.
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/>.
26===============================================================================
28"""
30from typing import Any, Dict, List, Tuple, Type
32from cardinal_pythonlib.stringfunc import strseq
33from sqlalchemy.ext.declarative import DeclarativeMeta
34from sqlalchemy.sql.sqltypes import Integer
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
51# =============================================================================
52# CAPS
53# =============================================================================
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]
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)
142class Caps(TaskHasPatientMixin, Task, metaclass=CapsMetaclass):
143 """
144 Server implementation of the CAPS task.
145 """
147 __tablename__ = "caps"
148 shortname = "CAPS"
149 provides_trackers = True
151 prohibits_commercial = True
153 NQUESTIONS = 32
154 ENDORSE_FIELDS = strseq("endorse", 1, NQUESTIONS)
156 @staticmethod
157 def longname(req: "CamcopsRequest") -> str:
158 _ = req.gettext
159 return _("Cardiff Anomalous Perceptions Scale")
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 ]
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 ]
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
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
220 def total_score(self) -> int:
221 return self.count_booleans(self.ENDORSE_FIELDS)
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
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
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
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()
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 )
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 """