Coverage for tasks/npiq.py : 44%

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