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

32import cardinal_pythonlib.rnc_web as ws 

33from cardinal_pythonlib.typetests import is_false 

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

54from camcops_server.cc_modules.cc_string import AS 

55from camcops_server.cc_modules.cc_summaryelement import SummaryElement 

56from camcops_server.cc_modules.cc_task import ( 

57 Task, 

58 TaskHasClinicianMixin, 

59 TaskHasPatientMixin, 

60) 

61 

62 

63# ============================================================================= 

64# Icd10Schizophrenia 

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

66 

67class Icd10Schizophrenia(TaskHasClinicianMixin, TaskHasPatientMixin, Task): 

68 """ 

69 Server implementation of the ICD10-SZ task. 

70 """ 

71 __tablename__ = "icd10schizophrenia" 

72 shortname = "ICD10-SZ" 

73 

74 passivity_bodily = CamcopsColumn( 

75 "passivity_bodily", Boolean, 

76 permitted_value_checker=BIT_CHECKER, 

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

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

79 ) 

80 passivity_mental = CamcopsColumn( 

81 "passivity_mental", Boolean, 

82 permitted_value_checker=BIT_CHECKER, 

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

84 "sensations." 

85 ) 

86 hv_commentary = CamcopsColumn( 

87 "hv_commentary", Boolean, 

88 permitted_value_checker=BIT_CHECKER, 

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

90 "patient's behaviour" 

91 ) 

92 hv_discussing = CamcopsColumn( 

93 "hv_discussing", Boolean, 

94 permitted_value_checker=BIT_CHECKER, 

95 comment="Hallucinatory voices discussing the patient among " 

96 "themselves" 

97 ) 

98 hv_from_body = CamcopsColumn( 

99 "hv_from_body", Boolean, 

100 permitted_value_checker=BIT_CHECKER, 

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

102 "part of the body" 

103 ) 

104 delusions = CamcopsColumn( 

105 "delusions", Boolean, 

106 permitted_value_checker=BIT_CHECKER, 

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

108 "culturally inappropriate and completely impossible, such as " 

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

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

111 "in communication with aliens from another world)." 

112 ) 

113 delusional_perception = CamcopsColumn( 

114 "delusional_perception", Boolean, 

115 permitted_value_checker=BIT_CHECKER, 

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

117 "delusionally interpreted]" 

118 ) 

119 thought_echo = CamcopsColumn( 

120 "thought_echo", Boolean, 

121 permitted_value_checker=BIT_CHECKER, 

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

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

124 ) 

125 thought_withdrawal = CamcopsColumn( 

126 "thought_withdrawal", Boolean, 

127 permitted_value_checker=BIT_CHECKER, 

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

129 "have been removed by an outside agency]" 

130 ) 

131 thought_insertion = CamcopsColumn( 

132 "thought_insertion", Boolean, 

133 permitted_value_checker=BIT_CHECKER, 

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

135 "been placed there from outside]" 

136 ) 

137 thought_broadcasting = CamcopsColumn( 

138 "thought_broadcasting", Boolean, 

139 permitted_value_checker=BIT_CHECKER, 

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

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

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

143 ) 

144 

145 hallucinations_other = CamcopsColumn( 

146 "hallucinations_other", Boolean, 

147 permitted_value_checker=BIT_CHECKER, 

148 comment="Hallucinations: persistent hallucinations in any " 

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

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

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

152 "months on end." 

153 ) 

154 thought_disorder = CamcopsColumn( 

155 "thought_disorder", Boolean, 

156 permitted_value_checker=BIT_CHECKER, 

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

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

159 "or neologisms." 

160 ) 

161 catatonia = CamcopsColumn( 

162 "catatonia", Boolean, 

163 permitted_value_checker=BIT_CHECKER, 

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

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

166 "stupor." 

167 ) 

168 

169 negative = CamcopsColumn( 

170 "negative", Boolean, 

171 permitted_value_checker=BIT_CHECKER, 

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

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

174 "emotional responses, usually resulting in social withdrawal " 

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

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

177 "medication." 

178 ) 

179 

180 present_one_month = CamcopsColumn( 

181 "present_one_month", Boolean, 

182 permitted_value_checker=BIT_CHECKER, 

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

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

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

186 ) 

187 

188 also_manic = CamcopsColumn( 

189 "also_manic", Boolean, 

190 permitted_value_checker=BIT_CHECKER, 

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

192 ) 

193 also_depressive = CamcopsColumn( 

194 "also_depressive", Boolean, 

195 permitted_value_checker=BIT_CHECKER, 

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

197 ) 

198 if_mood_psychosis_first = CamcopsColumn( 

199 "if_mood_psychosis_first", Boolean, 

200 permitted_value_checker=BIT_CHECKER, 

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

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

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

204 ) 

205 

206 not_organic_or_substance = CamcopsColumn( 

207 "not_organic_or_substance", Boolean, 

208 permitted_value_checker=BIT_CHECKER, 

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

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

211 "intoxication, dependence or withdrawal." 

212 ) 

213 

214 behaviour_change = CamcopsColumn( 

215 "behaviour_change", Boolean, 

216 permitted_value_checker=BIT_CHECKER, 

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

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

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

220 "attitude, and social withdrawal." 

221 ) 

222 performance_decline = CamcopsColumn( 

223 "performance_decline", Boolean, 

224 permitted_value_checker=BIT_CHECKER, 

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

226 "performance." 

227 ) 

228 

229 subtype_paranoid = CamcopsColumn( 

230 "subtype_paranoid", Boolean, 

231 permitted_value_checker=BIT_CHECKER, 

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

233 ) 

234 subtype_hebephrenic = CamcopsColumn( 

235 "subtype_hebephrenic", Boolean, 

236 permitted_value_checker=BIT_CHECKER, 

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

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

239 "either pronounced thought disorder or aimless, disjointed " 

240 "behaviour is present." 

241 ) 

242 subtype_catatonic = CamcopsColumn( 

243 "subtype_catatonic", Boolean, 

244 permitted_value_checker=BIT_CHECKER, 

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

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

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

248 "perseveration)." 

249 ) 

250 subtype_undifferentiated = CamcopsColumn( 

251 "subtype_undifferentiated", Boolean, 

252 permitted_value_checker=BIT_CHECKER, 

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

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

255 "types." 

256 ) 

257 subtype_postschizophrenic_depression = CamcopsColumn( 

258 "subtype_postschizophrenic_depression", Boolean, 

259 permitted_value_checker=BIT_CHECKER, 

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

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

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

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

264 "the depression." 

265 ) 

266 subtype_residual = CamcopsColumn( 

267 "subtype_residual", Boolean, 

268 permitted_value_checker=BIT_CHECKER, 

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

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

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

272 ) 

273 subtype_simple = CamcopsColumn( 

274 "subtype_simple", Boolean, 

275 permitted_value_checker=BIT_CHECKER, 

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

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

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

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

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

281 "scholastic, or occupational performance." 

282 ) 

283 subtype_cenesthopathic = CamcopsColumn( 

284 "subtype_cenesthopathic", Boolean, 

285 permitted_value_checker=BIT_CHECKER, 

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

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

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

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

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

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

292 "touched) dominate." 

293 ) 

294 

295 date_pertains_to = Column( 

296 "date_pertains_to", Date, 

297 comment="Date the assessment pertains to" 

298 ) 

299 comments = Column( 

300 "comments", UnicodeText, 

301 comment="Clinician's comments" 

302 ) 

303 

304 A_NAMES = [ 

305 "passivity_bodily", "passivity_mental", 

306 "hv_commentary", "hv_discussing", "hv_from_body", 

307 "delusions", "delusional_perception", 

308 "thought_echo", "thought_withdrawal", "thought_insertion", 

309 "thought_broadcasting" 

310 ] 

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

312 C_NAMES = ["negative"] 

313 D_NAMES = ["present_one_month"] 

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

315 F_NAMES = ["not_organic_or_substance"] 

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

317 H_NAMES = [ 

318 "subtype_paranoid", "subtype_hebephrenic", "subtype_catatonic", 

319 "subtype_undifferentiated", "subtype_postschizophrenic_depression", 

320 "subtype_residual", "subtype_simple", "subtype_cenesthopathic" 

321 ] 

322 

323 @staticmethod 

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

325 _ = req.gettext 

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

327 

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

329 if not self.is_complete(): 

330 return CTV_INCOMPLETE 

331 c = self.meets_general_criteria() 

332 if c is None: 

333 category = "Unknown if met or not met" 

334 elif c: 

335 category = "Met" 

336 else: 

337 category = "Not met" 

338 infolist = [CtvInfo( 

339 content=( 

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

341 "schizophrenia: {}.".format( 

342 format_datetime(self.date_pertains_to, 

343 DateFormat.LONG_DATE), 

344 category 

345 ) 

346 ) 

347 )] 

348 if self.comments: 

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

350 return infolist 

351 

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

353 return self.standard_task_summary_fields() + [ 

354 SummaryElement( 

355 name="meets_general_criteria", 

356 coltype=Boolean(), 

357 value=self.meets_general_criteria(), 

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

359 "catatonic/undifferentiated schizophrenia " 

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

361 ] 

362 

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

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

365 t_a = self.count_booleans(Icd10Schizophrenia.A_NAMES) 

366 u_a = self.n_fields_none(Icd10Schizophrenia.A_NAMES) 

367 t_b = ( 

368 self.count_booleans(Icd10Schizophrenia.B_NAMES) + 

369 self.count_booleans(Icd10Schizophrenia.C_NAMES) 

370 ) 

371 u_b = ( 

372 self.n_fields_none(Icd10Schizophrenia.B_NAMES) + 

373 self.n_fields_none(Icd10Schizophrenia.C_NAMES) 

374 ) 

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

376 return False 

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

378 return False 

379 if ((self.also_manic or self.also_depressive) and 

380 is_false(self.if_mood_psychosis_first)): 

381 return False 

382 if is_false(self.not_organic_or_substance): 

383 return False 

384 if ((t_a >= 1 or t_b >= 2) and 

385 self.present_one_month and 

386 ( 

387 (is_false(self.also_manic) and 

388 is_false(self.also_depressive)) or 

389 self.if_mood_psychosis_first 

390 ) and 

391 self.not_organic_or_substance): 

392 return True 

393 return None 

394 

395 def is_complete(self) -> bool: 

396 return ( 

397 self.date_pertains_to is not None and 

398 self.meets_general_criteria() is not None and 

399 self.field_contents_valid() 

400 ) 

401 

402 def heading_row(self, req: CamcopsRequest, wstringname: str, 

403 extra: str = None) -> str: 

404 return heading_spanning_two_columns( 

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

406 ) 

407 

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

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

410 

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

412 return self.get_twocol_bool_row_true_false( 

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

414 

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

416 return self.get_twocol_bool_row_present_absent( 

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

418 

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

420 h = """ 

421 {clinician_comments} 

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

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

424 {tr_is_complete} 

425 {date_pertains_to} 

426 {meets_general_criteria} 

427 </table> 

428 </div> 

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

430 {comments} 

431 </div> 

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

433 <tr> 

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

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

436 </tr> 

437 """.format( 

438 clinician_comments=self.get_standard_clinician_comments_block( 

439 req, self.comments), 

440 CssClass=CssClass, 

441 tr_is_complete=self.get_is_complete_tr(req), 

442 date_pertains_to=tr_qa( 

443 req.wappstring(AS.DATE_PERTAINS_TO), 

444 format_datetime(self.date_pertains_to, 

445 DateFormat.LONG_DATE, default=None) 

446 ), 

447 meets_general_criteria=tr_qa( 

448 self.wxstring(req, "meets_general_criteria") + " <sup>[1]</sup>", # noqa 

449 get_true_false_none(req, self.meets_general_criteria()) 

450 ), 

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

452 ) 

453 

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

455 for x in Icd10Schizophrenia.A_NAMES: 

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

457 

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

459 for x in Icd10Schizophrenia.B_NAMES: 

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

461 

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

463 for x in Icd10Schizophrenia.C_NAMES: 

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

465 

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

467 for x in Icd10Schizophrenia.D_NAMES: 

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

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

470 for x in Icd10Schizophrenia.E_NAMES: 

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

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

473 for x in Icd10Schizophrenia.F_NAMES: 

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

475 

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

477 for x in Icd10Schizophrenia.G_NAMES: 

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

479 

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

481 for x in Icd10Schizophrenia.H_NAMES: 

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

483 

484 h += f""" 

485 </table> 

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

487 [1] All of: 

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

489 positive or negative symptoms; 

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

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

492 came first; 

493 (d) not attributable to organic brain disease or 

494 psychoactive substances. 

495 [2] Symptom definitions from: 

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

497 Introduction to Descriptive Psychopathology. Fourth 

498 edition, Saunders, Elsevier, Edinburgh. 

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

500 </div> 

501 {ICD10_COPYRIGHT_DIV} 

502 """ 

503 return h