Coverage for tasks/icd10schizophrenia.py: 53%
132 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-15 14:23 +0100
1"""
2camcops_server/tasks/icd10schizophrenia.py
4===============================================================================
6 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
7 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
9 This file is part of CamCOPS.
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.
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.
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/>.
24===============================================================================
26"""
28import datetime
29from typing import List, Optional
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
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)
63# =============================================================================
64# Icd10Schizophrenia
65# =============================================================================
68class Icd10Schizophrenia(TaskHasClinicianMixin, TaskHasPatientMixin, Task): # type: ignore[misc] # noqa: E501
69 """
70 Server implementation of the ICD10-SZ task.
71 """
73 __tablename__ = "icd10schizophrenia"
74 shortname = "ICD10-SZ"
75 info_filename_stem = "icd"
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 ]
307 @staticmethod
308 def longname(req: "CamcopsRequest") -> str:
309 _ = req.gettext
310 return _("ICD-10 criteria for schizophrenia (F20)")
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
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 ]
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
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 )
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 )
397 def text_row(self, req: CamcopsRequest, wstringname: str) -> str:
398 return subheading_spanning_two_columns(self.wxstring(req, wstringname))
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 )
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 )
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 )
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)
452 h += self.heading_row(req, "other_positive")
453 for x in Icd10Schizophrenia.B_NAMES:
454 h += self.row_present_absent(req, x)
456 h += self.heading_row(req, "negative_title")
457 for x in Icd10Schizophrenia.C_NAMES:
458 h += self.row_present_absent(req, x)
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)
470 h += self.heading_row(req, "simple_title")
471 for x in Icd10Schizophrenia.G_NAMES:
472 h += self.row_present_absent(req, x)
474 h += self.heading_row(req, "subtypes")
475 for x in Icd10Schizophrenia.H_NAMES:
476 h += self.row_present_absent(req, x)
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 & Spence SA (2003), PMID 14519605.
494 </div>
495 {ICD10_COPYRIGHT_DIV}
496 """
497 return h