Coverage for tasks/phq15.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/phq15.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 CssClass
37from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
38from camcops_server.cc_modules.cc_db import add_multiple_columns
39from camcops_server.cc_modules.cc_html import answer, get_yes_no, tr, tr_qa
40from camcops_server.cc_modules.cc_request import CamcopsRequest
41from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup
42from camcops_server.cc_modules.cc_sqla_coltypes import SummaryCategoryColType
43from camcops_server.cc_modules.cc_summaryelement import SummaryElement
44from camcops_server.cc_modules.cc_task import (
45 get_from_dict,
46 Task,
47 TaskHasPatientMixin,
48)
49from camcops_server.cc_modules.cc_text import SS
50from camcops_server.cc_modules.cc_trackerhelpers import (
51 TrackerInfo,
52 TrackerLabel,
53)
56# =============================================================================
57# PHQ-15
58# =============================================================================
61class Phq15Metaclass(DeclarativeMeta):
62 # noinspection PyInitNewSignature
63 def __init__(
64 cls: Type["Phq15"],
65 name: str,
66 bases: Tuple[Type, ...],
67 classdict: Dict[str, Any],
68 ) -> None:
69 add_multiple_columns(
70 cls,
71 "q",
72 1,
73 cls.NQUESTIONS,
74 minimum=0,
75 maximum=2,
76 comment_fmt="Q{n} ({s}) (0 not bothered at all - "
77 "2 bothered a lot)",
78 comment_strings=[
79 "stomach pain",
80 "back pain",
81 "limb/joint pain",
82 "F - menstrual",
83 "headaches",
84 "chest pain",
85 "dizziness",
86 "fainting",
87 "palpitations",
88 "breathless",
89 "sex",
90 "constipation/diarrhoea",
91 "nausea/indigestion",
92 "energy",
93 "sleep",
94 ],
95 )
96 super().__init__(name, bases, classdict)
99class Phq15(TaskHasPatientMixin, Task, metaclass=Phq15Metaclass):
100 """
101 Server implementation of the PHQ-15 task.
102 """
104 __tablename__ = "phq15"
105 shortname = "PHQ-15"
106 provides_trackers = True
108 NQUESTIONS = 15
109 MAX_TOTAL = 30
111 ONE_TO_THREE = strseq("q", 1, 3)
112 FIVE_TO_END = strseq("q", 5, NQUESTIONS)
113 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
115 @staticmethod
116 def longname(req: "CamcopsRequest") -> str:
117 _ = req.gettext
118 return _("Patient Health Questionnaire-15")
120 # noinspection PyUnresolvedReferences
121 def is_complete(self) -> bool:
122 if not self.field_contents_valid():
123 return False
124 if self.any_fields_none(self.ONE_TO_THREE):
125 return False
126 if self.any_fields_none(self.FIVE_TO_END):
127 return False
128 if self.is_female():
129 return self.q4 is not None
130 else:
131 return True
133 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
134 return [
135 TrackerInfo(
136 value=self.total_score(),
137 plot_label="PHQ-15 total score (rating somatic symptoms)",
138 axis_label=f"Score for Q1-15 (out of {self.MAX_TOTAL})",
139 axis_min=-0.5,
140 axis_max=self.MAX_TOTAL + 0.5,
141 horizontal_lines=[14.5, 9.5, 4.5],
142 horizontal_labels=[
143 TrackerLabel(22, req.sstring(SS.SEVERE)),
144 TrackerLabel(12, req.sstring(SS.MODERATE)),
145 TrackerLabel(7, req.sstring(SS.MILD)),
146 TrackerLabel(2.25, req.sstring(SS.NONE)),
147 ],
148 )
149 ]
151 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
152 if not self.is_complete():
153 return CTV_INCOMPLETE
154 return [
155 CtvInfo(
156 content=(
157 f"PHQ-15 total score {self.total_score()}/{self.MAX_TOTAL} " # noqa: E501
158 f"({self.severity( req)})"
159 )
160 )
161 ]
163 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
164 return self.standard_task_summary_fields() + [
165 SummaryElement(
166 name="total",
167 coltype=Integer(),
168 value=self.total_score(),
169 comment=f"Total score (/{self.MAX_TOTAL})",
170 ),
171 SummaryElement(
172 name="severity",
173 coltype=SummaryCategoryColType,
174 value=self.severity(req),
175 comment="Severity",
176 ),
177 ]
179 def total_score(self) -> int:
180 return self.sum_fields(self.TASK_FIELDS)
182 def num_severe(self) -> int:
183 n = 0
184 for i in range(1, self.NQUESTIONS + 1):
185 value = getattr(self, "q" + str(i))
186 if value is not None and value >= 2:
187 n += 1
188 return n
190 def severity(self, req: CamcopsRequest) -> str:
191 score = self.total_score()
192 if score >= 15:
193 return req.sstring(SS.SEVERE)
194 elif score >= 10:
195 return req.sstring(SS.MODERATE)
196 elif score >= 5:
197 return req.sstring(SS.MILD)
198 else:
199 return req.sstring(SS.NONE)
201 def get_task_html(self, req: CamcopsRequest) -> str:
202 score = self.total_score()
203 nsevere = self.num_severe()
204 somatoform_likely = nsevere >= 3
205 severity = self.severity(req)
206 answer_dict = {None: None}
207 for option in range(0, 3):
208 answer_dict[option] = (
209 str(option) + " – " + self.wxstring(req, "a" + str(option))
210 )
211 q_a = ""
212 for q in range(1, self.NQUESTIONS + 1):
213 q_a += tr_qa(
214 self.wxstring(req, "q" + str(q)),
215 get_from_dict(answer_dict, getattr(self, "q" + str(q))),
216 )
217 h = """
218 <div class="{CssClass.SUMMARY}">
219 <table class="{CssClass.SUMMARY}">
220 {tr_is_complete}
221 {total_score}
222 {n_severe_symptoms}
223 {exceeds_somatoform_cutoff}
224 {symptom_severity}
225 </table>
226 </div>
227 <table class="{CssClass.TASKDETAIL}">
228 <tr>
229 <th width="70%">Question</th>
230 <th width="30%">Answer</th>
231 </tr>
232 {q_a}
233 </table>
234 <div class="{CssClass.FOOTNOTES}">
235 [1] In males, maximum score is actually 28.
236 [2] Questions with scores ≥2 are considered severe.
237 [3] ≥3 severe symptoms.
238 [4] Total score ≥15 severe, ≥10 moderate, ≥5 mild,
239 otherwise none.
240 </div>
241 """.format(
242 CssClass=CssClass,
243 tr_is_complete=self.get_is_complete_tr(req),
244 total_score=tr(
245 req.sstring(SS.TOTAL_SCORE) + " <sup>[1]</sup>",
246 answer(score) + f" / {self.MAX_TOTAL}",
247 ),
248 n_severe_symptoms=tr_qa(
249 self.wxstring(req, "n_severe_symptoms") + " <sup>[2]</sup>",
250 nsevere,
251 ),
252 exceeds_somatoform_cutoff=tr_qa(
253 self.wxstring(req, "exceeds_somatoform_cutoff")
254 + " <sup>[3]</sup>",
255 get_yes_no(req, somatoform_likely),
256 ),
257 symptom_severity=tr_qa(
258 self.wxstring(req, "symptom_severity") + " <sup>[4]</sup>",
259 severity,
260 ),
261 q_a=q_a,
262 )
263 return h
265 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
266 procedure = req.snomed(SnomedLookup.PHQ15_PROCEDURE)
267 codes = [SnomedExpression(procedure)]
268 if self.is_complete():
269 scale = req.snomed(SnomedLookup.PHQ15_SCALE)
270 score = req.snomed(SnomedLookup.PHQ15_SCORE)
271 codes.append(SnomedExpression(scale, {score: self.total_score()}))
272 return codes