Coverage for tasks/icd10schizophrenia.py : 53%

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
3"""
4camcops_server/tasks/icd10schizophrenia.py
6===============================================================================
8 Copyright (C) 2012-2020 Rudolf Cardinal (rudolf@pobox.com).
10 This file is part of CamCOPS.
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.
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.
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/>.
25===============================================================================
27"""
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.sql.schema import Column
35from sqlalchemy.sql.sqltypes import Boolean, Date, 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 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)
63# =============================================================================
64# Icd10Schizophrenia
65# =============================================================================
67class Icd10Schizophrenia(TaskHasClinicianMixin, TaskHasPatientMixin, Task):
68 """
69 Server implementation of the ICD10-SZ task.
70 """
71 __tablename__ = "icd10schizophrenia"
72 shortname = "ICD10-SZ"
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 ]
323 @staticmethod
324 def longname(req: "CamcopsRequest") -> str:
325 _ = req.gettext
326 return _("ICD-10 criteria for schizophrenia (F20)")
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
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 ]
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
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 )
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 )
408 def text_row(self, req: CamcopsRequest, wstringname: str) -> str:
409 return subheading_spanning_two_columns(self.wxstring(req, wstringname))
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))
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))
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 )
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)
458 h += self.heading_row(req, "other_positive")
459 for x in Icd10Schizophrenia.B_NAMES:
460 h += self.row_present_absent(req, x)
462 h += self.heading_row(req, "negative_title")
463 for x in Icd10Schizophrenia.C_NAMES:
464 h += self.row_present_absent(req, x)
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)
476 h += self.heading_row(req, "simple_title")
477 for x in Icd10Schizophrenia.G_NAMES:
478 h += self.row_present_absent(req, x)
480 h += self.heading_row(req, "subtypes")
481 for x in Icd10Schizophrenia.H_NAMES:
482 h += self.row_present_absent(req, x)
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 & Spence SA (2003), PMID 14519605.
500 </div>
501 {ICD10_COPYRIGHT_DIV}
502 """
503 return h