Source code for camcops_server.tasks.icd10specpd

#!/usr/bin/env python

"""
camcops_server/tasks/icd10specpd.py

===============================================================================

    Copyright (C) 2012-2019 Rudolf Cardinal (rudolf@pobox.com).

    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 <http://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" 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