Coverage for tasks/icd10schizophrenia.py: 53%

1"""

2 camcops_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

28 import datetime

29 from typing import List, Optional

30

31 from cardinal_pythonlib.datetimefunc import format_datetime

32 import cardinal_pythonlib.rnc_web as ws

33 from cardinal_pythonlib.typetests import is_false

34 from sqlalchemy.orm import Mapped, mapped_column

35 from sqlalchemy.sql.sqltypes import Boolean, UnicodeText

36

37 from camcops_server.cc_modules.cc_constants import (

38 CssClass,

39 DateFormat,

40 ICD10_COPYRIGHT_DIV,

41)

42 from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo

43 from 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)

49 from camcops_server.cc_modules.cc_request import CamcopsRequest

50 from camcops_server.cc_modules.cc_sqla_coltypes import (

51 BIT_CHECKER,

52 mapped_camcops_column,

53)

54 from camcops_server.cc_modules.cc_string import AS

55 from camcops_server.cc_modules.cc_summaryelement import SummaryElement

56 from camcops_server.cc_modules.cc_task import (

57 Task,

58 TaskHasClinicianMixin,

59 TaskHasPatientMixin,

60)

61

62

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

64# Icd10Schizophrenia

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

66

67

68 class 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