Coverage for tasks/pdss.py: 62%
60 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/tasks/pdss.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
11 CamCOPS is free software: you can redistribute it and/or modify
12 it under the terms of the GNU General Public License as published by
13 the Free Software Foundation, either version 3 of the License, or
14 (at your option) any later version.
16 CamCOPS is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
21 You should have received a copy of the GNU General Public License
22 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
24===============================================================================
26"""
28from typing import Any, cast, List, Type, Union
30import cardinal_pythonlib.rnc_web as ws
31from cardinal_pythonlib.stringfunc import strseq
32from sqlalchemy.sql.sqltypes import Float, Integer
34from camcops_server.cc_modules.cc_constants import (
35 CssClass,
36 DATA_COLLECTION_UNLESS_UPGRADED_DIV,
37)
38from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
39from camcops_server.cc_modules.cc_db import add_multiple_columns
40from camcops_server.cc_modules.cc_html import answer, tr
41from camcops_server.cc_modules.cc_request import CamcopsRequest
42from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup
43from camcops_server.cc_modules.cc_summaryelement import SummaryElement
44from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin
45from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
48# =============================================================================
49# PDSS
50# =============================================================================
52DP = 3
55class Pdss( # type: ignore[misc]
56 TaskHasPatientMixin,
57 Task,
58):
59 """
60 Server implementation of the PDSS task.
61 """
63 __tablename__ = "pdss"
64 shortname = "PDSS"
65 provides_trackers = True
67 MIN_PER_Q = 0
68 MAX_PER_Q = 4
69 NQUESTIONS = 7
71 @classmethod
72 def extend_columns(cls: Type["Pdss"], **kwargs: Any) -> None:
73 add_multiple_columns(
74 cls,
75 "q",
76 1,
77 cls.NQUESTIONS,
78 minimum=cls.MIN_PER_Q,
79 maximum=cls.MAX_PER_Q,
80 comment_fmt="Q{n}, {s} (0-4, higher worse)",
81 comment_strings=[
82 "frequency",
83 "distressing during",
84 "anxiety about panic",
85 "places or situations avoided",
86 "activities avoided",
87 "interference with responsibilities",
88 "interference with social life",
89 ],
90 )
92 QUESTION_FIELDS = strseq("q", 1, NQUESTIONS)
93 MAX_TOTAL = MAX_PER_Q * NQUESTIONS
94 MAX_COMPOSITE = 4
96 @staticmethod
97 def longname(req: "CamcopsRequest") -> str:
98 _ = req.gettext
99 return _("Panic Disorder Severity Scale")
101 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
102 return [
103 TrackerInfo(
104 value=self.total_score(),
105 plot_label="PDSS total score (lower is better)",
106 axis_label=f"Total score (out of {self.MAX_TOTAL})",
107 axis_min=-0.5,
108 axis_max=self.MAX_TOTAL + 0.5,
109 )
110 ]
112 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
113 return self.standard_task_summary_fields() + [
114 SummaryElement(
115 name="total_score",
116 coltype=Integer(),
117 value=self.total_score(),
118 comment=f"Total score (/ {self.MAX_TOTAL})",
119 ),
120 SummaryElement(
121 name="composite_score",
122 coltype=Float(),
123 value=self.composite_score(),
124 comment=f"Composite score (/ {self.MAX_COMPOSITE})",
125 ),
126 ]
128 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
129 if not self.is_complete():
130 return CTV_INCOMPLETE
131 t = self.total_score()
132 c = ws.number_to_dp(self.composite_score(), DP, default="?")
133 return [
134 CtvInfo(
135 content=f"PDSS total score {t}/{self.MAX_TOTAL} "
136 f"(composite {c}/{self.MAX_COMPOSITE})"
137 )
138 ]
140 def total_score(self) -> int:
141 return cast(int, self.sum_fields(self.QUESTION_FIELDS))
143 def composite_score(self) -> Union[int, float]:
144 return self.mean_fields(self.QUESTION_FIELDS)
146 def is_complete(self) -> bool:
147 return self.field_contents_valid() and self.all_fields_not_none(
148 self.QUESTION_FIELDS
149 )
151 def get_task_html(self, req: CamcopsRequest) -> str:
152 h = """
153 <div class="{CssClass.SUMMARY}">
154 <table class="{CssClass.SUMMARY}">
155 {complete_tr}
156 <tr>
157 <td>Total score</td>
158 <td>{total} / {tmax}</td>
159 </td>
160 <tr>
161 <td>Composite (mean) score</td>
162 <td>{composite} / {cmax}</td>
163 </td>
164 </table>
165 </div>
166 <table class="{CssClass.TASKDETAIL}">
167 <tr>
168 <th width="60%">Question</th>
169 <th width="40%">Answer ({qmin}–{qmax})</th>
170 </tr>
171 """.format(
172 CssClass=CssClass,
173 complete_tr=self.get_is_complete_tr(req),
174 total=answer(self.total_score()),
175 tmax=self.MAX_TOTAL,
176 composite=answer(
177 ws.number_to_dp(self.composite_score(), DP, default="?")
178 ),
179 cmax=self.MAX_COMPOSITE,
180 qmin=self.MIN_PER_Q,
181 qmax=self.MAX_PER_Q,
182 )
183 for q in range(1, self.NQUESTIONS + 1):
184 qtext = self.wxstring(req, "q" + str(q))
185 a = getattr(self, "q" + str(q))
186 atext = (
187 self.wxstring(req, f"q{q}_option{a}", str(a))
188 if a is not None
189 else None
190 )
191 h += tr(qtext, answer(atext))
192 h += (
193 """
194 </table>
195 """
196 + DATA_COLLECTION_UNLESS_UPGRADED_DIV
197 )
198 return h
200 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
201 if not self.is_complete():
202 return []
203 return [
204 SnomedExpression(
205 req.snomed(SnomedLookup.PDSS_SCALE),
206 {req.snomed(SnomedLookup.PDSS_SCORE): self.total_score()},
207 )
208 ]