Coverage for tasks/wsas.py: 61%
66 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/wsas.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, Optional, Type
30from cardinal_pythonlib.stringfunc import strseq
31from sqlalchemy.orm import Mapped, mapped_column
32from sqlalchemy.sql.sqltypes import 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, get_true_false, tr, tr_qa
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_string import AS
44from camcops_server.cc_modules.cc_summaryelement import SummaryElement
45from camcops_server.cc_modules.cc_task import (
46 get_from_dict,
47 Task,
48 TaskHasPatientMixin,
49)
50from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo
53# =============================================================================
54# WSAS
55# =============================================================================
58class Wsas( # type: ignore[misc]
59 TaskHasPatientMixin,
60 Task,
61):
62 """
63 Server implementation of the WSAS task.
64 """
66 __tablename__ = "wsas"
67 shortname = "WSAS"
68 provides_trackers = True
70 @classmethod
71 def extend_columns(cls: Type["Wsas"], **kwargs: Any) -> None:
72 add_multiple_columns(
73 cls,
74 "q",
75 1,
76 cls.NQUESTIONS,
77 minimum=cls.MIN_PER_Q,
78 maximum=cls.MAX_PER_Q,
79 comment_fmt="Q{n}, {s} (0-4, higher worse)",
80 comment_strings=[
81 "work",
82 "home management",
83 "social leisure",
84 "private leisure",
85 "relationships",
86 ],
87 )
89 retired_etc: Mapped[Optional[bool]] = mapped_column(
90 comment="Retired or choose not to have job for reason unrelated "
91 "to problem",
92 )
94 MIN_PER_Q = 0
95 MAX_PER_Q = 8
96 NQUESTIONS = 5
97 QUESTION_FIELDS = strseq("q", 1, NQUESTIONS)
98 Q2_TO_END = strseq("q", 2, NQUESTIONS)
99 TASK_FIELDS = QUESTION_FIELDS + ["retired_etc"]
100 MAX_IF_WORKING = MAX_PER_Q * NQUESTIONS
101 MAX_IF_RETIRED = MAX_PER_Q * (NQUESTIONS - 1)
103 @staticmethod
104 def longname(req: "CamcopsRequest") -> str:
105 _ = req.gettext
106 return _("Work and Social Adjustment Scale")
108 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
109 return [
110 TrackerInfo(
111 value=self.total_score(),
112 plot_label="WSAS total score (lower is better)",
113 axis_label=f"Total score (out of "
114 f"{self.MAX_IF_RETIRED}–{self.MAX_IF_WORKING})",
115 axis_min=-0.5,
116 axis_max=self.MAX_IF_WORKING + 0.5,
117 )
118 ]
120 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
121 return self.standard_task_summary_fields() + [
122 SummaryElement(
123 name="total_score",
124 coltype=Integer(),
125 value=self.total_score(),
126 comment=f"Total score (/ {self.max_score()})",
127 )
128 ]
130 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
131 if not self.is_complete():
132 return CTV_INCOMPLETE
133 return [
134 CtvInfo(
135 content=f"WSAS total score "
136 f"{self.total_score()}/{self.max_score()}"
137 )
138 ]
140 def total_score(self) -> int:
141 return cast(
142 int,
143 self.sum_fields(
144 self.Q2_TO_END if self.retired_etc else self.QUESTION_FIELDS
145 ),
146 )
148 def max_score(self) -> int:
149 return self.MAX_IF_RETIRED if self.retired_etc else self.MAX_IF_WORKING
151 def is_complete(self) -> bool:
152 return (
153 self.all_fields_not_none(
154 self.Q2_TO_END if self.retired_etc else self.QUESTION_FIELDS
155 )
156 and self.field_contents_valid()
157 )
159 def get_task_html(self, req: CamcopsRequest) -> str:
160 option_dict: dict[Optional[int], Optional[str]] = {None: None}
161 for a in range(self.MIN_PER_Q, self.MAX_PER_Q + 1):
162 option_dict[a] = req.wappstring(AS.WSAS_A_PREFIX + str(a))
163 q_a = ""
164 for q in range(1, self.NQUESTIONS + 1):
165 a = getattr(self, "q" + str(q))
166 fa = get_from_dict(option_dict, a) if a is not None else None
167 q_a += tr(self.wxstring(req, "q" + str(q)), answer(fa))
168 return f"""
169 <div class="{CssClass.SUMMARY}">
170 <table class="{CssClass.SUMMARY}">
171 {self.get_is_complete_tr(req)}
172 <tr>
173 <td>Total score</td>
174 <td>{answer(self.total_score())} / 40</td>
175 </td>
176 </table>
177 </div>
178 <table class="{CssClass.TASKDETAIL}">
179 <tr>
180 <th width="75%">Question</th>
181 <th width="25%">Answer</th>
182 </tr>
183 {tr_qa(self.wxstring(req, "q_retired_etc"),
184 get_true_false(req, self.retired_etc))}
185 </table>
186 <table class="{CssClass.TASKDETAIL}">
187 <tr>
188 <th width="75%">Question</th>
189 <th width="25%">Answer (0–8)</th>
190 </tr>
191 {q_a}
192 </table>
193 {DATA_COLLECTION_UNLESS_UPGRADED_DIV}
194 """
196 # noinspection PyUnresolvedReferences
197 def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
198 codes = [
199 SnomedExpression(
200 req.snomed(SnomedLookup.WSAS_PROCEDURE_ASSESSMENT)
201 )
202 ]
203 if self.is_complete():
204 d = {
205 req.snomed(SnomedLookup.WSAS_SCORE): self.total_score(),
206 req.snomed(SnomedLookup.WSAS_HOME_MANAGEMENT_SCORE): self.q2, # type: ignore[attr-defined] # noqa: E501
207 req.snomed(SnomedLookup.WSAS_SOCIAL_LEISURE_SCORE): self.q3, # type: ignore[attr-defined] # noqa: E501
208 req.snomed(SnomedLookup.WSAS_PRIVATE_LEISURE_SCORE): self.q4, # type: ignore[attr-defined] # noqa: E501
209 req.snomed(SnomedLookup.WSAS_RELATIONSHIPS_SCORE): self.q5, # type: ignore[attr-defined] # noqa: E501
210 }
211 if not self.retired_etc:
212 d[req.snomed(SnomedLookup.WSAS_WORK_SCORE)] = self.q1 # type: ignore[attr-defined] # noqa: E501
213 codes.append(
214 SnomedExpression(req.snomed(SnomedLookup.WSAS_SCALE), d) # type: ignore[arg-type] # noqa: E501
215 )
216 return codes