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

31import cardinal_pythonlib.rnc_web as ws 

32from sqlalchemy.ext.declarative import DeclarativeMeta 

33from sqlalchemy.sql.sqltypes import Float, Integer 

34 

35from camcops_server.cc_modules.cc_constants import CssClass 

36from camcops_server.cc_modules.cc_db import add_multiple_columns 

37from camcops_server.cc_modules.cc_html import answer, tr 

38from camcops_server.cc_modules.cc_request import CamcopsRequest 

39from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

40from camcops_server.cc_modules.cc_task import Task, TaskHasPatientMixin 

41from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo 

42 

43 

44# ============================================================================= 

45# CAPE-42 

46# ============================================================================= 

47 

48QUESTION_SNIPPETS = [ 

49 # 1-10 

50 "sad", "double meaning", "not very animated", "not a talker", 

51 "magazines/TV personal", "some people not what they seem", 

52 "persecuted", "few/no emotions", "pessimistic", "conspiracy", 

53 # 11-20 

54 "destined for importance", "no future", "special/unusual person", 

55 "no longer want to live", "telepathy", "no interest being with others", 

56 "electrical devices influence thinking", "lacking motivation", 

57 "cry about nothing", "occult", 

58 # 21-30 

59 "lack energy", "people look oddly because of appearance", "mind empty", 

60 "thoughts removed", "do nothing", "thoughts not own", 

61 "feelings lacking intensity", "others might hear thoughts", 

62 "lack spontaneity", "thought echo", 

63 # 31-40 

64 "controlled by other force", "emotions blunted", "hear voices", 

65 "hear voices conversing", "neglecting appearance/hygiene", 

66 "never get things done", "few hobbies/interests", "feel guilty", 

67 "feel a failure", "tense", 

68 # 41-42 

69 "Capgras", "see things others cannot" 

70] 

71NQUESTIONS = 42 

72POSITIVE = [2, 5, 6, 7, 10, 11, 13, 15, 17, 20, 22, 24, 26, 28, 30, 31, 33, 

73 34, 41, 42] 

74DEPRESSIVE = [1, 9, 12, 14, 19, 38, 39, 40] 

75NEGATIVE = [3, 4, 8, 16, 18, 21, 23, 25, 27, 29, 32, 35, 36, 37] 

76ALL = list(range(1, NQUESTIONS + 1)) 

77MIN_SCORE_PER_Q = 1 

78MAX_SCORE_PER_Q = 4 

79 

80ALL_MIN = MIN_SCORE_PER_Q * NQUESTIONS 

81ALL_MAX = MAX_SCORE_PER_Q * NQUESTIONS 

82POS_MIN = MIN_SCORE_PER_Q * len(POSITIVE) 

83POS_MAX = MAX_SCORE_PER_Q * len(POSITIVE) 

84NEG_MIN = MIN_SCORE_PER_Q * len(NEGATIVE) 

85NEG_MAX = MAX_SCORE_PER_Q * len(NEGATIVE) 

86DEP_MIN = MIN_SCORE_PER_Q * len(DEPRESSIVE) 

87DEP_MAX = MAX_SCORE_PER_Q * len(DEPRESSIVE) 

88 

89DP = 2 

90 

91 

92class Cape42Metaclass(DeclarativeMeta): 

93 # noinspection PyInitNewSignature 

94 def __init__(cls: Type['Cape42'], 

95 name: str, 

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

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

98 add_multiple_columns( 

99 cls, "frequency", 1, NQUESTIONS, 

100 minimum=MIN_SCORE_PER_Q, maximum=MAX_SCORE_PER_Q, 

101 comment_fmt=( 

102 "Q{n} ({s}): frequency? (1 never, 2 sometimes, 3 often, " 

103 "4 nearly always)" 

104 ), 

105 comment_strings=QUESTION_SNIPPETS 

106 ) 

107 add_multiple_columns( 

108 cls, "distress", 1, NQUESTIONS, 

109 minimum=MIN_SCORE_PER_Q, maximum=MAX_SCORE_PER_Q, 

110 comment_fmt=( 

111 "Q{n} ({s}): distress (1 not, 2 a bit, 3 quite, 4 very), if " 

112 "frequency > 1" 

113 ), 

114 comment_strings=QUESTION_SNIPPETS) 

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

116 

117 

118class Cape42(TaskHasPatientMixin, Task, 

119 metaclass=Cape42Metaclass): 

120 """ 

121 Server implementation of the CAPE-42 task. 

122 """ 

123 __tablename__ = "cape42" 

124 shortname = "CAPE-42" 

125 provides_trackers = True 

126 

127 @staticmethod 

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

129 _ = req.gettext 

130 return _("Community Assessment of Psychic Experiences") 

131 

132 def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: 

133 fstr1 = "CAPE-42 weighted frequency score: " 

134 dstr1 = "CAPE-42 weighted distress score: " 

135 wtr = f" ({MIN_SCORE_PER_Q}–{MAX_SCORE_PER_Q})" 

136 fstr2 = " weighted freq. score" + wtr 

137 dstr2 = " weighted distress score" + wtr 

138 axis_min = MIN_SCORE_PER_Q - 0.2 

139 axis_max = MAX_SCORE_PER_Q + 0.2 

140 return [ 

141 TrackerInfo( 

142 value=self.weighted_frequency_score(ALL), 

143 plot_label=fstr1 + "overall", 

144 axis_label="Overall" + fstr2, 

145 axis_min=axis_min, 

146 axis_max=axis_max 

147 ), 

148 TrackerInfo( 

149 value=self.weighted_distress_score(ALL), 

150 plot_label=dstr1 + "overall", 

151 axis_label="Overall" + dstr2, 

152 axis_min=axis_min, 

153 axis_max=axis_max, 

154 ), 

155 TrackerInfo( 

156 value=self.weighted_frequency_score(POSITIVE), 

157 plot_label=fstr1 + "positive symptoms", 

158 axis_label="Positive Sx" + fstr2, 

159 axis_min=axis_min, 

160 axis_max=axis_max 

161 ), 

162 TrackerInfo( 

163 value=self.weighted_distress_score(POSITIVE), 

164 plot_label=dstr1 + "positive symptoms", 

165 axis_label="Positive Sx" + dstr2, 

166 axis_min=axis_min, 

167 axis_max=axis_max 

168 ), 

169 TrackerInfo( 

170 value=self.weighted_frequency_score(NEGATIVE), 

171 plot_label=fstr1 + "negative symptoms", 

172 axis_label="Negative Sx" + fstr2, 

173 axis_min=axis_min, 

174 axis_max=axis_max, 

175 ), 

176 TrackerInfo( 

177 value=self.weighted_distress_score(NEGATIVE), 

178 plot_label=dstr1 + "negative symptoms", 

179 axis_label="Negative Sx" + dstr2, 

180 axis_min=axis_min, 

181 axis_max=axis_max, 

182 ), 

183 TrackerInfo( 

184 value=self.weighted_frequency_score(DEPRESSIVE), 

185 plot_label=fstr1 + "depressive symptoms", 

186 axis_label="Depressive Sx" + fstr2, 

187 axis_min=axis_min, 

188 axis_max=axis_max, 

189 ), 

190 TrackerInfo( 

191 value=self.weighted_distress_score(DEPRESSIVE), 

192 plot_label=dstr1 + "depressive symptoms", 

193 axis_label="Depressive Sx" + dstr2, 

194 axis_min=axis_min, 

195 axis_max=axis_max, 

196 ), 

197 ] 

198 

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

200 wtr = f" ({MIN_SCORE_PER_Q}-{MAX_SCORE_PER_Q})" 

201 return self.standard_task_summary_fields() + [ 

202 SummaryElement( 

203 name="all_freq", coltype=Integer(), 

204 value=self.frequency_score(ALL), 

205 comment=( 

206 "Total score = frequency score for all questions " 

207 f"({ALL_MIN}-{ALL_MAX})")), 

208 SummaryElement( 

209 name="all_distress", coltype=Integer(), 

210 value=self.distress_score(ALL), 

211 comment=( 

212 "Distress score for all questions " 

213 f"({ALL_MIN}-{ALL_MAX})")), 

214 

215 SummaryElement( 

216 name="positive_frequency", coltype=Integer(), 

217 value=self.frequency_score(POSITIVE), 

218 comment=( 

219 "Frequency score for positive symptom questions " 

220 f"({POS_MIN}-{POS_MAX})")), 

221 SummaryElement( 

222 name="positive_distress", coltype=Integer(), 

223 value=self.distress_score(POSITIVE), 

224 comment=( 

225 "Distress score for positive symptom questions " 

226 f"({POS_MIN}-{POS_MAX})")), 

227 

228 SummaryElement( 

229 name="negative_frequency", coltype=Integer(), 

230 value=self.frequency_score(NEGATIVE), 

231 comment=( 

232 "Frequency score for negative symptom questions " 

233 f"({NEG_MIN}-{NEG_MAX})")), 

234 SummaryElement( 

235 name="negative_distress", coltype=Integer(), 

236 value=self.distress_score(NEGATIVE), 

237 comment=( 

238 "Distress score for negative symptom questions " 

239 f"({NEG_MIN}-{NEG_MAX})")), 

240 

241 SummaryElement( 

242 name="depressive_frequency", coltype=Integer(), 

243 value=self.frequency_score(DEPRESSIVE), 

244 comment=( 

245 "Frequency score for depressive symptom questions " 

246 f"({DEP_MIN}-{DEP_MAX})")), 

247 SummaryElement( 

248 name="depressive_distress", coltype=Integer(), 

249 value=self.distress_score(DEPRESSIVE), 

250 comment=( 

251 "Distress score for depressive symptom questions " 

252 f"({DEP_MIN}-{DEP_MAX})")), 

253 

254 SummaryElement( 

255 name="wt_all_freq", coltype=Float(), 

256 value=self.weighted_frequency_score(ALL), 

257 comment="Weighted frequency score: overall" + wtr), 

258 SummaryElement( 

259 name="wt_all_distress", coltype=Float(), 

260 value=self.weighted_distress_score(ALL), 

261 comment="Weighted distress score: overall" + wtr), 

262 

263 SummaryElement( 

264 name="wt_pos_freq", coltype=Float(), 

265 value=self.weighted_frequency_score(POSITIVE), 

266 comment="Weighted frequency score: positive symptoms" + wtr), 

267 SummaryElement( 

268 name="wt_pos_distress", coltype=Float(), 

269 value=self.weighted_distress_score(POSITIVE), 

270 comment="Weighted distress score: positive symptoms" + wtr), 

271 

272 SummaryElement( 

273 name="wt_neg_freq", coltype=Float(), 

274 value=self.weighted_frequency_score(NEGATIVE), 

275 comment="Weighted frequency score: negative symptoms" + wtr), 

276 SummaryElement( 

277 name="wt_neg_distress", coltype=Float(), 

278 value=self.weighted_distress_score(NEGATIVE), 

279 comment="Weighted distress score: negative symptoms" + wtr), 

280 

281 SummaryElement( 

282 name="wt_dep_freq", coltype=Float(), 

283 value=self.weighted_frequency_score(DEPRESSIVE), 

284 comment="Weighted frequency score: depressive symptoms" + wtr), 

285 SummaryElement( 

286 name="wt_dep_distress", coltype=Float(), 

287 value=self.weighted_distress_score(DEPRESSIVE), 

288 comment="Weighted distress score: depressive symptoms" + wtr), 

289 ] 

290 

291 def is_question_complete(self, q: int) -> bool: 

292 f = self.get_frequency(q) 

293 if f is None: 

294 return False 

295 if f > 1 and self.get_distress(q) is None: 

296 return False 

297 return True 

298 

299 def is_complete(self) -> bool: 

300 if not self.field_contents_valid(): 

301 return False 

302 for q in ALL: 

303 if not self.is_question_complete(q): 

304 return False 

305 return True 

306 

307 def get_frequency(self, q: int) -> Optional[int]: 

308 return getattr(self, "frequency" + str(q)) 

309 

310 def get_distress(self, q: int) -> Optional[int]: 

311 return getattr(self, "distress" + str(q)) 

312 

313 def get_distress_score(self, q: int) -> Optional[int]: 

314 if not self.endorsed(q): 

315 return MIN_SCORE_PER_Q 

316 return self.get_distress(q) 

317 

318 def endorsed(self, q: int) -> bool: 

319 f = self.get_frequency(q) 

320 return f is not None and f > MIN_SCORE_PER_Q 

321 

322 def distress_score(self, qlist: List[int]) -> int: 

323 score = 0 

324 for q in qlist: 

325 d = self.get_distress_score(q) 

326 if d is not None: 

327 score += d 

328 return score 

329 

330 def frequency_score(self, qlist: List[int]) -> int: 

331 score = 0 

332 for q in qlist: 

333 f = self.get_frequency(q) 

334 if f is not None: 

335 score += f 

336 return score 

337 

338 def weighted_frequency_score(self, qlist: List[int]) -> Optional[float]: 

339 score = 0 

340 n = 0 

341 for q in qlist: 

342 f = self.get_frequency(q) 

343 if f is not None: 

344 score += f 

345 n += 1 

346 if n == 0: 

347 return None 

348 return score / n 

349 

350 def weighted_distress_score(self, qlist: List[int]) -> Optional[float]: 

351 score = 0 

352 n = 0 

353 for q in qlist: 

354 f = self.get_frequency(q) 

355 d = self.get_distress_score(q) 

356 if f is not None and d is not None: 

357 score += d 

358 n += 1 

359 if n == 0: 

360 return None 

361 return score / n 

362 

363 @staticmethod 

364 def question_category(q: int) -> str: 

365 if q in POSITIVE: 

366 return "P" 

367 if q in NEGATIVE: 

368 return "N" 

369 if q in DEPRESSIVE: 

370 return "D" 

371 return "?" 

372 

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

374 q_a = "" 

375 for q in ALL: 

376 q_a += tr( 

377 f"{q}. " + 

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

379 " (<i>" + self.question_category(q) + "</i>)", 

380 answer(self.get_frequency(q)), 

381 answer( 

382 self.get_distress_score(q) if self.endorsed(q) else None, 

383 default=str(MIN_SCORE_PER_Q)) 

384 ) 

385 

386 raw_overall = tr( 

387 f"Overall <sup>[1]</sup> ({ALL_MIN}–{ALL_MAX})", 

388 self.frequency_score(ALL), 

389 self.distress_score(ALL) 

390 ) 

391 raw_positive = tr( 

392 f"Positive symptoms ({POS_MIN}–{POS_MAX})", 

393 self.frequency_score(POSITIVE), 

394 self.distress_score(POSITIVE) 

395 ) 

396 raw_negative = tr( 

397 f"Negative symptoms ({NEG_MIN}–{NEG_MAX})", 

398 self.frequency_score(NEGATIVE), 

399 self.distress_score(NEGATIVE) 

400 ) 

401 raw_depressive = tr( 

402 f"Depressive symptoms ({DEP_MIN}–{DEP_MAX})", 

403 self.frequency_score(DEPRESSIVE), 

404 self.distress_score(DEPRESSIVE) 

405 ) 

406 weighted_overall = tr( 

407 f"Overall ({len(ALL)} questions)", 

408 ws.number_to_dp(self.weighted_frequency_score(ALL), DP), 

409 ws.number_to_dp(self.weighted_distress_score(ALL), DP) 

410 ) 

411 weighted_positive = tr( 

412 f"Positive symptoms ({len(POSITIVE)} questions)", 

413 ws.number_to_dp(self.weighted_frequency_score(POSITIVE), DP), 

414 ws.number_to_dp(self.weighted_distress_score(POSITIVE), DP) 

415 ) 

416 weighted_negative = tr( 

417 f"Negative symptoms ({len(NEGATIVE)} questions)", 

418 ws.number_to_dp(self.weighted_frequency_score(NEGATIVE), DP), 

419 ws.number_to_dp(self.weighted_distress_score(NEGATIVE), DP) 

420 ) 

421 weighted_depressive = tr( 

422 f"Depressive symptoms ({len(DEPRESSIVE)} questions)", 

423 ws.number_to_dp(self.weighted_frequency_score(DEPRESSIVE), DP), 

424 ws.number_to_dp(self.weighted_distress_score(DEPRESSIVE), DP) 

425 ) 

426 return f""" 

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

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

429 {self.get_is_complete_tr(req)} 

430 </table> 

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

432 <tr> 

433 <th>Domain (with score range)</th> 

434 <th>Frequency (total score)</th> 

435 <th>Distress (total score)</th> 

436 </tr> 

437 {raw_overall} 

438 {raw_positive} 

439 {raw_negative} 

440 {raw_depressive} 

441 </table> 

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

443 <tr> 

444 <th>Domain</th> 

445 <th>Weighted frequency score <sup>[3]</sup></th> 

446 <th>Weighted distress score <sup>[3]</sup></th> 

447 </tr> 

448 {weighted_overall} 

449 {weighted_positive} 

450 {weighted_negative} 

451 {weighted_depressive} 

452 </table> 

453 </div> 

454 <div class="{CssClass.EXPLANATION}"> 

455 FREQUENCY: 

456 1 {self.wxstring(req, "frequency_option1")}, 

457 2 {self.wxstring(req, "frequency_option2")}, 

458 3 {self.wxstring(req, "frequency_option3")}, 

459 4 {self.wxstring(req, "frequency_option4")}. 

460 DISTRESS: 

461 1 {self.wxstring(req, "distress_option1")}, 

462 2 {self.wxstring(req, "distress_option2")}, 

463 3 {self.wxstring(req, "distress_option3")}, 

464 4 {self.wxstring(req, "distress_option4")}. 

465 </div> 

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

467 <tr> 

468 <th width="70%"> 

469 Question (P positive, N negative, D depressive) 

470 </th> 

471 <th width="15%">Frequency 

472 ({MIN_SCORE_PER_Q}–{MAX_SCORE_PER_Q})</th> 

473 <th width="15%">Distress 

474 ({MIN_SCORE_PER_Q}–{MAX_SCORE_PER_Q}) 

475 <sup>[2]</sup></th> 

476 </tr> 

477 {q_a} 

478 </table> 

479 <div class="{CssClass.FOOTNOTES}"> 

480 [1] “Total” score is the overall frequency score (the sum of 

481 frequency scores for all questions). 

482 [2] Distress coerced to 1 if frequency is 1. 

483 [3] Sum score per dimension divided by number of completed 

484 items. Shown to {DP} decimal places. Will be in the range 

485 {MIN_SCORE_PER_Q}–{MAX_SCORE_PER_Q}, or blank if not 

486 calculable. 

487 </div> 

488 """