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