Coverage for tasks/icd10schizophrenia.py: 53%

132 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-15 14:23 +0100

1""" 

2camcops_server/tasks/icd10schizophrenia.py 

3 

4=============================================================================== 

5 

6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry. 

7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk). 

8 

9 This file is part of CamCOPS. 

10 

11 CamCOPS is free software: you can redistribute it and/or modify 

12 it under the terms of the GNU General Public License as published by 

13 the Free Software Foundation, either version 3 of the License, or 

14 (at your option) any later version. 

15 

16 CamCOPS is distributed in the hope that it will be useful, 

17 but WITHOUT ANY WARRANTY; without even the implied warranty of 

18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

19 GNU General Public License for more details. 

20 

21 You should have received a copy of the GNU General Public License 

22 along with CamCOPS. If not, see <https://www.gnu.org/licenses/>. 

23 

24=============================================================================== 

25 

26""" 

27 

28import datetime 

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.orm import Mapped, mapped_column 

35from sqlalchemy.sql.sqltypes import Boolean, 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 mapped_camcops_column, 

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 

67 

68class Icd10Schizophrenia(TaskHasClinicianMixin, TaskHasPatientMixin, Task): # type: ignore[misc] # noqa: E501 

69 """ 

70 Server implementation of the ICD10-SZ task. 

71 """ 

72 

73 __tablename__ = "icd10schizophrenia" 

74 shortname = "ICD10-SZ" 

75 info_filename_stem = "icd" 

76 

77 passivity_bodily: Mapped[Optional[bool]] = mapped_camcops_column( 

78 permitted_value_checker=BIT_CHECKER, 

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

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

81 ) 

82 passivity_mental: Mapped[Optional[bool]] = mapped_camcops_column( 

83 permitted_value_checker=BIT_CHECKER, 

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

85 "sensations.", 

86 ) 

87 hv_commentary: Mapped[Optional[bool]] = mapped_camcops_column( 

88 permitted_value_checker=BIT_CHECKER, 

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

90 "patient's behaviour", 

91 ) 

92 hv_discussing: Mapped[Optional[bool]] = mapped_camcops_column( 

93 permitted_value_checker=BIT_CHECKER, 

94 comment="Hallucinatory voices discussing the patient among " 

95 "themselves", 

96 ) 

97 hv_from_body: Mapped[Optional[bool]] = mapped_camcops_column( 

98 permitted_value_checker=BIT_CHECKER, 

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

100 "part of the body", 

101 ) 

102 delusions: Mapped[Optional[bool]] = mapped_camcops_column( 

103 permitted_value_checker=BIT_CHECKER, 

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

105 "culturally inappropriate and completely impossible, such as " 

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

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

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

109 ) 

110 delusional_perception: Mapped[Optional[bool]] = mapped_camcops_column( 

111 permitted_value_checker=BIT_CHECKER, 

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

113 "delusionally interpreted]", 

114 ) 

115 thought_echo: Mapped[Optional[bool]] = mapped_camcops_column( 

116 permitted_value_checker=BIT_CHECKER, 

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

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

119 ) 

120 thought_withdrawal: Mapped[Optional[bool]] = mapped_camcops_column( 

121 permitted_value_checker=BIT_CHECKER, 

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

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

124 ) 

125 thought_insertion: Mapped[Optional[bool]] = mapped_camcops_column( 

126 permitted_value_checker=BIT_CHECKER, 

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

128 "been placed there from outside]", 

129 ) 

130 thought_broadcasting: Mapped[Optional[bool]] = mapped_camcops_column( 

131 permitted_value_checker=BIT_CHECKER, 

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

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

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

135 ) 

136 

137 hallucinations_other: Mapped[Optional[bool]] = mapped_camcops_column( 

138 permitted_value_checker=BIT_CHECKER, 

139 comment="Hallucinations: persistent hallucinations in any " 

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

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

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

143 "months on end.", 

144 ) 

145 thought_disorder: Mapped[Optional[bool]] = mapped_camcops_column( 

146 permitted_value_checker=BIT_CHECKER, 

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

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

149 "or neologisms.", 

150 ) 

151 catatonia: Mapped[Optional[bool]] = mapped_camcops_column( 

152 permitted_value_checker=BIT_CHECKER, 

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

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

155 "stupor.", 

156 ) 

157 

158 negative: Mapped[Optional[bool]] = mapped_camcops_column( 

159 permitted_value_checker=BIT_CHECKER, 

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

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

162 "emotional responses, usually resulting in social withdrawal " 

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

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

165 "medication.", 

166 ) 

167 

168 present_one_month: Mapped[Optional[bool]] = mapped_camcops_column( 

169 permitted_value_checker=BIT_CHECKER, 

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

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

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

173 ) 

174 

175 also_manic: Mapped[Optional[bool]] = mapped_camcops_column( 

176 permitted_value_checker=BIT_CHECKER, 

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

178 ) 

179 also_depressive: Mapped[Optional[bool]] = mapped_camcops_column( 

180 permitted_value_checker=BIT_CHECKER, 

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

182 ) 

183 if_mood_psychosis_first: Mapped[Optional[bool]] = mapped_camcops_column( 

184 permitted_value_checker=BIT_CHECKER, 

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

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

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

188 ) 

189 

190 not_organic_or_substance: Mapped[Optional[bool]] = mapped_camcops_column( 

191 permitted_value_checker=BIT_CHECKER, 

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

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

194 "intoxication, dependence or withdrawal.", 

195 ) 

196 

197 behaviour_change: Mapped[Optional[bool]] = mapped_camcops_column( 

198 permitted_value_checker=BIT_CHECKER, 

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

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

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

202 "attitude, and social withdrawal.", 

203 ) 

204 performance_decline: Mapped[Optional[bool]] = mapped_camcops_column( 

205 permitted_value_checker=BIT_CHECKER, 

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

207 "performance.", 

208 ) 

209 

210 subtype_paranoid: Mapped[Optional[bool]] = mapped_camcops_column( 

211 permitted_value_checker=BIT_CHECKER, 

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

213 ) 

214 subtype_hebephrenic: Mapped[Optional[bool]] = mapped_camcops_column( 

215 permitted_value_checker=BIT_CHECKER, 

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

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

218 "either pronounced thought disorder or aimless, disjointed " 

219 "behaviour is present.", 

220 ) 

221 subtype_catatonic: Mapped[Optional[bool]] = mapped_camcops_column( 

222 permitted_value_checker=BIT_CHECKER, 

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

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

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

226 "perseveration).", 

227 ) 

228 subtype_undifferentiated: Mapped[Optional[bool]] = mapped_camcops_column( 

229 permitted_value_checker=BIT_CHECKER, 

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

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

232 "types.", 

233 ) 

234 subtype_postschizophrenic_depression: Mapped[Optional[bool]] = ( 

235 mapped_camcops_column( 

236 permitted_value_checker=BIT_CHECKER, 

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

238 "depressive episode has developed for at least 2 weeks following " 

239 "a schizophrenic episode within the last 12 months and in which " 

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

241 "the depression.", 

242 ) 

243 ) 

244 subtype_residual: Mapped[Optional[bool]] = mapped_camcops_column( 

245 permitted_value_checker=BIT_CHECKER, 

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

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

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

249 ) 

250 subtype_simple: Mapped[Optional[bool]] = mapped_camcops_column( 

251 permitted_value_checker=BIT_CHECKER, 

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

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

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

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

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

257 "scholastic, or occupational performance.", 

258 ) 

259 subtype_cenesthopathic: Mapped[Optional[bool]] = mapped_camcops_column( 

260 permitted_value_checker=BIT_CHECKER, 

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

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

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

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

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

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

267 "touched) dominate.", 

268 ) 

269 

270 date_pertains_to: Mapped[Optional[datetime.date]] = mapped_column( 

271 comment="Date the assessment pertains to" 

272 ) 

273 comments: Mapped[Optional[str]] = mapped_column( 

274 UnicodeText, comment="Clinician's comments" 

275 ) 

276 

277 A_NAMES = [ 

278 "passivity_bodily", 

279 "passivity_mental", 

280 "hv_commentary", 

281 "hv_discussing", 

282 "hv_from_body", 

283 "delusions", 

284 "delusional_perception", 

285 "thought_echo", 

286 "thought_withdrawal", 

287 "thought_insertion", 

288 "thought_broadcasting", 

289 ] 

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

291 C_NAMES = ["negative"] 

292 D_NAMES = ["present_one_month"] 

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

294 F_NAMES = ["not_organic_or_substance"] 

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

296 H_NAMES = [ 

297 "subtype_paranoid", 

298 "subtype_hebephrenic", 

299 "subtype_catatonic", 

300 "subtype_undifferentiated", 

301 "subtype_postschizophrenic_depression", 

302 "subtype_residual", 

303 "subtype_simple", 

304 "subtype_cenesthopathic", 

305 ] 

306 

307 @staticmethod 

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

309 _ = req.gettext 

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

311 

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

313 if not self.is_complete(): 

314 return CTV_INCOMPLETE 

315 c = self.meets_general_criteria() 

316 if c is None: 

317 category = "Unknown if met or not met" 

318 elif c: 

319 category = "Met" 

320 else: 

321 category = "Not met" 

322 infolist = [ 

323 CtvInfo( 

324 content=( 

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

326 "schizophrenia: {}.".format( 

327 format_datetime( 

328 self.date_pertains_to, DateFormat.LONG_DATE 

329 ), 

330 category, 

331 ) 

332 ) 

333 ) 

334 ] 

335 if self.comments: 

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

337 return infolist 

338 

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

340 return self.standard_task_summary_fields() + [ 

341 SummaryElement( 

342 name="meets_general_criteria", 

343 coltype=Boolean(), 

344 value=self.meets_general_criteria(), 

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

346 "catatonic/undifferentiated schizophrenia " 

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

348 ) 

349 ] 

350 

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

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

353 t_a = self.count_booleans(Icd10Schizophrenia.A_NAMES) 

354 u_a = self.n_fields_none(Icd10Schizophrenia.A_NAMES) 

355 t_b = self.count_booleans( 

356 Icd10Schizophrenia.B_NAMES 

357 ) + self.count_booleans(Icd10Schizophrenia.C_NAMES) 

358 u_b = self.n_fields_none( 

359 Icd10Schizophrenia.B_NAMES 

360 ) + self.n_fields_none(Icd10Schizophrenia.C_NAMES) 

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

362 return False 

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

364 return False 

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

366 self.if_mood_psychosis_first 

367 ): 

368 return False 

369 if is_false(self.not_organic_or_substance): 

370 return False 

371 if ( 

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

373 and self.present_one_month 

374 and ( 

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

376 or self.if_mood_psychosis_first 

377 ) 

378 and self.not_organic_or_substance 

379 ): 

380 return True 

381 return None 

382 

383 def is_complete(self) -> bool: 

384 return ( 

385 self.date_pertains_to is not None 

386 and self.meets_general_criteria() is not None 

387 and self.field_contents_valid() 

388 ) 

389 

390 def heading_row( 

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

392 ) -> str: 

393 return heading_spanning_two_columns( 

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

395 ) 

396 

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

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

399 

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

401 return self.get_twocol_bool_row_true_false( 

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

403 ) 

404 

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

406 return self.get_twocol_bool_row_present_absent( 

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

408 ) 

409 

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

411 h = """ 

412 {clinician_comments} 

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

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

415 {tr_is_complete} 

416 {date_pertains_to} 

417 {meets_general_criteria} 

418 </table> 

419 </div> 

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

421 {comments} 

422 </div> 

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

424 <tr> 

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

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

427 </tr> 

428 """.format( 

429 clinician_comments=self.get_standard_clinician_comments_block( 

430 req, self.comments 

431 ), 

432 CssClass=CssClass, 

433 tr_is_complete=self.get_is_complete_tr(req), 

434 date_pertains_to=tr_qa( 

435 req.wappstring(AS.DATE_PERTAINS_TO), 

436 format_datetime( 

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

438 ), 

439 ), 

440 meets_general_criteria=tr_qa( 

441 self.wxstring(req, "meets_general_criteria") 

442 + " <sup>[1]</sup>", 

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

444 ), 

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

446 ) 

447 

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

449 for x in Icd10Schizophrenia.A_NAMES: 

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

451 

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

453 for x in Icd10Schizophrenia.B_NAMES: 

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

455 

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

457 for x in Icd10Schizophrenia.C_NAMES: 

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

459 

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

461 for x in Icd10Schizophrenia.D_NAMES: 

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

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

464 for x in Icd10Schizophrenia.E_NAMES: 

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

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

467 for x in Icd10Schizophrenia.F_NAMES: 

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

469 

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

471 for x in Icd10Schizophrenia.G_NAMES: 

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

473 

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

475 for x in Icd10Schizophrenia.H_NAMES: 

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

477 

478 h += f""" 

479 </table> 

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

481 [1] All of: 

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

483 positive or negative symptoms; 

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

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

486 came first; 

487 (d) not attributable to organic brain disease or 

488 psychoactive substances. 

489 [2] Symptom definitions from: 

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

491 Introduction to Descriptive Psychopathology. Fourth 

492 edition, Saunders, Elsevier, Edinburgh. 

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

494 </div> 

495 {ICD10_COPYRIGHT_DIV} 

496 """ 

497 return h