Coverage for tasks/dad.py: 40%
98 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/dad.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, Dict, Iterable, List, Tuple, Type
30from sqlalchemy.sql.sqltypes import Integer
32from camcops_server.cc_modules.cc_constants import (
33 CssClass,
34 DATA_COLLECTION_UNLESS_UPGRADED_DIV,
35)
36from camcops_server.cc_modules.cc_html import (
37 answer,
38 subheading_spanning_two_columns,
39 tr,
40)
41from camcops_server.cc_modules.cc_request import CamcopsRequest
42from camcops_server.cc_modules.cc_sqla_coltypes import (
43 camcops_column,
44 PermittedValueChecker,
45)
46from camcops_server.cc_modules.cc_summaryelement import SummaryElement
47from camcops_server.cc_modules.cc_task import (
48 Task,
49 TaskHasClinicianMixin,
50 TaskHasPatientMixin,
51 TaskHasRespondentMixin,
52)
54YES = 1
55NO = 0
56NA = -99
57YN_NA_CHECKER = PermittedValueChecker(permitted_values=[YES, NO, NA])
60# =============================================================================
61# DAD
62# =============================================================================
65class Dad( # type: ignore[misc]
66 TaskHasPatientMixin,
67 TaskHasRespondentMixin,
68 TaskHasClinicianMixin,
69 Task,
70):
71 """
72 Server implementation of the DAD task.
73 """
75 __tablename__ = "dad"
76 shortname = "DAD"
78 GROUPS = [
79 "hygiene",
80 "dressing",
81 "continence",
82 "eating",
83 "mealprep",
84 "telephone",
85 "outing",
86 "finance",
87 "medications",
88 "leisure",
89 ]
90 ITEMS = [
91 "hygiene_init_wash",
92 "hygiene_init_teeth",
93 "hygiene_init_hair",
94 "hygiene_plan_wash",
95 "hygiene_exec_wash",
96 "hygiene_exec_hair",
97 "hygiene_exec_teeth",
98 "dressing_init_dress",
99 "dressing_plan_clothing",
100 "dressing_plan_order",
101 "dressing_exec_dress",
102 "dressing_exec_undress",
103 "continence_init_toilet",
104 "continence_exec_toilet",
105 "eating_init_eat",
106 "eating_plan_utensils",
107 "eating_exec_eat",
108 "mealprep_init_meal",
109 "mealprep_plan_meal",
110 "mealprep_exec_meal",
111 "telephone_init_phone",
112 "telephone_plan_dial",
113 "telephone_exec_conversation",
114 "telephone_exec_message",
115 "outing_init_outing",
116 "outing_plan_outing",
117 "outing_exec_reach_destination",
118 "outing_exec_mode_transportation",
119 "outing_exec_return_with_shopping",
120 "finance_init_interest",
121 "finance_plan_pay_bills",
122 "finance_plan_organise_correspondence",
123 "finance_exec_handle_money",
124 "medications_init_medication",
125 "medications_exec_take_medications",
126 "leisure_init_interest_leisure",
127 "leisure_init_interest_chores",
128 "leisure_plan_chores",
129 "leisure_exec_complete_chores",
130 "leisure_exec_safe_at_home",
131 ]
133 @classmethod
134 def extend_columns(cls: Type["Dad"], **kwargs: Any) -> None:
135 explan = f" ({YES} yes, {NO} no, {NA} not applicable)"
136 for colname in cls.ITEMS:
137 setattr(
138 cls,
139 colname,
140 camcops_column(
141 colname,
142 Integer,
143 permitted_value_checker=YN_NA_CHECKER,
144 comment=colname + explan,
145 ),
146 )
148 @staticmethod
149 def longname(req: "CamcopsRequest") -> str:
150 _ = req.gettext
151 return _("Disability Assessment for Dementia")
153 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
154 d = self.get_score_dict()
155 s = self.standard_task_summary_fields()
156 for item in d:
157 s.extend(
158 [
159 SummaryElement(
160 name=item + "_n",
161 coltype=Integer(),
162 value=d[item][0],
163 comment=item + " (numerator)",
164 ),
165 SummaryElement(
166 name=item + "_d",
167 coltype=Integer(),
168 value=d[item][1],
169 comment=item + " (denominator)",
170 ),
171 ]
172 )
173 return s
175 # noinspection PyMethodOverriding
176 @staticmethod
177 def is_complete() -> bool:
178 return True
180 @classmethod
181 def get_items_activity(cls, activity: str) -> List[str]:
182 return [item for item in cls.ITEMS if item.startswith(activity)]
184 @classmethod
185 def get_items_activities(cls, activities: Iterable[str]) -> List[str]:
186 return [
187 item
188 for item in cls.ITEMS
189 if any(item.startswith(activity) for activity in activities)
190 ]
192 @classmethod
193 def get_items_phase(cls, phase: str) -> List[str]:
194 return [item for item in cls.ITEMS if phase in item]
196 def get_score(self, fields: List[str]) -> Tuple[int, int]:
197 score = self.count_where(fields, [YES])
198 possible = self.count_wherenot(fields, [None, NA])
199 return score, possible
201 def get_score_dict(self) -> Dict:
202 total = self.get_score(self.ITEMS)
203 hygiene = self.get_score(self.get_items_activity("hygiene"))
204 dressing = self.get_score(self.get_items_activity("dressing"))
205 continence = self.get_score(self.get_items_activity("continence"))
206 eating = self.get_score(self.get_items_activity("eating"))
207 badl = self.get_score(
208 self.get_items_activities(
209 ["hygiene", "dressing", "continence", "eating"]
210 )
211 )
212 mealprep = self.get_score(self.get_items_activity("mealprep"))
213 telephone = self.get_score(self.get_items_activity("telephone"))
214 outing = self.get_score(self.get_items_activity("outing"))
215 finance = self.get_score(self.get_items_activity("finance"))
216 medications = self.get_score(self.get_items_activity("medications"))
217 leisure = self.get_score(self.get_items_activity("leisure"))
218 iadl = self.get_score(
219 self.get_items_activities(
220 [
221 "mealprep",
222 "telephone",
223 "outing",
224 "finance",
225 "medications",
226 "leisure",
227 ]
228 )
229 )
230 initiation = self.get_score(self.get_items_phase("init"))
231 planning = self.get_score(self.get_items_phase("plan"))
232 execution = self.get_score(self.get_items_phase("exec"))
233 # n for numerator, d for denominator
234 return dict(
235 total=total,
236 hygiene=hygiene,
237 dressing=dressing,
238 continence=continence,
239 eating=eating,
240 badl=badl,
241 mealprep=mealprep,
242 telephone=telephone,
243 outing=outing,
244 finance=finance,
245 medications=medications,
246 leisure=leisure,
247 iadl=iadl,
248 initiation=initiation,
249 planning=planning,
250 execution=execution,
251 )
253 @staticmethod
254 def report_score(score_tuple: Tuple[int, int]) -> str:
255 return f"{answer(score_tuple[0])} / {score_tuple[1]}"
257 def report_answer(self, field: str) -> str:
258 value = getattr(self, field)
259 if value == YES:
260 text = "Yes (1)"
261 elif value == NO:
262 text = "No (0)"
263 elif value == NA:
264 text = "N/A"
265 else:
266 text = None
267 return answer(text)
269 def get_task_html(self, req: CamcopsRequest) -> str:
270 d = self.get_score_dict()
271 h = f"""
272 <div class="{CssClass.SUMMARY}">
273 <table class="{CssClass.SUMMARY}">
274 {self.get_is_complete_tr(req)}
275 <tr>
276 <td>Total</td>
277 <td>{self.report_score(d['total'])}</td>
278 </tr>
279 <tr>
280 <td>Activity: hygiene</td>
281 <td>{self.report_score(d['hygiene'])}</td>
282 </tr>
283 <tr>
284 <td>Activity: dressing</td>
285 <td>{self.report_score(d['dressing'])}</td>
286 </tr>
287 <tr>
288 <td>Activity: continence</td>
289 <td>{self.report_score(d['continence'])}</td>
290 </tr>
291 <tr>
292 <td>Activity: eating</td>
293 <td>{self.report_score(d['eating'])}</td>
294 </tr>
295 <tr>
296 <td>Basic activities of daily living (BADLs) (hygiene,
297 dressing, continence, eating)</td>
298 <td>{self.report_score(d['badl'])}</td>
299 </tr>
300 <tr>
301 <td>Activity: meal preparation</td>
302 <td>{self.report_score(d['mealprep'])}</td>
303 </tr>
304 <tr>
305 <td>Activity: telephone</td>
306 <td>{self.report_score(d['telephone'])}</td>
307 </tr>
308 <tr>
309 <td>Activity: outings</td>
310 <td>{self.report_score(d['outing'])}</td>
311 </tr>
312 <tr>
313 <td>Activity: finance</td>
314 <td>{self.report_score(d['finance'])}</td>
315 </tr>
316 <tr>
317 <td>Activity: medications</td>
318 <td>{self.report_score(d['medications'])}</td>
319 </tr>
320 <tr>
321 <td>Activity: leisure</td>
322 <td>{self.report_score(d['leisure'])}</td>
323 </tr>
324 <tr>
325 <td>Instrumental activities of daily living (IADLs)
326 (meal prep., telephone, outings, finance, medications,
327 leisure)</td>
328 <td>{self.report_score(d['iadl'])}</td>
329 </tr>
330 <tr>
331 <td>Phase: initiation</td>
332 <td>{self.report_score(d['initiation'])}</td>
333 </tr>
334 <tr>
335 <td>Phase: planning/organisation</td>
336 <td>{self.report_score(d['planning'])}</td>
337 </tr>
338 <tr>
339 <td>Phase: execution/performance</td>
340 <td>{self.report_score(d['execution'])}</td>
341 </tr>
342 </table>
343 </div>
344 <table class="{CssClass.TASKDETAIL}">
345 <tr>
346 <th width="50%">Question (I = initiation, P = planning,
347 E = execution)</th>
348 <th width="50%">Answer</th>
349 </tr>
350 """
351 for group in self.GROUPS:
352 h += subheading_spanning_two_columns(self.wxstring(req, group))
353 for item in self.ITEMS:
354 if not item.startswith(group):
355 continue
356 q = self.wxstring(req, item)
357 if "_init_" in item:
358 q += " (I)"
359 elif "_plan_" in item:
360 q += " (P)"
361 elif "_exec_" in item:
362 q += " (E)"
363 else:
364 # Shouldn't happen
365 q += " (?)"
366 h += tr(q, self.report_answer(item))
367 h += f"""
368 </table>
369 {DATA_COLLECTION_UNLESS_UPGRADED_DIV}
370 """
371 return h