Coverage for tasks/empsa.py: 73%
95 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/empsa.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===============================================================================
26Eating and Meal Preparation Skills Assessment (EMPSA)
28"""
30from typing import Optional, TYPE_CHECKING
32import cardinal_pythonlib.rnc_web as ws
33from cardinal_pythonlib.stringfunc import strseq
34from sqlalchemy import UnicodeText
35from sqlalchemy.orm import Mapped
37from camcops_server.cc_modules.cc_constants import CssClass
38from camcops_server.cc_modules.cc_html import answer, tr
39from camcops_server.cc_modules.cc_sqla_coltypes import (
40 mapped_camcops_column,
41 ZERO_TO_10_CHECKER,
42)
43from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin
45if TYPE_CHECKING:
46 from camcops_server.cc_modules.cc_request import CamcopsRequest
48DP = 2
49RANGE_SUFFIX = " (0 none - 10 total)"
52class Empsa(TaskHasPatientMixin, Task): # type: ignore[misc]
53 __tablename__ = "empsa"
54 shortname = "EMPSA"
56 q1_ability: Mapped[Optional[int]] = mapped_camcops_column(
57 permitted_value_checker=ZERO_TO_10_CHECKER,
58 comment="Q1 ability (planning)" + RANGE_SUFFIX,
59 )
60 q1_motivation: Mapped[Optional[int]] = mapped_camcops_column(
61 permitted_value_checker=ZERO_TO_10_CHECKER,
62 comment="Q1 motivation (planning)" + RANGE_SUFFIX,
63 )
64 q1_comments: Mapped[Optional[str]] = mapped_camcops_column(
65 UnicodeText, comment="Q1 comments (planning)"
66 )
68 q2_ability: Mapped[Optional[int]] = mapped_camcops_column(
69 permitted_value_checker=ZERO_TO_10_CHECKER,
70 comment="Q2 ability (budget)" + RANGE_SUFFIX,
71 )
72 q2_motivation: Mapped[Optional[int]] = mapped_camcops_column(
73 permitted_value_checker=ZERO_TO_10_CHECKER,
74 comment="Q2 motivation (budget)" + RANGE_SUFFIX,
75 )
76 q2_comments: Mapped[Optional[str]] = mapped_camcops_column(
77 UnicodeText, comment="Q2 comments (budget)"
78 )
80 q3_ability: Mapped[Optional[int]] = mapped_camcops_column(
81 permitted_value_checker=ZERO_TO_10_CHECKER,
82 comment="Q3 ability (shopping)" + RANGE_SUFFIX,
83 )
84 q3_motivation: Mapped[Optional[int]] = mapped_camcops_column(
85 permitted_value_checker=ZERO_TO_10_CHECKER,
86 comment="Q3 motivation (shopping)" + RANGE_SUFFIX,
87 )
88 q3_comments: Mapped[Optional[str]] = mapped_camcops_column(
89 UnicodeText, comment="Q3 comments (shopping)"
90 )
92 q4_ability: Mapped[Optional[int]] = mapped_camcops_column(
93 permitted_value_checker=ZERO_TO_10_CHECKER,
94 comment="Q4 ability (cooking)" + RANGE_SUFFIX,
95 )
96 q4_motivation: Mapped[Optional[int]] = mapped_camcops_column(
97 permitted_value_checker=ZERO_TO_10_CHECKER,
98 comment="Q4 motivation (cooking)" + RANGE_SUFFIX,
99 )
100 q4_comments: Mapped[Optional[str]] = mapped_camcops_column(
101 UnicodeText, comment="Q4 comments (cooking)"
102 )
104 q5_ability: Mapped[Optional[int]] = mapped_camcops_column(
105 permitted_value_checker=ZERO_TO_10_CHECKER,
106 comment="Q5 ability (preparing)" + RANGE_SUFFIX,
107 )
108 q5_motivation: Mapped[Optional[int]] = mapped_camcops_column(
109 permitted_value_checker=ZERO_TO_10_CHECKER,
110 comment="Q5 motivation (preparing)" + RANGE_SUFFIX,
111 )
112 q5_comments: Mapped[Optional[str]] = mapped_camcops_column(
113 UnicodeText, comment="Q5 comments (preparing)"
114 )
116 q6_ability: Mapped[Optional[int]] = mapped_camcops_column(
117 permitted_value_checker=ZERO_TO_10_CHECKER,
118 comment="Q6 ability (portions)" + RANGE_SUFFIX,
119 )
120 q6_motivation: Mapped[Optional[int]] = mapped_camcops_column(
121 permitted_value_checker=ZERO_TO_10_CHECKER,
122 comment="Q6 motivation (portions)" + RANGE_SUFFIX,
123 )
124 q6_comments: Mapped[Optional[str]] = mapped_camcops_column(
125 UnicodeText, comment="Q6 comments (portions)"
126 )
128 q7_ability: Mapped[Optional[int]] = mapped_camcops_column(
129 permitted_value_checker=ZERO_TO_10_CHECKER,
130 comment="Q7 ability (throwing away)" + RANGE_SUFFIX,
131 )
132 q7_motivation: Mapped[Optional[int]] = mapped_camcops_column(
133 permitted_value_checker=ZERO_TO_10_CHECKER,
134 comment="Q7 motivation (throwing away)" + RANGE_SUFFIX,
135 )
136 q7_comments: Mapped[Optional[str]] = mapped_camcops_column(
137 UnicodeText, comment="Q7 comments (throwing away)"
138 )
140 q8_ability: Mapped[Optional[int]] = mapped_camcops_column(
141 permitted_value_checker=ZERO_TO_10_CHECKER,
142 comment="Q8 ability (difficult food)" + RANGE_SUFFIX,
143 )
144 q8_motivation: Mapped[Optional[int]] = mapped_camcops_column(
145 permitted_value_checker=ZERO_TO_10_CHECKER,
146 comment="Q8 motivation (difficult food)" + RANGE_SUFFIX,
147 )
148 q8_comments: Mapped[Optional[str]] = mapped_camcops_column(
149 UnicodeText, comment="Q8 comments (difficult food)"
150 )
152 q9_ability: Mapped[Optional[int]] = mapped_camcops_column(
153 permitted_value_checker=ZERO_TO_10_CHECKER,
154 comment="Q9 ability (normal pace)" + RANGE_SUFFIX,
155 )
156 q9_motivation: Mapped[Optional[int]] = mapped_camcops_column(
157 permitted_value_checker=ZERO_TO_10_CHECKER,
158 comment="Q9 motivation (normal pace)" + RANGE_SUFFIX,
159 )
160 q9_comments: Mapped[Optional[str]] = mapped_camcops_column(
161 UnicodeText, comment="Q9 comments (normal pace)"
162 )
164 q10_ability: Mapped[Optional[int]] = mapped_camcops_column(
165 permitted_value_checker=ZERO_TO_10_CHECKER,
166 comment="Q10 ability (others)" + RANGE_SUFFIX,
167 )
168 q10_motivation: Mapped[Optional[int]] = mapped_camcops_column(
169 permitted_value_checker=ZERO_TO_10_CHECKER,
170 comment="Q10 motivation (others)" + RANGE_SUFFIX,
171 )
172 q10_comments: Mapped[Optional[str]] = mapped_camcops_column(
173 UnicodeText, comment="Q10 comments (others)"
174 )
176 q11_ability: Mapped[Optional[int]] = mapped_camcops_column(
177 permitted_value_checker=ZERO_TO_10_CHECKER,
178 comment="Q11 ability (public)" + RANGE_SUFFIX,
179 )
180 q11_motivation: Mapped[Optional[int]] = mapped_camcops_column(
181 permitted_value_checker=ZERO_TO_10_CHECKER,
182 comment="Q11 motivation (public)" + RANGE_SUFFIX,
183 )
184 q11_comments: Mapped[Optional[str]] = mapped_camcops_column(
185 UnicodeText, comment="Q11 comments (public)"
186 )
188 q12_ability: Mapped[Optional[int]] = mapped_camcops_column(
189 permitted_value_checker=ZERO_TO_10_CHECKER,
190 comment="Q12 ability (distress)" + RANGE_SUFFIX,
191 )
192 q12_motivation: Mapped[Optional[int]] = mapped_camcops_column(
193 permitted_value_checker=ZERO_TO_10_CHECKER,
194 comment="Q12 motivation (distress)" + RANGE_SUFFIX,
195 )
196 q12_comments: Mapped[Optional[str]] = mapped_camcops_column(
197 UnicodeText, comment="Q12 comments (distress)"
198 )
200 PREFIX = "q"
201 ABILITY_SUFFIX = "_ability"
202 MOTIVATION_SUFFIX = "_motivation"
203 COMMENTS_SUFFIX = "_comments"
204 ALL_ABILITY_FIELD_NAMES = strseq(PREFIX, 1, 12, ABILITY_SUFFIX)
205 ALL_MOTIVATION_FIELD_NAMES = strseq(PREFIX, 1, 12, MOTIVATION_SUFFIX)
206 ALL_MANDATORY_FIELD_NAMES = (
207 ALL_ABILITY_FIELD_NAMES + ALL_MOTIVATION_FIELD_NAMES
208 )
209 FIRST_Q = 1
210 LAST_Q = 12
211 MAX_SCORE = 10 # per question, or for subscales that are means
213 @staticmethod
214 def longname(req: "CamcopsRequest") -> str:
215 _ = req.gettext
216 return _("Eating and Meal Preparation Skills Assessment")
218 def is_complete(self) -> bool:
219 if self.any_fields_none(self.ALL_MANDATORY_FIELD_NAMES):
220 return False
222 return True
224 def ability_subscale(self) -> Optional[float]:
225 return self.mean_fields(self.ALL_ABILITY_FIELD_NAMES, ignorevalues=[])
227 def motivation_subscale(self) -> Optional[float]:
228 return self.mean_fields(
229 self.ALL_MOTIVATION_FIELD_NAMES, ignorevalues=[]
230 )
232 def get_task_html(self, req: "CamcopsRequest") -> str:
233 rows = self.get_task_html_rows(req)
235 html = """
236 <div class="{CssClass.SUMMARY}">
237 <table class="{CssClass.SUMMARY}">
238 {tr_is_complete}
239 {ability_subscale}
240 {motivation_subscale}
241 </table>
242 </div>
243 <table class="{CssClass.TASKDETAIL}">
244 {rows}
245 </table>
246 <div class="{CssClass.FOOTNOTES}">
247 [1] {ability_footnote}
248 [2] {motivation_footnote}
249 </div>
250 """.format(
251 CssClass=CssClass,
252 tr_is_complete=self.get_is_complete_tr(req),
253 ability_subscale=tr(
254 self.wxstring(req, "ability") + "<sup>[1]</sup>",
255 answer(
256 ws.number_to_dp(self.ability_subscale(), DP, default=None)
257 )
258 + f" / {self.MAX_SCORE}",
259 ),
260 motivation_subscale=tr(
261 self.wxstring(req, "motivation") + "<sup>[2]</sup>",
262 answer(
263 ws.number_to_dp(
264 self.motivation_subscale(), DP, default=None
265 ),
266 )
267 + f" / {self.MAX_SCORE}",
268 ),
269 rows=rows,
270 ability_footnote=self.wxstring(req, "ability_footnote"),
271 motivation_footnote=self.wxstring(req, "motivation_footnote"),
272 )
273 return html
275 def get_task_html_rows(self, req: "CamcopsRequest") -> str:
276 task_text = self.xstring(req, "task")
277 ability_text = self.xstring(req, "ability")
278 motivation_text = self.xstring(req, "motivation")
279 comments_text = self.xstring(req, "comments")
280 header = f"""
281 <tr>
282 <th width="2%"></th>
283 <th width="41%">{task_text}</th>
284 <th width="8%">{ability_text}</th>
285 <th width="8%">{motivation_text}</th>
286 <th width="41%">{comments_text}</th>
287 </tr>
288 """
289 return header + self.get_task_html_rows_for_range(
290 req, self.FIRST_Q, self.LAST_Q
291 )
293 def get_task_html_rows_for_range(
294 self, req: "CamcopsRequest", first_q: int, last_q: int
295 ) -> str:
296 rows = ""
297 for q_num in range(first_q, last_q + 1):
298 q_str = f"{self.PREFIX}{q_num}"
299 ability_field = f"{q_str}{self.ABILITY_SUFFIX}"
300 motivation_field = f"{q_str}{self.MOTIVATION_SUFFIX}"
301 comments_field = f"{q_str}{self.COMMENTS_SUFFIX}"
302 question_cell = self.xstring(req, q_str)
304 rows += tr(
305 str(q_num),
306 question_cell,
307 answer(getattr(self, ability_field)),
308 answer(getattr(self, motivation_field)),
309 answer(getattr(self, comments_field), default=""),
310 )
312 return rows