Coverage for tasks/panss.py: 67%
82 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/panss.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 (
37 CssClass,
38 DATA_COLLECTION_ONLY_DIV,
39)
40from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
41from camcops_server.cc_modules.cc_db import add_multiple_columns
42from camcops_server.cc_modules.cc_html import tr_qa
43from camcops_server.cc_modules.cc_request import CamcopsRequest
44from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup
45from camcops_server.cc_modules.cc_summaryelement import SummaryElement
46from camcops_server.cc_modules.cc_task import (
47 get_from_dict,
48 Task,
49 TaskHasClinicianMixin,
50 TaskHasPatientMixin,
51)
52from camcops_server.cc_modules.cc_text import SS
53from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
56# =============================================================================
57# PANSS
58# =============================================================================
61class PanssMetaclass(DeclarativeMeta):
62 # noinspection PyInitNewSignature
63 def __init__(
64 cls: Type["Panss"],
65 name: str,
66 bases: Tuple[Type, ...],
67 classdict: Dict[str, Any],
68 ) -> None:
69 add_multiple_columns(
70 cls,
71 "p",
72 1,
73 cls.NUM_P,
74 minimum=1,
75 maximum=7,
76 comment_fmt="P{n}: {s} (1 absent - 7 extreme)",
77 comment_strings=[
78 "delusions",
79 "conceptual disorganisation",
80 "hallucinatory behaviour",
81 "excitement",
82 "grandiosity",
83 "suspiciousness/persecution",
84 "hostility",
85 ],
86 )
87 add_multiple_columns(
88 cls,
89 "n",
90 1,
91 cls.NUM_N,
92 minimum=1,
93 maximum=7,
94 comment_fmt="N{n}: {s} (1 absent - 7 extreme)",
95 comment_strings=[
96 "blunted affect",
97 "emotional withdrawal",
98 "poor rapport",
99 "passive/apathetic social withdrawal",
100 "difficulty in abstract thinking",
101 "lack of spontaneity/conversation flow",
102 "stereotyped thinking",
103 ],
104 )
105 add_multiple_columns(
106 cls,
107 "g",
108 1,
109 cls.NUM_G,
110 minimum=1,
111 maximum=7,
112 comment_fmt="G{n}: {s} (1 absent - 7 extreme)",
113 comment_strings=[
114 "somatic concern",
115 "anxiety",
116 "guilt feelings",
117 "tension",
118 "mannerisms/posturing",
119 "depression",
120 "motor retardation",
121 "uncooperativeness",
122 "unusual thought content",
123 "disorientation",
124 "poor attention",
125 "lack of judgement/insight",
126 "disturbance of volition",
127 "poor impulse control",
128 "preoccupation",
129 "active social avoidance",
130 ],
131 )
132 super().__init__(name, bases, classdict)
135class Panss(
136 TaskHasPatientMixin, TaskHasClinicianMixin, Task, metaclass=PanssMetaclass
137):
138 """
139 Server implementation of the PANSS task.
140 """
142 __tablename__ = "panss"
143 shortname = "PANSS"
144 provides_trackers = True
146 NUM_P = 7
147 NUM_N = 7
148 NUM_G = 16
150 P_FIELDS = strseq("p", 1, NUM_P)
151 N_FIELDS = strseq("n", 1, NUM_N)
152 G_FIELDS = strseq("g", 1, NUM_G)
153 TASK_FIELDS = P_FIELDS + N_FIELDS + G_FIELDS
155 MIN_P = 1 * NUM_P
156 MAX_P = 7 * NUM_P
157 MIN_N = 1 * NUM_N
158 MAX_N = 7 * NUM_N
159 MIN_G = 1 * NUM_G
160 MAX_G = 7 * NUM_G
161 MIN_TOTAL = MIN_P + MIN_N + MIN_G
162 MAX_TOTAL = MAX_P + MAX_N + MAX_G
163 MIN_P_MINUS_N = MIN_P - MAX_N
164 MAX_P_MINUS_N = MAX_P - MIN_N
166 @staticmethod
167 def longname(req: "CamcopsRequest") -> str:
168 _ = req.gettext
169 return _("Positive and Negative Syndrome Scale")
171 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
172 return [
173 TrackerInfo(
174 value=self.total_score(),
175 plot_label="PANSS total score",
176 axis_label=f"Total score ({self.MIN_TOTAL}-{self.MAX_TOTAL})",
177 axis_min=self.MIN_TOTAL - 0.5,
178 axis_max=self.MAX_TOTAL + 0.5,
179 ),
180 TrackerInfo(
181 value=self.score_p(),
182 plot_label="PANSS P score",
183 axis_label=f"P score ({self.MIN_P}-{self.MAX_P})",
184 axis_min=self.MIN_P - 0.5,
185 axis_max=self.MAX_P + 0.5,
186 ),
187 TrackerInfo(
188 value=self.score_n(),
189 plot_label="PANSS N score",
190 axis_label=f"N score ({self.MIN_N}-{self.MAX_N})",
191 axis_min=self.MIN_N - 0.5,
192 axis_max=self.MAX_N + 0.5,
193 ),
194 TrackerInfo(
195 value=self.score_g(),
196 plot_label="PANSS G score",
197 axis_label=f"G score ({self.MIN_G}-{self.MAX_G})",
198 axis_min=self.MIN_G - 0.5,
199 axis_max=self.MAX_G + 0.5,
200 ),
201 TrackerInfo(
202 value=self.composite(),
203 plot_label=f"PANSS composite score "
204 f"({self.MIN_P_MINUS_N} to {self.MAX_P_MINUS_N})",
205 axis_label="P - N",
206 ),
207 ]
209 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
210 if not self.is_complete():
211 return CTV_INCOMPLETE
212 return [
213 CtvInfo(
214 content=(
215 f"PANSS total score {self.total_score()} "
216 f"(P {self.score_p()}, "
217 f"N {self.score_n()}, "
218 f"G {self.score_g()}, "
219 f"composite P–N {self.composite()})"
220 )
221 )
222 ]
224 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
225 return self.standard_task_summary_fields() + [
226 SummaryElement(
227 name="total",
228 coltype=Integer(),
229 value=self.total_score(),
230 comment=f"Total score ({self.MIN_TOTAL}-{self.MAX_TOTAL})",
231 ),
232 SummaryElement(
233 name="p",
234 coltype=Integer(),
235 value=self.score_p(),
236 comment=f"Positive symptom (P) score ({self.MIN_P}-{self.MAX_P})", # noqa
237 ),
238 SummaryElement(
239 name="n",
240 coltype=Integer(),
241 value=self.score_n(),
242 comment=f"Negative symptom (N) score ({self.MIN_N}-{self.MAX_N})", # noqa
243 ),
244 SummaryElement(
245 name="g",
246 coltype=Integer(),
247 value=self.score_g(),
248 comment=f"General symptom (G) score ({self.MIN_G}-{self.MAX_G})", # noqa
249 ),
250 SummaryElement(
251 name="composite",
252 coltype=Integer(),
253 value=self.composite(),
254 comment=f"Composite score (P - N) ({self.MIN_P_MINUS_N} "
255 f"to {self.MAX_P_MINUS_N})",
256 ),
257 ]
259 def is_complete(self) -> bool:
260 return (
261 self.all_fields_not_none(self.TASK_FIELDS)
262 and self.field_contents_valid()
263 )
265 def total_score(self) -> int:
266 return self.sum_fields(self.TASK_FIELDS)
268 def score_p(self) -> int:
269 return self.sum_fields(self.P_FIELDS)
271 def score_n(self) -> int:
272 return self.sum_fields(self.N_FIELDS)
274 def score_g(self) -> int:
275 return self.sum_fields(self.G_FIELDS)
277 def composite(self) -> int:
278 return self.score_p() - self.score_n()
280 def get_task_html(self, req: CamcopsRequest) -> str:
281 p = self.score_p()
282 n = self.score_n()
283 g = self.score_g()
284 composite = self.composite()
285 total = p + n + g
286 answers = {
287 None: None,
288 1: self.wxstring(req, "option1"),
289 2: self.wxstring(req, "option2"),
290 3: self.wxstring(req, "option3"),
291 4: self.wxstring(req, "option4"),
292 5: self.wxstring(req, "option5"),
293 6: self.wxstring(req, "option6"),
294 7: self.wxstring(req, "option7"),
295 }
296 q_a = ""
297 for q in self.TASK_FIELDS:
298 q_a += tr_qa(
299 self.wxstring(req, "" + q + "_s"),
300 get_from_dict(answers, getattr(self, q)),
301 )
302 h = """
303 <div class="{CssClass.SUMMARY}">
304 <table class="{CssClass.SUMMARY}">
305 {tr_is_complete}
306 {total_score}
307 {p}
308 {n}
309 {g}
310 {composite}
311 </table>
312 </div>
313 <table class="{CssClass.TASKDETAIL}">
314 <tr>
315 <th width="40%">Question</th>
316 <th width="60%">Answer</th>
317 </tr>
318 {q_a}
319 </table>
320 {DATA_COLLECTION_ONLY_DIV}
321 """.format(
322 CssClass=CssClass,
323 tr_is_complete=self.get_is_complete_tr(req),
324 total_score=tr_qa(
325 f"{req.sstring(SS.TOTAL_SCORE)} "
326 f"({self.MIN_TOTAL}–{self.MAX_TOTAL})",
327 total,
328 ),
329 p=tr_qa(
330 f"{self.wxstring(req, 'p')} ({self.MIN_P}–{self.MAX_P})", p
331 ),
332 n=tr_qa(
333 f"{self.wxstring(req, 'n')} ({self.MIN_N}–{self.MAX_N})", n
334 ),
335 g=tr_qa(
336 f"{self.wxstring(req, 'g')} ({self.MIN_G}–{self.MAX_G})", g
337 ),
338 composite=tr_qa(
339 f"{self.wxstring(req, 'composite')} "
340 f"({self.MIN_P_MINUS_N}–{self.MAX_P_MINUS_N})",
341 composite,
342 ),
343 q_a=q_a,
344 DATA_COLLECTION_ONLY_DIV=DATA_COLLECTION_ONLY_DIV,
345 )
346 return h
348 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
349 if not self.is_complete():
350 return []
351 return [SnomedExpression(req.snomed(SnomedLookup.PANSS_SCALE))]