Coverage for tasks/icd10schizophrenia.py: 53%

132 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/icd10schizophrenia.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 

33import cardinal_pythonlib.rnc_web as ws 

34from cardinal_pythonlib.typetests import is_false 

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_true_false_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) 

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) 

62 

63 

64# ============================================================================= 

65# Icd10Schizophrenia 

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

67 

68 

69class Icd10Schizophrenia(TaskHasClinicianMixin, TaskHasPatientMixin, Task): 

70 """ 

71 Server implementation of the ICD10-SZ task. 

72 """ 

73 

74 __tablename__ = "icd10schizophrenia" 

75 shortname = "ICD10-SZ" 

76 info_filename_stem = "icd" 

77 

78 passivity_bodily = CamcopsColumn( 

79 "passivity_bodily", 

80 Boolean, 

81 permitted_value_checker=BIT_CHECKER, 

82 comment="Passivity: delusions of control, influence, or " 

83 "passivity, clearly referred to body or limb movements...", 

84 ) 

85 passivity_mental = CamcopsColumn( 

86 "passivity_mental", 

87 Boolean, 

88 permitted_value_checker=BIT_CHECKER, 

89 comment="(passivity) ... or to specific thoughts, actions, or " 

90 "sensations.", 

91 ) 

92 hv_commentary = CamcopsColumn( 

93 "hv_commentary", 

94 Boolean, 

95 permitted_value_checker=BIT_CHECKER, 

96 comment="Hallucinatory voices giving a running commentary on the " 

97 "patient's behaviour", 

98 ) 

99 hv_discussing = CamcopsColumn( 

100 "hv_discussing", 

101 Boolean, 

102 permitted_value_checker=BIT_CHECKER, 

103 comment="Hallucinatory voices discussing the patient among " 

104 "themselves", 

105 ) 

106 hv_from_body = CamcopsColumn( 

107 "hv_from_body", 

108 Boolean, 

109 permitted_value_checker=BIT_CHECKER, 

110 comment="Other types of hallucinatory voices coming from some " 

111 "part of the body", 

112 ) 

113 delusions = CamcopsColumn( 

114 "delusions", 

115 Boolean, 

116 permitted_value_checker=BIT_CHECKER, 

117 comment="Delusions: persistent delusions of other kinds that are " 

118 "culturally inappropriate and completely impossible, such as " 

119 "religious or political identity, or superhuman powers and " 

120 "abilities (e.g. being able to control the weather, or being " 

121 "in communication with aliens from another world).", 

122 ) 

123 delusional_perception = CamcopsColumn( 

124 "delusional_perception", 

125 Boolean, 

126 permitted_value_checker=BIT_CHECKER, 

127 comment="Delusional perception [a normal perception, " 

128 "delusionally interpreted]", 

129 ) 

130 thought_echo = CamcopsColumn( 

131 "thought_echo", 

132 Boolean, 

133 permitted_value_checker=BIT_CHECKER, 

134 comment="Thought echo [hearing one's own thoughts aloud, just " 

135 "before, just after, or simultaneously with the thought]", 

136 ) 

137 thought_withdrawal = CamcopsColumn( 

138 "thought_withdrawal", 

139 Boolean, 

140 permitted_value_checker=BIT_CHECKER, 

141 comment="Thought withdrawal [the feeling that one's thoughts " 

142 "have been removed by an outside agency]", 

143 ) 

144 thought_insertion = CamcopsColumn( 

145 "thought_insertion", 

146 Boolean, 

147 permitted_value_checker=BIT_CHECKER, 

148 comment="Thought insertion [the feeling that one's thoughts have " 

149 "been placed there from outside]", 

150 ) 

151 thought_broadcasting = CamcopsColumn( 

152 "thought_broadcasting", 

153 Boolean, 

154 permitted_value_checker=BIT_CHECKER, 

155 comment="Thought broadcasting [the feeling that one's thoughts " 

156 "leave oneself and are diffused widely, or are audible to " 

157 "others, or that others think the same thoughts in unison]", 

158 ) 

159 

160 hallucinations_other = CamcopsColumn( 

161 "hallucinations_other", 

162 Boolean, 

163 permitted_value_checker=BIT_CHECKER, 

164 comment="Hallucinations: persistent hallucinations in any " 

165 "modality, when accompanied either by fleeting or half-formed " 

166 "delusions without clear affective content, or by persistent " 

167 "over-valued ideas, or when occurring every day for weeks or " 

168 "months on end.", 

169 ) 

170 thought_disorder = CamcopsColumn( 

171 "thought_disorder", 

172 Boolean, 

173 permitted_value_checker=BIT_CHECKER, 

174 comment="Thought disorder: breaks or interpolations in the train " 

175 "of thought, resulting in incoherence or irrelevant speech, " 

176 "or neologisms.", 

177 ) 

178 catatonia = CamcopsColumn( 

179 "catatonia", 

180 Boolean, 

181 permitted_value_checker=BIT_CHECKER, 

182 comment="Catatonia: catatonic behaviour, such as excitement, " 

183 "posturing, or waxy flexibility, negativism, mutism, and " 

184 "stupor.", 

185 ) 

186 

187 negative = CamcopsColumn( 

188 "negative", 

189 Boolean, 

190 permitted_value_checker=BIT_CHECKER, 

191 comment="Negative symptoms: 'negative' symptoms such as marked " 

192 "apathy, paucity of speech, and blunting or incongruity of " 

193 "emotional responses, usually resulting in social withdrawal " 

194 "and lowering of social performance; it must be clear that " 

195 "these are not due to depression or to neuroleptic " 

196 "medication.", 

197 ) 

198 

199 present_one_month = CamcopsColumn( 

200 "present_one_month", 

201 Boolean, 

202 permitted_value_checker=BIT_CHECKER, 

203 comment="Symptoms in groups A-C present for most of the time " 

204 "during an episode of psychotic illness lasting for at least " 

205 "one month (or at some time during most of the days).", 

206 ) 

207 

208 also_manic = CamcopsColumn( 

209 "also_manic", 

210 Boolean, 

211 permitted_value_checker=BIT_CHECKER, 

212 comment="Also meets criteria for manic episode (F30)?", 

213 ) 

214 also_depressive = CamcopsColumn( 

215 "also_depressive", 

216 Boolean, 

217 permitted_value_checker=BIT_CHECKER, 

218 comment="Also meets criteria for depressive episode (F32)?", 

219 ) 

220 if_mood_psychosis_first = CamcopsColumn( 

221 "if_mood_psychosis_first", 

222 Boolean, 

223 permitted_value_checker=BIT_CHECKER, 

224 comment="If the patient also meets criteria for manic episode " 

225 "(F30) or depressive episode (F32), the criteria listed above " 

226 "must have been met before the disturbance of mood developed.", 

227 ) 

228 

229 not_organic_or_substance = CamcopsColumn( 

230 "not_organic_or_substance", 

231 Boolean, 

232 permitted_value_checker=BIT_CHECKER, 

233 comment="The disorder is not attributable to organic brain " 

234 "disease (in the sense of F0), or to alcohol- or drug-related " 

235 "intoxication, dependence or withdrawal.", 

236 ) 

237 

238 behaviour_change = CamcopsColumn( 

239 "behaviour_change", 

240 Boolean, 

241 permitted_value_checker=BIT_CHECKER, 

242 comment="A significant and consistent change in the overall " 

243 "quality of some aspects of personal behaviour, manifest as " 

244 "loss of interest, aimlessness, idleness, a self-absorbed " 

245 "attitude, and social withdrawal.", 

246 ) 

247 performance_decline = CamcopsColumn( 

248 "performance_decline", 

249 Boolean, 

250 permitted_value_checker=BIT_CHECKER, 

251 comment="Marked decline in social, scholastic, or occupational " 

252 "performance.", 

253 ) 

254 

255 subtype_paranoid = CamcopsColumn( 

256 "subtype_paranoid", 

257 Boolean, 

258 permitted_value_checker=BIT_CHECKER, 

259 comment="PARANOID (F20.0): dominated by delusions or hallucinations.", 

260 ) 

261 subtype_hebephrenic = CamcopsColumn( 

262 "subtype_hebephrenic", 

263 Boolean, 

264 permitted_value_checker=BIT_CHECKER, 

265 comment="HEBEPHRENIC (F20.1): dominated by affective changes " 

266 "(shallow, flat, incongruous, or inappropriate affect) and " 

267 "either pronounced thought disorder or aimless, disjointed " 

268 "behaviour is present.", 

269 ) 

270 subtype_catatonic = CamcopsColumn( 

271 "subtype_catatonic", 

272 Boolean, 

273 permitted_value_checker=BIT_CHECKER, 

274 comment="CATATONIC (F20.2): psychomotor disturbances dominate " 

275 "(such as stupor, mutism, excitement, posturing, negativism, " 

276 "rigidity, waxy flexibility, command automatisms, or verbal " 

277 "perseveration).", 

278 ) 

279 subtype_undifferentiated = CamcopsColumn( 

280 "subtype_undifferentiated", 

281 Boolean, 

282 permitted_value_checker=BIT_CHECKER, 

283 comment="UNDIFFERENTIATED (F20.3): schizophrenia with active " 

284 "psychosis fitting none or more than one of the above three " 

285 "types.", 

286 ) 

287 subtype_postschizophrenic_depression = CamcopsColumn( 

288 "subtype_postschizophrenic_depression", 

289 Boolean, 

290 permitted_value_checker=BIT_CHECKER, 

291 comment="POST-SCHIZOPHRENIC DEPRESSION (F20.4): in which a depressive " 

292 "episode has developed for at least 2 weeks following a " 

293 "schizophrenic episode within the last 12 months and in which " 

294 "schizophrenic symptoms persist but are not as prominent as " 

295 "the depression.", 

296 ) 

297 subtype_residual = CamcopsColumn( 

298 "subtype_residual", 

299 Boolean, 

300 permitted_value_checker=BIT_CHECKER, 

301 comment="RESIDUAL (F20.5): in which previous psychotic episodes " 

302 "of schizophrenia have given way to a chronic condition with " 

303 "'negative' symptoms of schizophrenia for at least 1 year.", 

304 ) 

305 subtype_simple = CamcopsColumn( 

306 "subtype_simple", 

307 Boolean, 

308 permitted_value_checker=BIT_CHECKER, 

309 comment="SIMPLE SCHIZOPHRENIA (F20.6), in which 'negative' " 

310 "symptoms (C) with a change in personal behaviour (D) develop " 

311 "for at least one year without any psychotic episodes (no " 

312 "symptoms from groups A or B or other hallucinations or " 

313 "well-formed delusions), and with a marked decline in social, " 

314 "scholastic, or occupational performance.", 

315 ) 

316 subtype_cenesthopathic = CamcopsColumn( 

317 "subtype_cenesthopathic", 

318 Boolean, 

319 permitted_value_checker=BIT_CHECKER, 

320 comment="CENESTHOPATHIC (within OTHER F20.8): body image " 

321 "aberration (e.g. desomatization, loss of bodily boundaries, " 

322 "feelings of body size change) or abnormal bodily sensations " 

323 "(e.g. numbness, stiffness, feeling strange, " 

324 "depersonalization, or sensations of pain, temperature, " 

325 "electricity, heaviness, lightness, or discomfort when " 

326 "touched) dominate.", 

327 ) 

328 

329 date_pertains_to = Column( 

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

331 ) 

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

333 

334 A_NAMES = [ 

335 "passivity_bodily", 

336 "passivity_mental", 

337 "hv_commentary", 

338 "hv_discussing", 

339 "hv_from_body", 

340 "delusions", 

341 "delusional_perception", 

342 "thought_echo", 

343 "thought_withdrawal", 

344 "thought_insertion", 

345 "thought_broadcasting", 

346 ] 

347 B_NAMES = ["hallucinations_other", "thought_disorder", "catatonia"] 

348 C_NAMES = ["negative"] 

349 D_NAMES = ["present_one_month"] 

350 E_NAMES = ["also_manic", "also_depressive", "if_mood_psychosis_first"] 

351 F_NAMES = ["not_organic_or_substance"] 

352 G_NAMES = ["behaviour_change", "performance_decline"] 

353 H_NAMES = [ 

354 "subtype_paranoid", 

355 "subtype_hebephrenic", 

356 "subtype_catatonic", 

357 "subtype_undifferentiated", 

358 "subtype_postschizophrenic_depression", 

359 "subtype_residual", 

360 "subtype_simple", 

361 "subtype_cenesthopathic", 

362 ] 

363 

364 @staticmethod 

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

366 _ = req.gettext 

367 return _("ICD-10 criteria for schizophrenia (F20)") 

368 

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

370 if not self.is_complete(): 

371 return CTV_INCOMPLETE 

372 c = self.meets_general_criteria() 

373 if c is None: 

374 category = "Unknown if met or not met" 

375 elif c: 

376 category = "Met" 

377 else: 

378 category = "Not met" 

379 infolist = [ 

380 CtvInfo( 

381 content=( 

382 "Pertains to: {}. General criteria for " 

383 "schizophrenia: {}.".format( 

384 format_datetime( 

385 self.date_pertains_to, DateFormat.LONG_DATE 

386 ), 

387 category, 

388 ) 

389 ) 

390 ) 

391 ] 

392 if self.comments: 

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

394 return infolist 

395 

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

397 return self.standard_task_summary_fields() + [ 

398 SummaryElement( 

399 name="meets_general_criteria", 

400 coltype=Boolean(), 

401 value=self.meets_general_criteria(), 

402 comment="Meets general criteria for paranoid/hebephrenic/" 

403 "catatonic/undifferentiated schizophrenia " 

404 "(F20.0-F20.3)?", 

405 ) 

406 ] 

407 

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

409 def meets_general_criteria(self) -> Optional[bool]: 

410 t_a = self.count_booleans(Icd10Schizophrenia.A_NAMES) 

411 u_a = self.n_fields_none(Icd10Schizophrenia.A_NAMES) 

412 t_b = self.count_booleans( 

413 Icd10Schizophrenia.B_NAMES 

414 ) + self.count_booleans(Icd10Schizophrenia.C_NAMES) 

415 u_b = self.n_fields_none( 

416 Icd10Schizophrenia.B_NAMES 

417 ) + self.n_fields_none(Icd10Schizophrenia.C_NAMES) 

418 if t_a + u_a < 1 and t_b + u_b < 2: 

419 return False 

420 if self.present_one_month is not None and not self.present_one_month: 

421 return False 

422 if (self.also_manic or self.also_depressive) and is_false( 

423 self.if_mood_psychosis_first 

424 ): 

425 return False 

426 if is_false(self.not_organic_or_substance): 

427 return False 

428 if ( 

429 (t_a >= 1 or t_b >= 2) 

430 and self.present_one_month 

431 and ( 

432 (is_false(self.also_manic) and is_false(self.also_depressive)) 

433 or self.if_mood_psychosis_first 

434 ) 

435 and self.not_organic_or_substance 

436 ): 

437 return True 

438 return None 

439 

440 def is_complete(self) -> bool: 

441 return ( 

442 self.date_pertains_to is not None 

443 and self.meets_general_criteria() is not None 

444 and self.field_contents_valid() 

445 ) 

446 

447 def heading_row( 

448 self, req: CamcopsRequest, wstringname: str, extra: str = None 

449 ) -> str: 

450 return heading_spanning_two_columns( 

451 self.wxstring(req, wstringname) + (extra or "") 

452 ) 

453 

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

455 return subheading_spanning_two_columns(self.wxstring(req, wstringname)) 

456 

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

458 return self.get_twocol_bool_row_true_false( 

459 req, fieldname, self.wxstring(req, fieldname) 

460 ) 

461 

462 def row_present_absent(self, req: CamcopsRequest, fieldname: str) -> str: 

463 return self.get_twocol_bool_row_present_absent( 

464 req, fieldname, self.wxstring(req, fieldname) 

465 ) 

466 

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

468 h = """ 

469 {clinician_comments} 

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

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

472 {tr_is_complete} 

473 {date_pertains_to} 

474 {meets_general_criteria} 

475 </table> 

476 </div> 

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

478 {comments} 

479 </div> 

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

481 <tr> 

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

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

484 </tr> 

485 """.format( 

486 clinician_comments=self.get_standard_clinician_comments_block( 

487 req, self.comments 

488 ), 

489 CssClass=CssClass, 

490 tr_is_complete=self.get_is_complete_tr(req), 

491 date_pertains_to=tr_qa( 

492 req.wappstring(AS.DATE_PERTAINS_TO), 

493 format_datetime( 

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

495 ), 

496 ), 

497 meets_general_criteria=tr_qa( 

498 self.wxstring(req, "meets_general_criteria") 

499 + " <sup>[1]</sup>", # noqa 

500 get_true_false_none(req, self.meets_general_criteria()), 

501 ), 

502 comments=self.wxstring(req, "comments"), 

503 ) 

504 

505 h += self.heading_row(req, "core", " <sup>[2]</sup>") 

506 for x in Icd10Schizophrenia.A_NAMES: 

507 h += self.row_present_absent(req, x) 

508 

509 h += self.heading_row(req, "other_positive") 

510 for x in Icd10Schizophrenia.B_NAMES: 

511 h += self.row_present_absent(req, x) 

512 

513 h += self.heading_row(req, "negative_title") 

514 for x in Icd10Schizophrenia.C_NAMES: 

515 h += self.row_present_absent(req, x) 

516 

517 h += self.heading_row(req, "other_criteria") 

518 for x in Icd10Schizophrenia.D_NAMES: 

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

520 h += self.text_row(req, "duration_comment") 

521 for x in Icd10Schizophrenia.E_NAMES: 

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

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

524 for x in Icd10Schizophrenia.F_NAMES: 

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

526 

527 h += self.heading_row(req, "simple_title") 

528 for x in Icd10Schizophrenia.G_NAMES: 

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

530 

531 h += self.heading_row(req, "subtypes") 

532 for x in Icd10Schizophrenia.H_NAMES: 

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

534 

535 h += f""" 

536 </table> 

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

538 [1] All of: 

539 (a) at least one core symptom, or at least two of the other 

540 positive or negative symptoms; 

541 (b) present for a month (etc.); 

542 (c) if also manic/depressed, schizophreniform psychosis 

543 came first; 

544 (d) not attributable to organic brain disease or 

545 psychoactive substances. 

546 [2] Symptom definitions from: 

547 (a) Oyebode F (2008). Sims’ Symptoms in the Mind: An 

548 Introduction to Descriptive Psychopathology. Fourth 

549 edition, Saunders, Elsevier, Edinburgh. 

550 (b) Pawar AV &amp; Spence SA (2003), PMID 14519605. 

551 </div> 

552 {ICD10_COPYRIGHT_DIV} 

553 """ 

554 return h