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, List, Optional, 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.orm import Mapped
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,
    mapped_camcops_column,
)
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))


[docs]class Icd10SpecPD( # type: ignore[misc] TaskHasClinicianMixin, TaskHasPatientMixin, Task, ): """ Server implementation of the ICD10-PD task. """ __tablename__ = "icd10specpd" shortname = "ICD10-PD" info_filename_stem = "icd" @classmethod def extend_columns(cls: Type["Icd10SpecPD"], **kwargs: 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", ], ) date_pertains_to = Column( "date_pertains_to", Date, comment="Date the assessment pertains to" ) comments = Column("comments", UnicodeText, comment="Clinician's comments") skip_paranoid: Mapped[Optional[bool]] = mapped_camcops_column( permitted_value_checker=BIT_CHECKER, comment="Skip questions for paranoid PD?", ) skip_schizoid: Mapped[Optional[bool]] = mapped_camcops_column( permitted_value_checker=BIT_CHECKER, comment="Skip questions for schizoid PD?", ) skip_dissocial: Mapped[Optional[bool]] = mapped_camcops_column( permitted_value_checker=BIT_CHECKER, comment="Skip questions for dissocial PD?", ) skip_eu: Mapped[Optional[bool]] = mapped_camcops_column( permitted_value_checker=BIT_CHECKER, comment="Skip questions for emotionally unstable PD?", ) skip_histrionic: Mapped[Optional[bool]] = mapped_camcops_column( permitted_value_checker=BIT_CHECKER, comment="Skip questions for histrionic PD?", ) skip_anankastic: Mapped[Optional[bool]] = mapped_camcops_column( permitted_value_checker=BIT_CHECKER, comment="Skip questions for anankastic PD?", ) skip_anxious: Mapped[Optional[bool]] = mapped_camcops_column( permitted_value_checker=BIT_CHECKER, comment="Skip questions for anxious PD?", ) skip_dependent: Mapped[Optional[bool]] = mapped_camcops_column( permitted_value_checker=BIT_CHECKER, comment="Skip questions for dependent PD?", ) other_pd_present: Mapped[Optional[bool]] = mapped_camcops_column( 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) # type: ignore[attr-defined] or is_false(self.g2) # type: ignore[attr-defined] or is_false(self.g3) # type: ignore[attr-defined] or is_false(self.g4) # type: ignore[attr-defined] or is_false(self.g5) # type: ignore[attr-defined] or is_false(self.g6) # type: ignore[attr-defined] 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 # type: ignore[attr-defined] # noqa: E501 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) # type: ignore[arg-type] # noqa: E501 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 # type: ignore[arg-type] # noqa: E501 ), ) 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