"""
camcops_server/tasks/ybocs.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 Any, Dict, List, Tuple, Type
from cardinal_pythonlib.stringfunc import strseq
from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.sql.schema import Column
from sqlalchemy.sql.sqltypes import Boolean, Integer, UnicodeText
from camcops_server.cc_modules.cc_constants import (
CssClass,
DATA_COLLECTION_UNLESS_UPGRADED_DIV,
)
from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
from camcops_server.cc_modules.cc_html import (
answer,
get_ternary,
subheading_spanning_four_columns,
tr,
)
from camcops_server.cc_modules.cc_request import CamcopsRequest
from camcops_server.cc_modules.cc_sqla_coltypes import (
BIT_CHECKER,
CamcopsColumn,
PermittedValueChecker,
)
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_trackerhelpers import TrackerInfo
# =============================================================================
# Y-BOCS
# =============================================================================
class YbocsMetaclass(DeclarativeMeta):
# noinspection PyInitNewSignature
def __init__(
cls: Type["Ybocs"],
name: str,
bases: Tuple[Type, ...],
classdict: Dict[str, Any],
) -> None:
cls.TARGET_COLUMNS = [] # type: List[Column]
for target in ("obsession", "compulsion", "avoidance"):
for n in range(1, cls.NTARGETS + 1):
fname = f"target_{target}_{n}"
col = Column(
fname,
UnicodeText,
comment=f"Target symptoms: {target} {n}",
)
setattr(cls, fname, col)
cls.TARGET_COLUMNS.append(col)
for qnumstr, maxscore, comment in cls.QINFO:
fname = "q" + qnumstr
setattr(
cls,
fname,
CamcopsColumn(
fname,
Integer,
permitted_value_checker=PermittedValueChecker(
minimum=0, maximum=maxscore
),
comment=f"Q{qnumstr}, {comment} "
f"(0-{maxscore}, higher worse)",
),
)
super().__init__(name, bases, classdict)
[docs]class Ybocs(
TaskHasClinicianMixin, TaskHasPatientMixin, Task, metaclass=YbocsMetaclass
):
"""
Server implementation of the Y-BOCS task.
"""
__tablename__ = "ybocs"
shortname = "Y-BOCS"
provides_trackers = True
NTARGETS = 3
QINFO = [ # number, max score, minimal comment
("1", 4, "obsessions: time"),
("1b", 4, "obsessions: obsession-free interval"),
("2", 4, "obsessions: interference"),
("3", 4, "obsessions: distress"),
("4", 4, "obsessions: resistance"),
("5", 4, "obsessions: control"),
("6", 4, "compulsions: time"),
("6b", 4, "compulsions: compulsion-free interval"),
("7", 4, "compulsions: interference"),
("8", 4, "compulsions: distress"),
("9", 4, "compulsions: resistance"),
("10", 4, "compulsions: control"),
("11", 4, "insight"),
("12", 4, "avoidance"),
("13", 4, "indecisiveness"),
("14", 4, "overvalued responsibility"),
("15", 4, "slowness"),
("16", 4, "doubting"),
("17", 6, "global severity"),
("18", 6, "global improvement"),
("19", 3, "reliability"),
]
QUESTION_FIELDS = ["q" + x[0] for x in QINFO]
SCORED_QUESTIONS = strseq("q", 1, 10)
OBSESSION_QUESTIONS = strseq("q", 1, 5)
COMPULSION_QUESTIONS = strseq("q", 6, 10)
MAX_TOTAL = 40
MAX_OBS = 20
MAX_COM = 20
[docs] @staticmethod
def longname(req: "CamcopsRequest") -> str:
_ = req.gettext
return _("Yale–Brown Obsessive Compulsive Scale")
[docs] def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
return [
TrackerInfo(
value=self.total_score(),
plot_label="Y-BOCS total score (lower is better)",
axis_label=f"Total score (out of {self.MAX_TOTAL})",
axis_min=-0.5,
axis_max=self.MAX_TOTAL + 0.5,
),
TrackerInfo(
value=self.obsession_score(),
plot_label="Y-BOCS obsession score (lower is better)",
axis_label=f"Total score (out of {self.MAX_OBS})",
axis_min=-0.5,
axis_max=self.MAX_OBS + 0.5,
),
TrackerInfo(
value=self.compulsion_score(),
plot_label="Y-BOCS compulsion score (lower is better)",
axis_label=f"Total score (out of {self.MAX_COM})",
axis_min=-0.5,
axis_max=self.MAX_COM + 0.5,
),
]
[docs] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
return self.standard_task_summary_fields() + [
SummaryElement(
name="total_score",
coltype=Integer(),
value=self.total_score(),
comment=f"Total score (/ {self.MAX_TOTAL})",
),
SummaryElement(
name="obsession_score",
coltype=Integer(),
value=self.obsession_score(),
comment=f"Obsession score (/ {self.MAX_OBS})",
),
SummaryElement(
name="compulsion_score",
coltype=Integer(),
value=self.compulsion_score(),
comment=f"Compulsion score (/ {self.MAX_COM})",
),
]
[docs] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
if not self.is_complete():
return CTV_INCOMPLETE
t = self.total_score()
o = self.obsession_score()
c = self.compulsion_score()
return [
CtvInfo(
content=(
"Y-BOCS total score {t}/{mt} (obsession {o}/{mo}, "
"compulsion {c}/{mc})".format(
t=t,
mt=self.MAX_TOTAL,
o=o,
mo=self.MAX_OBS,
c=c,
mc=self.MAX_COM,
)
)
)
]
def total_score(self) -> int:
return self.sum_fields(self.SCORED_QUESTIONS)
def obsession_score(self) -> int:
return self.sum_fields(self.OBSESSION_QUESTIONS)
def compulsion_score(self) -> int:
return self.sum_fields(self.COMPULSION_QUESTIONS)
[docs] def is_complete(self) -> bool:
return self.field_contents_valid() and self.all_fields_not_none(
self.SCORED_QUESTIONS
)
[docs] def get_task_html(self, req: CamcopsRequest) -> str:
target_symptoms = ""
for col in self.TARGET_COLUMNS:
target_symptoms += tr(col.comment, answer(getattr(self, col.name)))
q_a = ""
for qi in self.QINFO:
fieldname = "q" + qi[0]
value = getattr(self, fieldname)
q_a += tr(
self.wxstring(req, fieldname + "_title"),
answer(
self.wxstring(req, fieldname + "_a" + str(value), value)
if value is not None
else None
),
)
return f"""
<div class="{CssClass.SUMMARY}">
<table class="{CssClass.SUMMARY}">
{self.get_is_complete_tr(req)}
<tr>
<td>Total score</td>
<td>{answer(self.total_score())} /
{self.MAX_TOTAL}</td>
</td>
<tr>
<td>Obsession score</td>
<td>{answer(self.obsession_score())} /
{self.MAX_OBS}</td>
</td>
<tr>
<td>Compulsion score</td>
<td>{answer(self.compulsion_score())} /
{self.MAX_COM}</td>
</td>
</table>
</div>
<table class="{CssClass.TASKDETAIL}">
<tr>
<th width="50%">Target symptom</th>
<th width="50%">Detail</th>
</tr>
{target_symptoms}
</table>
<table class="{CssClass.TASKDETAIL}">
<tr>
<th width="50%">Question</th>
<th width="50%">Answer</th>
</tr>
{q_a}
</table>
{DATA_COLLECTION_UNLESS_UPGRADED_DIV}
"""
# =============================================================================
# Y-BOCS-SC
# =============================================================================
class YbocsScMetaclass(DeclarativeMeta):
# noinspection PyInitNewSignature
def __init__(
cls: Type["YbocsSc"],
name: str,
bases: Tuple[Type, ...],
classdict: Dict[str, Any],
) -> None:
for item in cls.ITEMS:
setattr(
cls,
item + cls.SUFFIX_CURRENT,
CamcopsColumn(
item + cls.SUFFIX_CURRENT,
Boolean,
permitted_value_checker=BIT_CHECKER,
comment=item + " (current symptom)",
),
)
setattr(
cls,
item + cls.SUFFIX_PAST,
CamcopsColumn(
item + cls.SUFFIX_PAST,
Boolean,
permitted_value_checker=BIT_CHECKER,
comment=item + " (past symptom)",
),
)
setattr(
cls,
item + cls.SUFFIX_PRINCIPAL,
CamcopsColumn(
item + cls.SUFFIX_PRINCIPAL,
Boolean,
permitted_value_checker=BIT_CHECKER,
comment=item + " (principal symptom)",
),
)
if item.endswith(cls.SUFFIX_OTHER):
setattr(
cls,
item + cls.SUFFIX_DETAIL,
Column(
item + cls.SUFFIX_DETAIL,
UnicodeText,
comment=item + " (details)",
),
)
super().__init__(name, bases, classdict)
[docs]class YbocsSc(
TaskHasClinicianMixin,
TaskHasPatientMixin,
Task,
metaclass=YbocsScMetaclass,
):
"""
Server implementation of the Y-BOCS-SC task.
"""
__tablename__ = "ybocssc"
shortname = "Y-BOCS-SC"
extrastring_taskname = "ybocs" # shares with Y-BOCS
info_filename_stem = extrastring_taskname
SC_PREFIX = "sc_"
SUFFIX_CURRENT = "_current"
SUFFIX_PAST = "_past"
SUFFIX_PRINCIPAL = "_principal"
SUFFIX_OTHER = "_other"
SUFFIX_DETAIL = "_detail"
GROUPS = [
"obs_aggressive",
"obs_contamination",
"obs_sexual",
"obs_hoarding",
"obs_religious",
"obs_symmetry",
"obs_misc",
"obs_somatic",
"com_cleaning",
"com_checking",
"com_repeat",
"com_counting",
"com_arranging",
"com_hoarding",
"com_misc",
]
ITEMS = [
"obs_aggressive_harm_self",
"obs_aggressive_harm_others",
"obs_aggressive_imagery",
"obs_aggressive_obscenities",
"obs_aggressive_embarrassing",
"obs_aggressive_impulses",
"obs_aggressive_steal",
"obs_aggressive_accident",
"obs_aggressive_responsible",
"obs_aggressive_other",
"obs_contamination_bodily_waste",
"obs_contamination_dirt",
"obs_contamination_environmental",
"obs_contamination_household",
"obs_contamination_animals",
"obs_contamination_sticky",
"obs_contamination_ill",
"obs_contamination_others_ill",
"obs_contamination_feeling",
"obs_contamination_other",
"obs_sexual_forbidden",
"obs_sexual_children_incest",
"obs_sexual_homosexuality",
"obs_sexual_to_others",
"obs_sexual_other",
"obs_hoarding_other",
"obs_religious_sacrilege",
"obs_religious_morality",
"obs_religious_other",
"obs_symmetry_with_magical",
"obs_symmetry_without_magical",
"obs_misc_know_remember",
"obs_misc_fear_saying",
"obs_misc_fear_not_saying",
"obs_misc_fear_losing",
"obs_misc_intrusive_nonviolent_images",
"obs_misc_intrusive_sounds",
"obs_misc_bothered_sounds",
"obs_misc_numbers",
"obs_misc_colours",
"obs_misc_superstitious",
"obs_misc_other",
"obs_somatic_illness",
"obs_somatic_appearance",
"obs_somatic_other",
"com_cleaning_handwashing",
"com_cleaning_toileting",
"com_cleaning_cleaning_items",
"com_cleaning_other_contaminant_avoidance",
"com_cleaning_other",
"com_checking_locks_appliances",
"com_checking_not_harm_others",
"com_checking_not_harm_self",
"com_checking_nothing_bad_happens",
"com_checking_no_mistake",
"com_checking_somatic",
"com_checking_other",
"com_repeat_reread_rewrite",
"com_repeat_routine",
"com_repeat_other",
"com_counting_other",
"com_arranging_other",
"com_hoarding_other",
"com_misc_mental_rituals",
"com_misc_lists",
"com_misc_tell_ask",
"com_misc_touch",
"com_misc_blink_stare",
"com_misc_prevent_harm_self",
"com_misc_prevent_harm_others",
"com_misc_prevent_terrible",
"com_misc_eating_ritual",
"com_misc_superstitious",
"com_misc_trichotillomania",
"com_misc_self_harm",
"com_misc_other",
]
[docs] @staticmethod
def longname(req: "CamcopsRequest") -> str:
_ = req.gettext
return _("Y-BOCS Symptom Checklist")
[docs] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
if not self.is_complete():
return CTV_INCOMPLETE
current_list = []
past_list = []
principal_list = []
for item in self.ITEMS:
if getattr(self, item + self.SUFFIX_CURRENT):
current_list.append(item)
if getattr(self, item + self.SUFFIX_PAST):
past_list.append(item)
if getattr(self, item + self.SUFFIX_PRINCIPAL):
principal_list.append(item)
return [
CtvInfo(content=f"Current symptoms: {', '.join(current_list)}"),
CtvInfo(content=f"Past symptoms: {', '.join(past_list)}"),
CtvInfo(
content=f"Principal symptoms: {', '.join(principal_list)}"
),
]
# noinspection PyMethodOverriding
[docs] @staticmethod
def is_complete() -> bool:
return True
[docs] def get_task_html(self, req: CamcopsRequest) -> str:
h = f"""
<table class="{CssClass.TASKDETAIL}">
<tr>
<th width="55%">Symptom</th>
<th width="15%">Current</th>
<th width="15%">Past</th>
<th width="15%">Principal</th>
</tr>
"""
for group in self.GROUPS:
h += subheading_spanning_four_columns(
self.wxstring(req, self.SC_PREFIX + group)
)
for item in self.ITEMS:
if not item.startswith(group):
continue
h += tr(
self.wxstring(req, self.SC_PREFIX + item),
answer(
get_ternary(
getattr(self, item + self.SUFFIX_CURRENT),
value_true="Current",
value_false="",
value_none="",
)
),
answer(
get_ternary(
getattr(self, item + self.SUFFIX_PAST),
value_true="Past",
value_false="",
value_none="",
)
),
answer(
get_ternary(
getattr(self, item + self.SUFFIX_PRINCIPAL),
value_true="Principal",
value_false="",
value_none="",
)
),
)
if item.endswith(self.SUFFIX_OTHER):
h += f"""
<tr>
<td><i>Specify:</i></td>
<td colspan="3">{
answer(getattr(self, item + self.SUFFIX_DETAIL), "")}</td>
</tr>
"""
h += f"""
</table>
{DATA_COLLECTION_UNLESS_UPGRADED_DIV}
"""
return h