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

30 

31from cardinal_pythonlib.datetimefunc import format_datetime 

32from cardinal_pythonlib.typetests import is_false 

33import cardinal_pythonlib.rnc_web as ws 

34from sqlalchemy.sql.schema import Column 

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

36 

37from camcops_server.cc_modules.cc_constants import ( 

38 CssClass, 

39 DateFormat, 

40 ICD10_COPYRIGHT_DIV, 

41) 

42from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo 

43from camcops_server.cc_modules.cc_html import ( 

44 get_present_absent_none, 

45 heading_spanning_two_columns, 

46 subheading_spanning_two_columns, 

47 tr_qa, 

48) 

49from camcops_server.cc_modules.cc_request import CamcopsRequest 

50from camcops_server.cc_modules.cc_sqla_coltypes import ( 

51 BIT_CHECKER, 

52 CamcopsColumn, 

53 SummaryCategoryColType, 

54) 

55from camcops_server.cc_modules.cc_string import AS 

56from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

57from camcops_server.cc_modules.cc_task import ( 

58 Task, 

59 TaskHasClinicianMixin, 

60 TaskHasPatientMixin, 

61) 

62from camcops_server.cc_modules.cc_text import SS 

63 

64 

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

66# Icd10Manic 

67# ============================================================================= 

68 

69class Icd10Manic(TaskHasClinicianMixin, TaskHasPatientMixin, Task): 

70 """ 

71 Server implementation of the ICD10-MANIC task. 

72 """ 

73 __tablename__ = "icd10manic" 

74 shortname = "ICD10-MANIC" 

75 

76 mood_elevated = CamcopsColumn( 

77 "mood_elevated", Boolean, 

78 permitted_value_checker=BIT_CHECKER, 

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

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

81 "definitely abnormal for the individual concerned." 

82 ) 

83 mood_irritable = CamcopsColumn( 

84 "mood_irritable", Boolean, 

85 permitted_value_checker=BIT_CHECKER, 

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

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

88 "for the individual concerned." 

89 ) 

90 

91 distractible = CamcopsColumn( 

92 "distractible", Boolean, 

93 permitted_value_checker=BIT_CHECKER, 

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

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

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

97 ) 

98 activity = CamcopsColumn( 

99 "activity", Boolean, 

100 permitted_value_checker=BIT_CHECKER, 

101 comment="Increased activity or physical restlessness." 

102 ) 

103 sleep = CamcopsColumn( 

104 "sleep", Boolean, 

105 permitted_value_checker=BIT_CHECKER, 

106 comment="Decreased need for sleep." 

107 ) 

108 talkativeness = CamcopsColumn( 

109 "talkativeness", Boolean, 

110 permitted_value_checker=BIT_CHECKER, 

111 comment="Increased talkativeness (pressure of speech)." 

112 ) 

113 recklessness = CamcopsColumn( 

114 "recklessness", Boolean, 

115 permitted_value_checker=BIT_CHECKER, 

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

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

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

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

120 "reckless driving [mania]." 

121 ) 

122 social_disinhibition = CamcopsColumn( 

123 "social_disinhibition", Boolean, 

124 permitted_value_checker=BIT_CHECKER, 

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

126 "loss of normal social inhibitions resulting in behaviour " 

127 "which is inappropriate to the circumstances [mania]." 

128 ) 

129 sexual = CamcopsColumn( 

130 "sexual", Boolean, 

131 permitted_value_checker=BIT_CHECKER, 

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

133 "energy or sexual indiscretions [mania]." 

134 ) 

135 

136 grandiosity = CamcopsColumn( 

137 "grandiosity", Boolean, 

138 permitted_value_checker=BIT_CHECKER, 

139 comment="Inflated self-esteem or grandiosity." 

140 ) 

141 flight_of_ideas = CamcopsColumn( 

142 "flight_of_ideas", Boolean, 

143 permitted_value_checker=BIT_CHECKER, 

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

145 "thoughts racing." 

146 ) 

147 

148 sustained4days = CamcopsColumn( 

149 "sustained4days", Boolean, 

150 permitted_value_checker=BIT_CHECKER, 

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

152 ) 

153 sustained7days = CamcopsColumn( 

154 "sustained7days", Boolean, 

155 permitted_value_checker=BIT_CHECKER, 

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

157 ) 

158 admission_required = CamcopsColumn( 

159 "admission_required", Boolean, 

160 permitted_value_checker=BIT_CHECKER, 

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

162 "hospital admission." 

163 ) 

164 some_interference_functioning = CamcopsColumn( 

165 "some_interference_functioning", Boolean, 

166 permitted_value_checker=BIT_CHECKER, 

167 comment="Some interference with personal functioning " 

168 "in daily living." 

169 ) 

170 severe_interference_functioning = CamcopsColumn( 

171 "severe_interference_functioning", Boolean, 

172 permitted_value_checker=BIT_CHECKER, 

173 comment="Severe interference with personal " 

174 "functioning in daily living." 

175 ) 

176 

177 perceptual_alterations = CamcopsColumn( 

178 "perceptual_alterations", Boolean, 

179 permitted_value_checker=BIT_CHECKER, 

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

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

182 ) # ... not psychotic 

183 hallucinations_schizophrenic = CamcopsColumn( 

184 "hallucinations_schizophrenic", Boolean, 

185 permitted_value_checker=BIT_CHECKER, 

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

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

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

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

190 "of the body)." 

191 ) 

192 hallucinations_other = CamcopsColumn( 

193 "hallucinations_other", Boolean, 

194 permitted_value_checker=BIT_CHECKER, 

195 comment="Hallucinations (of any other kind)." 

196 ) 

197 delusions_schizophrenic = CamcopsColumn( 

198 "delusions_schizophrenic", Boolean, 

199 permitted_value_checker=BIT_CHECKER, 

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

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

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

203 "sensations; delusional perception; persistent delusions of " 

204 "other kinds that are culturally inappropriate and completely " 

205 "impossible)." 

206 ) 

207 delusions_other = CamcopsColumn( 

208 "delusions_other", Boolean, 

209 permitted_value_checker=BIT_CHECKER, 

210 comment="Delusions (of any other kind)." 

211 ) 

212 

213 date_pertains_to = Column( 

214 "date_pertains_to", Date, 

215 comment="Date the assessment pertains to" 

216 ) 

217 comments = Column( 

218 "comments", UnicodeText, 

219 comment="Clinician's comments" 

220 ) 

221 

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

223 HYPOMANIA_MANIA_NAMES = [ 

224 "distractible", "activity", "sleep", 

225 "talkativeness", "recklessness", "social_disinhibition", "sexual" 

226 ] 

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

228 OTHER_CRITERIA_NAMES = [ 

229 "sustained4days", "sustained7days", "admission_required", 

230 "some_interference_functioning", "severe_interference_functioning" 

231 ] 

232 PSYCHOSIS_NAMES = [ 

233 "perceptual_alterations", # not psychotic 

234 "hallucinations_schizophrenic", "hallucinations_other", 

235 "delusions_schizophrenic", "delusions_other" 

236 ] 

237 

238 @staticmethod 

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

240 _ = req.gettext 

241 return _( 

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

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

244 ) 

245 

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

247 if not self.is_complete(): 

248 return CTV_INCOMPLETE 

249 infolist = [CtvInfo( 

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

251 format_datetime(self.date_pertains_to, DateFormat.LONG_DATE), 

252 self.get_description(req) 

253 ) 

254 )] 

255 if self.comments: 

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

257 return infolist 

258 

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

260 return self.standard_task_summary_fields() + [ 

261 SummaryElement(name="category", 

262 coltype=SummaryCategoryColType, 

263 value=self.get_description(req), 

264 comment="Diagnostic category"), 

265 SummaryElement(name="psychotic_symptoms", 

266 coltype=Boolean(), 

267 value=self.psychosis_present(), 

268 comment="Psychotic symptoms present?"), 

269 ] 

270 

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

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

273 x = self.meets_criteria_mania_ignoring_psychosis() 

274 if not x: 

275 return x 

276 if self.hallucinations_other or self.delusions_other: 

277 return False # that counts as manic psychosis 

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

279 return None # might be manic psychosis 

280 if self.hallucinations_schizophrenic or self.delusions_schizophrenic: 

281 return True 

282 if (self.hallucinations_schizophrenic is None or 

283 self.delusions_schizophrenic is None): 

284 return None 

285 return False 

286 

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

288 x = self.meets_criteria_mania_ignoring_psychosis() 

289 if not x: 

290 return x 

291 if self.hallucinations_other or self.delusions_other: 

292 return True 

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

294 return None 

295 return False 

296 

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

298 x = self.meets_criteria_mania_ignoring_psychosis() 

299 if not x: 

300 return x 

301 if (self.hallucinations_schizophrenic is None or 

302 self.delusions_schizophrenic is None or 

303 self.hallucinations_other is None or 

304 self.delusions_other is None): 

305 return None 

306 if (self.hallucinations_schizophrenic or 

307 self.delusions_schizophrenic or 

308 self.hallucinations_other or 

309 self.delusions_other): 

310 return False 

311 return True 

312 

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

314 # When can we say "definitely not"? 

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

316 return False 

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

318 return False 

319 t = self.count_booleans(self.HYPOMANIA_MANIA_NAMES) + \ 

320 self.count_booleans(self.MANIA_NAMES) 

321 u = self.n_fields_none(self.HYPOMANIA_MANIA_NAMES) + \ 

322 self.n_fields_none(self.MANIA_NAMES) 

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

324 # With elevated mood, need at least 3 symptoms 

325 return False 

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

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

328 return False 

329 if is_false(self.severe_interference_functioning): 

330 return False 

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

332 if ((self.mood_elevated or self.mood_irritable) and 

333 (self.sustained7days or self.admission_required) and 

334 ((self.mood_elevated and t >= 3) or 

335 (self.mood_irritable and t >= 4)) and 

336 self.severe_interference_functioning): 

337 return True 

338 return None 

339 

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

341 # When can we say "definitely not"? 

342 if self.meets_criteria_mania_ignoring_psychosis(): 

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

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

345 return False 

346 if is_false(self.sustained4days): 

347 return False 

348 t = self.count_booleans(self.HYPOMANIA_MANIA_NAMES) 

349 u = self.n_fields_none(self.HYPOMANIA_MANIA_NAMES) 

350 if t + u < 3: 

351 # Need at least 3 symptoms 

352 return False 

353 if is_false(self.some_interference_functioning): 

354 return False 

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

356 if ((self.mood_elevated or self.mood_irritable) and 

357 self.sustained4days and 

358 t >= 3 and 

359 self.some_interference_functioning): 

360 return True 

361 return None 

362 

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

364 h = self.meets_criteria_hypomania() 

365 m = self.meets_criteria_mania_ignoring_psychosis() 

366 if h or m: 

367 return False 

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

369 return True 

370 return None 

371 

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

373 if (self.hallucinations_other or 

374 self.hallucinations_schizophrenic or 

375 self.delusions_other or 

376 self.delusions_schizophrenic): 

377 return True 

378 if (self.hallucinations_other is None or 

379 self.hallucinations_schizophrenic is None or 

380 self.delusions_other is None or 

381 self.delusions_schizophrenic is None): 

382 return None 

383 return False 

384 

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

386 if self.meets_criteria_mania_psychotic_schizophrenic(): 

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

388 elif self.meets_criteria_mania_psychotic_icd(): 

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

390 elif self.meets_criteria_mania_nonpsychotic(): 

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

392 elif self.meets_criteria_hypomania(): 

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

394 elif self.meets_criteria_none(): 

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

396 else: 

397 return req.sstring(SS.UNKNOWN) 

398 

399 def is_complete(self) -> bool: 

400 return ( 

401 self.date_pertains_to is not None and 

402 self.meets_criteria_none() is not None and 

403 self.field_contents_valid() 

404 ) 

405 

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

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

408 

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

410 return self.get_twocol_bool_row_true_false( 

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

412 

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

414 h = """ 

415 {clinician_comments} 

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

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

418 {tr_is_complete} 

419 {date_pertains_to} 

420 {category} 

421 {psychotic_symptoms} 

422 </table> 

423 </div> 

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

425 {icd10_symptomatic_disclaimer} 

426 </div> 

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

428 <tr> 

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

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

431 </tr> 

432 """.format( 

433 clinician_comments=self.get_standard_clinician_comments_block( 

434 req, self.comments), 

435 CssClass=CssClass, 

436 tr_is_complete=self.get_is_complete_tr(req), 

437 date_pertains_to=tr_qa( 

438 req.wappstring(AS.DATE_PERTAINS_TO), 

439 format_datetime(self.date_pertains_to, 

440 DateFormat.LONG_DATE, default=None) 

441 ), 

442 category=tr_qa( 

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

444 self.get_description(req) 

445 ), 

446 psychotic_symptoms=tr_qa( 

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

448 get_present_absent_none(req, self.psychosis_present()) 

449 ), 

450 icd10_symptomatic_disclaimer=req.wappstring( 

451 AS.ICD10_SYMPTOMATIC_DISCLAIMER), 

452 ) 

453 

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

455 for x in self.CORE_NAMES: 

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

457 

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

459 for x in self.HYPOMANIA_MANIA_NAMES: 

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

461 

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

463 for x in self.MANIA_NAMES: 

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

465 

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

467 for x in self.OTHER_CRITERIA_NAMES: 

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

469 

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

471 for x in self.PSYCHOSIS_NAMES: 

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

473 

474 h += f""" 

475 </table> 

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

477 [1] Hypomania: 

478 elevated/irritable mood 

479 + sustained for ≥4 days 

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

481 + some interference with functioning. 

482 Mania: 

483 elevated/irritable mood 

484 + sustained for ≥7 days or hospital admission required 

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

486 (4 if mood only irritable) 

487 + severe interference with functioning. 

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

489 hallucinations/delusions. 

490 ICD-10 psychotic mania requires mania plus 

491 hallucinations/delusions other than those that are 

492 “typically schizophrenic”. 

493 ICD-10 does not clearly categorize mania with only 

494 schizophreniform psychotic symptoms; however, Schneiderian 

495 first-rank symptoms can occur in manic psychosis 

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

497 </div> 

498 {ICD10_COPYRIGHT_DIV} 

499 """ 

500 return h