Coverage for tasks/cbir.py: 51%

90 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/cbir.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, List, Optional, Tuple, Type 

31 

32from cardinal_pythonlib.stringfunc import strseq 

33from sqlalchemy.ext.declarative import DeclarativeMeta 

34from sqlalchemy.sql.schema import Column 

35from sqlalchemy.sql.sqltypes import Float, Integer, UnicodeText 

36 

37from camcops_server.cc_modules.cc_constants import CssClass 

38from camcops_server.cc_modules.cc_db import add_multiple_columns 

39from camcops_server.cc_modules.cc_html import ( 

40 answer, 

41 get_yes_no, 

42 subheading_spanning_three_columns, 

43 tr, 

44) 

45from camcops_server.cc_modules.cc_request import CamcopsRequest 

46from camcops_server.cc_modules.cc_sqla_coltypes import ( 

47 BIT_CHECKER, 

48 CamcopsColumn, 

49) 

50from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

51from camcops_server.cc_modules.cc_task import ( 

52 get_from_dict, 

53 Task, 

54 TaskHasPatientMixin, 

55 TaskHasRespondentMixin, 

56) 

57 

58 

59# ============================================================================= 

60# CBI-R 

61# ============================================================================= 

62 

63QUESTION_SNIPPETS = [ 

64 "memory: poor day to day memory", # 1 

65 "memory: asks same questions", 

66 "memory: loses things", 

67 "memory: forgets familiar names", 

68 "memory: forgets names of objects", # 5 

69 "memory: poor concentration", 

70 "memory: forgets day", 

71 "memory: confused in unusual surroundings", 

72 "everyday: electrical appliances", 

73 "everyday: writing", # 10 

74 "everyday: using telephone", 

75 "everyday: making hot drink", 

76 "everyday: money", 

77 "self-care: grooming", 

78 "self-care: dressing", # 15 

79 "self-care: feeding", 

80 "self-care: bathing", 

81 "behaviour: inappropriate humour", 

82 "behaviour: temper outbursts", 

83 "behaviour: uncooperative", # 20 

84 "behaviour: socially embarrassing", 

85 "behaviour: tactless/suggestive", 

86 "behaviour: impulsive", 

87 "mood: cries", 

88 "mood: sad/depressed", # 25 

89 "mood: restless/agitated", 

90 "mood: irritable", 

91 "beliefs: visual hallucinations", 

92 "beliefs: auditory hallucinations", 

93 "beliefs: delusions", # 30 

94 "eating: sweet tooth", 

95 "eating: repetitive", 

96 "eating: increased appetite", 

97 "eating: table manners", 

98 "sleep: disturbed at night", # 35 

99 "sleep: daytime sleep increased", 

100 "stereotypy/motor: rigid/fixed opinions", 

101 "stereotypy/motor: routines", 

102 "stereotypy/motor: preoccupied with time", 

103 "stereotypy/motor: expression/catchphrase", # 40 

104 "motivation: less enthusiasm in usual interests", 

105 "motivation: no interest in new things", 

106 "motivation: fails to contact friends/family", 

107 "motivation: indifferent to family/friend concerns", 

108 "motivation: reduced affection", # 45 

109] 

110 

111 

112class CbiRMetaclass(DeclarativeMeta): 

113 # noinspection PyInitNewSignature 

114 def __init__( 

115 cls: Type["CbiR"], 

116 name: str, 

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

118 classdict: Dict[str, Any], 

119 ) -> None: 

120 add_multiple_columns( 

121 cls, 

122 "frequency", 

123 1, 

124 cls.NQUESTIONS, 

125 comment_fmt="Frequency Q{n}, {s} (0-4, higher worse)", 

126 minimum=cls.MIN_SCORE, 

127 maximum=cls.MAX_SCORE, 

128 comment_strings=QUESTION_SNIPPETS, 

129 ) 

130 add_multiple_columns( 

131 cls, 

132 "distress", 

133 1, 

134 cls.NQUESTIONS, 

135 comment_fmt="Distress Q{n}, {s} (0-4, higher worse)", 

136 minimum=cls.MIN_SCORE, 

137 maximum=cls.MAX_SCORE, 

138 comment_strings=QUESTION_SNIPPETS, 

139 ) 

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

141 

142 

143class CbiR( 

144 TaskHasPatientMixin, TaskHasRespondentMixin, Task, metaclass=CbiRMetaclass 

145): 

146 """ 

147 Server implementation of the CBI-R task. 

148 """ 

149 

150 __tablename__ = "cbir" 

151 shortname = "CBI-R" 

152 

153 confirm_blanks = CamcopsColumn( 

154 "confirm_blanks", 

155 Integer, 

156 permitted_value_checker=BIT_CHECKER, 

157 comment="Respondent confirmed that blanks are deliberate (N/A) " 

158 "(0/NULL no, 1 yes)", 

159 ) 

160 comments = Column("comments", UnicodeText, comment="Additional comments") 

161 

162 MIN_SCORE = 0 

163 MAX_SCORE = 4 

164 QNUMS_MEMORY = (1, 8) # tuple: first, last 

165 QNUMS_EVERYDAY = (9, 13) 

166 QNUMS_SELF = (14, 17) 

167 QNUMS_BEHAVIOUR = (18, 23) 

168 QNUMS_MOOD = (24, 27) 

169 QNUMS_BELIEFS = (28, 30) 

170 QNUMS_EATING = (31, 34) 

171 QNUMS_SLEEP = (35, 36) 

172 QNUMS_STEREOTYPY = (37, 40) 

173 QNUMS_MOTIVATION = (41, 45) 

174 

175 NQUESTIONS = 45 

176 TASK_FIELDS = strseq("frequency", 1, NQUESTIONS) + strseq( 

177 "distress", 1, NQUESTIONS 

178 ) 

179 

180 @staticmethod 

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

182 _ = req.gettext 

183 return _("Cambridge Behavioural Inventory, Revised") 

184 

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

186 return self.standard_task_summary_fields() + [ 

187 SummaryElement( 

188 name="memory_frequency_pct", 

189 coltype=Float(), 

190 value=self.frequency_subscore(*self.QNUMS_MEMORY), 

191 comment="Memory/orientation: frequency score (% of max)", 

192 ), 

193 SummaryElement( 

194 name="memory_distress_pct", 

195 coltype=Float(), 

196 value=self.distress_subscore(*self.QNUMS_MEMORY), 

197 comment="Memory/orientation: distress score (% of max)", 

198 ), 

199 SummaryElement( 

200 name="everyday_frequency_pct", 

201 coltype=Float(), 

202 value=self.frequency_subscore(*self.QNUMS_EVERYDAY), 

203 comment="Everyday skills: frequency score (% of max)", 

204 ), 

205 SummaryElement( 

206 name="everyday_distress_pct", 

207 coltype=Float(), 

208 value=self.distress_subscore(*self.QNUMS_EVERYDAY), 

209 comment="Everyday skills: distress score (% of max)", 

210 ), 

211 SummaryElement( 

212 name="selfcare_frequency_pct", 

213 coltype=Float(), 

214 value=self.frequency_subscore(*self.QNUMS_SELF), 

215 comment="Self-care: frequency score (% of max)", 

216 ), 

217 SummaryElement( 

218 name="selfcare_distress_pct", 

219 coltype=Float(), 

220 value=self.distress_subscore(*self.QNUMS_SELF), 

221 comment="Self-care: distress score (% of max)", 

222 ), 

223 SummaryElement( 

224 name="behaviour_frequency_pct", 

225 coltype=Float(), 

226 value=self.frequency_subscore(*self.QNUMS_BEHAVIOUR), 

227 comment="Abnormal behaviour: frequency score (% of max)", 

228 ), 

229 SummaryElement( 

230 name="behaviour_distress_pct", 

231 coltype=Float(), 

232 value=self.distress_subscore(*self.QNUMS_BEHAVIOUR), 

233 comment="Abnormal behaviour: distress score (% of max)", 

234 ), 

235 SummaryElement( 

236 name="mood_frequency_pct", 

237 coltype=Float(), 

238 value=self.frequency_subscore(*self.QNUMS_MOOD), 

239 comment="Mood: frequency score (% of max)", 

240 ), 

241 SummaryElement( 

242 name="mood_distress_pct", 

243 coltype=Float(), 

244 value=self.distress_subscore(*self.QNUMS_MOOD), 

245 comment="Mood: distress score (% of max)", 

246 ), 

247 SummaryElement( 

248 name="beliefs_frequency_pct", 

249 coltype=Float(), 

250 value=self.frequency_subscore(*self.QNUMS_BELIEFS), 

251 comment="Beliefs: frequency score (% of max)", 

252 ), 

253 SummaryElement( 

254 name="beliefs_distress_pct", 

255 coltype=Float(), 

256 value=self.distress_subscore(*self.QNUMS_BELIEFS), 

257 comment="Beliefs: distress score (% of max)", 

258 ), 

259 SummaryElement( 

260 name="eating_frequency_pct", 

261 coltype=Float(), 

262 value=self.frequency_subscore(*self.QNUMS_EATING), 

263 comment="Eating habits: frequency score (% of max)", 

264 ), 

265 SummaryElement( 

266 name="eating_distress_pct", 

267 coltype=Float(), 

268 value=self.distress_subscore(*self.QNUMS_EATING), 

269 comment="Eating habits: distress score (% of max)", 

270 ), 

271 SummaryElement( 

272 name="sleep_frequency_pct", 

273 coltype=Float(), 

274 value=self.frequency_subscore(*self.QNUMS_SLEEP), 

275 comment="Sleep: frequency score (% of max)", 

276 ), 

277 SummaryElement( 

278 name="sleep_distress_pct", 

279 coltype=Float(), 

280 value=self.distress_subscore(*self.QNUMS_SLEEP), 

281 comment="Sleep: distress score (% of max)", 

282 ), 

283 SummaryElement( 

284 name="stereotypic_frequency_pct", 

285 coltype=Float(), 

286 value=self.frequency_subscore(*self.QNUMS_STEREOTYPY), 

287 comment="Stereotypic and motor behaviours: frequency " 

288 "score (% of max)", 

289 ), 

290 SummaryElement( 

291 name="stereotypic_distress_pct", 

292 coltype=Float(), 

293 value=self.distress_subscore(*self.QNUMS_STEREOTYPY), 

294 comment="Stereotypic and motor behaviours: distress " 

295 "score (% of max)", 

296 ), 

297 SummaryElement( 

298 name="motivation_frequency_pct", 

299 coltype=Float(), 

300 value=self.frequency_subscore(*self.QNUMS_MOTIVATION), 

301 comment="Motivation: frequency score (% of max)", 

302 ), 

303 SummaryElement( 

304 name="motivation_distress_pct", 

305 coltype=Float(), 

306 value=self.distress_subscore(*self.QNUMS_MOTIVATION), 

307 comment="Motivation: distress score (% of max)", 

308 ), 

309 ] 

310 

311 def subscore( 

312 self, first: int, last: int, fieldprefix: str 

313 ) -> Optional[float]: 

314 score = 0 

315 n = 0 

316 for q in range(first, last + 1): 

317 value = getattr(self, fieldprefix + str(q)) 

318 if value is not None: 

319 score += value / self.MAX_SCORE 

320 n += 1 

321 return 100 * score / n if n > 0 else None 

322 

323 def frequency_subscore(self, first: int, last: int) -> Optional[float]: 

324 return self.subscore(first, last, "frequency") 

325 

326 def distress_subscore(self, first: int, last: int) -> Optional[float]: 

327 return self.subscore(first, last, "distress") 

328 

329 def is_complete(self) -> bool: 

330 if ( 

331 not self.field_contents_valid() 

332 or not self.is_respondent_complete() 

333 ): 

334 return False 

335 if self.confirm_blanks: 

336 return True 

337 return self.all_fields_not_none(self.TASK_FIELDS) 

338 

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

340 freq_dict = {None: None} 

341 distress_dict = {None: None} 

342 for a in range(self.MIN_SCORE, self.MAX_SCORE + 1): 

343 freq_dict[a] = self.wxstring(req, "f" + str(a)) 

344 distress_dict[a] = self.wxstring(req, "d" + str(a)) 

345 

346 heading_memory = self.wxstring(req, "h_memory") 

347 heading_everyday = self.wxstring(req, "h_everyday") 

348 heading_selfcare = self.wxstring(req, "h_selfcare") 

349 heading_behaviour = self.wxstring(req, "h_abnormalbehaviour") 

350 heading_mood = self.wxstring(req, "h_mood") 

351 heading_beliefs = self.wxstring(req, "h_beliefs") 

352 heading_eating = self.wxstring(req, "h_eating") 

353 heading_sleep = self.wxstring(req, "h_sleep") 

354 heading_motor = self.wxstring(req, "h_stereotypy_motor") 

355 heading_motivation = self.wxstring(req, "h_motivation") 

356 

357 def get_question_rows(first, last): 

358 html = "" 

359 for q in range(first, last + 1): 

360 f = getattr(self, "frequency" + str(q)) 

361 d = getattr(self, "distress" + str(q)) 

362 fa = ( 

363 f"{f}: {get_from_dict(freq_dict, f)}" 

364 if f is not None 

365 else None 

366 ) 

367 da = ( 

368 f"{d}: {get_from_dict(distress_dict, d)}" 

369 if d is not None 

370 else None 

371 ) 

372 html += tr( 

373 self.wxstring(req, "q" + str(q)), answer(fa), answer(da) 

374 ) 

375 return html 

376 

377 h = f""" 

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

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

380 {self.get_is_complete_tr(req)} 

381 </table> 

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

383 <tr> 

384 <th>Subscale</th> 

385 <th>Frequency (% of max)</th> 

386 <th>Distress (% of max)</th> 

387 </tr> 

388 <tr> 

389 <td>{heading_memory}</td> 

390 <td>{answer(self.frequency_subscore(*self.QNUMS_MEMORY))}</td> 

391 <td>{answer(self.distress_subscore(*self.QNUMS_MEMORY))}</td> 

392 </tr> 

393 <tr> 

394 <td>{heading_everyday}</td> 

395 <td>{answer(self.frequency_subscore(*self.QNUMS_EVERYDAY))}</td> 

396 <td>{answer(self.distress_subscore(*self.QNUMS_EVERYDAY))}</td> 

397 </tr> 

398 <tr> 

399 <td>{heading_selfcare}</td> 

400 <td>{answer(self.frequency_subscore(*self.QNUMS_SELF))}</td> 

401 <td>{answer(self.distress_subscore(*self.QNUMS_SELF))}</td> 

402 </tr> 

403 <tr> 

404 <td>{heading_behaviour}</td> 

405 <td>{answer(self.frequency_subscore(*self.QNUMS_BEHAVIOUR))}</td> 

406 <td>{answer(self.distress_subscore(*self.QNUMS_BEHAVIOUR))}</td> 

407 </tr> 

408 <tr> 

409 <td>{heading_mood}</td> 

410 <td>{answer(self.frequency_subscore(*self.QNUMS_MOOD))}</td> 

411 <td>{answer(self.distress_subscore(*self.QNUMS_MOOD))}</td> 

412 </tr> 

413 <tr> 

414 <td>{heading_beliefs}</td> 

415 <td>{answer(self.frequency_subscore(*self.QNUMS_BELIEFS))}</td> 

416 <td>{answer(self.distress_subscore(*self.QNUMS_BELIEFS))}</td> 

417 </tr> 

418 <tr> 

419 <td>{heading_eating}</td> 

420 <td>{answer(self.frequency_subscore(*self.QNUMS_EATING))}</td> 

421 <td>{answer(self.distress_subscore(*self.QNUMS_EATING))}</td> 

422 </tr> 

423 <tr> 

424 <td>{heading_sleep}</td> 

425 <td>{answer(self.frequency_subscore(*self.QNUMS_SLEEP))}</td> 

426 <td>{answer(self.distress_subscore(*self.QNUMS_SLEEP))}</td> 

427 </tr> 

428 <tr> 

429 <td>{heading_motor}</td> 

430 <td>{answer(self.frequency_subscore(*self.QNUMS_STEREOTYPY))}</td> 

431 <td>{answer(self.distress_subscore(*self.QNUMS_STEREOTYPY))}</td> 

432 </tr> 

433 <tr> 

434 <td>{heading_motivation}</td> 

435 <td>{answer(self.frequency_subscore(*self.QNUMS_MOTIVATION))}</td> 

436 <td>{answer(self.distress_subscore(*self.QNUMS_MOTIVATION))}</td> 

437 </tr> 

438 </table> 

439 </div> 

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

441 {tr( 

442 "Respondent confirmed that blanks are deliberate (N/A)", 

443 answer(get_yes_no(req, self.confirm_blanks)) 

444 )} 

445 {tr("Comments", answer(self.comments, default=""))} 

446 </table> 

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

448 <tr> 

449 <th width="50%">Question</th> 

450 <th width="25%">Frequency (0–4)</th> 

451 <th width="25%">Distress (0–4)</th> 

452 </tr> 

453 {subheading_spanning_three_columns(heading_memory)} 

454 {get_question_rows(*self.QNUMS_MEMORY)} 

455 {subheading_spanning_three_columns(heading_everyday)} 

456 {get_question_rows(*self.QNUMS_EVERYDAY)} 

457 {subheading_spanning_three_columns(heading_selfcare)} 

458 {get_question_rows(*self.QNUMS_SELF)} 

459 {subheading_spanning_three_columns(heading_behaviour)} 

460 {get_question_rows(*self.QNUMS_BEHAVIOUR)} 

461 {subheading_spanning_three_columns(heading_mood)} 

462 {get_question_rows(*self.QNUMS_MOOD)} 

463 {subheading_spanning_three_columns(heading_beliefs)} 

464 {get_question_rows(*self.QNUMS_BELIEFS)} 

465 {subheading_spanning_three_columns(heading_eating)} 

466 {get_question_rows(*self.QNUMS_EATING)} 

467 {subheading_spanning_three_columns(heading_sleep)} 

468 {get_question_rows(*self.QNUMS_SLEEP)} 

469 {subheading_spanning_three_columns(heading_motor)} 

470 {get_question_rows(*self.QNUMS_STEREOTYPY)} 

471 {subheading_spanning_three_columns(heading_motivation)} 

472 {get_question_rows(*self.QNUMS_MOTIVATION)} 

473 </table> 

474 """ 

475 return h