Coverage for tasks/panss.py : 67%

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