Coverage for tasks/caps.py : 45%

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
3"""
4camcops_server/tasks/caps.py
6===============================================================================
8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
10 This file is part of CamCOPS.
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.
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.
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/>.
25===============================================================================
27"""
29from typing import Any, Dict, List, Tuple, Type
31from cardinal_pythonlib.stringfunc import strseq
32from sqlalchemy.ext.declarative import DeclarativeMeta
33from sqlalchemy.sql.sqltypes import Integer
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
50# =============================================================================
51# CAPS
52# =============================================================================
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]
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)
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
116 prohibits_commercial = True
118 NQUESTIONS = 32
119 ENDORSE_FIELDS = strseq("endorse", 1, NQUESTIONS)
121 @staticmethod
122 def longname(req: "CamcopsRequest") -> str:
123 _ = req.gettext
124 return _("Cardiff Anomalous Perceptions Scale")
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 )]
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 ]
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
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
175 def total_score(self) -> int:
176 return self.count_booleans(self.ENDORSE_FIELDS)
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
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
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
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()
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 )
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 """