Coverage for tasks/dad.py: 42%

101 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2022-11-08 23:14 +0000

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/dad.py 

5 

6=============================================================================== 

7 

8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

10 

11 This file is part of CamCOPS. 

12 

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. 

17 

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. 

22 

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/>. 

25 

26=============================================================================== 

27 

28""" 

29 

30from typing import Any, Dict, Iterable, List, Tuple, Type 

31 

32from sqlalchemy.ext.declarative import DeclarativeMeta 

33from sqlalchemy.sql.sqltypes import Integer 

34 

35from camcops_server.cc_modules.cc_constants import ( 

36 CssClass, 

37 DATA_COLLECTION_UNLESS_UPGRADED_DIV, 

38) 

39from camcops_server.cc_modules.cc_html import ( 

40 answer, 

41 subheading_spanning_two_columns, 

42 tr, 

43) 

44from camcops_server.cc_modules.cc_request import CamcopsRequest 

45from camcops_server.cc_modules.cc_sqla_coltypes import ( 

46 CamcopsColumn, 

47 PermittedValueChecker, 

48) 

49from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

50from camcops_server.cc_modules.cc_task import ( 

51 Task, 

52 TaskHasClinicianMixin, 

53 TaskHasPatientMixin, 

54 TaskHasRespondentMixin, 

55) 

56 

57YES = 1 

58NO = 0 

59NA = -99 

60YN_NA_CHECKER = PermittedValueChecker(permitted_values=[YES, NO, NA]) 

61 

62 

63# ============================================================================= 

64# DAD 

65# ============================================================================= 

66 

67 

68class DadMetaclass(DeclarativeMeta): 

69 # noinspection PyInitNewSignature 

70 def __init__( 

71 cls: Type["Dad"], 

72 name: str, 

73 bases: Tuple[Type, ...], 

74 classdict: Dict[str, Any], 

75 ) -> None: 

76 explan = f" ({YES} yes, {NO} no, {NA} not applicable)" 

77 for colname in cls.ITEMS: 

78 setattr( 

79 cls, 

80 colname, 

81 CamcopsColumn( 

82 colname, 

83 Integer, 

84 permitted_value_checker=YN_NA_CHECKER, 

85 comment=colname + explan, 

86 ), 

87 ) 

88 super().__init__(name, bases, classdict) 

89 

90 

91class Dad( 

92 TaskHasPatientMixin, 

93 TaskHasRespondentMixin, 

94 TaskHasClinicianMixin, 

95 Task, 

96 metaclass=DadMetaclass, 

97): 

98 """ 

99 Server implementation of the DAD task. 

100 """ 

101 

102 __tablename__ = "dad" 

103 shortname = "DAD" 

104 

105 GROUPS = [ 

106 "hygiene", 

107 "dressing", 

108 "continence", 

109 "eating", 

110 "mealprep", 

111 "telephone", 

112 "outing", 

113 "finance", 

114 "medications", 

115 "leisure", 

116 ] 

117 ITEMS = [ 

118 "hygiene_init_wash", 

119 "hygiene_init_teeth", 

120 "hygiene_init_hair", 

121 "hygiene_plan_wash", 

122 "hygiene_exec_wash", 

123 "hygiene_exec_hair", 

124 "hygiene_exec_teeth", 

125 "dressing_init_dress", 

126 "dressing_plan_clothing", 

127 "dressing_plan_order", 

128 "dressing_exec_dress", 

129 "dressing_exec_undress", 

130 "continence_init_toilet", 

131 "continence_exec_toilet", 

132 "eating_init_eat", 

133 "eating_plan_utensils", 

134 "eating_exec_eat", 

135 "mealprep_init_meal", 

136 "mealprep_plan_meal", 

137 "mealprep_exec_meal", 

138 "telephone_init_phone", 

139 "telephone_plan_dial", 

140 "telephone_exec_conversation", 

141 "telephone_exec_message", 

142 "outing_init_outing", 

143 "outing_plan_outing", 

144 "outing_exec_reach_destination", 

145 "outing_exec_mode_transportation", 

146 "outing_exec_return_with_shopping", 

147 "finance_init_interest", 

148 "finance_plan_pay_bills", 

149 "finance_plan_organise_correspondence", 

150 "finance_exec_handle_money", 

151 "medications_init_medication", 

152 "medications_exec_take_medications", 

153 "leisure_init_interest_leisure", 

154 "leisure_init_interest_chores", 

155 "leisure_plan_chores", 

156 "leisure_exec_complete_chores", 

157 "leisure_exec_safe_at_home", 

158 ] 

159 

160 @staticmethod 

161 def longname(req: "CamcopsRequest") -> str: 

162 _ = req.gettext 

163 return _("Disability Assessment for Dementia") 

164 

165 def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: 

166 d = self.get_score_dict() 

167 s = self.standard_task_summary_fields() 

168 for item in d: 

169 s.extend( 

170 [ 

171 SummaryElement( 

172 name=item + "_n", 

173 coltype=Integer(), 

174 value=d[item][0], 

175 comment=item + " (numerator)", 

176 ), 

177 SummaryElement( 

178 name=item + "_d", 

179 coltype=Integer(), 

180 value=d[item][1], 

181 comment=item + " (denominator)", 

182 ), 

183 ] 

184 ) 

185 return s 

186 

187 # noinspection PyMethodOverriding 

188 @staticmethod 

189 def is_complete() -> bool: 

190 return True 

191 

192 @classmethod 

193 def get_items_activity(cls, activity: str) -> List[str]: 

194 return [item for item in cls.ITEMS if item.startswith(activity)] 

195 

196 @classmethod 

197 def get_items_activities(cls, activities: Iterable[str]) -> List[str]: 

198 return [ 

199 item 

200 for item in cls.ITEMS 

201 if any(item.startswith(activity) for activity in activities) 

202 ] 

203 

204 @classmethod 

205 def get_items_phase(cls, phase: str) -> List[str]: 

206 return [item for item in cls.ITEMS if phase in item] 

207 

208 def get_score(self, fields: List[str]) -> Tuple[int, int]: 

209 score = self.count_where(fields, [YES]) 

210 possible = self.count_wherenot(fields, [None, NA]) 

211 return score, possible 

212 

213 def get_score_dict(self) -> Dict: 

214 total = self.get_score(self.ITEMS) 

215 hygiene = self.get_score(self.get_items_activity("hygiene")) 

216 dressing = self.get_score(self.get_items_activity("dressing")) 

217 continence = self.get_score(self.get_items_activity("continence")) 

218 eating = self.get_score(self.get_items_activity("eating")) 

219 badl = self.get_score( 

220 self.get_items_activities( 

221 ["hygiene", "dressing", "continence", "eating"] 

222 ) 

223 ) 

224 mealprep = self.get_score(self.get_items_activity("mealprep")) 

225 telephone = self.get_score(self.get_items_activity("telephone")) 

226 outing = self.get_score(self.get_items_activity("outing")) 

227 finance = self.get_score(self.get_items_activity("finance")) 

228 medications = self.get_score(self.get_items_activity("medications")) 

229 leisure = self.get_score(self.get_items_activity("leisure")) 

230 iadl = self.get_score( 

231 self.get_items_activities( 

232 [ 

233 "mealprep", 

234 "telephone", 

235 "outing", 

236 "finance", 

237 "medications", 

238 "leisure", 

239 ] 

240 ) 

241 ) 

242 initiation = self.get_score(self.get_items_phase("init")) 

243 planning = self.get_score(self.get_items_phase("plan")) 

244 execution = self.get_score(self.get_items_phase("exec")) 

245 # n for numerator, d for denominator 

246 return dict( 

247 total=total, 

248 hygiene=hygiene, 

249 dressing=dressing, 

250 continence=continence, 

251 eating=eating, 

252 badl=badl, 

253 mealprep=mealprep, 

254 telephone=telephone, 

255 outing=outing, 

256 finance=finance, 

257 medications=medications, 

258 leisure=leisure, 

259 iadl=iadl, 

260 initiation=initiation, 

261 planning=planning, 

262 execution=execution, 

263 ) 

264 

265 @staticmethod 

266 def report_score(score_tuple: Tuple[int, int]) -> str: 

267 return f"{answer(score_tuple[0])} / {score_tuple[1]}" 

268 

269 def report_answer(self, field: str) -> str: 

270 value = getattr(self, field) 

271 if value == YES: 

272 text = "Yes (1)" 

273 elif value == NO: 

274 text = "No (0)" 

275 elif value == NA: 

276 text = "N/A" 

277 else: 

278 text = None 

279 return answer(text) 

280 

281 def get_task_html(self, req: CamcopsRequest) -> str: 

282 d = self.get_score_dict() 

283 h = f""" 

284 <div class="{CssClass.SUMMARY}"> 

285 <table class="{CssClass.SUMMARY}"> 

286 {self.get_is_complete_tr(req)} 

287 <tr> 

288 <td>Total</td> 

289 <td>{self.report_score(d['total'])}</td> 

290 </tr> 

291 <tr> 

292 <td>Activity: hygiene</td> 

293 <td>{self.report_score(d['hygiene'])}</td> 

294 </tr> 

295 <tr> 

296 <td>Activity: dressing</td> 

297 <td>{self.report_score(d['dressing'])}</td> 

298 </tr> 

299 <tr> 

300 <td>Activity: continence</td> 

301 <td>{self.report_score(d['continence'])}</td> 

302 </tr> 

303 <tr> 

304 <td>Activity: eating</td> 

305 <td>{self.report_score(d['eating'])}</td> 

306 </tr> 

307 <tr> 

308 <td>Basic activities of daily living (BADLs) (hygiene, 

309 dressing, continence, eating)</td> 

310 <td>{self.report_score(d['badl'])}</td> 

311 </tr> 

312 <tr> 

313 <td>Activity: meal preparation</td> 

314 <td>{self.report_score(d['mealprep'])}</td> 

315 </tr> 

316 <tr> 

317 <td>Activity: telephone</td> 

318 <td>{self.report_score(d['telephone'])}</td> 

319 </tr> 

320 <tr> 

321 <td>Activity: outings</td> 

322 <td>{self.report_score(d['outing'])}</td> 

323 </tr> 

324 <tr> 

325 <td>Activity: finance</td> 

326 <td>{self.report_score(d['finance'])}</td> 

327 </tr> 

328 <tr> 

329 <td>Activity: medications</td> 

330 <td>{self.report_score(d['medications'])}</td> 

331 </tr> 

332 <tr> 

333 <td>Activity: leisure</td> 

334 <td>{self.report_score(d['leisure'])}</td> 

335 </tr> 

336 <tr> 

337 <td>Instrumental activities of daily living (IADLs) 

338 (meal prep., telephone, outings, finance, medications, 

339 leisure)</td> 

340 <td>{self.report_score(d['iadl'])}</td> 

341 </tr> 

342 <tr> 

343 <td>Phase: initiation</td> 

344 <td>{self.report_score(d['initiation'])}</td> 

345 </tr> 

346 <tr> 

347 <td>Phase: planning/organisation</td> 

348 <td>{self.report_score(d['planning'])}</td> 

349 </tr> 

350 <tr> 

351 <td>Phase: execution/performance</td> 

352 <td>{self.report_score(d['execution'])}</td> 

353 </tr> 

354 </table> 

355 </div> 

356 <table class="{CssClass.TASKDETAIL}"> 

357 <tr> 

358 <th width="50%">Question (I = initiation, P = planning, 

359 E = execution)</th> 

360 <th width="50%">Answer</th> 

361 </tr> 

362 """ 

363 for group in self.GROUPS: 

364 h += subheading_spanning_two_columns(self.wxstring(req, group)) 

365 for item in self.ITEMS: 

366 if not item.startswith(group): 

367 continue 

368 q = self.wxstring(req, item) 

369 if "_init_" in item: 

370 q += " (I)" 

371 elif "_plan_" in item: 

372 q += " (P)" 

373 elif "_exec_" in item: 

374 q += " (E)" 

375 else: 

376 # Shouldn't happen 

377 q += " (?)" 

378 h += tr(q, self.report_answer(item)) 

379 h += f""" 

380 </table> 

381 {DATA_COLLECTION_UNLESS_UPGRADED_DIV} 

382 """ 

383 return h