Source code for camcops_server.tasks.icd10specpd

"""
camcops_server/tasks/icd10specpd.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, Optional, Tuple, Type

from cardinal_pythonlib.datetimefunc import format_datetime
import cardinal_pythonlib.rnc_web as ws
from cardinal_pythonlib.stringfunc import strseq
from cardinal_pythonlib.typetests import is_false
from sqlalchemy.ext.declarative import DeclarativeMeta
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,
    PV,
)
from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
from camcops_server.cc_modules.cc_db import add_multiple_columns
from camcops_server.cc_modules.cc_html import (
    answer,
    get_yes_no_none,
    get_yes_no_unknown,
    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,
)
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,
)


# =============================================================================
# Icd10SpecPD
# =============================================================================


def ctv_info_pd(
    req: CamcopsRequest, condition: str, has_it: Optional[bool]
) -> CtvInfo:
    return CtvInfo(content=condition + ": " + get_yes_no_unknown(req, has_it))


class Icd10SpecPDMetaclass(DeclarativeMeta):
    # noinspection PyInitNewSignature
    def __init__(
        cls: Type["Icd10SpecPD"],
        name: str,
        bases: Tuple[Type, ...],
        classdict: Dict[str, Any],
    ) -> None:
        add_multiple_columns(
            cls,
            "g",
            1,
            cls.N_GENERAL,
            Boolean,
            pv=PV.BIT,
            comment_fmt="G{n}: {s}",
            comment_strings=[
                "pathological 1",
                "pervasive",
                "pathological 2",
                "persistent",
                "primary 1",
                "primary 2",
            ],
        )
        add_multiple_columns(
            cls,
            "g1_",
            1,
            cls.N_GENERAL_1,
            Boolean,
            pv=PV.BIT,
            comment_fmt="G1{n}: {s}",
            comment_strings=[
                "cognition",
                "affectivity",
                "impulse control",
                "interpersonal",
            ],
        )
        add_multiple_columns(
            cls,
            "paranoid",
            1,
            cls.N_PARANOID,
            Boolean,
            pv=PV.BIT,
            comment_fmt="Paranoid ({n}): {s}",
            comment_strings=[
                "sensitive",
                "grudges",
                "suspicious",
                "personal rights",
                "sexual jealousy",
                "self-referential",
                "conspiratorial",
            ],
        )
        add_multiple_columns(
            cls,
            "schizoid",
            1,
            cls.N_SCHIZOID,
            Boolean,
            pv=PV.BIT,
            comment_fmt="Schizoid ({n}): {s}",
            comment_strings=[
                "little pleasure",
                "cold/detached",
                "limited capacity for warmth",
                "indifferent to praise/criticism",
                "little interest in sex",
                "solitary",
                "fantasy/introspection",
                "0/1 close friends/confidants",
                "insensitive to social norms",
            ],
        )
        add_multiple_columns(
            cls,
            "dissocial",
            1,
            cls.N_DISSOCIAL,
            Boolean,
            pv=PV.BIT,
            comment_fmt="Dissocial ({n}): {s}",
            comment_strings=[
                "unconcern",
                "irresponsibility",
                "incapacity to maintain relationships",
                "low tolerance to frustration",
                "incapacity for guilt",
                "prone to blame others",
            ],
        )
        add_multiple_columns(
            cls,
            "eu",
            1,
            cls.N_EU,
            Boolean,
            pv=PV.BIT,
            comment_fmt="Emotionally unstable ({n}): {s}",
            comment_strings=[
                "act without considering consequences",
                "quarrelsome",
                "outbursts of anger",
                "can't maintain actions with immediate reward",
                "unstable/capricious mood",
                "uncertain self-image",
                "intense/unstable relationships",
                "avoids abandonment",
                "threats/acts of self-harm",
                "feelings of emptiness",
            ],
        )
        add_multiple_columns(
            cls,
            "histrionic",
            1,
            cls.N_HISTRIONIC,
            Boolean,
            pv=PV.BIT,
            comment_fmt="Histrionic ({n}): {s}",
            comment_strings=[
                "theatricality",
                "suggestibility",
                "shallow/labile affect",
                "centre of attention",
                "inappropriately seductive",
                "concerned with attractiveness",
            ],
        )
        add_multiple_columns(
            cls,
            "anankastic",
            1,
            cls.N_ANANKASTIC,
            Boolean,
            pv=PV.BIT,
            comment_fmt="Anankastic ({n}): {s}",
            comment_strings=[
                "doubt/caution",
                "preoccupation with details",
                "perfectionism",
                "excessively conscientious",
                "preoccupied with productivity",
                "excessive pedantry",
                "rigid/stubborn",
                "require others do things specific way",
            ],
        )
        add_multiple_columns(
            cls,
            "anxious",
            1,
            cls.N_ANXIOUS,
            Boolean,
            pv=PV.BIT,
            comment_fmt="Anxious ({n}), {s}",
            comment_strings=[
                "tension/apprehension",
                "preoccupied with criticism/rejection",
                "won't get involved unless certain liked",
                "need for security restricts lifestyle",
                "avoidance of interpersonal contact",
            ],
        )
        add_multiple_columns(
            cls,
            "dependent",
            1,
            cls.N_DEPENDENT,
            Boolean,
            pv=PV.BIT,
            comment_fmt="Dependent ({n}): {s}",
            comment_strings=[
                "others decide",
                "subordinate needs to those of others",
                "unwilling to make reasonable demands",
                "uncomfortable/helpless when alone",
                "fears of being left to oneself",
                "everyday decisions require advice/reassurance",
            ],
        )
        super().__init__(name, bases, classdict)


[docs]class Icd10SpecPD( TaskHasClinicianMixin, TaskHasPatientMixin, Task, metaclass=Icd10SpecPDMetaclass, ): """ Server implementation of the ICD10-PD task. """ __tablename__ = "icd10specpd" shortname = "ICD10-PD" info_filename_stem = "icd" date_pertains_to = Column( "date_pertains_to", Date, comment="Date the assessment pertains to" ) comments = Column("comments", UnicodeText, comment="Clinician's comments") skip_paranoid = CamcopsColumn( "skip_paranoid", Boolean, permitted_value_checker=BIT_CHECKER, comment="Skip questions for paranoid PD?", ) skip_schizoid = CamcopsColumn( "skip_schizoid", Boolean, permitted_value_checker=BIT_CHECKER, comment="Skip questions for schizoid PD?", ) skip_dissocial = CamcopsColumn( "skip_dissocial", Boolean, permitted_value_checker=BIT_CHECKER, comment="Skip questions for dissocial PD?", ) skip_eu = CamcopsColumn( "skip_eu", Boolean, permitted_value_checker=BIT_CHECKER, comment="Skip questions for emotionally unstable PD?", ) skip_histrionic = CamcopsColumn( "skip_histrionic", Boolean, permitted_value_checker=BIT_CHECKER, comment="Skip questions for histrionic PD?", ) skip_anankastic = CamcopsColumn( "skip_anankastic", Boolean, permitted_value_checker=BIT_CHECKER, comment="Skip questions for anankastic PD?", ) skip_anxious = CamcopsColumn( "skip_anxious", Boolean, permitted_value_checker=BIT_CHECKER, comment="Skip questions for anxious PD?", ) skip_dependent = CamcopsColumn( "skip_dependent", Boolean, permitted_value_checker=BIT_CHECKER, comment="Skip questions for dependent PD?", ) other_pd_present = CamcopsColumn( "other_pd_present", Boolean, permitted_value_checker=BIT_CHECKER, comment="Is another personality disorder present?", ) vignette = Column("vignette", UnicodeText, comment="Vignette") N_GENERAL = 6 N_GENERAL_1 = 4 N_PARANOID = 7 N_SCHIZOID = 9 N_DISSOCIAL = 6 N_EU = 10 N_EUPD_I = 5 N_HISTRIONIC = 6 N_ANANKASTIC = 8 N_ANXIOUS = 5 N_DEPENDENT = 6 GENERAL_FIELDS = strseq("g", 1, N_GENERAL) GENERAL_1_FIELDS = strseq("g1_", 1, N_GENERAL_1) PARANOID_FIELDS = strseq("paranoid", 1, N_PARANOID) SCHIZOID_FIELDS = strseq("schizoid", 1, N_SCHIZOID) DISSOCIAL_FIELDS = strseq("dissocial", 1, N_DISSOCIAL) EU_FIELDS = strseq("eu", 1, N_EU) EUPD_I_FIELDS = strseq("eu", 1, N_EUPD_I) # impulsive EUPD_B_FIELDS = strseq("eu", N_EUPD_I + 1, N_EU) # borderline HISTRIONIC_FIELDS = strseq("histrionic", 1, N_HISTRIONIC) ANANKASTIC_FIELDS = strseq("anankastic", 1, N_ANANKASTIC) ANXIOUS_FIELDS = strseq("anxious", 1, N_ANXIOUS) DEPENDENT_FIELDS = strseq("dependent", 1, N_DEPENDENT)
[docs] @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("ICD-10 criteria for specific personality disorders (F60)")
[docs] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: if not self.is_complete(): return CTV_INCOMPLETE infolist = [ ctv_info_pd( req, self.wxstring(req, "meets_general_criteria"), self.has_pd(), ), ctv_info_pd( req, self.wxstring(req, "paranoid_pd_title"), self.has_paranoid_pd(), ), ctv_info_pd( req, self.wxstring(req, "schizoid_pd_title"), self.has_schizoid_pd(), ), ctv_info_pd( req, self.wxstring(req, "dissocial_pd_title"), self.has_dissocial_pd(), ), ctv_info_pd( req, self.wxstring(req, "eu_pd_i_title"), self.has_eupd_i() ), ctv_info_pd( req, self.wxstring(req, "eu_pd_b_title"), self.has_eupd_b() ), ctv_info_pd( req, self.wxstring(req, "histrionic_pd_title"), self.has_histrionic_pd(), ), ctv_info_pd( req, self.wxstring(req, "anankastic_pd_title"), self.has_anankastic_pd(), ), ctv_info_pd( req, self.wxstring(req, "anxious_pd_title"), self.has_anxious_pd(), ), ctv_info_pd( req, self.wxstring(req, "dependent_pd_title"), self.has_dependent_pd(), ), ] return infolist
[docs] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: return self.standard_task_summary_fields() + [ SummaryElement( name="meets_general_criteria", coltype=Boolean(), value=self.has_pd(), comment="Meets general criteria for personality disorder?", ), SummaryElement( name="paranoid_pd", coltype=Boolean(), value=self.has_paranoid_pd(), comment="Meets criteria for paranoid PD?", ), SummaryElement( name="schizoid_pd", coltype=Boolean(), value=self.has_schizoid_pd(), comment="Meets criteria for schizoid PD?", ), SummaryElement( name="dissocial_pd", coltype=Boolean(), value=self.has_dissocial_pd(), comment="Meets criteria for dissocial PD?", ), SummaryElement( name="eupd_i", coltype=Boolean(), value=self.has_eupd_i(), comment="Meets criteria for EUPD (impulsive type)?", ), SummaryElement( name="eupd_b", coltype=Boolean(), value=self.has_eupd_b(), comment="Meets criteria for EUPD (borderline type)?", ), SummaryElement( name="histrionic_pd", coltype=Boolean(), value=self.has_histrionic_pd(), comment="Meets criteria for histrionic PD?", ), SummaryElement( name="anankastic_pd", coltype=Boolean(), value=self.has_anankastic_pd(), comment="Meets criteria for anankastic PD?", ), SummaryElement( name="anxious_pd", coltype=Boolean(), value=self.has_anxious_pd(), comment="Meets criteria for anxious PD?", ), SummaryElement( name="dependent_pd", coltype=Boolean(), value=self.has_dependent_pd(), comment="Meets criteria for dependent PD?", ), ]
# noinspection PyUnresolvedReferences def is_pd_excluded(self) -> bool: return ( is_false(self.g1) or is_false(self.g2) or is_false(self.g3) or is_false(self.g4) or is_false(self.g5) or is_false(self.g6) or ( self.all_fields_not_none(self.GENERAL_1_FIELDS) and self.count_booleans(self.GENERAL_1_FIELDS) <= 1 ) ) def is_complete_general(self) -> bool: return self.all_fields_not_none( self.GENERAL_FIELDS ) and self.all_fields_not_none(self.GENERAL_1_FIELDS) def is_complete_paranoid(self) -> bool: return self.all_fields_not_none(self.PARANOID_FIELDS) def is_complete_schizoid(self) -> bool: return self.all_fields_not_none(self.SCHIZOID_FIELDS) def is_complete_dissocial(self) -> bool: return self.all_fields_not_none(self.DISSOCIAL_FIELDS) def is_complete_eu(self) -> bool: return self.all_fields_not_none(self.EU_FIELDS) def is_complete_histrionic(self) -> bool: return self.all_fields_not_none(self.HISTRIONIC_FIELDS) def is_complete_anankastic(self) -> bool: return self.all_fields_not_none(self.ANANKASTIC_FIELDS) def is_complete_anxious(self) -> bool: return self.all_fields_not_none(self.ANXIOUS_FIELDS) def is_complete_dependent(self) -> bool: return self.all_fields_not_none(self.DEPENDENT_FIELDS) # Meets criteria? These also return null for unknown. def has_pd(self) -> Optional[bool]: if self.is_pd_excluded(): return False if not self.is_complete_general(): return None return ( self.all_truthy(self.GENERAL_FIELDS) and self.count_booleans(self.GENERAL_1_FIELDS) > 1 ) def has_paranoid_pd(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_paranoid(): return None return self.count_booleans(self.PARANOID_FIELDS) >= 4 def has_schizoid_pd(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_schizoid(): return None return self.count_booleans(self.SCHIZOID_FIELDS) >= 4 def has_dissocial_pd(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_dissocial(): return None return self.count_booleans(self.DISSOCIAL_FIELDS) >= 3 # noinspection PyUnresolvedReferences def has_eupd_i(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_eu(): return None return self.count_booleans(self.EUPD_I_FIELDS) >= 3 and self.eu2 def has_eupd_b(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_eu(): return None return ( self.count_booleans(self.EUPD_I_FIELDS) >= 3 and self.count_booleans(self.EUPD_B_FIELDS) >= 2 ) def has_histrionic_pd(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_histrionic(): return None return self.count_booleans(self.HISTRIONIC_FIELDS) >= 4 def has_anankastic_pd(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_anankastic(): return None return self.count_booleans(self.ANANKASTIC_FIELDS) >= 4 def has_anxious_pd(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_anxious(): return None return self.count_booleans(self.ANXIOUS_FIELDS) >= 4 def has_dependent_pd(self) -> Optional[bool]: hpd = self.has_pd() if not hpd: return hpd if not self.is_complete_dependent(): return None return self.count_booleans(self.DEPENDENT_FIELDS) >= 4
[docs] def is_complete(self) -> bool: return ( self.date_pertains_to is not None and ( self.is_pd_excluded() or ( self.is_complete_general() and (self.skip_paranoid or self.is_complete_paranoid()) and (self.skip_schizoid or self.is_complete_schizoid()) and (self.skip_dissocial or self.is_complete_dissocial()) and (self.skip_eu or self.is_complete_eu()) and (self.skip_histrionic or self.is_complete_histrionic()) and (self.skip_anankastic or self.is_complete_anankastic()) and (self.skip_anxious or self.is_complete_anxious()) and (self.skip_dependent or self.is_complete_dependent()) ) ) and self.field_contents_valid() )
def pd_heading(self, req: CamcopsRequest, wstringname: str) -> str: return f""" <tr class="{CssClass.HEADING}"> <td colspan="2">{self.wxstring(req, wstringname)}</td> </tr> """ def pd_skiprow(self, req: CamcopsRequest, stem: str) -> str: return self.get_twocol_bool_row( req, "skip_" + stem, label=self.wxstring(req, "skip_this_pd") ) def pd_subheading(self, req: CamcopsRequest, wstringname: str) -> str: return f""" <tr class="{CssClass.SUBHEADING}"> <td colspan="2">{self.wxstring(req, wstringname)}</td> </tr> """ def pd_general_criteria_bits(self, req: CamcopsRequest) -> str: return f""" <tr> <td>{self.wxstring(req, "general_criteria_must_be_met")}</td> <td><i><b>{get_yes_no_unknown(req, self.has_pd())}</b></i></td> </tr> """ def pd_b_text(self, req: CamcopsRequest, wstringname: str) -> str: return f""" <tr> <td>{self.wxstring(req, wstringname)}</td> <td class="{CssClass.SUBHEADING}"></td> </tr> """ def pd_basic_row(self, req: CamcopsRequest, stem: str, i: int) -> str: return self.get_twocol_bool_row_true_false( req, stem + str(i), self.wxstring(req, stem + str(i)) ) def standard_pd_html(self, req: CamcopsRequest, stem: str, n: int) -> str: html = self.pd_heading(req, stem + "_pd_title") html += self.pd_skiprow(req, stem) html += self.pd_general_criteria_bits(req) html += self.pd_b_text(req, stem + "_pd_B") for i in range(1, n + 1): html += self.pd_basic_row(req, stem, i) return html
[docs] def get_task_html(self, req: CamcopsRequest) -> str: h = self.get_standard_clinician_comments_block(req, self.comments) h += f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> """ h += self.get_is_complete_tr(req) h += tr_qa( req.wappstring(AS.DATE_PERTAINS_TO), format_datetime( self.date_pertains_to, DateFormat.LONG_DATE, default=None ), ) h += tr_qa( self.wxstring(req, "meets_general_criteria"), get_yes_no_none(req, self.has_pd()), ) h += tr_qa( self.wxstring(req, "paranoid_pd_title"), get_yes_no_none(req, self.has_paranoid_pd()), ) h += tr_qa( self.wxstring(req, "schizoid_pd_title"), get_yes_no_none(req, self.has_schizoid_pd()), ) h += tr_qa( self.wxstring(req, "dissocial_pd_title"), get_yes_no_none(req, self.has_dissocial_pd()), ) h += tr_qa( self.wxstring(req, "eu_pd_i_title"), get_yes_no_none(req, self.has_eupd_i()), ) h += tr_qa( self.wxstring(req, "eu_pd_b_title"), get_yes_no_none(req, self.has_eupd_b()), ) h += tr_qa( self.wxstring(req, "histrionic_pd_title"), get_yes_no_none(req, self.has_histrionic_pd()), ) h += tr_qa( self.wxstring(req, "anankastic_pd_title"), get_yes_no_none(req, self.has_anankastic_pd()), ) h += tr_qa( self.wxstring(req, "anxious_pd_title"), get_yes_no_none(req, self.has_anxious_pd()), ) h += tr_qa( self.wxstring(req, "dependent_pd_title"), get_yes_no_none(req, self.has_dependent_pd()), ) h += f""" </table> </div> <div> <p><i>Vignette:</i></p> <p>{answer(ws.webify(self.vignette), default_for_blank_strings=True)}</p> </div> <table class="{CssClass.TASKDETAIL}"> <tr> <th width="80%">Question</th> <th width="20%">Answer</th> </tr> """ # General h += subheading_spanning_two_columns(self.wxstring(req, "general")) h += self.get_twocol_bool_row_true_false( req, "g1", self.wxstring(req, "G1") ) h += self.pd_b_text(req, "G1b") for i in range(1, Icd10SpecPD.N_GENERAL_1 + 1): h += self.get_twocol_bool_row_true_false( req, "g1_" + str(i), self.wxstring(req, "G1_" + str(i)) ) for i in range(2, Icd10SpecPD.N_GENERAL + 1): h += self.get_twocol_bool_row_true_false( req, "g" + str(i), self.wxstring(req, "G" + str(i)) ) # Paranoid, etc. h += self.standard_pd_html(req, "paranoid", Icd10SpecPD.N_PARANOID) h += self.standard_pd_html(req, "schizoid", Icd10SpecPD.N_SCHIZOID) h += self.standard_pd_html(req, "dissocial", Icd10SpecPD.N_DISSOCIAL) # EUPD is special h += self.pd_heading(req, "eu_pd_title") h += self.pd_skiprow(req, "eu") h += self.pd_general_criteria_bits(req) h += self.pd_subheading(req, "eu_pd_i_title") h += self.pd_b_text(req, "eu_pd_i_B") for i in range(1, Icd10SpecPD.N_EUPD_I + 1): h += self.pd_basic_row(req, "eu", i) h += self.pd_subheading(req, "eu_pd_b_title") h += self.pd_b_text(req, "eu_pd_b_B") for i in range(Icd10SpecPD.N_EUPD_I + 1, Icd10SpecPD.N_EU + 1): h += self.pd_basic_row(req, "eu", i) # Back to plain ones h += self.standard_pd_html(req, "histrionic", Icd10SpecPD.N_HISTRIONIC) h += self.standard_pd_html(req, "anankastic", Icd10SpecPD.N_ANANKASTIC) h += self.standard_pd_html(req, "anxious", Icd10SpecPD.N_ANXIOUS) h += self.standard_pd_html(req, "dependent", Icd10SpecPD.N_DEPENDENT) # Done h += ( """ </table> """ + ICD10_COPYRIGHT_DIV ) return h