Coverage for tasks/icd10manic.py: 37%

175 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/icd10manic.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 List, Optional 

31 

32from cardinal_pythonlib.datetimefunc import format_datetime 

33from cardinal_pythonlib.typetests import is_false 

34import cardinal_pythonlib.rnc_web as ws 

35from sqlalchemy.sql.schema import Column 

36from sqlalchemy.sql.sqltypes import Boolean, Date, UnicodeText 

37 

38from camcops_server.cc_modules.cc_constants import ( 

39 CssClass, 

40 DateFormat, 

41 ICD10_COPYRIGHT_DIV, 

42) 

43from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

44from camcops_server.cc_modules.cc_html import ( 

45 get_present_absent_none, 

46 heading_spanning_two_columns, 

47 subheading_spanning_two_columns, 

48 tr_qa, 

49) 

50from camcops_server.cc_modules.cc_request import CamcopsRequest 

51from camcops_server.cc_modules.cc_sqla_coltypes import ( 

52 BIT_CHECKER, 

53 CamcopsColumn, 

54 SummaryCategoryColType, 

55) 

56from camcops_server.cc_modules.cc_string import AS 

57from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

58from camcops_server.cc_modules.cc_task import ( 

59 Task, 

60 TaskHasClinicianMixin, 

61 TaskHasPatientMixin, 

62) 

63from camcops_server.cc_modules.cc_text import SS 

64 

65 

66# ============================================================================= 

67# Icd10Manic 

68# ============================================================================= 

69 

70 

71class Icd10Manic(TaskHasClinicianMixin, TaskHasPatientMixin, Task): 

72 """ 

73 Server implementation of the ICD10-MANIC task. 

74 """ 

75 

76 __tablename__ = "icd10manic" 

77 shortname = "ICD10-MANIC" 

78 info_filename_stem = "icd" 

79 

80 mood_elevated = CamcopsColumn( 

81 "mood_elevated", 

82 Boolean, 

83 permitted_value_checker=BIT_CHECKER, 

84 comment="The mood is 'elevated' [hypomania] or 'predominantly " 

85 "elevated [or] expansive' [mania] to a degree that is " 

86 "definitely abnormal for the individual concerned.", 

87 ) 

88 mood_irritable = CamcopsColumn( 

89 "mood_irritable", 

90 Boolean, 

91 permitted_value_checker=BIT_CHECKER, 

92 comment="The mood is 'irritable' [hypomania] or 'predominantly " 

93 "irritable' [mania] to a degree that is definitely abnormal " 

94 "for the individual concerned.", 

95 ) 

96 

97 distractible = CamcopsColumn( 

98 "distractible", 

99 Boolean, 

100 permitted_value_checker=BIT_CHECKER, 

101 comment="Difficulty in concentration or distractibility [from " 

102 "the criteria for hypomania]; distractibility or constant " 

103 "changes in activity or plans [from the criteria for mania].", 

104 ) 

105 activity = CamcopsColumn( 

106 "activity", 

107 Boolean, 

108 permitted_value_checker=BIT_CHECKER, 

109 comment="Increased activity or physical restlessness.", 

110 ) 

111 sleep = CamcopsColumn( 

112 "sleep", 

113 Boolean, 

114 permitted_value_checker=BIT_CHECKER, 

115 comment="Decreased need for sleep.", 

116 ) 

117 talkativeness = CamcopsColumn( 

118 "talkativeness", 

119 Boolean, 

120 permitted_value_checker=BIT_CHECKER, 

121 comment="Increased talkativeness (pressure of speech).", 

122 ) 

123 recklessness = CamcopsColumn( 

124 "recklessness", 

125 Boolean, 

126 permitted_value_checker=BIT_CHECKER, 

127 comment="Mild spending sprees, or other types of reckless or " 

128 "irresponsible behaviour [hypomania]; behaviour which is " 

129 "foolhardy or reckless and whose risks the subject does not " 

130 "recognize e.g. spending sprees, foolish enterprises, " 

131 "reckless driving [mania].", 

132 ) 

133 social_disinhibition = CamcopsColumn( 

134 "social_disinhibition", 

135 Boolean, 

136 permitted_value_checker=BIT_CHECKER, 

137 comment="Increased sociability or over-familiarity [hypomania]; " 

138 "loss of normal social inhibitions resulting in behaviour " 

139 "which is inappropriate to the circumstances [mania].", 

140 ) 

141 sexual = CamcopsColumn( 

142 "sexual", 

143 Boolean, 

144 permitted_value_checker=BIT_CHECKER, 

145 comment="Increased sexual energy [hypomania]; marked sexual " 

146 "energy or sexual indiscretions [mania].", 

147 ) 

148 

149 grandiosity = CamcopsColumn( 

150 "grandiosity", 

151 Boolean, 

152 permitted_value_checker=BIT_CHECKER, 

153 comment="Inflated self-esteem or grandiosity.", 

154 ) 

155 flight_of_ideas = CamcopsColumn( 

156 "flight_of_ideas", 

157 Boolean, 

158 permitted_value_checker=BIT_CHECKER, 

159 comment="Flight of ideas or the subjective experience of " 

160 "thoughts racing.", 

161 ) 

162 

163 sustained4days = CamcopsColumn( 

164 "sustained4days", 

165 Boolean, 

166 permitted_value_checker=BIT_CHECKER, 

167 comment="Elevated/irritable mood sustained for at least 4 days.", 

168 ) 

169 sustained7days = CamcopsColumn( 

170 "sustained7days", 

171 Boolean, 

172 permitted_value_checker=BIT_CHECKER, 

173 comment="Elevated/irritable mood sustained for at least 7 days.", 

174 ) 

175 admission_required = CamcopsColumn( 

176 "admission_required", 

177 Boolean, 

178 permitted_value_checker=BIT_CHECKER, 

179 comment="Elevated/irritable mood severe enough to require " 

180 "hospital admission.", 

181 ) 

182 some_interference_functioning = CamcopsColumn( 

183 "some_interference_functioning", 

184 Boolean, 

185 permitted_value_checker=BIT_CHECKER, 

186 comment="Some interference with personal functioning " 

187 "in daily living.", 

188 ) 

189 severe_interference_functioning = CamcopsColumn( 

190 "severe_interference_functioning", 

191 Boolean, 

192 permitted_value_checker=BIT_CHECKER, 

193 comment="Severe interference with personal " 

194 "functioning in daily living.", 

195 ) 

196 

197 perceptual_alterations = CamcopsColumn( 

198 "perceptual_alterations", 

199 Boolean, 

200 permitted_value_checker=BIT_CHECKER, 

201 comment="Perceptual alterations (e.g. subjective hyperacusis, " 

202 "appreciation of colours as specially vivid, etc.).", 

203 ) # ... not psychotic 

204 hallucinations_schizophrenic = CamcopsColumn( 

205 "hallucinations_schizophrenic", 

206 Boolean, 

207 permitted_value_checker=BIT_CHECKER, 

208 comment="Hallucinations that are 'typically schizophrenic' " 

209 "(hallucinatory voices giving a running commentary on the " 

210 "patient's behaviour, or discussing him between themselves, " 

211 "or other types of hallucinatory voices coming from some part " 

212 "of the body).", 

213 ) 

214 hallucinations_other = CamcopsColumn( 

215 "hallucinations_other", 

216 Boolean, 

217 permitted_value_checker=BIT_CHECKER, 

218 comment="Hallucinations (of any other kind).", 

219 ) 

220 delusions_schizophrenic = CamcopsColumn( 

221 "delusions_schizophrenic", 

222 Boolean, 

223 permitted_value_checker=BIT_CHECKER, 

224 comment="Delusions that are 'typically schizophrenic' (delusions " 

225 "of control, influence or passivity, clearly referred to body " 

226 "or limb movements or specific thoughts, actions, or " 

227 "sensations; delusional perception; persistent delusions of " 

228 "other kinds that are culturally inappropriate and completely " 

229 "impossible).", 

230 ) 

231 delusions_other = CamcopsColumn( 

232 "delusions_other", 

233 Boolean, 

234 permitted_value_checker=BIT_CHECKER, 

235 comment="Delusions (of any other kind).", 

236 ) 

237 

238 date_pertains_to = Column( 

239 "date_pertains_to", Date, comment="Date the assessment pertains to" 

240 ) 

241 comments = Column("comments", UnicodeText, comment="Clinician's comments") 

242 

243 CORE_NAMES = ["mood_elevated", "mood_irritable"] 

244 HYPOMANIA_MANIA_NAMES = [ 

245 "distractible", 

246 "activity", 

247 "sleep", 

248 "talkativeness", 

249 "recklessness", 

250 "social_disinhibition", 

251 "sexual", 

252 ] 

253 MANIA_NAMES = ["grandiosity", "flight_of_ideas"] 

254 OTHER_CRITERIA_NAMES = [ 

255 "sustained4days", 

256 "sustained7days", 

257 "admission_required", 

258 "some_interference_functioning", 

259 "severe_interference_functioning", 

260 ] 

261 PSYCHOSIS_NAMES = [ 

262 "perceptual_alterations", # not psychotic 

263 "hallucinations_schizophrenic", 

264 "hallucinations_other", 

265 "delusions_schizophrenic", 

266 "delusions_other", 

267 ] 

268 

269 @staticmethod 

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

271 _ = req.gettext 

272 return _( 

273 "ICD-10 symptomatic criteria for a manic/hypomanic episode " 

274 "(as in e.g. F06.3, F25, F30, F31)" 

275 ) 

276 

277 def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: 

278 if not self.is_complete(): 

279 return CTV_INCOMPLETE 

280 infolist = [ 

281 CtvInfo( 

282 content="Pertains to: {}. Category: {}.".format( 

283 format_datetime( 

284 self.date_pertains_to, DateFormat.LONG_DATE 

285 ), 

286 self.get_description(req), 

287 ) 

288 ) 

289 ] 

290 if self.comments: 

291 infolist.append(CtvInfo(content=ws.webify(self.comments))) 

292 return infolist 

293 

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

295 return self.standard_task_summary_fields() + [ 

296 SummaryElement( 

297 name="category", 

298 coltype=SummaryCategoryColType, 

299 value=self.get_description(req), 

300 comment="Diagnostic category", 

301 ), 

302 SummaryElement( 

303 name="psychotic_symptoms", 

304 coltype=Boolean(), 

305 value=self.psychosis_present(), 

306 comment="Psychotic symptoms present?", 

307 ), 

308 ] 

309 

310 # Meets criteria? These also return null for unknown. 

311 def meets_criteria_mania_psychotic_schizophrenic(self) -> Optional[bool]: 

312 x = self.meets_criteria_mania_ignoring_psychosis() 

313 if not x: 

314 return x 

315 if self.hallucinations_other or self.delusions_other: 

316 return False # that counts as manic psychosis 

317 if self.hallucinations_other is None or self.delusions_other is None: 

318 return None # might be manic psychosis 

319 if self.hallucinations_schizophrenic or self.delusions_schizophrenic: 

320 return True 

321 if ( 

322 self.hallucinations_schizophrenic is None 

323 or self.delusions_schizophrenic is None 

324 ): 

325 return None 

326 return False 

327 

328 def meets_criteria_mania_psychotic_icd(self) -> Optional[bool]: 

329 x = self.meets_criteria_mania_ignoring_psychosis() 

330 if not x: 

331 return x 

332 if self.hallucinations_other or self.delusions_other: 

333 return True 

334 if self.hallucinations_other is None or self.delusions_other is None: 

335 return None 

336 return False 

337 

338 def meets_criteria_mania_nonpsychotic(self) -> Optional[bool]: 

339 x = self.meets_criteria_mania_ignoring_psychosis() 

340 if not x: 

341 return x 

342 if ( 

343 self.hallucinations_schizophrenic is None 

344 or self.delusions_schizophrenic is None 

345 or self.hallucinations_other is None 

346 or self.delusions_other is None 

347 ): 

348 return None 

349 if ( 

350 self.hallucinations_schizophrenic 

351 or self.delusions_schizophrenic 

352 or self.hallucinations_other 

353 or self.delusions_other 

354 ): 

355 return False 

356 return True 

357 

358 def meets_criteria_mania_ignoring_psychosis(self) -> Optional[bool]: 

359 # When can we say "definitely not"? 

360 if is_false(self.mood_elevated) and is_false(self.mood_irritable): 

361 return False 

362 if is_false(self.sustained7days) and is_false(self.admission_required): 

363 return False 

364 t = self.count_booleans( 

365 self.HYPOMANIA_MANIA_NAMES 

366 ) + self.count_booleans(self.MANIA_NAMES) 

367 u = self.n_fields_none( 

368 self.HYPOMANIA_MANIA_NAMES 

369 ) + self.n_fields_none(self.MANIA_NAMES) 

370 if self.mood_elevated and (t + u < 3): 

371 # With elevated mood, need at least 3 symptoms 

372 return False 

373 if is_false(self.mood_elevated) and (t + u < 4): 

374 # With only irritable mood, need at least 4 symptoms 

375 return False 

376 if is_false(self.severe_interference_functioning): 

377 return False 

378 # OK. When can we say "yes"? 

379 if ( 

380 (self.mood_elevated or self.mood_irritable) 

381 and (self.sustained7days or self.admission_required) 

382 and ( 

383 (self.mood_elevated and t >= 3) 

384 or (self.mood_irritable and t >= 4) 

385 ) 

386 and self.severe_interference_functioning 

387 ): 

388 return True 

389 return None 

390 

391 def meets_criteria_hypomania(self) -> Optional[bool]: 

392 # When can we say "definitely not"? 

393 if self.meets_criteria_mania_ignoring_psychosis(): 

394 return False # silly to call it hypomania if it's mania 

395 if is_false(self.mood_elevated) and is_false(self.mood_irritable): 

396 return False 

397 if is_false(self.sustained4days): 

398 return False 

399 t = self.count_booleans(self.HYPOMANIA_MANIA_NAMES) 

400 u = self.n_fields_none(self.HYPOMANIA_MANIA_NAMES) 

401 if t + u < 3: 

402 # Need at least 3 symptoms 

403 return False 

404 if is_false(self.some_interference_functioning): 

405 return False 

406 # OK. When can we say "yes"? 

407 if ( 

408 (self.mood_elevated or self.mood_irritable) 

409 and self.sustained4days 

410 and t >= 3 

411 and self.some_interference_functioning 

412 ): 

413 return True 

414 return None 

415 

416 def meets_criteria_none(self) -> Optional[bool]: 

417 h = self.meets_criteria_hypomania() 

418 m = self.meets_criteria_mania_ignoring_psychosis() 

419 if h or m: 

420 return False 

421 if is_false(h) and is_false(m): 

422 return True 

423 return None 

424 

425 def psychosis_present(self) -> Optional[bool]: 

426 if ( 

427 self.hallucinations_other 

428 or self.hallucinations_schizophrenic 

429 or self.delusions_other 

430 or self.delusions_schizophrenic 

431 ): 

432 return True 

433 if ( 

434 self.hallucinations_other is None 

435 or self.hallucinations_schizophrenic is None 

436 or self.delusions_other is None 

437 or self.delusions_schizophrenic is None 

438 ): 

439 return None 

440 return False 

441 

442 def get_description(self, req: CamcopsRequest) -> str: 

443 if self.meets_criteria_mania_psychotic_schizophrenic(): 

444 return self.wxstring(req, "category_manic_psychotic_schizophrenic") 

445 elif self.meets_criteria_mania_psychotic_icd(): 

446 return self.wxstring(req, "category_manic_psychotic") 

447 elif self.meets_criteria_mania_nonpsychotic(): 

448 return self.wxstring(req, "category_manic_nonpsychotic") 

449 elif self.meets_criteria_hypomania(): 

450 return self.wxstring(req, "category_hypomanic") 

451 elif self.meets_criteria_none(): 

452 return self.wxstring(req, "category_none") 

453 else: 

454 return req.sstring(SS.UNKNOWN) 

455 

456 def is_complete(self) -> bool: 

457 return ( 

458 self.date_pertains_to is not None 

459 and self.meets_criteria_none() is not None 

460 and self.field_contents_valid() 

461 ) 

462 

463 def text_row(self, req: CamcopsRequest, wstringname: str) -> str: 

464 return heading_spanning_two_columns(self.wxstring(req, wstringname)) 

465 

466 def row_true_false(self, req: CamcopsRequest, fieldname: str) -> str: 

467 return self.get_twocol_bool_row_true_false( 

468 req, fieldname, self.wxstring(req, "" + fieldname) 

469 ) 

470 

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

472 h = """ 

473 {clinician_comments} 

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

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

476 {tr_is_complete} 

477 {date_pertains_to} 

478 {category} 

479 {psychotic_symptoms} 

480 </table> 

481 </div> 

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

483 {icd10_symptomatic_disclaimer} 

484 </div> 

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

486 <tr> 

487 <th width="80%">Question</th> 

488 <th width="20%">Answer</th> 

489 </tr> 

490 """.format( 

491 clinician_comments=self.get_standard_clinician_comments_block( 

492 req, self.comments 

493 ), 

494 CssClass=CssClass, 

495 tr_is_complete=self.get_is_complete_tr(req), 

496 date_pertains_to=tr_qa( 

497 req.wappstring(AS.DATE_PERTAINS_TO), 

498 format_datetime( 

499 self.date_pertains_to, DateFormat.LONG_DATE, default=None 

500 ), 

501 ), 

502 category=tr_qa( 

503 req.sstring(SS.CATEGORY) + " <sup>[1,2]</sup>", 

504 self.get_description(req), 

505 ), 

506 psychotic_symptoms=tr_qa( 

507 self.wxstring(req, "psychotic_symptoms") + " <sup>[2]</sup>", 

508 get_present_absent_none(req, self.psychosis_present()), 

509 ), 

510 icd10_symptomatic_disclaimer=req.wappstring( 

511 AS.ICD10_SYMPTOMATIC_DISCLAIMER 

512 ), 

513 ) 

514 

515 h += self.text_row(req, "core") 

516 for x in self.CORE_NAMES: 

517 h += self.row_true_false(req, x) 

518 

519 h += self.text_row(req, "hypomania_mania") 

520 for x in self.HYPOMANIA_MANIA_NAMES: 

521 h += self.row_true_false(req, x) 

522 

523 h += self.text_row(req, "other_mania") 

524 for x in self.MANIA_NAMES: 

525 h += self.row_true_false(req, x) 

526 

527 h += self.text_row(req, "other_criteria") 

528 for x in self.OTHER_CRITERIA_NAMES: 

529 h += self.row_true_false(req, x) 

530 

531 h += subheading_spanning_two_columns(self.wxstring(req, "psychosis")) 

532 for x in self.PSYCHOSIS_NAMES: 

533 h += self.row_true_false(req, x) 

534 

535 h += f""" 

536 </table> 

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

538 [1] Hypomania: 

539 elevated/irritable mood 

540 + sustained for ≥4 days 

541 + at least 3 of the “other hypomania” symptoms 

542 + some interference with functioning. 

543 Mania: 

544 elevated/irritable mood 

545 + sustained for ≥7 days or hospital admission required 

546 + at least 3 of the “other mania/hypomania” symptoms 

547 (4 if mood only irritable) 

548 + severe interference with functioning. 

549 [2] ICD-10 nonpsychotic mania requires mania without 

550 hallucinations/delusions. 

551 ICD-10 psychotic mania requires mania plus 

552 hallucinations/delusions other than those that are 

553 “typically schizophrenic”. 

554 ICD-10 does not clearly categorize mania with only 

555 schizophreniform psychotic symptoms; however, Schneiderian 

556 first-rank symptoms can occur in manic psychosis 

557 (e.g. Conus P et al., 2004, PMID 15337330.). 

558 </div> 

559 {ICD10_COPYRIGHT_DIV} 

560 """ 

561 return h