"""
camcops_server/tasks/icd10manic.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
from cardinal_pythonlib.typetests import is_false
import cardinal_pythonlib.rnc_web as ws
from sqlalchemy.sql.schema import Column
from sqlalchemy.sql.sqltypes import Boolean, Date, 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 (
get_present_absent_none,
heading_spanning_two_columns,
subheading_spanning_two_columns,
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
# =============================================================================
# Icd10Manic
# =============================================================================
[docs]class Icd10Manic(TaskHasClinicianMixin, TaskHasPatientMixin, Task):
"""
Server implementation of the ICD10-MANIC task.
"""
__tablename__ = "icd10manic"
shortname = "ICD10-MANIC"
info_filename_stem = "icd"
mood_elevated = CamcopsColumn(
"mood_elevated",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="The mood is 'elevated' [hypomania] or 'predominantly "
"elevated [or] expansive' [mania] to a degree that is "
"definitely abnormal for the individual concerned.",
)
mood_irritable = CamcopsColumn(
"mood_irritable",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="The mood is 'irritable' [hypomania] or 'predominantly "
"irritable' [mania] to a degree that is definitely abnormal "
"for the individual concerned.",
)
distractible = CamcopsColumn(
"distractible",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Difficulty in concentration or distractibility [from "
"the criteria for hypomania]; distractibility or constant "
"changes in activity or plans [from the criteria for mania].",
)
activity = CamcopsColumn(
"activity",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Increased activity or physical restlessness.",
)
sleep = CamcopsColumn(
"sleep",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Decreased need for sleep.",
)
talkativeness = CamcopsColumn(
"talkativeness",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Increased talkativeness (pressure of speech).",
)
recklessness = CamcopsColumn(
"recklessness",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Mild spending sprees, or other types of reckless or "
"irresponsible behaviour [hypomania]; behaviour which is "
"foolhardy or reckless and whose risks the subject does not "
"recognize e.g. spending sprees, foolish enterprises, "
"reckless driving [mania].",
)
social_disinhibition = CamcopsColumn(
"social_disinhibition",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Increased sociability or over-familiarity [hypomania]; "
"loss of normal social inhibitions resulting in behaviour "
"which is inappropriate to the circumstances [mania].",
)
sexual = CamcopsColumn(
"sexual",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Increased sexual energy [hypomania]; marked sexual "
"energy or sexual indiscretions [mania].",
)
grandiosity = CamcopsColumn(
"grandiosity",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Inflated self-esteem or grandiosity.",
)
flight_of_ideas = CamcopsColumn(
"flight_of_ideas",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Flight of ideas or the subjective experience of "
"thoughts racing.",
)
sustained4days = CamcopsColumn(
"sustained4days",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Elevated/irritable mood sustained for at least 4 days.",
)
sustained7days = CamcopsColumn(
"sustained7days",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Elevated/irritable mood sustained for at least 7 days.",
)
admission_required = CamcopsColumn(
"admission_required",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Elevated/irritable mood severe enough to require "
"hospital admission.",
)
some_interference_functioning = CamcopsColumn(
"some_interference_functioning",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Some interference with personal functioning "
"in daily living.",
)
severe_interference_functioning = CamcopsColumn(
"severe_interference_functioning",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Severe interference with personal "
"functioning in daily living.",
)
perceptual_alterations = CamcopsColumn(
"perceptual_alterations",
Boolean,
permitted_value_checker=BIT_CHECKER,
comment="Perceptual alterations (e.g. subjective hyperacusis, "
"appreciation of colours as specially vivid, etc.).",
) # ... not psychotic
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).",
)
date_pertains_to = Column(
"date_pertains_to", Date, comment="Date the assessment pertains to"
)
comments = Column("comments", UnicodeText, comment="Clinician's comments")
CORE_NAMES = ["mood_elevated", "mood_irritable"]
HYPOMANIA_MANIA_NAMES = [
"distractible",
"activity",
"sleep",
"talkativeness",
"recklessness",
"social_disinhibition",
"sexual",
]
MANIA_NAMES = ["grandiosity", "flight_of_ideas"]
OTHER_CRITERIA_NAMES = [
"sustained4days",
"sustained7days",
"admission_required",
"some_interference_functioning",
"severe_interference_functioning",
]
PSYCHOSIS_NAMES = [
"perceptual_alterations", # not psychotic
"hallucinations_schizophrenic",
"hallucinations_other",
"delusions_schizophrenic",
"delusions_other",
]
[docs] @staticmethod
def longname(req: "CamcopsRequest") -> str:
_ = req.gettext
return _(
"ICD-10 symptomatic criteria for a manic/hypomanic episode "
"(as in e.g. F06.3, F25, F30, F31)"
)
[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_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="category",
coltype=SummaryCategoryColType,
value=self.get_description(req),
comment="Diagnostic category",
),
SummaryElement(
name="psychotic_symptoms",
coltype=Boolean(),
value=self.psychosis_present(),
comment="Psychotic symptoms present?",
),
]
# Meets criteria? These also return null for unknown.
def meets_criteria_mania_psychotic_schizophrenic(self) -> Optional[bool]:
x = self.meets_criteria_mania_ignoring_psychosis()
if not x:
return x
if self.hallucinations_other or self.delusions_other:
return False # that counts as manic psychosis
if self.hallucinations_other is None or self.delusions_other is None:
return None # might be manic psychosis
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_mania_psychotic_icd(self) -> Optional[bool]:
x = self.meets_criteria_mania_ignoring_psychosis()
if not x:
return x
if self.hallucinations_other or self.delusions_other:
return True
if self.hallucinations_other is None or self.delusions_other is None:
return None
return False
def meets_criteria_mania_nonpsychotic(self) -> Optional[bool]:
x = self.meets_criteria_mania_ignoring_psychosis()
if not x:
return x
if (
self.hallucinations_schizophrenic is None
or self.delusions_schizophrenic is None
or self.hallucinations_other is None
or self.delusions_other is None
):
return None
if (
self.hallucinations_schizophrenic
or self.delusions_schizophrenic
or self.hallucinations_other
or self.delusions_other
):
return False
return True
def meets_criteria_mania_ignoring_psychosis(self) -> Optional[bool]:
# When can we say "definitely not"?
if is_false(self.mood_elevated) and is_false(self.mood_irritable):
return False
if is_false(self.sustained7days) and is_false(self.admission_required):
return False
t = self.count_booleans(
self.HYPOMANIA_MANIA_NAMES
) + self.count_booleans(self.MANIA_NAMES)
u = self.n_fields_none(
self.HYPOMANIA_MANIA_NAMES
) + self.n_fields_none(self.MANIA_NAMES)
if self.mood_elevated and (t + u < 3):
# With elevated mood, need at least 3 symptoms
return False
if is_false(self.mood_elevated) and (t + u < 4):
# With only irritable mood, need at least 4 symptoms
return False
if is_false(self.severe_interference_functioning):
return False
# OK. When can we say "yes"?
if (
(self.mood_elevated or self.mood_irritable)
and (self.sustained7days or self.admission_required)
and (
(self.mood_elevated and t >= 3)
or (self.mood_irritable and t >= 4)
)
and self.severe_interference_functioning
):
return True
return None
def meets_criteria_hypomania(self) -> Optional[bool]:
# When can we say "definitely not"?
if self.meets_criteria_mania_ignoring_psychosis():
return False # silly to call it hypomania if it's mania
if is_false(self.mood_elevated) and is_false(self.mood_irritable):
return False
if is_false(self.sustained4days):
return False
t = self.count_booleans(self.HYPOMANIA_MANIA_NAMES)
u = self.n_fields_none(self.HYPOMANIA_MANIA_NAMES)
if t + u < 3:
# Need at least 3 symptoms
return False
if is_false(self.some_interference_functioning):
return False
# OK. When can we say "yes"?
if (
(self.mood_elevated or self.mood_irritable)
and self.sustained4days
and t >= 3
and self.some_interference_functioning
):
return True
return None
def meets_criteria_none(self) -> Optional[bool]:
h = self.meets_criteria_hypomania()
m = self.meets_criteria_mania_ignoring_psychosis()
if h or m:
return False
if is_false(h) and is_false(m):
return True
return None
def psychosis_present(self) -> Optional[bool]:
if (
self.hallucinations_other
or self.hallucinations_schizophrenic
or self.delusions_other
or self.delusions_schizophrenic
):
return True
if (
self.hallucinations_other is None
or self.hallucinations_schizophrenic is None
or self.delusions_other is None
or self.delusions_schizophrenic is None
):
return None
return False
def get_description(self, req: CamcopsRequest) -> str:
if self.meets_criteria_mania_psychotic_schizophrenic():
return self.wxstring(req, "category_manic_psychotic_schizophrenic")
elif self.meets_criteria_mania_psychotic_icd():
return self.wxstring(req, "category_manic_psychotic")
elif self.meets_criteria_mania_nonpsychotic():
return self.wxstring(req, "category_manic_nonpsychotic")
elif self.meets_criteria_hypomania():
return self.wxstring(req, "category_hypomanic")
elif self.meets_criteria_none():
return self.wxstring(req, "category_none")
else:
return req.sstring(SS.UNKNOWN)
[docs] def is_complete(self) -> bool:
return (
self.date_pertains_to is not None
and self.meets_criteria_none() is not None
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)
)
[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}
{psychotic_symptoms}
</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_description(req),
),
psychotic_symptoms=tr_qa(
self.wxstring(req, "psychotic_symptoms") + " <sup>[2]</sup>",
get_present_absent_none(req, self.psychosis_present()),
),
icd10_symptomatic_disclaimer=req.wappstring(
AS.ICD10_SYMPTOMATIC_DISCLAIMER
),
)
h += self.text_row(req, "core")
for x in self.CORE_NAMES:
h += self.row_true_false(req, x)
h += self.text_row(req, "hypomania_mania")
for x in self.HYPOMANIA_MANIA_NAMES:
h += self.row_true_false(req, x)
h += self.text_row(req, "other_mania")
for x in self.MANIA_NAMES:
h += self.row_true_false(req, x)
h += self.text_row(req, "other_criteria")
for x in self.OTHER_CRITERIA_NAMES:
h += self.row_true_false(req, x)
h += subheading_spanning_two_columns(self.wxstring(req, "psychosis"))
for x in self.PSYCHOSIS_NAMES:
h += self.row_true_false(req, x)
h += f"""
</table>
<div class="{CssClass.FOOTNOTES}">
[1] Hypomania:
elevated/irritable mood
+ sustained for ≥4 days
+ at least 3 of the “other hypomania” symptoms
+ some interference with functioning.
Mania:
elevated/irritable mood
+ sustained for ≥7 days or hospital admission required
+ at least 3 of the “other mania/hypomania” symptoms
(4 if mood only irritable)
+ severe interference with functioning.
[2] ICD-10 nonpsychotic mania requires mania without
hallucinations/delusions.
ICD-10 psychotic mania requires mania plus
hallucinations/delusions other than those that are
“typically schizophrenic”.
ICD-10 does not clearly categorize mania with only
schizophreniform psychotic symptoms; however, Schneiderian
first-rank symptoms can occur in manic psychosis
(e.g. Conus P et al., 2004, PMID 15337330.).
</div>
{ICD10_COPYRIGHT_DIV}
"""
return h