Coverage for tasks/pswq.py: 59%
66 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/pswq.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, Optional, 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, tr
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_summaryelement import SummaryElement
43from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin
44from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
47# =============================================================================
48# PSWQ
49# =============================================================================
52class PswqMetaclass(DeclarativeMeta):
53 # noinspection PyInitNewSignature
54 def __init__(
55 cls: Type["Pswq"],
56 name: str,
57 bases: Tuple[Type, ...],
58 classdict: Dict[str, Any],
59 ) -> None:
60 add_multiple_columns(
61 cls,
62 "q",
63 1,
64 cls.NQUESTIONS,
65 minimum=cls.MIN_PER_Q,
66 maximum=cls.MAX_PER_Q,
67 comment_fmt="Q{n}, {s} (1-5)",
68 comment_strings=[
69 "OK if not enough time [REVERSE SCORE]", # 1
70 "worries overwhelm",
71 "do not tend to worry [REVERSE SCORE]",
72 "many situations make me worry",
73 "cannot help worrying", # 5
74 "worry under pressure",
75 "always worrying",
76 "easily dismiss worries [REVERSE SCORE]",
77 "finish then worry about next thing",
78 "never worry [REVERSE SCORE]", # 10
79 "if nothing more to do, I do not worry [REVERSE SCORE]",
80 "lifelong worrier",
81 "have been worrying",
82 "when start worrying cannot stop",
83 "worry all the time", # 15
84 "worry about projects until done",
85 ],
86 )
87 super().__init__(name, bases, classdict)
90class Pswq(TaskHasPatientMixin, Task, metaclass=PswqMetaclass):
91 """
92 Server implementation of the PSWQ task.
93 """
95 __tablename__ = "pswq"
96 shortname = "PSWQ"
97 provides_trackers = True
99 MIN_PER_Q = 1
100 MAX_PER_Q = 5
101 NQUESTIONS = 16
102 REVERSE_SCORE = [1, 3, 8, 10, 11]
103 TASK_FIELDS = strseq("q", 1, NQUESTIONS)
104 MIN_TOTAL = MIN_PER_Q * NQUESTIONS
105 MAX_TOTAL = MAX_PER_Q * NQUESTIONS
107 @staticmethod
108 def longname(req: "CamcopsRequest") -> str:
109 _ = req.gettext
110 return _("Penn State Worry Questionnaire")
112 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
113 return [
114 TrackerInfo(
115 value=self.total_score(),
116 plot_label="PSWQ total score (lower is better)",
117 axis_label=f"Total score ({self.MIN_TOTAL}–{self.MAX_TOTAL})",
118 axis_min=self.MIN_TOTAL - 0.5,
119 axis_max=self.MAX_TOTAL + 0.5,
120 )
121 ]
123 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
124 return self.standard_task_summary_fields() + [
125 SummaryElement(
126 name="total_score",
127 coltype=Integer(),
128 value=self.total_score(),
129 comment="Total score (16-80)",
130 )
131 ]
133 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
134 if not self.is_complete():
135 return CTV_INCOMPLETE
136 return [
137 CtvInfo(
138 content=f"PSWQ total score {self.total_score()} "
139 f"(range {self.MIN_TOTAL}–{self.MAX_TOTAL})"
140 )
141 ]
143 def score(self, q: int) -> Optional[int]:
144 value = getattr(self, "q" + str(q))
145 if value is None:
146 return None
147 if q in self.REVERSE_SCORE:
148 return self.MAX_PER_Q + 1 - value
149 else:
150 return value
152 def total_score(self) -> int:
153 values = [self.score(q) for q in range(1, self.NQUESTIONS + 1)]
154 return sum(v for v in values if v is not None)
156 def is_complete(self) -> bool:
157 return (
158 self.all_fields_not_none(self.TASK_FIELDS)
159 and self.field_contents_valid()
160 )
162 def get_task_html(self, req: CamcopsRequest) -> str:
163 h = f"""
164 <div class="{CssClass.SUMMARY}">
165 <table class="{CssClass.SUMMARY}">
166 {self.get_is_complete_tr(req)}
167 <tr>
168 <td>Total score (16–80)</td>
169 <td>{answer(self.total_score())}</td>
170 </td>
171 </table>
172 </div>
173 <div class="{CssClass.EXPLANATION}">
174 Anchor points are 1 = {self.wxstring(req, "anchor1")},
175 5 = {self.wxstring(req, "anchor5")}.
176 Questions {", ".join(str(x) for x in self.REVERSE_SCORE)}
177 are reverse-scored.
178 </div>
179 <table class="{CssClass.TASKDETAIL}">
180 <tr>
181 <th width="70%">Question</th>
182 <th width="15%">Answer (1–5)</th>
183 <th width="15%">Score (1–5)</th>
184 </tr>
185 """
186 for q in range(1, self.NQUESTIONS + 1):
187 a = getattr(self, "q" + str(q))
188 score = self.score(q)
189 h += tr(self.wxstring(req, "q" + str(q)), answer(a), score)
190 h += """
191 </table>
192 """
193 return h
195 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
196 codes = [
197 SnomedExpression(
198 req.snomed(SnomedLookup.PSWQ_PROCEDURE_ASSESSMENT)
199 )
200 ]
201 if self.is_complete():
202 codes.append(
203 SnomedExpression(
204 req.snomed(SnomedLookup.PSWQ_SCALE),
205 {req.snomed(SnomedLookup.PSWQ_SCORE): self.total_score()},
206 )
207 )
208 return codes