Coverage for tasks/icd10schizophrenia.py: 53%
132 statements
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2022-11-08 23:14 +0000
1#!/usr/bin/env python
3"""
4camcops_server/tasks/icd10schizophrenia.py
6===============================================================================
8 Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
9 Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
11 This file is part of CamCOPS.
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.
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.
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/>.
26===============================================================================
28"""
30from typing import List, Optional
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
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)
64# =============================================================================
65# Icd10Schizophrenia
66# =============================================================================
69class Icd10Schizophrenia(TaskHasClinicianMixin, TaskHasPatientMixin, Task):
70 """
71 Server implementation of the ICD10-SZ task.
72 """
74 __tablename__ = "icd10schizophrenia"
75 shortname = "ICD10-SZ"
76 info_filename_stem = "icd"
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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")
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 ]
364 @staticmethod
365 def longname(req: "CamcopsRequest") -> str:
366 _ = req.gettext
367 return _("ICD-10 criteria for schizophrenia (F20)")
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
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 ]
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
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 )
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 )
454 def text_row(self, req: CamcopsRequest, wstringname: str) -> str:
455 return subheading_spanning_two_columns(self.wxstring(req, wstringname))
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 )
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 )
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 )
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)
509 h += self.heading_row(req, "other_positive")
510 for x in Icd10Schizophrenia.B_NAMES:
511 h += self.row_present_absent(req, x)
513 h += self.heading_row(req, "negative_title")
514 for x in Icd10Schizophrenia.C_NAMES:
515 h += self.row_present_absent(req, x)
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)
527 h += self.heading_row(req, "simple_title")
528 for x in Icd10Schizophrenia.G_NAMES:
529 h += self.row_present_absent(req, x)
531 h += self.heading_row(req, "subtypes")
532 for x in Icd10Schizophrenia.H_NAMES:
533 h += self.row_present_absent(req, x)
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 & Spence SA (2003), PMID 14519605.
551 </div>
552 {ICD10_COPYRIGHT_DIV}
553 """
554 return h