"""
camcops_server/tasks/icd10depressive.py
===============================================================================
Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
Created by Rudolf Cardinal (rnc1001@cam.ac.uk).
This file is part of CamCOPS.
CamCOPS is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
CamCOPS is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.
===============================================================================
"""
from typing import List, Optional
from cardinal_pythonlib.datetimefunc import format_datetime
import cardinal_pythonlib.rnc_web as ws
from sqlalchemy.sql.schema import Column
from sqlalchemy.sql.sqltypes import Boolean, Date, Integer, UnicodeText
from camcops_server.cc_modules.cc_constants import (
CssClass,
DateFormat,
ICD10_COPYRIGHT_DIV,
)
from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
from camcops_server.cc_modules.cc_html import (
answer,
get_present_absent_none,
heading_spanning_two_columns,
tr,
tr_qa,
)
from camcops_server.cc_modules.cc_request import CamcopsRequest
from camcops_server.cc_modules.cc_sqla_coltypes import (
BIT_CHECKER,
CamcopsColumn,
SummaryCategoryColType,
)
from camcops_server.cc_modules.cc_string import AS
from camcops_server.cc_modules.cc_summaryelement import SummaryElement
from camcops_server.cc_modules.cc_task import (
Task,
TaskHasClinicianMixin,
TaskHasPatientMixin,
)
from camcops_server.cc_modules.cc_text import SS
# =============================================================================
# Icd10Depressive
# =============================================================================
[docs]class Icd10Depressive(TaskHasClinicianMixin, TaskHasPatientMixin, Task):
"""
Server implementation of the ICD10-DEPR task.
"""
__tablename__ = "icd10depressive"
shortname = "ICD10-DEPR"
info_filename_stem = "icd"
mood = CamcopsColumn(
"mood",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Depressed mood to a degree that is definitely abnormal "
"for the individual, present for most of the day and almost "
"every day, largely uninfluenced by circumstances, and "
"sustained for at least 2 weeks.",
)
anhedonia = CamcopsColumn(
"anhedonia",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Loss of interest or pleasure in activities that are "
"normally pleasurable.",
)
energy = CamcopsColumn(
"energy",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Decreased energy or increased fatiguability.",
)
sleep = CamcopsColumn(
"sleep",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Sleep disturbance of any type.",
)
worth = CamcopsColumn(
"worth",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Loss of confidence and self-esteem.",
)
appetite = CamcopsColumn(
"appetite",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Change in appetite (decrease or increase) with "
"corresponding weight change.",
)
guilt = CamcopsColumn(
"guilt",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Unreasonable feelings of self-reproach or excessive and "
"inappropriate guilt.",
)
concentration = CamcopsColumn(
"concentration",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Complaints or evidence of diminished ability to think "
"or concentrate, such as indecisiveness or vacillation.",
)
activity = CamcopsColumn(
"activity",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Change in psychomotor activity, with agitation or "
"retardation (either subjective or objective).",
)
death = CamcopsColumn(
"death",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Recurrent thoughts of death or suicide, or any "
"suicidal behaviour.",
)
somatic_anhedonia = CamcopsColumn(
"somatic_anhedonia",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Marked loss of interest or pleasure in activities that "
"are normally pleasurable",
)
somatic_emotional_unreactivity = CamcopsColumn(
"somatic_emotional_unreactivity",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Lack of emotional reactions to events or "
"activities that normally produce an emotional response",
)
somatic_early_morning_waking = CamcopsColumn(
"somatic_early_morning_waking",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Waking in the morning 2 hours or more before "
"the usual time",
)
somatic_mood_worse_morning = CamcopsColumn(
"somatic_mood_worse_morning",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Depression worse in the morning",
)
somatic_psychomotor = CamcopsColumn(
"somatic_psychomotor",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Objective evidence of marked psychomotor retardation or "
"agitation (remarked on or reported by other people)",
)
somatic_appetite = CamcopsColumn(
"somatic_appetite",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Marked loss of appetite",
)
somatic_weight = CamcopsColumn(
"somatic_weight",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Weight loss (5 percent or more of body weight in the past "
"month)",
# 2017-08-24: AVOID A PERCENT SYMBOL (%) FOR NOW; SEE THIS BUG:
# https://bitbucket.org/zzzeek/sqlalchemy/issues/4052/comment-attribute-causes-crash-during # noqa
)
somatic_libido = CamcopsColumn(
"somatic_libido",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Marked loss of libido",
)
hallucinations_schizophrenic = CamcopsColumn(
"hallucinations_schizophrenic",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Hallucinations that are 'typically schizophrenic' "
"(hallucinatory voices giving a running commentary on the "
"patient's behaviour, or discussing him between themselves, "
"or other types of hallucinatory voices coming from some part "
"of the body).",
)
hallucinations_other = CamcopsColumn(
"hallucinations_other",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Hallucinations (of any other kind).",
)
delusions_schizophrenic = CamcopsColumn(
"delusions_schizophrenic",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Delusions that are 'typically schizophrenic' (delusions "
"of control, influence or passivity, clearly referred to body "
"or limb movements or specific thoughts, actions, or "
"sensations; delusional perception; persistent delusions of "
"other kinds that are culturally inappropriate and completely "
"impossible).",
)
delusions_other = CamcopsColumn(
"delusions_other",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Delusions (of any other kind).",
)
stupor = CamcopsColumn(
"stupor",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Depressive stupor.",
)
date_pertains_to = CamcopsColumn(
"date_pertains_to", Date, comment="Date the assessment pertains to"
)
comments = Column("comments", UnicodeText, comment="Clinician's comments")
duration_at_least_2_weeks = CamcopsColumn(
"duration_at_least_2_weeks",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Depressive episode lasts at least 2 weeks?",
)
severe_clinically = CamcopsColumn(
"severe_clinically",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Clinical impression of severe depression, in a "
"patient unwilling or unable to describe many symptoms in "
"detail",
)
CORE_NAMES = ["mood", "anhedonia", "energy"]
ADDITIONAL_NAMES = [
"sleep",
"worth",
"appetite",
"guilt",
"concentration",
"activity",
"death",
]
SOMATIC_NAMES = [
"somatic_anhedonia",
"somatic_emotional_unreactivity",
"somatic_early_morning_waking",
"somatic_mood_worse_morning",
"somatic_psychomotor",
"somatic_appetite",
"somatic_weight",
"somatic_libido",
]
PSYCHOSIS_NAMES = [
"hallucinations_schizophrenic",
"hallucinations_other",
"delusions_schizophrenic",
"delusions_other",
"stupor",
]
[docs] @staticmethod
def longname(req: "CamcopsRequest") -> str:
_ = req.gettext
return _(
"ICD-10 symptomatic criteria for a depressive episode "
"(as in e.g. F06.3, F25, F31, F32, F33)"
)
[docs] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
if not self.is_complete():
return CTV_INCOMPLETE
infolist = [
CtvInfo(
content="Pertains to: {}. Category: {}.".format(
format_datetime(
self.date_pertains_to, DateFormat.LONG_DATE
),
self.get_full_description(req),
)
)
]
if self.comments:
infolist.append(CtvInfo(content=ws.webify(self.comments)))
return infolist
[docs] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
return self.standard_task_summary_fields() + [
SummaryElement(
name="n_core",
coltype=Integer(),
value=self.n_core(),
comment="Number of core diagnostic symptoms (/3)",
),
SummaryElement(
name="n_additional",
coltype=Integer(),
value=self.n_additional(),
comment="Number of additional diagnostic symptoms (/7)",
),
SummaryElement(
name="n_total",
coltype=Integer(),
value=self.n_total(),
comment="Total number of diagnostic symptoms (/10)",
),
SummaryElement(
name="n_somatic",
coltype=Integer(),
value=self.n_somatic(),
comment="Number of somatic syndrome symptoms (/8)",
),
SummaryElement(
name="category",
coltype=SummaryCategoryColType,
value=self.get_full_description(req),
comment="Diagnostic category",
),
SummaryElement(
name="psychosis_or_stupor",
coltype=Boolean(),
value=self.is_psychotic_or_stupor(),
comment="Psychotic symptoms or stupor present?",
),
]
# Scoring
def n_core(self) -> int:
return self.count_booleans(self.CORE_NAMES)
def n_additional(self) -> int:
return self.count_booleans(self.ADDITIONAL_NAMES)
def n_total(self) -> int:
return self.n_core() + self.n_additional()
def n_somatic(self) -> int:
return self.count_booleans(self.SOMATIC_NAMES)
def main_complete(self) -> bool:
return (
self.duration_at_least_2_weeks is not None
and self.all_fields_not_none(self.CORE_NAMES)
and self.all_fields_not_none(self.ADDITIONAL_NAMES)
) or bool(self.severe_clinically)
# Meets criteria? These also return null for unknown.
def meets_criteria_severe_psychotic_schizophrenic(self) -> Optional[bool]:
x = self.meets_criteria_severe_ignoring_psychosis()
if not x:
return x
if self.stupor or self.hallucinations_other or self.delusions_other:
return False # that counts as F32.3
if (
self.stupor is None
or self.hallucinations_other is None
or self.delusions_other is None
):
return None # might be F32.3
if self.hallucinations_schizophrenic or self.delusions_schizophrenic:
return True
if (
self.hallucinations_schizophrenic is None
or self.delusions_schizophrenic is None
):
return None
return False
def meets_criteria_severe_psychotic_icd(self) -> Optional[bool]:
x = self.meets_criteria_severe_ignoring_psychosis()
if not x:
return x
if self.stupor or self.hallucinations_other or self.delusions_other:
return True
if (
self.stupor is None
or self.hallucinations_other is None
or self.delusions_other is None
):
return None
return False
def meets_criteria_severe_nonpsychotic(self) -> Optional[bool]:
x = self.meets_criteria_severe_ignoring_psychosis()
if not x:
return x
if self.any_fields_none(self.PSYCHOSIS_NAMES):
return None
return self.count_booleans(self.PSYCHOSIS_NAMES) == 0
def meets_criteria_severe_ignoring_psychosis(self) -> Optional[bool]:
if self.severe_clinically:
return True
if (
self.duration_at_least_2_weeks is not None
and not self.duration_at_least_2_weeks
):
return False # too short
if self.n_core() >= 3 and self.n_total() >= 8:
return True
if not self.main_complete():
return None # addition of more information might increase severity
return False
def meets_criteria_moderate(self) -> Optional[bool]:
if self.severe_clinically:
return False # too severe
if (
self.duration_at_least_2_weeks is not None
and not self.duration_at_least_2_weeks
):
return False # too short
if self.n_core() >= 3 and self.n_total() >= 8:
return False # too severe; that's severe
if not self.main_complete():
return None # addition of more information might increase severity
if self.n_core() >= 2 and self.n_total() >= 6:
return True
return False
def meets_criteria_mild(self) -> Optional[bool]:
if self.severe_clinically:
return False # too severe
if (
self.duration_at_least_2_weeks is not None
and not self.duration_at_least_2_weeks
):
return False # too short
if self.n_core() >= 2 and self.n_total() >= 6:
return False # too severe; that's moderate
if not self.main_complete():
return None # addition of more information might increase severity
if self.n_core() >= 2 and self.n_total() >= 4:
return True
return False
def meets_criteria_none(self) -> Optional[bool]:
if self.severe_clinically:
return False # too severe
if (
self.duration_at_least_2_weeks is not None
and not self.duration_at_least_2_weeks
):
return True # too short for depression
if self.n_core() >= 2 and self.n_total() >= 4:
return False # too severe
if not self.main_complete():
return None # addition of more information might increase severity
return True
def meets_criteria_somatic(self) -> Optional[bool]:
t = self.n_somatic()
u = self.n_fields_none(self.SOMATIC_NAMES)
if t >= 4:
return True
elif t + u < 4:
return False
else:
return None
def get_somatic_description(self, req: CamcopsRequest) -> str:
s = self.meets_criteria_somatic()
if s is None:
return self.wxstring(req, "category_somatic_unknown")
elif s:
return self.wxstring(req, "category_with_somatic")
else:
return self.wxstring(req, "category_without_somatic")
def get_main_description(self, req: CamcopsRequest) -> str:
if self.meets_criteria_severe_psychotic_schizophrenic():
return self.wxstring(
req, "category_severe_psychotic_schizophrenic"
)
elif self.meets_criteria_severe_psychotic_icd():
return self.wxstring(req, "category_severe_psychotic")
elif self.meets_criteria_severe_nonpsychotic():
return self.wxstring(req, "category_severe_nonpsychotic")
elif self.meets_criteria_moderate():
return self.wxstring(req, "category_moderate")
elif self.meets_criteria_mild():
return self.wxstring(req, "category_mild")
elif self.meets_criteria_none():
return self.wxstring(req, "category_none")
else:
return req.sstring(SS.UNKNOWN)
def get_full_description(self, req: CamcopsRequest) -> str:
skip_somatic = self.main_complete() and self.meets_criteria_none()
return self.get_main_description(req) + (
"" if skip_somatic else " " + self.get_somatic_description(req)
)
def is_psychotic_or_stupor(self) -> Optional[bool]:
if self.count_booleans(self.PSYCHOSIS_NAMES) > 0:
return True
elif self.all_fields_not_none(self.PSYCHOSIS_NAMES) > 0:
return False
else:
return None
[docs] def is_complete(self) -> bool:
return (
self.date_pertains_to is not None
and self.main_complete()
and self.field_contents_valid()
)
def text_row(self, req: CamcopsRequest, wstringname: str) -> str:
return heading_spanning_two_columns(self.wxstring(req, wstringname))
def row_true_false(self, req: CamcopsRequest, fieldname: str) -> str:
return self.get_twocol_bool_row_true_false(
req, fieldname, self.wxstring(req, "" + fieldname)
)
def row_present_absent(self, req: CamcopsRequest, fieldname: str) -> str:
return self.get_twocol_bool_row_present_absent(
req, fieldname, self.wxstring(req, "" + fieldname)
)
[docs] def get_task_html(self, req: CamcopsRequest) -> str:
h = """
{clinician_comments}
<div class="{CssClass.SUMMARY}">
<table class="{CssClass.SUMMARY}">
{tr_is_complete}
{date_pertains_to}
{category}
{n_core}
{n_total}
{n_somatic}
{psychotic_symptoms_or_stupor}
</table>
</div>
<div class="{CssClass.EXPLANATION}">
{icd10_symptomatic_disclaimer}
</div>
<table class="{CssClass.TASKDETAIL}">
<tr>
<th width="80%">Question</th>
<th width="20%">Answer</th>
</tr>
""".format(
clinician_comments=self.get_standard_clinician_comments_block(
req, self.comments
),
CssClass=CssClass,
tr_is_complete=self.get_is_complete_tr(req),
date_pertains_to=tr_qa(
req.wappstring(AS.DATE_PERTAINS_TO),
format_datetime(
self.date_pertains_to, DateFormat.LONG_DATE, default=None
),
),
category=tr_qa(
req.sstring(SS.CATEGORY) + " <sup>[1,2]</sup>",
self.get_full_description(req),
),
n_core=tr(
self.wxstring(req, "n_core"), answer(self.n_core()) + " / 3"
),
n_total=tr(
self.wxstring(req, "n_total"), answer(self.n_total()) + " / 10"
),
n_somatic=tr(
self.wxstring(req, "n_somatic"),
answer(self.n_somatic()) + " / 8",
),
psychotic_symptoms_or_stupor=tr(
self.wxstring(req, "psychotic_symptoms_or_stupor")
+ " <sup>[2]</sup>",
answer(
get_present_absent_none(req, self.is_psychotic_or_stupor())
),
),
icd10_symptomatic_disclaimer=req.wappstring(
AS.ICD10_SYMPTOMATIC_DISCLAIMER
),
)
h += self.text_row(req, "duration_text")
h += self.row_true_false(req, "duration_at_least_2_weeks")
h += self.text_row(req, "core")
for x in self.CORE_NAMES:
h += self.row_present_absent(req, x)
h += self.text_row(req, "additional")
for x in self.ADDITIONAL_NAMES:
h += self.row_present_absent(req, x)
h += self.text_row(req, "clinical_text")
h += self.row_true_false(req, "severe_clinically")
h += self.text_row(req, "somatic")
for x in self.SOMATIC_NAMES:
h += self.row_present_absent(req, x)
h += self.text_row(req, "psychotic")
for x in self.PSYCHOSIS_NAMES:
h += self.row_present_absent(req, x)
extradetail = [
f"n_core() = {self.n_core()}",
f"n_additional() = {self.n_additional()}",
f"n_total() = {self.n_total()}",
f"n_somatic() = {self.n_somatic()}",
f"main_complete() = {self.main_complete()}",
f"meets_criteria_severe_psychotic_schizophrenic() = {self.meets_criteria_severe_psychotic_schizophrenic()}", # noqa
f"meets_criteria_severe_psychotic_icd() = {self.meets_criteria_severe_psychotic_icd()}", # noqa
f"meets_criteria_severe_nonpsychotic() = {self.meets_criteria_severe_nonpsychotic()}", # noqa
f"meets_criteria_severe_ignoring_psychosis() = {self.meets_criteria_severe_ignoring_psychosis()}", # noqa
f"meets_criteria_moderate() = {self.meets_criteria_moderate()}",
f"meets_criteria_mild() = {self.meets_criteria_mild()}",
f"meets_criteria_none() = {self.meets_criteria_none()}",
f"meets_criteria_somatic() = {self.meets_criteria_somatic()}",
]
h += f"""
</table>
<div class="{CssClass.HEADING}">Working</div>
<div class="{CssClass.EXTRADETAIL2}">
<pre>{"<br>".join(ws.webify(f"‣ {x}") for x in extradetail)}</pre>
</div>
<div class="{CssClass.FOOTNOTES}">
[1] Mild depression requires ≥2 core symptoms and ≥4 total
diagnostic symptoms.
Moderate depression requires ≥2 core and ≥6 total.
Severe depression requires 3 core and ≥8 total.
All three require a duration of ≥2 weeks.
In addition, the diagnosis of severe depression is allowed with
a clinical impression of “severe” in a patient unable/unwilling
to describe symptoms in detail.
[2] ICD-10 nonpsychotic severe depression requires severe
depression without hallucinations/delusions/depressive stupor.
ICD-10 psychotic depression requires severe depression plus
hallucinations/delusions other than those that are “typically
schizophrenic”, or stupor.
ICD-10 does not clearly categorize severe depression with only
schizophreniform psychotic symptoms;
however, such symptoms can occur in severe depression with
psychosis (e.g. Tandon R & Greden JF, 1987, PMID 2884810).
Moreover, psychotic symptoms can occur in mild/moderate
depression (Maj M et al., 2007, PMID 17915981).
</div>
{ICD10_COPYRIGHT_DIV}
""" # noqa
return h