Coverage for tasks/paradise24.py: 47%
55 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/paradise24.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**PARADISE 24 task.**
28"""
30from typing import Any, cast, Optional, Type
32from cardinal_pythonlib.stringfunc import strseq
33from sqlalchemy.sql.sqltypes import Integer
35from camcops_server.cc_modules.cc_constants import CssClass
36from camcops_server.cc_modules.cc_db import add_multiple_columns
37from camcops_server.cc_modules.cc_html import tr_qa, tr, answer
38from camcops_server.cc_modules.cc_request import CamcopsRequest
39from camcops_server.cc_modules.cc_task import TaskHasPatientMixin, Task
42class Paradise24( # type: ignore[misc]
43 TaskHasPatientMixin,
44 Task,
45):
46 __tablename__ = "paradise24"
47 shortname = "PARADISE 24"
49 Q_PREFIX = "q"
50 FIRST_Q = 1
51 LAST_Q = 24
53 @classmethod
54 def extend_columns(cls: Type["Paradise24"], **kwargs: Any) -> None:
56 add_multiple_columns(
57 cls,
58 cls.Q_PREFIX,
59 cls.FIRST_Q,
60 cls.LAST_Q,
61 coltype=Integer,
62 minimum=0,
63 maximum=2,
64 comment_fmt="Q{n} - {s}",
65 comment_strings=[
66 "rested 0-2 (none - a lot)",
67 "loss interest 0-2 (none - a lot)",
68 "appetite 0-2 (none - a lot)",
69 "sleeping 0-2 (none - a lot)",
70 "irritable 0-2 (none - a lot)",
71 "slowed down 0-2 (none - a lot)",
72 "sad 0-2 (none - a lot)",
73 "worry 0-2 (none - a lot)",
74 "cope 0-2 (none - a lot)",
75 "pain 0-2 (none - a lot)",
76 "concentrating 0-2 (none - a lot)",
77 "remembering 0-2 (none - a lot)",
78 "decisions 0-2 (none - a lot)",
79 "conversation 0-2 (none - a lot)",
80 "walking 0-2 (none - a lot)",
81 "grooming 0-2 (none - a lot)",
82 "sexual 0-2 (none - a lot)",
83 "staying by yourself 0-2 (none - a lot)",
84 "health 0-2 (none - a lot)",
85 "friendship 0-2 (none - a lot)",
86 "getting along 0-2 (none - a lot)",
87 "work or school 0-2 (none - a lot)",
88 "money 0-2 (none - a lot)",
89 "community 0-2 (none - a lot)",
90 ],
91 )
93 ALL_FIELD_NAMES = strseq(Q_PREFIX, FIRST_Q, LAST_Q)
95 @staticmethod
96 def longname(req: CamcopsRequest) -> str:
97 _ = req.gettext
98 return _(
99 "Psychosocial fActors Relevant to BrAin DISorders in Europe–24"
100 )
102 def is_complete(self) -> bool:
103 if self.any_fields_none(self.ALL_FIELD_NAMES):
104 return False
106 return True
108 def total_score(self) -> Optional[int]:
109 if not self.is_complete():
110 return None
112 return cast(int, self.sum_fields(self.ALL_FIELD_NAMES))
114 def metric_score(self) -> Optional[int]:
115 total_score = self.total_score()
117 if total_score is None:
118 return None
120 # Table 3 of Cieza et al. (2015); see help.
121 # - doi:10.1371/journal.pone.0132410.t003
122 # - Indexes are raw scores.
123 # - Values are transformed scores.
124 score_lookup = [
125 0, # 0
126 10,
127 19,
128 25,
129 29,
130 33,
131 36,
132 38,
133 41,
134 43,
135 45, # 10
136 46,
137 48,
138 50,
139 51,
140 53,
141 54,
142 55,
143 57,
144 58,
145 59, # 20
146 60,
147 61,
148 63,
149 64,
150 65,
151 66,
152 67,
153 68,
154 69,
155 71, # 30
156 72,
157 73,
158 74,
159 76,
160 77,
161 78,
162 80,
163 81,
164 83,
165 85, # 40
166 87,
167 89,
168 91,
169 92,
170 94,
171 96,
172 98,
173 100, # 48
174 ]
176 try:
177 return score_lookup[total_score]
178 except (IndexError, TypeError):
179 return None
181 def get_task_html(self, req: CamcopsRequest) -> str:
182 rows = ""
183 for q_num in range(self.FIRST_Q, self.LAST_Q + 1):
184 field = self.Q_PREFIX + str(q_num)
185 question_cell = f"{q_num}. {self.xstring(req, field)}"
187 rows += tr_qa(question_cell, self.get_answer_cell(req, q_num))
189 html = """
190 <div class="{CssClass.SUMMARY}">
191 <table class="{CssClass.SUMMARY}">
192 {tr_is_complete}
193 {total_score}
194 {metric_score}
195 </table>
196 </div>
197 <table class="{CssClass.TASKDETAIL}">
198 <tr>
199 <th width="60%">Question</th>
200 <th width="40%">Score</th>
201 </tr>
202 {rows}
203 </table>
204 <div class="{CssClass.FOOTNOTES}">
205 [1] Sum of all questions, range 0–48.
206 [2] Transformed metric scale, range 0–100.
207 </div>
208 """.format(
209 CssClass=CssClass,
210 tr_is_complete=self.get_is_complete_tr(req),
211 total_score=tr(
212 self.wxstring(req, "raw_score") + " <sup>[1]</sup>",
213 f"{answer(self.total_score())}",
214 ),
215 metric_score=tr(
216 self.wxstring(req, "metric_score") + " <sup>[2]</sup>",
217 f"{answer(self.metric_score())}",
218 ),
219 rows=rows,
220 )
221 return html
223 def get_answer_cell(self, req: CamcopsRequest, q_num: int) -> str:
224 q_field = self.Q_PREFIX + str(q_num)
226 score = getattr(self, q_field)
227 meaning = self.get_score_meaning(req, score)
228 answer_cell = f"{score} [{meaning}]"
230 return answer_cell
232 def get_score_meaning(self, req: CamcopsRequest, score: int) -> str:
233 return self.wxstring(req, f"option_{score}")