Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python 

2 

3""" 

4camcops_server/tasks/cbir.py 

5 

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

7 

8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com). 

9 

10 This file is part of CamCOPS. 

11 

12 CamCOPS is free software: you can redistribute it and/or modify 

13 it under the terms of the GNU General Public License as published by 

14 the Free Software Foundation, either version 3 of the License, or 

15 (at your option) any later version. 

16 

17 CamCOPS is distributed in the hope that it will be useful, 

18 but WITHOUT ANY WARRANTY; without even the implied warranty of 

19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

20 GNU General Public License for more details. 

21 

22 You should have received a copy of the GNU General Public License 

23 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

24 

25=============================================================================== 

26 

27""" 

28 

29from typing import Any, Dict, List, Optional, Tuple, Type 

30 

31from cardinal_pythonlib.stringfunc import strseq 

32from sqlalchemy.ext.declarative import DeclarativeMeta 

33from sqlalchemy.sql.schema import Column 

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

35 

36from camcops_server.cc_modules.cc_constants import CssClass 

37from camcops_server.cc_modules.cc_db import add_multiple_columns 

38from camcops_server.cc_modules.cc_html import ( 

39 answer, 

40 get_yes_no, 

41 subheading_spanning_three_columns, 

42 tr, 

43) 

44from camcops_server.cc_modules.cc_request import CamcopsRequest 

45from camcops_server.cc_modules.cc_sqla_coltypes import ( 

46 BIT_CHECKER, 

47 CamcopsColumn, 

48) 

49from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

50from camcops_server.cc_modules.cc_task import ( 

51 get_from_dict, 

52 Task, 

53 TaskHasPatientMixin, 

54 TaskHasRespondentMixin, 

55) 

56 

57 

58# ============================================================================= 

59# CBI-R 

60# ============================================================================= 

61 

62QUESTION_SNIPPETS = [ 

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

64 "memory: asks same questions", 

65 "memory: loses things", 

66 "memory: forgets familiar names", 

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

68 "memory: poor concentration", 

69 "memory: forgets day", 

70 "memory: confused in unusual surroundings", 

71 "everyday: electrical appliances", 

72 "everyday: writing", # 10 

73 "everyday: using telephone", 

74 "everyday: making hot drink", 

75 "everyday: money", 

76 "self-care: grooming", 

77 "self-care: dressing", # 15 

78 "self-care: feeding", 

79 "self-care: bathing", 

80 "behaviour: inappropriate humour", 

81 "behaviour: temper outbursts", 

82 "behaviour: uncooperative", # 20 

83 "behaviour: socially embarrassing", 

84 "behaviour: tactless/suggestive", 

85 "behaviour: impulsive", 

86 "mood: cries", 

87 "mood: sad/depressed", # 25 

88 "mood: restless/agitated", 

89 "mood: irritable", 

90 "beliefs: visual hallucinations", 

91 "beliefs: auditory hallucinations", 

92 "beliefs: delusions", # 30 

93 "eating: sweet tooth", 

94 "eating: repetitive", 

95 "eating: increased appetite", 

96 "eating: table manners", 

97 "sleep: disturbed at night", # 35 

98 "sleep: daytime sleep increased", 

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

100 "stereotypy/motor: routines", 

101 "stereotypy/motor: preoccupied with time", 

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

103 "motivation: less enthusiasm in usual interests", 

104 "motivation: no interest in new things", 

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

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

107 "motivation: reduced affection", # 45 

108] 

109 

110 

111class CbiRMetaclass(DeclarativeMeta): 

112 # noinspection PyInitNewSignature 

113 def __init__(cls: Type['CbiR'], 

114 name: str, 

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

116 classdict: Dict[str, Any]) -> None: 

117 add_multiple_columns( 

118 cls, "frequency", 1, cls.NQUESTIONS, 

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

120 minimum=cls.MIN_SCORE, maximum=cls.MAX_SCORE, 

121 comment_strings=QUESTION_SNIPPETS 

122 ) 

123 add_multiple_columns( 

124 cls, "distress", 1, cls.NQUESTIONS, 

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

126 minimum=cls.MIN_SCORE, maximum=cls.MAX_SCORE, 

127 comment_strings=QUESTION_SNIPPETS 

128 ) 

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

130 

131 

132class CbiR(TaskHasPatientMixin, TaskHasRespondentMixin, Task, 

133 metaclass=CbiRMetaclass): 

134 """ 

135 Server implementation of the CBI-R task. 

136 """ 

137 __tablename__ = "cbir" 

138 shortname = "CBI-R" 

139 

140 confirm_blanks = CamcopsColumn( 

141 "confirm_blanks", Integer, 

142 permitted_value_checker=BIT_CHECKER, 

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

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

145 ) 

146 comments = Column( 

147 "comments", UnicodeText, 

148 comment="Additional comments" 

149 ) 

150 

151 MIN_SCORE = 0 

152 MAX_SCORE = 4 

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

154 QNUMS_EVERYDAY = (9, 13) 

155 QNUMS_SELF = (14, 17) 

156 QNUMS_BEHAVIOUR = (18, 23) 

157 QNUMS_MOOD = (24, 27) 

158 QNUMS_BELIEFS = (28, 30) 

159 QNUMS_EATING = (31, 34) 

160 QNUMS_SLEEP = (35, 36) 

161 QNUMS_STEREOTYPY = (37, 40) 

162 QNUMS_MOTIVATION = (41, 45) 

163 

164 NQUESTIONS = 45 

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

166 strseq("distress", 1, NQUESTIONS)) 

167 

168 @staticmethod 

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

170 _ = req.gettext 

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

172 

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

174 return self.standard_task_summary_fields() + [ 

175 SummaryElement( 

176 name="memory_frequency_pct", 

177 coltype=Float(), 

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

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

180 SummaryElement( 

181 name="memory_distress_pct", 

182 coltype=Float(), 

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

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

185 SummaryElement( 

186 name="everyday_frequency_pct", 

187 coltype=Float(), 

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

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

190 SummaryElement( 

191 name="everyday_distress_pct", 

192 coltype=Float(), 

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

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

195 SummaryElement( 

196 name="selfcare_frequency_pct", 

197 coltype=Float(), 

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

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

200 SummaryElement( 

201 name="selfcare_distress_pct", 

202 coltype=Float(), 

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

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

205 SummaryElement( 

206 name="behaviour_frequency_pct", 

207 coltype=Float(), 

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

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

210 SummaryElement( 

211 name="behaviour_distress_pct", 

212 coltype=Float(), 

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

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

215 SummaryElement( 

216 name="mood_frequency_pct", 

217 coltype=Float(), 

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

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

220 SummaryElement( 

221 name="mood_distress_pct", 

222 coltype=Float(), 

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

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

225 SummaryElement( 

226 name="beliefs_frequency_pct", 

227 coltype=Float(), 

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

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

230 SummaryElement( 

231 name="beliefs_distress_pct", 

232 coltype=Float(), 

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

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

235 SummaryElement( 

236 name="eating_frequency_pct", 

237 coltype=Float(), 

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

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

240 SummaryElement( 

241 name="eating_distress_pct", 

242 coltype=Float(), 

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

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

245 SummaryElement( 

246 name="sleep_frequency_pct", 

247 coltype=Float(), 

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

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

250 SummaryElement( 

251 name="sleep_distress_pct", 

252 coltype=Float(), 

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

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

255 SummaryElement( 

256 name="stereotypic_frequency_pct", 

257 coltype=Float(), 

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

259 comment="Stereotypic and motor behaviours: frequency " 

260 "score (% of max)"), 

261 SummaryElement( 

262 name="stereotypic_distress_pct", 

263 coltype=Float(), 

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

265 comment="Stereotypic and motor behaviours: distress " 

266 "score (% of max)"), 

267 SummaryElement( 

268 name="motivation_frequency_pct", 

269 coltype=Float(), 

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

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

272 SummaryElement( 

273 name="motivation_distress_pct", 

274 coltype=Float(), 

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

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

277 ] 

278 

279 def subscore(self, first: int, last: int, fieldprefix: str) \ 

280 -> Optional[float]: 

281 score = 0 

282 n = 0 

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

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

285 if value is not None: 

286 score += value / self.MAX_SCORE 

287 n += 1 

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

289 

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

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

292 

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

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

295 

296 def is_complete(self) -> bool: 

297 if (not self.field_contents_valid() or 

298 not self.is_respondent_complete()): 

299 return False 

300 if self.confirm_blanks: 

301 return True 

302 return self.all_fields_not_none(self.TASK_FIELDS) 

303 

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

305 freq_dict = {None: None} 

306 distress_dict = {None: None} 

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

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

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

310 

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

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

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

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

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

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

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

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

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

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

321 

322 def get_question_rows(first, last): 

323 html = "" 

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

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

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

327 fa = (f"{f}: {get_from_dict(freq_dict, f)}" 

328 if f is not None else None) 

329 da = (f"{d}: {get_from_dict(distress_dict, d)}" 

330 if d is not None else None) 

331 html += tr( 

332 self.wxstring(req, "q" + str(q)), 

333 answer(fa), 

334 answer(da), 

335 ) 

336 return html 

337 

338 h = f""" 

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

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

341 {self.get_is_complete_tr(req)} 

342 </table> 

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

344 <tr> 

345 <th>Subscale</th> 

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

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

348 </tr> 

349 <tr> 

350 <td>{heading_memory}</td> 

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

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

353 </tr> 

354 <tr> 

355 <td>{heading_everyday}</td> 

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

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

358 </tr> 

359 <tr> 

360 <td>{heading_selfcare}</td> 

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

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

363 </tr> 

364 <tr> 

365 <td>{heading_behaviour}</td> 

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

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

368 </tr> 

369 <tr> 

370 <td>{heading_mood}</td> 

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

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

373 </tr> 

374 <tr> 

375 <td>{heading_beliefs}</td> 

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

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

378 </tr> 

379 <tr> 

380 <td>{heading_eating}</td> 

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

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

383 </tr> 

384 <tr> 

385 <td>{heading_sleep}</td> 

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

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

388 </tr> 

389 <tr> 

390 <td>{heading_motor}</td> 

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

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

393 </tr> 

394 <tr> 

395 <td>{heading_motivation}</td> 

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

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

398 </tr> 

399 </table> 

400 </div> 

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

402 {tr( 

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

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

405 )} 

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

407 </table> 

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

409 <tr> 

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

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

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

413 </tr> 

414 {subheading_spanning_three_columns(heading_memory)} 

415 {get_question_rows(*self.QNUMS_MEMORY)} 

416 {subheading_spanning_three_columns(heading_everyday)} 

417 {get_question_rows(*self.QNUMS_EVERYDAY)} 

418 {subheading_spanning_three_columns(heading_selfcare)} 

419 {get_question_rows(*self.QNUMS_SELF)} 

420 {subheading_spanning_three_columns(heading_behaviour)} 

421 {get_question_rows(*self.QNUMS_BEHAVIOUR)} 

422 {subheading_spanning_three_columns(heading_mood)} 

423 {get_question_rows(*self.QNUMS_MOOD)} 

424 {subheading_spanning_three_columns(heading_beliefs)} 

425 {get_question_rows(*self.QNUMS_BELIEFS)} 

426 {subheading_spanning_three_columns(heading_eating)} 

427 {get_question_rows(*self.QNUMS_EATING)} 

428 {subheading_spanning_three_columns(heading_sleep)} 

429 {get_question_rows(*self.QNUMS_SLEEP)} 

430 {subheading_spanning_three_columns(heading_motor)} 

431 {get_question_rows(*self.QNUMS_STEREOTYPY)} 

432 {subheading_spanning_three_columns(heading_motivation)} 

433 {get_question_rows(*self.QNUMS_MOTIVATION)} 

434 </table> 

435 """ 

436 return h