Coverage for tasks/pswq.py : 59%

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