Coverage for tasks/npiq.py: 44%
90 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/npiq.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 Boolean, Integer
36from camcops_server.cc_modules.cc_constants import (
37 CssClass,
38 DATA_COLLECTION_UNLESS_UPGRADED_DIV,
39 PV,
40)
41from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
42from camcops_server.cc_modules.cc_db import add_multiple_columns
43from camcops_server.cc_modules.cc_html import answer, get_yes_no_unknown, tr
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 (
47 Task,
48 TaskHasPatientMixin,
49 TaskHasRespondentMixin,
50)
53# =============================================================================
54# NPI-Q
55# =============================================================================
57ENDORSED = "endorsed"
58SEVERITY = "severity"
59DISTRESS = "distress"
62class NpiQMetaclass(DeclarativeMeta):
63 # noinspection PyInitNewSignature
64 def __init__(
65 cls: Type["NpiQ"],
66 name: str,
67 bases: Tuple[Type, ...],
68 classdict: Dict[str, Any],
69 ) -> None:
70 question_snippets = [
71 "delusions", # 1
72 "hallucinations",
73 "agitation/aggression",
74 "depression/dysphoria",
75 "anxiety", # 5
76 "elation/euphoria",
77 "apathy/indifference",
78 "disinhibition",
79 "irritability/lability",
80 "motor disturbance", # 10
81 "night-time behaviour",
82 "appetite/eating",
83 ]
84 add_multiple_columns(
85 cls,
86 ENDORSED,
87 1,
88 cls.NQUESTIONS,
89 Boolean,
90 pv=PV.BIT,
91 comment_fmt="Q{n}, {s}, endorsed?",
92 comment_strings=question_snippets,
93 )
94 add_multiple_columns(
95 cls,
96 SEVERITY,
97 1,
98 cls.NQUESTIONS,
99 pv=list(range(1, 3 + 1)),
100 comment_fmt="Q{n}, {s}, severity (1-3), if endorsed",
101 comment_strings=question_snippets,
102 )
103 add_multiple_columns(
104 cls,
105 DISTRESS,
106 1,
107 cls.NQUESTIONS,
108 pv=list(range(0, 5 + 1)),
109 comment_fmt="Q{n}, {s}, distress (0-5), if endorsed",
110 comment_strings=question_snippets,
111 )
112 super().__init__(name, bases, classdict)
115class NpiQ(
116 TaskHasPatientMixin, TaskHasRespondentMixin, Task, metaclass=NpiQMetaclass
117):
118 """
119 Server implementation of the NPI-Q task.
120 """
122 __tablename__ = "npiq"
123 shortname = "NPI-Q"
125 NQUESTIONS = 12
126 ENDORSED_FIELDS = strseq(ENDORSED, 1, NQUESTIONS)
127 MAX_SEVERITY = 3 * NQUESTIONS
128 MAX_DISTRESS = 5 * NQUESTIONS
130 @staticmethod
131 def longname(req: "CamcopsRequest") -> str:
132 _ = req.gettext
133 return _("Neuropsychiatric Inventory Questionnaire")
135 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
136 return self.standard_task_summary_fields() + [
137 SummaryElement(
138 name="n_endorsed",
139 coltype=Integer(),
140 value=self.n_endorsed(),
141 comment=f"Number endorsed (/ {self.NQUESTIONS})",
142 ),
143 SummaryElement(
144 name="severity_score",
145 coltype=Integer(),
146 value=self.severity_score(),
147 comment=f"Severity score (/ {self.MAX_SEVERITY})",
148 ),
149 SummaryElement(
150 name="distress_score",
151 coltype=Integer(),
152 value=self.distress_score(),
153 comment=f"Distress score (/ {self.MAX_DISTRESS})",
154 ),
155 ]
157 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
158 if not self.is_complete():
159 return CTV_INCOMPLETE
160 return [
161 CtvInfo(
162 content=(
163 "Endorsed: {e}/{me}; severity {s}/{ms}; "
164 "distress {d}/{md}".format(
165 e=self.n_endorsed(),
166 me=self.NQUESTIONS,
167 s=self.severity_score(),
168 ms=self.MAX_SEVERITY,
169 d=self.distress_score(),
170 md=self.MAX_DISTRESS,
171 )
172 )
173 )
174 ]
176 def q_endorsed(self, q: int) -> bool:
177 return bool(getattr(self, ENDORSED + str(q)))
179 def n_endorsed(self) -> int:
180 return self.count_booleans(self.ENDORSED_FIELDS)
182 def severity_score(self) -> int:
183 total = 0
184 for q in range(1, self.NQUESTIONS + 1):
185 if self.q_endorsed(q):
186 s = getattr(self, SEVERITY + str(q))
187 if s is not None:
188 total += s
189 return total
191 def distress_score(self) -> int:
192 total = 0
193 for q in range(1, self.NQUESTIONS + 1):
194 if self.q_endorsed(q):
195 d = getattr(self, DISTRESS + str(q))
196 if d is not None:
197 total += d
198 return total
200 def q_complete(self, q: int) -> bool:
201 qstr = str(q)
202 endorsed = getattr(self, ENDORSED + qstr)
203 if endorsed is None:
204 return False
205 if not endorsed:
206 return True
207 if getattr(self, SEVERITY + qstr) is None:
208 return False
209 if getattr(self, DISTRESS + qstr) is None:
210 return False
211 return True
213 def is_complete(self) -> bool:
214 return (
215 self.is_respondent_complete()
216 and all(self.q_complete(q) for q in range(1, self.NQUESTIONS + 1))
217 and self.field_contents_valid()
218 )
220 def get_task_html(self, req: CamcopsRequest) -> str:
221 h = f"""
222 <div class="{CssClass.SUMMARY}">
223 <table class="{CssClass.SUMMARY}">
224 {self.get_is_complete_tr(req)}
225 <tr>
226 <td>Endorsed</td>
227 <td>{self.n_endorsed()} / 12</td>
228 </td>
229 <tr>
230 <td>Severity score</td>
231 <td>{self.severity_score()} / 36</td>
232 </td>
233 <tr>
234 <td>Distress score</td>
235 <td>{self.distress_score()} / 60</td>
236 </td>
237 </table>
238 </div>
239 <table class="{CssClass.TASKDETAIL}">
240 <tr>
241 <th width="40%">Question</th>
242 <th width="20%">Endorsed</th>
243 <th width="20%">Severity (patient)</th>
244 <th width="20%">Distress (carer)</th>
245 </tr>
246 """
247 for q in range(1, self.NQUESTIONS + 1):
248 qstr = str(q)
249 e = getattr(self, ENDORSED + qstr)
250 s = getattr(self, SEVERITY + qstr)
251 d = getattr(self, DISTRESS + qstr)
252 qtext = "<b>{}:</b> {}".format(
253 self.wxstring(req, "t" + qstr), self.wxstring(req, "q" + qstr)
254 )
255 etext = get_yes_no_unknown(req, e)
256 if e:
257 stext = self.wxstring(
258 req, f"severity_{s}", s, provide_default_if_none=False
259 )
260 dtext = self.wxstring(
261 req, f"distress_{d}", d, provide_default_if_none=False
262 )
263 else:
264 stext = ""
265 dtext = ""
266 h += tr(qtext, answer(etext), answer(stext), answer(dtext))
267 h += f"""
268 </table>
269 {DATA_COLLECTION_UNLESS_UPGRADED_DIV}
270 """
271 return h