Coverage for tasks/pdss.py: 63%
63 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/pdss.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
32import cardinal_pythonlib.rnc_web as ws
33from cardinal_pythonlib.stringfunc import strseq
34from sqlalchemy.ext.declarative import DeclarativeMeta
35from sqlalchemy.sql.sqltypes import Float, Integer
37from camcops_server.cc_modules.cc_constants import (
38 CssClass,
39 DATA_COLLECTION_UNLESS_UPGRADED_DIV,
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, tr
44from camcops_server.cc_modules.cc_request import CamcopsRequest
45from camcops_server.cc_modules.cc_snomed import SnomedExpression, SnomedLookup
46from camcops_server.cc_modules.cc_summaryelement import SummaryElement
47from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin
48from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
51# =============================================================================
52# PDSS
53# =============================================================================
55DP = 3
58class PdssMetaclass(DeclarativeMeta):
59 # noinspection PyInitNewSignature
60 def __init__(
61 cls: Type["Pdss"],
62 name: str,
63 bases: Tuple[Type, ...],
64 classdict: Dict[str, Any],
65 ) -> None:
66 add_multiple_columns(
67 cls,
68 "q",
69 1,
70 cls.NQUESTIONS,
71 minimum=cls.MIN_PER_Q,
72 maximum=cls.MAX_PER_Q,
73 comment_fmt="Q{n}, {s} (0-4, higher worse)",
74 comment_strings=[
75 "frequency",
76 "distressing during",
77 "anxiety about panic",
78 "places or situations avoided",
79 "activities avoided",
80 "interference with responsibilities",
81 "interference with social life",
82 ],
83 )
84 super().__init__(name, bases, classdict)
87class Pdss(TaskHasPatientMixin, Task, metaclass=PdssMetaclass):
88 """
89 Server implementation of the PDSS task.
90 """
92 __tablename__ = "pdss"
93 shortname = "PDSS"
94 provides_trackers = True
96 MIN_PER_Q = 0
97 MAX_PER_Q = 4
98 NQUESTIONS = 7
99 QUESTION_FIELDS = strseq("q", 1, NQUESTIONS)
100 MAX_TOTAL = MAX_PER_Q * NQUESTIONS
101 MAX_COMPOSITE = 4
103 @staticmethod
104 def longname(req: "CamcopsRequest") -> str:
105 _ = req.gettext
106 return _("Panic Disorder Severity Scale")
108 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
109 return [
110 TrackerInfo(
111 value=self.total_score(),
112 plot_label="PDSS total score (lower is better)",
113 axis_label=f"Total score (out of {self.MAX_TOTAL})",
114 axis_min=-0.5,
115 axis_max=self.MAX_TOTAL + 0.5,
116 )
117 ]
119 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
120 return self.standard_task_summary_fields() + [
121 SummaryElement(
122 name="total_score",
123 coltype=Integer(),
124 value=self.total_score(),
125 comment=f"Total score (/ {self.MAX_TOTAL})",
126 ),
127 SummaryElement(
128 name="composite_score",
129 coltype=Float(),
130 value=self.composite_score(),
131 comment=f"Composite score (/ {self.MAX_COMPOSITE})",
132 ),
133 ]
135 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
136 if not self.is_complete():
137 return CTV_INCOMPLETE
138 t = self.total_score()
139 c = ws.number_to_dp(self.composite_score(), DP, default="?")
140 return [
141 CtvInfo(
142 content=f"PDSS total score {t}/{self.MAX_TOTAL} "
143 f"(composite {c}/{self.MAX_COMPOSITE})"
144 )
145 ]
147 def total_score(self) -> int:
148 return self.sum_fields(self.QUESTION_FIELDS)
150 def composite_score(self) -> int:
151 return self.mean_fields(self.QUESTION_FIELDS)
153 def is_complete(self) -> bool:
154 return self.field_contents_valid() and self.all_fields_not_none(
155 self.QUESTION_FIELDS
156 )
158 def get_task_html(self, req: CamcopsRequest) -> str:
159 h = """
160 <div class="{CssClass.SUMMARY}">
161 <table class="{CssClass.SUMMARY}">
162 {complete_tr}
163 <tr>
164 <td>Total score</td>
165 <td>{total} / {tmax}</td>
166 </td>
167 <tr>
168 <td>Composite (mean) score</td>
169 <td>{composite} / {cmax}</td>
170 </td>
171 </table>
172 </div>
173 <table class="{CssClass.TASKDETAIL}">
174 <tr>
175 <th width="60%">Question</th>
176 <th width="40%">Answer ({qmin}–{qmax})</th>
177 </tr>
178 """.format(
179 CssClass=CssClass,
180 complete_tr=self.get_is_complete_tr(req),
181 total=answer(self.total_score()),
182 tmax=self.MAX_TOTAL,
183 composite=answer(
184 ws.number_to_dp(self.composite_score(), DP, default="?")
185 ),
186 cmax=self.MAX_COMPOSITE,
187 qmin=self.MIN_PER_Q,
188 qmax=self.MAX_PER_Q,
189 )
190 for q in range(1, self.NQUESTIONS + 1):
191 qtext = self.wxstring(req, "q" + str(q))
192 a = getattr(self, "q" + str(q))
193 atext = (
194 self.wxstring(req, f"q{q}_option{a}", str(a))
195 if a is not None
196 else None
197 )
198 h += tr(qtext, answer(atext))
199 h += (
200 """
201 </table>
202 """
203 + DATA_COLLECTION_UNLESS_UPGRADED_DIV
204 )
205 return h
207 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
208 if not self.is_complete():
209 return []
210 return [
211 SnomedExpression(
212 req.snomed(SnomedLookup.PDSS_SCALE),
213 {req.snomed(SnomedLookup.PDSS_SCORE): self.total_score()},
214 )
215 ]