Example #1
0
class IsaaqEd(IsaaqCommon, metaclass=IsaaqEdMetaclass):
    __tablename__ = "isaaqed"
    shortname = "ISAAQ-ED"

    Q_PREFIX = "e"
    FIRST_Q = 11
    LAST_Q = 20

    ALL_FIELD_NAMES = strseq(Q_PREFIX, FIRST_Q, LAST_Q)

    @staticmethod
    def longname(req: CamcopsRequest) -> str:
        _ = req.gettext
        return _("Internet Severity and Activities Addiction Questionnaire, "
                 "Eating Disorders Appendix")

    def is_complete(self) -> bool:
        if self.any_fields_none(self.ALL_FIELD_NAMES):
            return False

        return True

    def get_task_html_rows(self, req: CamcopsRequest) -> str:
        header = """
            <tr>
                <th width="70%">{title}</th>
                <th width="30%">{scale}</th>
            </tr>
        """.format(
            title=self.xstring(req, "grid_title"),
            scale=self.xstring(req, "scale"),
        )

        return header + self.get_task_html_rows_for_range(
            req, self.Q_PREFIX, self.FIRST_Q, self.LAST_Q)
Example #2
0
class Ctqsf(TaskHasPatientMixin, Task):
    """
    Server implementation of the CTQ-SF task.
    """

    __tablename__ = "ctqsf"
    shortname = "CTQ-SF"
    provides_trackers = False

    # todo: Ctqsf fields

    N_QUESTIONS = 28
    QUESTION_FIELDNAMES = strseq("q", 1, N_QUESTIONS)

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Childhood Trauma Questionnaire, Short Form")

    # noinspection PyMethodParameters
    @classproperty
    def minimum_client_version(cls) -> Version:
        return Version("2.2.8")

    def is_complete(self) -> bool:
        return self.all_fields_not_none(self.QUESTION_FIELDNAMES)

    def get_task_html(self, req: CamcopsRequest) -> str:
        return ""  # todo: IMPLEMENT Ctqsf
Example #3
0
    def basdai(self) -> Optional[float]:
        """
        Calculating the BASDAI
        A. Add scores for questions 1 – 4
        B. Calculate the mean for questions 5 and 6
        C. Add A and B and divide by 5

        The higher the BASDAI score, the more severe the patient’s disability
        due to their AS.
        """
        if not self.is_complete():
            return None

        score_a_field_names = strseq("q", 1, 4)
        score_b_field_names = strseq("q", 5, 6)

        a = sum([getattr(self, q) for q in score_a_field_names])
        b = statistics.mean([getattr(self, q) for q in score_b_field_names])

        return (a + b) / 5
Example #4
0
class Isaaq(IsaaqCommon, metaclass=IsaaqMetaclass):
    __tablename__ = "isaaq"
    shortname = "ISAAQ"

    A_PREFIX = "a"
    B_PREFIX = "b"
    FIRST_Q = 1
    LAST_A_Q = 15
    LAST_B_Q = 10

    ALL_FIELD_NAMES = strseq(A_PREFIX, FIRST_Q, LAST_A_Q) + strseq(
        B_PREFIX, FIRST_Q, LAST_B_Q)

    @staticmethod
    def longname(req: CamcopsRequest) -> str:
        _ = req.gettext
        return _("Internet Severity and Activities Addiction Questionnaire")

    def get_task_html_rows(self, req: CamcopsRequest) -> str:
        header_format = """
            <tr>
                <th width="70%">{title}</th>
                <th width="30%">{scale}</th>
            </tr>
        """

        a_header = header_format.format(
            title=self.xstring(req, "a_title"),
            scale=self.xstring(req, "scale"),
        )
        b_header = header_format.format(
            title=self.xstring(req, "b_title"),
            scale=self.xstring(req, "scale"),
        )

        return (a_header + self.get_task_html_rows_for_range(
            req, self.A_PREFIX, self.FIRST_Q, self.LAST_A_Q) +
                b_header + self.get_task_html_rows_for_range(
                    req, self.B_PREFIX, self.FIRST_Q, self.LAST_B_Q))
Example #5
0
class Badls(TaskHasPatientMixin,
            TaskHasRespondentMixin,
            Task,
            metaclass=BadlsMetaclass):
    """
    Server implementation of the BADLS task.
    """

    __tablename__ = "badls"
    shortname = "BADLS"
    provides_trackers = True

    SCORING = {"a": 0, "b": 1, "c": 2, "d": 3, "e": 0}
    NQUESTIONS = 20
    QUESTION_SNIPPETS = [
        "food",  # 1
        "eating",
        "drink",
        "drinking",
        "dressing",  # 5
        "hygiene",
        "teeth",
        "bath/shower",
        "toilet/commode",
        "transfers",  # 10
        "mobility",
        "orientation: time",
        "orientation: space",
        "communication",
        "telephone",  # 15
        "hosuework/gardening",
        "shopping",
        "finances",
        "games/hobbies",
        "transport",  # 20
    ]
    TASK_FIELDS = strseq("q", 1, NQUESTIONS)

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Bristol Activities of Daily Living Scale")

    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="Total score (/ 48)",
            )
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [
            CtvInfo(content="BADLS total score {}/60 (lower is better)".format(
                self.total_score()))
        ]

    def score(self, q: str) -> int:
        text_value = getattr(self, q)
        return self.SCORING.get(text_value, 0)

    def total_score(self) -> int:
        return sum(self.score(q) for q in self.TASK_FIELDS)

    def is_complete(self) -> bool:
        return (self.field_contents_valid() and self.is_respondent_complete()
                and self.all_fields_not_none(self.TASK_FIELDS))

    def get_task_html(self, req: CamcopsRequest) -> str:
        q_a = ""
        for q in range(1, self.NQUESTIONS + 1):
            fieldname = "q" + str(q)
            qtext = self.wxstring(req, fieldname)  # happens to be the same
            avalue = getattr(self, "q" + str(q))
            atext = (self.wxstring(req, "q{}_{}".format(q, avalue))
                     if q is not None else None)
            score = self.score(fieldname)
            q_a += tr(qtext, answer(atext), score)
        return f"""
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {self.get_is_complete_tr(req)}
                    <tr>
                        <td>Total score (0–60, higher worse)</td>
                        <td>{answer(self.total_score())}</td>
                    </td>
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="30%">Question</th>
                    <th width="50%">Answer <sup>[1]</sup></th>
                    <th width="20%">Score</th>
                </tr>
                {q_a}
            </table>
            <div class="{CssClass.FOOTNOTES}">
                [1] Scored a = 0, b = 1, c = 2, d = 3, e = 0.
            </div>
            {DATA_COLLECTION_UNLESS_UPGRADED_DIV}
        """

    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        # The BADLS is ALWAYS carer-rated, so it's appropriate to put the
        # SNOMED-CT codes in.
        codes = [
            SnomedExpression(
                req.snomed(SnomedLookup.BADLS_PROCEDURE_ASSESSMENT))
        ]
        if self.is_complete():
            codes.append(
                SnomedExpression(
                    req.snomed(SnomedLookup.BADLS_SCALE),
                    {req.snomed(SnomedLookup.BADLS_SCORE): self.total_score()},
                ))
        return codes
Example #6
0
    18: "changes in appetite",  # decrease or increase
    19: "concentration difficulty",
    20: "tiredness or fatigue",
    21: "loss of interest in sex",
}
SCALE_BDI_I = "BDI-I"  # must match client
SCALE_BDI_IA = "BDI-IA"  # must match client
SCALE_BDI_II = "BDI-II"  # must match client
TOPICS_BY_SCALE = {
    SCALE_BDI_I: BDI_I_QUESTION_TOPICS,
    SCALE_BDI_IA: BDI_IA_QUESTION_TOPICS,
    SCALE_BDI_II: BDI_II_QUESTION_TOPICS,
}

NQUESTIONS = 21
TASK_SCORED_FIELDS = strseq("q", 1, NQUESTIONS)
MAX_SCORE = NQUESTIONS * 3
SUICIDALITY_QNUM = 9  # Q9 in all versions of the BDI (I, IA, II)
SUICIDALITY_FN = "q9"  # fieldname
CUSTOM_SOMATIC_KHANDAKER_BDI_II_QNUMS = [4, 15, 16, 18, 19, 20, 21]
CUSTOM_SOMATIC_KHANDAKER_BDI_II_FIELDS = Task.fieldnames_from_list(
    "q", CUSTOM_SOMATIC_KHANDAKER_BDI_II_QNUMS)

# =============================================================================
# BDI (crippled)
# =============================================================================


class BdiMetaclass(DeclarativeMeta):
    # noinspection PyInitNewSignature
    def __init__(
Example #7
0
class Factg(TaskHasPatientMixin, Task, metaclass=FactgMetaclass):
    """
    Server implementation of the Fact-G task.
    """
    __tablename__ = "factg"
    shortname = "FACT-G"
    longname = "Functional Assessment of Cancer Therapy — General"
    provides_trackers = True

    N_QUESTIONS_PHYSICAL = 7
    N_QUESTIONS_SOCIAL = 7
    N_QUESTIONS_EMOTIONAL = 6
    N_QUESTIONS_FUNCTIONAL = 7

    MAX_SCORE_PHYSICAL = 28
    MAX_SCORE_SOCIAL = 28
    MAX_SCORE_EMOTIONAL = 24
    MAX_SCORE_FUNCTIONAL = 28

    N_ALL = (N_QUESTIONS_PHYSICAL + N_QUESTIONS_SOCIAL +
             N_QUESTIONS_EMOTIONAL + N_QUESTIONS_FUNCTIONAL)

    MAX_SCORE_TOTAL = N_ALL * MAX_QSCORE

    PHYSICAL_PREFIX = "p_q"
    SOCIAL_PREFIX = "s_q"
    EMOTIONAL_PREFIX = "e_q"
    FUNCTIONAL_PREFIX = "f_q"

    QUESTIONS_PHYSICAL = strseq(PHYSICAL_PREFIX, 1, N_QUESTIONS_PHYSICAL)
    QUESTIONS_SOCIAL = strseq(SOCIAL_PREFIX, 1, N_QUESTIONS_SOCIAL)
    QUESTIONS_EMOTIONAL = strseq(EMOTIONAL_PREFIX, 1, N_QUESTIONS_EMOTIONAL)
    QUESTIONS_FUNCTIONAL = strseq(FUNCTIONAL_PREFIX, 1, N_QUESTIONS_FUNCTIONAL)

    GROUPS = [
        FactgGroupInfo("h1",
                       PHYSICAL_PREFIX,
                       QUESTIONS_PHYSICAL,
                       "physical_wellbeing",
                       "Physical wellbeing subscore",
                       MAX_SCORE_PHYSICAL,
                       reverse_score_all=True),
        FactgGroupInfo("h2", SOCIAL_PREFIX, QUESTIONS_SOCIAL,
                       "social_family_wellbeing",
                       "Social/family wellbeing subscore", MAX_SCORE_SOCIAL),
        FactgGroupInfo("h3",
                       EMOTIONAL_PREFIX,
                       QUESTIONS_EMOTIONAL,
                       "emotional_wellbeing",
                       "Emotional wellbeing subscore",
                       MAX_SCORE_EMOTIONAL,
                       reverse_score_all_but_q2=True),
        FactgGroupInfo("h4", FUNCTIONAL_PREFIX, QUESTIONS_FUNCTIONAL,
                       "functional_wellbeing", "Functional wellbeing subscore",
                       MAX_SCORE_FUNCTIONAL),
    ]

    OPTIONAL_Q = "s_q7"

    ignore_s_q7 = CamcopsColumn("ignore_s_q7",
                                Boolean,
                                permitted_value_checker=BIT_CHECKER)

    def is_complete(self) -> bool:
        questions_social = self.QUESTIONS_SOCIAL.copy()
        if self.ignore_s_q7:
            questions_social.remove(self.OPTIONAL_Q)

        all_qs = [
            self.QUESTIONS_PHYSICAL, questions_social,
            self.QUESTIONS_EMOTIONAL, self.QUESTIONS_FUNCTIONAL
        ]

        for qlist in all_qs:
            if not self.are_all_fields_complete(qlist):
                return False

        if not self.field_contents_valid():
            return False

        return True

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label="FACT-G total score (rating well-being)",
                axis_label="Total score".format(self.MAX_SCORE_TOTAL),
                axis_min=-0.5,
                axis_max=self.MAX_SCORE_TOTAL + 0.5,
                axis_ticks=[
                    TrackerAxisTick(108, "108"),
                    TrackerAxisTick(100, "100"),
                    TrackerAxisTick(80, "80"),
                    TrackerAxisTick(60, "60"),
                    TrackerAxisTick(40, "40"),
                    TrackerAxisTick(20, "20"),
                    TrackerAxisTick(0, "0"),
                ],
                horizontal_lines=[80, 60, 40, 20],
            )
        ]

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        elements = self.standard_task_summary_fields()
        for info in self.GROUPS:
            subscore = info.subscore(self)
            elements.append(
                SummaryElement(name=info.summary_fieldname,
                               coltype=Float(),
                               value=subscore,
                               comment="{} (out of {})".format(
                                   info.summary_description, info.max_score)))
        elements.append(
            SummaryElement(name="total_score",
                           coltype=Float(),
                           value=self.total_score(),
                           comment="Total score (out of {})".format(
                               self.MAX_SCORE_TOTAL)))
        return elements

    def subscores(self) -> List[float]:
        sscores = []
        for info in self.GROUPS:
            sscores.append(info.subscore(self))
        return sscores

    def total_score(self) -> float:
        return sum(self.subscores())

    def get_task_html(self, req: CamcopsRequest) -> str:
        answers = {
            None: None,
            0: "0 — " + self.wxstring(req, "a0"),
            1: "1 — " + self.wxstring(req, "a1"),
            2: "2 — " + self.wxstring(req, "a2"),
            3: "3 — " + self.wxstring(req, "a3"),
            4: "4 — " + self.wxstring(req, "a4"),
        }
        subscore_html = ""
        answer_html = ""

        for info in self.GROUPS:
            heading = self.wxstring(req, info.heading_xstring_name)
            subscore = info.subscore(self)
            subscore_html += tr(heading, (answer(round(subscore, DISPLAY_DP)) +
                                          " / {}".format(info.max_score)))
            answer_html += subheading_spanning_two_columns(heading)
            for q in info.fieldnames:
                if q == self.OPTIONAL_Q:
                    # insert additional row
                    answer_html += tr_qa(self.xstring(req, "prefer_no_answer"),
                                         self.ignore_s_q7)
                answer_val = getattr(self, q)
                answer_html += tr_qa(self.wxstring(req, q),
                                     get_from_dict(answers, answer_val))

        tscore = round(self.total_score(), DISPLAY_DP)

        h = """
            <div class="{CssClass.SUMMARY}">
                 <table class="{CssClass.SUMMARY}">
                     {tr_is_complete}
                     {total_score}
                     {subscore_html}
                 </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="50%">Question</th>
                    <th width="50%">Answer</th>
                </tr>
                {answer_html}
        """.format(
            CssClass=CssClass,
            tr_is_complete=self.get_is_complete_tr(req),
            total_score=tr(
                req.wappstring("total_score"),
                answer(tscore) + " / {}".format(self.MAX_SCORE_TOTAL)),
            subscore_html=subscore_html,
            answer_html=answer_html,
        )
        h += """
            </table>
        """
        return h
Example #8
0
class Cesd(TaskHasPatientMixin, Task, metaclass=CesdMetaclass):
    """
    Server implementation of the CESD task.
    """

    __tablename__ = "cesd"
    shortname = "CESD"
    provides_trackers = True
    extrastring_taskname = "cesd"
    N_QUESTIONS = 20
    N_ANSWERS = 4
    DEPRESSION_RISK_THRESHOLD = 16
    SCORED_FIELDS = strseq("q", 1, N_QUESTIONS)
    TASK_FIELDS = SCORED_FIELDS
    MIN_SCORE = 0
    MAX_SCORE = 3 * N_QUESTIONS
    REVERSE_SCORED_QUESTIONS = [4, 8, 12, 16]

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Center for Epidemiologic Studies Depression Scale")

    # noinspection PyMethodParameters
    @classproperty
    def minimum_client_version(cls) -> Version:
        return Version("2.2.8")

    def is_complete(self) -> bool:
        return (self.all_fields_not_none(self.TASK_FIELDS)
                and self.field_contents_valid())

    def total_score(self) -> int:
        # Need to store values as per original then flip here
        total = 0
        for qnum, fieldname in enumerate(self.SCORED_FIELDS, start=1):
            score = getattr(self, fieldname)
            if score is None:
                continue
            if qnum in self.REVERSE_SCORED_QUESTIONS:
                total += 3 - score
            else:
                total += score
        return total

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        line_step = 20
        threshold_line = self.DEPRESSION_RISK_THRESHOLD - 0.5
        # noinspection PyTypeChecker
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label="CESD total score",
                axis_label=f"Total score ({self.MIN_SCORE}-{self.MAX_SCORE})",
                axis_min=self.MIN_SCORE - 0.5,
                axis_max=self.MAX_SCORE + 0.5,
                axis_ticks=regular_tracker_axis_ticks_int(self.MIN_SCORE,
                                                          self.MAX_SCORE,
                                                          step=line_step),
                horizontal_lines=equally_spaced_int(
                    self.MIN_SCORE + line_step,
                    self.MAX_SCORE - line_step,
                    step=line_step,
                ) + [threshold_line],
                horizontal_labels=[
                    TrackerLabel(
                        threshold_line,
                        self.wxstring(req, "depression_or_risk_of"),
                    )
                ],
            )
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [CtvInfo(content=f"CESD total score {self.total_score()}")]

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(
                name="depression_risk",
                coltype=Boolean(),
                value=self.has_depression_risk(),
                comment="Has depression or at risk of depression",
            )
        ]

    def has_depression_risk(self) -> bool:
        return self.total_score() >= self.DEPRESSION_RISK_THRESHOLD

    def get_task_html(self, req: CamcopsRequest) -> str:
        score = self.total_score()
        answer_dict = {None: None}
        for option in range(self.N_ANSWERS):
            answer_dict[option] = (str(option) + " – " +
                                   self.wxstring(req, "a" + str(option)))
        q_a = ""
        for q in range(1, self.N_QUESTIONS):
            q_a += tr_qa(
                self.wxstring(req, "q" + str(q) + "_s"),
                get_from_dict(answer_dict, getattr(self, "q" + str(q))),
            )

        tr_total_score = (tr_qa(f"{req.sstring(SS.TOTAL_SCORE)} (0–60)",
                                score), )
        tr_depression_or_risk_of = (tr_qa(
            self.wxstring(req, "depression_or_risk_of") + "? <sup>[1]</sup>",
            get_yes_no(req, self.has_depression_risk()),
        ), )
        return f"""
Example #9
0
class Wsas(TaskHasPatientMixin, Task, metaclass=WsasMetaclass):
    """
    Server implementation of the WSAS task.
    """

    __tablename__ = "wsas"
    shortname = "WSAS"
    provides_trackers = True

    retired_etc = Column(
        "retired_etc",
        Boolean,
        comment="Retired or choose not to have job for reason unrelated "
        "to problem",
    )

    MIN_PER_Q = 0
    MAX_PER_Q = 8
    NQUESTIONS = 5
    QUESTION_FIELDS = strseq("q", 1, NQUESTIONS)
    Q2_TO_END = strseq("q", 2, NQUESTIONS)
    TASK_FIELDS = QUESTION_FIELDS + ["retired_etc"]
    MAX_IF_WORKING = MAX_PER_Q * NQUESTIONS
    MAX_IF_RETIRED = MAX_PER_Q * (NQUESTIONS - 1)

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Work and Social Adjustment Scale")

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label="WSAS total score (lower is better)",
                axis_label=f"Total score (out of "
                f"{self.MAX_IF_RETIRED}–{self.MAX_IF_WORKING})",
                axis_min=-0.5,
                axis_max=self.MAX_IF_WORKING + 0.5,
            )
        ]

    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_score()})",
            )
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [
            CtvInfo(content=f"WSAS total score "
                    f"{self.total_score()}/{self.max_score()}")
        ]

    def total_score(self) -> int:
        return self.sum_fields(
            self.Q2_TO_END if self.retired_etc else self.QUESTION_FIELDS)

    def max_score(self) -> int:
        return self.MAX_IF_RETIRED if self.retired_etc else self.MAX_IF_WORKING

    def is_complete(self) -> bool:
        return (self.all_fields_not_none(
            self.Q2_TO_END if self.retired_etc else self.QUESTION_FIELDS)
                and self.field_contents_valid())

    def get_task_html(self, req: CamcopsRequest) -> str:
        option_dict = {None: None}
        for a in range(self.MIN_PER_Q, self.MAX_PER_Q + 1):
            option_dict[a] = req.wappstring(AS.WSAS_A_PREFIX + str(a))
        q_a = ""
        for q in range(1, self.NQUESTIONS + 1):
            a = getattr(self, "q" + str(q))
            fa = get_from_dict(option_dict, a) if a is not None else None
            q_a += tr(self.wxstring(req, "q" + str(q)), answer(fa))
        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())} / 40</td>
                    </td>
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="75%">Question</th>
                    <th width="25%">Answer</th>
                </tr>
                {tr_qa(self.wxstring(req, "q_retired_etc"),
                       get_true_false(req, self.retired_etc))}
            </table>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="75%">Question</th>
                    <th width="25%">Answer (0–8)</th>
                </tr>
                {q_a}
            </table>
            {DATA_COLLECTION_UNLESS_UPGRADED_DIV}
        """

    # noinspection PyUnresolvedReferences
    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        codes = [
            SnomedExpression(req.snomed(
                SnomedLookup.WSAS_PROCEDURE_ASSESSMENT))
        ]
        if self.is_complete():
            d = {
                req.snomed(SnomedLookup.WSAS_SCORE): self.total_score(),
                req.snomed(SnomedLookup.WSAS_HOME_MANAGEMENT_SCORE): self.q2,
                req.snomed(SnomedLookup.WSAS_SOCIAL_LEISURE_SCORE): self.q3,
                req.snomed(SnomedLookup.WSAS_PRIVATE_LEISURE_SCORE): self.q4,
                req.snomed(SnomedLookup.WSAS_RELATIONSHIPS_SCORE): self.q5,
            }
            if not self.retired_etc:
                d[req.snomed(SnomedLookup.WSAS_WORK_SCORE)] = self.q1
            codes.append(
                SnomedExpression(req.snomed(SnomedLookup.WSAS_SCALE), d))
        return codes
Example #10
0
class Icd10SpecPD(TaskHasClinicianMixin,
                  TaskHasPatientMixin,
                  Task,
                  metaclass=Icd10SpecPDMetaclass):
    """
    Server implementation of the ICD10-PD task.
    """
    __tablename__ = "icd10specpd"
    shortname = "ICD10-PD"
    longname = "ICD-10 criteria for specific personality disorders (F60)"

    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)

    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

    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.are_all_fields_complete(self.GENERAL_1_FIELDS)
                    and self.count_booleans(self.GENERAL_1_FIELDS) <= 1))

    def is_complete_general(self) -> bool:
        return (self.are_all_fields_complete(self.GENERAL_FIELDS)
                and self.are_all_fields_complete(self.GENERAL_1_FIELDS))

    def is_complete_paranoid(self) -> bool:
        return self.are_all_fields_complete(self.PARANOID_FIELDS)

    def is_complete_schizoid(self) -> bool:
        return self.are_all_fields_complete(self.SCHIZOID_FIELDS)

    def is_complete_dissocial(self) -> bool:
        return self.are_all_fields_complete(self.DISSOCIAL_FIELDS)

    def is_complete_eu(self) -> bool:
        return self.are_all_fields_complete(self.EU_FIELDS)

    def is_complete_histrionic(self) -> bool:
        return self.are_all_fields_complete(self.HISTRIONIC_FIELDS)

    def is_complete_anankastic(self) -> bool:
        return self.are_all_fields_complete(self.ANANKASTIC_FIELDS)

    def is_complete_anxious(self) -> bool:
        return self.are_all_fields_complete(self.ANXIOUS_FIELDS)

    def is_complete_dependent(self) -> bool:
        return self.are_all_fields_complete(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_true(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

    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 """
            <tr class="{CssClass.HEADING}"><td colspan="2">{s}</td></tr>
        """.format(CssClass=CssClass, s=self.wxstring(req, wstringname))

    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 """
            <tr class="{CssClass.SUBHEADING}"><td colspan="2">{s}</td></tr>
        """.format(CssClass=CssClass, s=self.wxstring(req, wstringname))

    def pd_general_criteria_bits(self, req: CamcopsRequest) -> str:
        return """
            <tr><td>{}</td><td><i><b>{}</b></i></td></tr>
        """.format(self.wxstring(req, "general_criteria_must_be_met"),
                   get_yes_no_unknown(req, self.has_pd()))

    def pd_b_text(self, req: CamcopsRequest, wstringname: str) -> str:
        return """
            <tr><td>{s}</td><td class="{CssClass.SUBHEADING}"></td></tr>
        """.format(
            CssClass=CssClass,
            s=self.wxstring(req, wstringname),
        )

    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

    def get_task_html(self, req: CamcopsRequest) -> str:
        h = self.get_standard_clinician_comments_block(req, self.comments)
        h += """
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
        """.format(CssClass=CssClass, )
        h += self.get_is_complete_tr(req)
        h += tr_qa(
            req.wappstring("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 += """
                </table>
            </div>
            <div>
                <p><i>Vignette:</i></p>
                <p>{vignette}</p>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="80%">Question</th>
                    <th width="20%">Answer</th>
                </tr>
        """.format(
            CssClass=CssClass,
            vignette=answer(ws.webify(self.vignette),
                            default_for_blank_strings=True),
        )

        # 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
Example #11
0
class Audit(TaskHasPatientMixin, Task, metaclass=AuditMetaclass):
    """
    Server implementation of the AUDIT task.
    """

    __tablename__ = "audit"
    shortname = "AUDIT"
    provides_trackers = True

    prohibits_commercial = True

    NQUESTIONS = 10
    TASK_FIELDS = strseq("q", 1, NQUESTIONS)

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("WHO Alcohol Use Disorders Identification Test")

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label="AUDIT total score",
                axis_label="Total score (out of 40)",
                axis_min=-0.5,
                axis_max=40.5,
                horizontal_lines=[7.5],
            )
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [CtvInfo(content=f"AUDIT total score {self.total_score()}/40")]

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(
                name="total",
                coltype=Integer(),
                value=self.total_score(),
                comment="Total score (/40)",
            )
        ]

    # noinspection PyUnresolvedReferences
    def is_complete(self) -> bool:
        if not self.field_contents_valid():
            return False
        if self.q1 is None or self.q9 is None or self.q10 is None:
            return False
        if self.q1 == 0:
            # Special limited-information completeness
            return True
        if (self.q2 is not None and self.q3 is not None
                and (self.q2 + self.q3 == 0)):
            # Special limited-information completeness
            return True
        # Otherwise, any null values cause problems
        return self.all_fields_not_none(self.TASK_FIELDS)

    def total_score(self) -> int:
        return self.sum_fields(self.TASK_FIELDS)

    # noinspection PyUnresolvedReferences
    def get_task_html(self, req: CamcopsRequest) -> str:
        score = self.total_score()
        exceeds_cutoff = score >= 8
        q1_dict = {None: None}
        q2_dict = {None: None}
        q3_to_8_dict = {None: None}
        q9_to_10_dict = {None: None}
        for option in range(0, 5):
            q1_dict[option] = (str(option) + " – " +
                               self.wxstring(req, "q1_option" + str(option)))
            q2_dict[option] = (str(option) + " – " +
                               self.wxstring(req, "q2_option" + str(option)))
            q3_to_8_dict[option] = (
                str(option) + " – " +
                self.wxstring(req, "q3to8_option" + str(option)))
            if option != 1 and option != 3:
                q9_to_10_dict[option] = (
                    str(option) + " – " +
                    self.wxstring(req, "q9to10_option" + str(option)))

        q_a = tr_qa(self.wxstring(req, "q1_s"),
                    get_from_dict(q1_dict, self.q1))
        q_a += tr_qa(self.wxstring(req, "q2_s"),
                     get_from_dict(q2_dict, self.q2))
        for q in range(3, 8 + 1):
            q_a += tr_qa(
                self.wxstring(req, "q" + str(q) + "_s"),
                get_from_dict(q3_to_8_dict, getattr(self, "q" + str(q))),
            )
        q_a += tr_qa(self.wxstring(req, "q9_s"),
                     get_from_dict(q9_to_10_dict, self.q9))
        q_a += tr_qa(self.wxstring(req, "q10_s"),
                     get_from_dict(q9_to_10_dict, self.q10))

        return f"""
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {self.get_is_complete_tr(req)}
                    {tr(req.wsstring(SS.TOTAL_SCORE),
                        answer(score) + " / 40")}
                    {tr_qa(self.wxstring(req, "exceeds_standard_cutoff"),
                           get_yes_no(req, exceeds_cutoff))}
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="50%">Question</th>
                    <th width="50%">Answer</th>
                </tr>
                {q_a}
            </table>
            <div class="{CssClass.COPYRIGHT}">
                AUDIT: Copyright © World Health Organization.
                Reproduced here under the permissions granted for
                NON-COMMERCIAL use only. You must obtain permission from the
                copyright holder for any other use.
            </div>
        """

    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        codes = [
            SnomedExpression(
                req.snomed(SnomedLookup.AUDIT_PROCEDURE_ASSESSMENT))
        ]
        if self.is_complete():
            codes.append(
                SnomedExpression(
                    req.snomed(SnomedLookup.AUDIT_SCALE),
                    {req.snomed(SnomedLookup.AUDIT_SCORE): self.total_score()},
                ))
        return codes
Example #12
0
class AuditC(TaskHasPatientMixin, Task, metaclass=AuditMetaclass):
    __tablename__ = "audit_c"
    shortname = "AUDIT-C"
    extrastring_taskname = "audit"  # shares strings with AUDIT
    info_filename_stem = extrastring_taskname

    prohibits_commercial = True

    NQUESTIONS = 3
    TASK_FIELDS = strseq("q", 1, NQUESTIONS)

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("AUDIT Alcohol Consumption Questions")

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label="AUDIT-C total score",
                axis_label="Total score (out of 12)",
                axis_min=-0.5,
                axis_max=12.5,
            )
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [
            CtvInfo(content=f"AUDIT-C total score {self.total_score()}/12")
        ]

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(
                name="total",
                coltype=Integer(),
                value=self.total_score(),
                comment="Total score (/12)",
            )
        ]

    def is_complete(self) -> bool:
        return self.all_fields_not_none(self.TASK_FIELDS)

    def total_score(self) -> int:
        return self.sum_fields(self.TASK_FIELDS)

    def get_task_html(self, req: CamcopsRequest) -> str:
        score = self.total_score()
        q1_dict = {None: None}
        q2_dict = {None: None}
        q3_dict = {None: None}
        for option in range(0, 5):
            q1_dict[option] = (str(option) + " – " +
                               self.wxstring(req, "q1_option" + str(option)))
            if option == 0:  # special!
                q2_dict[option] = (str(option) + " – " +
                                   self.wxstring(req, "c_q2_option0"))
            else:
                q2_dict[option] = (
                    str(option) + " – " +
                    self.wxstring(req, "q2_option" + str(option)))
            q3_dict[option] = (
                str(option) + " – " +
                self.wxstring(req, "q3to8_option" + str(option)))

        # noinspection PyUnresolvedReferences
        return f"""
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {self.get_is_complete_tr(req)}
                    {tr(req.sstring(SS.TOTAL_SCORE),
                        answer(score) + " / 12")}
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="50%">Question</th>
                    <th width="50%">Answer</th>
                </tr>
                {tr_qa(self.wxstring(req, "c_q1_question"),
                       get_from_dict(q1_dict, self.q1))}
                {tr_qa(self.wxstring(req, "c_q2_question"),
                       get_from_dict(q2_dict, self.q2))}
                {tr_qa(self.wxstring(req, "c_q3_question"),
                       get_from_dict(q3_dict, self.q3))}
            </table>
            <div class="{CssClass.COPYRIGHT}">
                AUDIT: Copyright © World Health Organization.
                Reproduced here under the permissions granted for
                NON-COMMERCIAL use only. You must obtain permission from the
                copyright holder for any other use.

                AUDIT-C: presumed to have the same restrictions.
            </div>
        """

    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        codes = [
            SnomedExpression(
                req.snomed(SnomedLookup.AUDITC_PROCEDURE_ASSESSMENT))
        ]
        if self.is_complete():
            codes.append(
                SnomedExpression(
                    req.snomed(SnomedLookup.AUDITC_SCALE),
                    {
                        req.snomed(SnomedLookup.AUDITC_SCORE):
                        self.total_score()
                    },
                ))
        return codes
Example #13
0
class Cesd(TaskHasPatientMixin, Task, metaclass=CesdMetaclass):
    """
    Server implementation of the CESD task.
    """
    __tablename__ = 'cesd'
    shortname = 'CESD'
    longname = 'Center for Epidemiologic Studies Depression Scale'
    provides_trackers = True
    extrastring_taskname = "cesd"
    N_QUESTIONS = 20
    N_ANSWERS = 4
    DEPRESSION_RISK_THRESHOLD = 16
    SCORED_FIELDS = strseq("q", 1, N_QUESTIONS)
    TASK_FIELDS = SCORED_FIELDS
    MIN_SCORE = 0
    MAX_SCORE = 3 * N_QUESTIONS
    REVERSE_SCORED_QUESTIONS = [4, 8, 12, 16]

    # noinspection PyMethodParameters
    @classproperty
    def minimum_client_version(cls) -> Version:
        return Version("2.2.8")

    def is_complete(self) -> bool:
        return (self.are_all_fields_complete(self.TASK_FIELDS)
                and self.field_contents_valid())

    def total_score(self) -> int:
        # Need to store values as per original then flip here
        total = 0
        for qnum, fieldname in enumerate(self.SCORED_FIELDS, start=1):
            score = getattr(self, fieldname)
            if score is None:
                continue
            if qnum in self.REVERSE_SCORED_QUESTIONS:
                total += 3 - score
            else:
                total += score
        return total

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        line_step = 20
        threshold_line = self.DEPRESSION_RISK_THRESHOLD - 0.5
        # noinspection PyTypeChecker
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label="CESD total score",
                axis_label="Total score ({}-{})".format(
                    self.MIN_SCORE, self.MAX_SCORE),
                axis_min=self.MIN_SCORE - 0.5,
                axis_max=self.MAX_SCORE + 0.5,
                axis_ticks=regular_tracker_axis_ticks_int(self.MIN_SCORE,
                                                          self.MAX_SCORE,
                                                          step=line_step),
                horizontal_lines=equally_spaced_int(self.MIN_SCORE + line_step,
                                                    self.MAX_SCORE - line_step,
                                                    step=line_step) +
                [threshold_line],
                horizontal_labels=[
                    TrackerLabel(threshold_line,
                                 self.wxstring(req, "depression_or_risk_of")),
                ])
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [
            CtvInfo(content="CESD total score {}".format(self.total_score()))
        ]

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(name="depression_risk",
                           coltype=Boolean(),
                           value=self.has_depression_risk(),
                           comment="Has depression or at risk of depression"),
        ]

    def has_depression_risk(self) -> bool:
        return self.total_score() >= self.DEPRESSION_RISK_THRESHOLD

    def get_task_html(self, req: CamcopsRequest) -> str:
        score = self.total_score()
        answer_dict = {None: None}
        for option in range(self.N_ANSWERS):
            answer_dict[option] = str(option) + " – " + \
                self.wxstring(req, "a" + str(option))
        q_a = ""
        for q in range(1, self.N_QUESTIONS):
            q_a += tr_qa(
                self.wxstring(req, "q" + str(q) + "_s"),
                get_from_dict(answer_dict, getattr(self, "q" + str(q))))

        h = """
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {tr_is_complete}
                    {total_score}
                    {depression_or_risk_of}
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="70%">Question</th>
                    <th width="30%">Answer</th>
                </tr>
                {q_a}
            </table>
            <div class="{CssClass.FOOTNOTES}">
            [1] Presence of depression (or depression risk) is indicated by a
                score &ge; 16
            </div>
        """.format(
            CssClass=CssClass,
            tr_is_complete=self.get_is_complete_tr(req),
            total_score=tr_qa(
                "{} (0–60)".format(req.wappstring("total_score")), score),
            depression_or_risk_of=tr_qa(
                self.wxstring(req, "depression_or_risk_of") +
                "? <sup>[1]</sup>", get_yes_no(req,
                                               self.has_depression_risk())),
            q_a=q_a,
        )
        return h
Example #14
0
class Cesdr(TaskHasPatientMixin, Task, metaclass=CesdrMetaclass):
    """
    Server implementation of the CESD task.
    """
    __tablename__ = 'cesdr'
    shortname = 'CESD-R'
    longname = 'Center for Epidemiologic Studies Depression Scale (Revised)'
    provides_trackers = True
    extrastring_taskname = "cesdr"

    CAT_NONCLINICAL = 0
    CAT_SUB = 1
    CAT_POSS_MAJOR = 2
    CAT_PROB_MAJOR = 3
    CAT_MAJOR = 4

    DEPRESSION_RISK_THRESHOLD = 16

    FREQ_NOT_AT_ALL = 0
    FREQ_1_2_DAYS_LAST_WEEK = 1
    FREQ_3_4_DAYS_LAST_WEEK = 2
    FREQ_5_7_DAYS_LAST_WEEK = 3
    FREQ_DAILY_2_WEEKS = 4

    N_QUESTIONS = 20
    N_ANSWERS = 5

    POSS_MAJOR_THRESH = 2
    PROB_MAJOR_THRESH = 3
    MAJOR_THRESH = 4

    SCORED_FIELDS = strseq("q", 1, N_QUESTIONS)
    TASK_FIELDS = SCORED_FIELDS
    MIN_SCORE = 0
    MAX_SCORE = 3 * N_QUESTIONS

    # noinspection PyMethodParameters
    @classproperty
    def minimum_client_version(cls) -> Version:
        return Version("2.2.8")

    def is_complete(self) -> bool:
        return (self.are_all_fields_complete(self.TASK_FIELDS)
                and self.field_contents_valid())

    def total_score(self) -> int:
        return (
            self.sum_fields(self.SCORED_FIELDS) -
            self.count_where(self.SCORED_FIELDS, [self.FREQ_DAILY_2_WEEKS]))

    def get_depression_category(self) -> int:

        if not self.has_depression_risk():
            return self.CAT_SUB

        q_group_anhedonia = [8, 10]
        q_group_dysphoria = [2, 4, 6]
        other_q_groups = {
            'appetite': [1, 18],
            'sleep': [5, 11, 19],
            'thinking': [3, 20],
            'guilt': [9, 17],
            'tired': [7, 16],
            'movement': [12, 13],
            'suicidal': [14, 15]
        }

        # Dysphoria or anhedonia must be present at frequency FREQ_DAILY_2_WEEKS
        anhedonia_criterion = (
            self.fulfils_group_criteria(q_group_anhedonia, True)
            or self.fulfils_group_criteria(q_group_dysphoria, True))
        if anhedonia_criterion:
            category_count_high_freq = 0
            category_count_lower_freq = 0
            for qgroup in other_q_groups.values():
                if self.fulfils_group_criteria(qgroup, True):
                    # Category contains an answer == FREQ_DAILY_2_WEEKS
                    category_count_high_freq += 1
                if self.fulfils_group_criteria(qgroup, False):
                    # Category contains an answer == FREQ_DAILY_2_WEEKS or
                    # FREQ_5_7_DAYS_LAST_WEEK
                    category_count_lower_freq += 1

            if category_count_high_freq >= self.MAJOR_THRESH:
                # Anhedonia or dysphoria (at FREQ_DAILY_2_WEEKS)
                # plus 4 other symptom groups at FREQ_DAILY_2_WEEKS
                return self.CAT_MAJOR
            if category_count_lower_freq >= self.PROB_MAJOR_THRESH:
                # Anhedonia or dysphoria (at FREQ_DAILY_2_WEEKS)
                # plus 3 other symptom groups at FREQ_DAILY_2_WEEKS or
                # FREQ_5_7_DAYS_LAST_WEEK
                return self.CAT_PROB_MAJOR
            if category_count_lower_freq >= self.POSS_MAJOR_THRESH:
                # Anhedonia or dysphoria (at FREQ_DAILY_2_WEEKS)
                # plus 2 other symptom groups at FREQ_DAILY_2_WEEKS or
                # FREQ_5_7_DAYS_LAST_WEEK
                return self.CAT_POSS_MAJOR

        if self.has_depression_risk():
            # Total CESD-style score >= 16 but doesn't meet other criteria.
            return self.CAT_SUB

        return self.CAT_NONCLINICAL

    def fulfils_group_criteria(self, qnums: List[int],
                               nearly_every_day_2w: bool) -> bool:
        qstrings = ["q" + str(qnum) for qnum in qnums]
        if nearly_every_day_2w:
            possible_values = [self.FREQ_DAILY_2_WEEKS]
        else:
            possible_values = [
                self.FREQ_5_7_DAYS_LAST_WEEK, self.FREQ_DAILY_2_WEEKS
            ]
        count = self.count_where(qstrings, possible_values)
        return count > 0

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        line_step = 20
        threshold_line = self.DEPRESSION_RISK_THRESHOLD - 0.5
        # noinspection PyTypeChecker
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label="CESD-R total score",
                axis_label="Total score ({}-{})".format(
                    self.MIN_SCORE, self.MAX_SCORE),
                axis_min=self.MIN_SCORE - 0.5,
                axis_max=self.MAX_SCORE + 0.5,
                axis_ticks=regular_tracker_axis_ticks_int(self.MIN_SCORE,
                                                          self.MAX_SCORE,
                                                          step=line_step),
                horizontal_lines=equally_spaced_int(self.MIN_SCORE + line_step,
                                                    self.MAX_SCORE - line_step,
                                                    step=line_step) +
                [threshold_line],
                horizontal_labels=[
                    TrackerLabel(threshold_line,
                                 self.wxstring(req, "depression_or_risk_of")),
                ])
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [
            CtvInfo(content="CESD-R total score {}".format(self.total_score()))
        ]

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(name="depression_risk",
                           coltype=Boolean(),
                           value=self.has_depression_risk(),
                           comment="Has depression or at risk of depression"),
        ]

    def has_depression_risk(self) -> bool:
        return self.total_score() >= self.DEPRESSION_RISK_THRESHOLD

    def get_task_html(self, req: CamcopsRequest) -> str:
        score = self.total_score()
        answer_dict = {None: None}
        for option in range(self.N_ANSWERS):
            answer_dict[option] = str(option) + " – " + \
                self.wxstring(req, "a" + str(option))
        q_a = ""
        for q in range(1, self.N_QUESTIONS):
            q_a += tr_qa(
                self.wxstring(req, "q" + str(q) + "_s"),
                get_from_dict(answer_dict, getattr(self, "q" + str(q))))

        h = """
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {tr_is_complete}
                    {total_score}
                    {depression_or_risk_of}
                    {provisional_diagnosis}
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="70%">Question</th>
                    <th width="30%">Answer</th>
                </tr>
                {q_a}
            </table>
            <div class="{CssClass.FOOTNOTES}">
            [1] Presence of depression (or depression risk) is indicated by a
                score &ge; 16
            [2] Diagnostic criteria described at
                <a href="https://cesd-r.com/cesdr/">https://cesd-r.com/cesdr/</a>
            </div>
        """.format(  # noqa
            CssClass=CssClass,
            tr_is_complete=self.get_is_complete_tr(req),
            total_score=tr_qa(
                "{} (0–60)".format(req.wappstring("total_score")), score),
            depression_or_risk_of=tr_qa(
                self.wxstring(req, "depression_or_risk_of") +
                "? <sup>[1]</sup>", get_yes_no(req,
                                               self.has_depression_risk())),
            provisional_diagnosis=tr(
                'Provisional diagnosis <sup>[2]</sup>',
                self.wxstring(
                    req, "category_" + str(self.get_depression_category()))),
            q_a=q_a,
        )
        return h
Example #15
0
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

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Yale–Brown Obsessive Compulsive Scale")

    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,
            ),
        ]

    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})",
            ),
        ]

    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)

    def is_complete(self) -> bool:
        return self.field_contents_valid() and self.all_fields_not_none(
            self.SCORED_QUESTIONS)

    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"""
Example #16
0
class Ciwa(TaskHasPatientMixin,
           TaskHasClinicianMixin,
           Task,
           metaclass=CiwaMetaclass):
    """
    Server implementation of the CIWA-Ar task.
    """

    __tablename__ = "ciwa"
    shortname = "CIWA-Ar"
    provides_trackers = True

    NSCOREDQUESTIONS = 10
    SCORED_QUESTIONS = strseq("q", 1, NSCOREDQUESTIONS)

    q10 = CamcopsColumn(
        "q10",
        Integer,
        permitted_value_checker=PermittedValueChecker(minimum=0, maximum=4),
        comment="Q10, orientation/clouding of sensorium (0-4, higher worse)",
    )
    t = Column("t", Float, comment="Temperature (degrees C)")
    hr = CamcopsColumn(
        "hr",
        Integer,
        permitted_value_checker=MIN_ZERO_CHECKER,
        comment="Heart rate (beats/minute)",
    )
    sbp = CamcopsColumn(
        "sbp",
        Integer,
        permitted_value_checker=MIN_ZERO_CHECKER,
        comment="Systolic blood pressure (mmHg)",
    )
    dbp = CamcopsColumn(
        "dbp",
        Integer,
        permitted_value_checker=MIN_ZERO_CHECKER,
        comment="Diastolic blood pressure (mmHg)",
    )
    rr = CamcopsColumn(
        "rr",
        Integer,
        permitted_value_checker=MIN_ZERO_CHECKER,
        comment="Respiratory rate (breaths/minute)",
    )

    MAX_SCORE = 67

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Clinical Institute Withdrawal Assessment for Alcohol "
                 "Scale, Revised")

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label="CIWA total score",
                axis_label=f"Total score (out of {self.MAX_SCORE})",
                axis_min=-0.5,
                axis_max=self.MAX_SCORE + 0.5,
                horizontal_lines=[14.5, 7.5],
                horizontal_labels=[
                    TrackerLabel(17, req.sstring(SS.SEVERE)),
                    TrackerLabel(11, req.sstring(SS.MODERATE)),
                    TrackerLabel(3.75, req.sstring(SS.MILD)),
                ],
            )
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [
            CtvInfo(content=f"CIWA total score: "
                    f"{self.total_score()}/{self.MAX_SCORE}")
        ]

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(
                name="total",
                coltype=Integer(),
                value=self.total_score(),
                comment=f"Total score (/{self.MAX_SCORE})",
            ),
            SummaryElement(
                name="severity",
                coltype=SummaryCategoryColType,
                value=self.severity(req),
                comment="Likely severity",
            ),
        ]

    def is_complete(self) -> bool:
        return (self.all_fields_not_none(self.SCORED_QUESTIONS)
                and self.field_contents_valid())

    def total_score(self) -> int:
        return self.sum_fields(self.SCORED_QUESTIONS)

    def severity(self, req: CamcopsRequest) -> str:
        score = self.total_score()
        if score >= 15:
            severity = self.wxstring(req, "category_severe")
        elif score >= 8:
            severity = self.wxstring(req, "category_moderate")
        else:
            severity = self.wxstring(req, "category_mild")
        return severity

    def get_task_html(self, req: CamcopsRequest) -> str:
        score = self.total_score()
        severity = self.severity(req)
        answer_dicts_dict = {}
        for q in self.SCORED_QUESTIONS:
            d = {None: None}
            for option in range(0, 8):
                if option > 4 and q == "q10":
                    continue
                d[option] = self.wxstring(req, q + "_option" + str(option))
            answer_dicts_dict[q] = d
        q_a = ""
        for q in range(1, Ciwa.NSCOREDQUESTIONS + 1):
            q_a += tr_qa(
                self.wxstring(req, "q" + str(q) + "_s"),
                get_from_dict(
                    answer_dicts_dict["q" + str(q)],
                    getattr(self, "q" + str(q)),
                ),
            )
        tr_total_score = tr(req.sstring(SS.TOTAL_SCORE),
                            answer(score) + f" / {self.MAX_SCORE}")
        tr_severity = tr_qa(
            self.wxstring(req, "severity") + " <sup>[1]</sup>", severity)
        return f"""
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {self.get_is_complete_tr(req)}
                    {tr_total_score}
                    {tr_severity}
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="35%">Question</th>
                    <th width="65%">Answer</th>
                </tr>
                {q_a}
                {subheading_spanning_two_columns(
                    self.wxstring(req, "vitals_title"))}
                {tr_qa(self.wxstring(req, "t"), self.t)}
                {tr_qa(self.wxstring(req, "hr"), self.hr)}
                {tr(self.wxstring(req, "bp"),
                    answer(self.sbp) + " / " + answer(self.dbp))}
                {tr_qa(self.wxstring(req, "rr"), self.rr)}
            </table>
            <div class="{CssClass.FOOTNOTES}">
                [1] Total score ≥15 severe, ≥8 moderate, otherwise
                    mild/minimal.
            </div>
        """

    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        codes = [
            SnomedExpression(
                req.snomed(SnomedLookup.CIWA_AR_PROCEDURE_ASSESSMENT))
        ]
        if self.is_complete():
            codes.append(
                SnomedExpression(
                    req.snomed(SnomedLookup.CIWA_AR_SCALE),
                    {
                        req.snomed(SnomedLookup.CIWA_AR_SCORE):
                        self.total_score()
                    },
                ))
        return codes
Example #17
0
class Pswq(TaskHasPatientMixin, Task, metaclass=PswqMetaclass):
    """
    Server implementation of the PSWQ task.
    """

    __tablename__ = "pswq"
    shortname = "PSWQ"
    provides_trackers = True

    MIN_PER_Q = 1
    MAX_PER_Q = 5
    NQUESTIONS = 16
    REVERSE_SCORE = [1, 3, 8, 10, 11]
    TASK_FIELDS = strseq("q", 1, NQUESTIONS)
    MIN_TOTAL = MIN_PER_Q * NQUESTIONS
    MAX_TOTAL = MAX_PER_Q * NQUESTIONS

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Penn State Worry Questionnaire")

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label="PSWQ total score (lower is better)",
                axis_label=f"Total score ({self.MIN_TOTAL}–{self.MAX_TOTAL})",
                axis_min=self.MIN_TOTAL - 0.5,
                axis_max=self.MAX_TOTAL + 0.5,
            )
        ]

    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="Total score (16-80)",
            )
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [
            CtvInfo(content=f"PSWQ total score {self.total_score()} "
                    f"(range {self.MIN_TOTAL}–{self.MAX_TOTAL})")
        ]

    def score(self, q: int) -> Optional[int]:
        value = getattr(self, "q" + str(q))
        if value is None:
            return None
        if q in self.REVERSE_SCORE:
            return self.MAX_PER_Q + 1 - value
        else:
            return value

    def total_score(self) -> int:
        values = [self.score(q) for q in range(1, self.NQUESTIONS + 1)]
        return sum(v for v in values if v is not None)

    def is_complete(self) -> bool:
        return (self.all_fields_not_none(self.TASK_FIELDS)
                and self.field_contents_valid())

    def get_task_html(self, req: CamcopsRequest) -> str:
        h = f"""
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {self.get_is_complete_tr(req)}
                    <tr>
                        <td>Total score (16–80)</td>
                        <td>{answer(self.total_score())}</td>
                    </td>
                </table>
            </div>
            <div class="{CssClass.EXPLANATION}">
                Anchor points are 1 = {self.wxstring(req, "anchor1")},
                5 = {self.wxstring(req, "anchor5")}.
                Questions {", ".join(str(x) for x in self.REVERSE_SCORE)}
                are reverse-scored.
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="70%">Question</th>
                    <th width="15%">Answer (1–5)</th>
                    <th width="15%">Score (1–5)</th>
                </tr>
        """
        for q in range(1, self.NQUESTIONS + 1):
            a = getattr(self, "q" + str(q))
            score = self.score(q)
            h += tr(self.wxstring(req, "q" + str(q)), answer(a), score)
        h += """
            </table>
        """
        return h

    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        codes = [
            SnomedExpression(req.snomed(
                SnomedLookup.PSWQ_PROCEDURE_ASSESSMENT))
        ]
        if self.is_complete():
            codes.append(
                SnomedExpression(
                    req.snomed(SnomedLookup.PSWQ_SCALE),
                    {req.snomed(SnomedLookup.PSWQ_SCORE): self.total_score()},
                ))
        return codes
Example #18
0
class Rand36(TaskHasPatientMixin, Task, metaclass=Rand36Metaclass):
    """
    Server implementation of the RAND-36 task.
    """
    __tablename__ = "rand36"
    shortname = "RAND-36"
    longname = "RAND 36-Item Short Form Health Survey 1.0"
    provides_trackers = True

    NQUESTIONS = 36

    q1 = CamcopsColumn("q1",
                       Integer,
                       permitted_value_checker=ONE_TO_FIVE_CHECKER,
                       comment="Q1 (general health) (1 excellent - 5 poor)")
    q2 = CamcopsColumn(
        "q2",
        Integer,
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Q2 (health cf. 1y ago) (1 much better - 5 much worse)")

    q20 = CamcopsColumn(
        "q20",
        Integer,
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Q20 (past 4 weeks, to what extent physical health/"
        "emotional problems interfered with social activity) "
        "(1 not at all - 5 extremely)")
    q21 = CamcopsColumn(
        "q21",
        Integer,
        permitted_value_checker=ONE_TO_SIX_CHECKER,
        comment="Q21 (past 4 weeks, how much pain (1 none - 6 very severe)")
    q22 = CamcopsColumn(
        "q22",
        Integer,
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Q22 (past 4 weeks, pain interfered with normal activity "
        "(1 not at all - 5 extremely)")

    q32 = CamcopsColumn(
        "q32",
        Integer,
        permitted_value_checker=ONE_TO_FIVE_CHECKER,
        comment="Q32 (past 4 weeks, how much of the time has physical "
        "health/emotional problems interfered with social activities "
        "(1 all of the time - 5 none of the time)")
    # ... note Q32 extremely similar to Q20.

    TASK_FIELDS = strseq("q", 1, NQUESTIONS)

    def is_complete(self) -> bool:
        return (self.are_all_fields_complete(self.TASK_FIELDS)
                and self.field_contents_valid())

    @classmethod
    def tracker_element(cls, value: float, plot_label: str) -> TrackerInfo:
        return TrackerInfo(value=value,
                           plot_label="RAND-36: " + plot_label,
                           axis_label="Scale score (out of 100)",
                           axis_min=-0.5,
                           axis_max=100.5)

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        return [
            self.tracker_element(self.score_overall(),
                                 self.wxstring(req, "score_overall")),
            self.tracker_element(
                self.score_physical_functioning(),
                self.wxstring(req, "score_physical_functioning")),
            self.tracker_element(
                self.score_role_limitations_physical(),
                self.wxstring(req, "score_role_limitations_physical")),
            self.tracker_element(
                self.score_role_limitations_emotional(),
                self.wxstring(req, "score_role_limitations_emotional")),
            self.tracker_element(self.score_energy(),
                                 self.wxstring(req, "score_energy")),
            self.tracker_element(
                self.score_emotional_wellbeing(),
                self.wxstring(req, "score_emotional_wellbeing")),
            self.tracker_element(
                self.score_social_functioning(),
                self.wxstring(req, "score_social_functioning")),
            self.tracker_element(self.score_pain(),
                                 self.wxstring(req, "score_pain")),
            self.tracker_element(self.score_general_health(),
                                 self.wxstring(req, "score_general_health")),
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [
            CtvInfo(content=(
                "RAND-36 (scores out of 100, 100 best): overall {ov}, "
                "physical functioning {pf}, physical role "
                "limitations {prl}, emotional role limitations {erl}, "
                "energy {e}, emotional wellbeing {ew}, social "
                "functioning {sf}, pain {p}, general health {gh}.".format(
                    ov=self.score_overall(),
                    pf=self.score_physical_functioning(),
                    prl=self.score_role_limitations_physical(),
                    erl=self.score_role_limitations_emotional(),
                    e=self.score_energy(),
                    ew=self.score_emotional_wellbeing(),
                    sf=self.score_social_functioning(),
                    p=self.score_pain(),
                    gh=self.score_general_health(),
                )))
        ]

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(
                name="overall",
                coltype=Float(),
                value=self.score_overall(),
                comment="Overall mean score (0-100, higher better)"),
            SummaryElement(
                name="physical_functioning",
                coltype=Float(),
                value=self.score_physical_functioning(),
                comment="Physical functioning score (0-100, higher better)"),
            SummaryElement(
                name="role_limitations_physical",
                coltype=Float(),
                value=self.score_role_limitations_physical(),
                comment="Role limitations due to physical health score "
                "(0-100, higher better)"),
            SummaryElement(
                name="role_limitations_emotional",
                coltype=Float(),
                value=self.score_role_limitations_emotional(),
                comment="Role limitations due to emotional problems score "
                "(0-100, higher better)"),
            SummaryElement(
                name="energy",
                coltype=Float(),
                value=self.score_energy(),
                comment="Energy/fatigue score (0-100, higher better)"),
            SummaryElement(
                name="emotional_wellbeing",
                coltype=Float(),
                value=self.score_emotional_wellbeing(),
                comment="Emotional well-being score (0-100, higher better)"),
            SummaryElement(
                name="social_functioning",
                coltype=Float(),
                value=self.score_social_functioning(),
                comment="Social functioning score (0-100, higher better)"),
            SummaryElement(name="pain",
                           coltype=Float(),
                           value=self.score_pain(),
                           comment="Pain score (0-100, higher better)"),
            SummaryElement(
                name="general_health",
                coltype=Float(),
                value=self.score_general_health(),
                comment="General health score (0-100, higher better)"),
        ]

    # Scoring
    def recode(self, q: int) -> Optional[float]:
        x = getattr(self, "q" + str(q))  # response
        if x is None or x < 1:
            return None
        # http://m.rand.org/content/dam/rand/www/external/health/
        #        surveys_tools/mos/mos_core_36item_scoring.pdf
        if q == 1 or q == 2 or q == 20 or q == 22 or q == 34 or q == 36:
            # 1 becomes 100, 2 => 75, 3 => 50, 4 =>25, 5 => 0
            if x > 5:
                return None
            return 100 - 25 * (x - 1)
        elif 3 <= q <= 12:
            # 1 => 0, 2 => 50, 3 => 100
            if x > 3:
                return None
            return 50 * (x - 1)
        elif 13 <= q <= 19:
            # 1 => 0, 2 => 100
            if x > 2:
                return None
            return 100 * (x - 1)
        elif q == 21 or q == 23 or q == 26 or q == 27 or q == 30:
            # 1 => 100, 2 => 80, 3 => 60, 4 => 40, 5 => 20, 6 => 0
            if x > 6:
                return None
            return 100 - 20 * (x - 1)
        elif q == 24 or q == 25 or q == 28 or q == 29 or q == 31:
            # 1 => 0, 2 => 20, 3 => 40, 4 => 60, 5 => 80, 6 => 100
            if x > 6:
                return None
            return 20 * (x - 1)
        elif q == 32 or q == 33 or q == 35:
            # 1 => 0, 2 => 25, 3 => 50, 4 => 75, 5 => 100
            if x > 5:
                return None
            return 25 * (x - 1)
        return None

    def score_physical_functioning(self) -> Optional[float]:
        return mean([
            self.recode(3),
            self.recode(4),
            self.recode(5),
            self.recode(6),
            self.recode(7),
            self.recode(8),
            self.recode(9),
            self.recode(10),
            self.recode(11),
            self.recode(12)
        ])

    def score_role_limitations_physical(self) -> Optional[float]:
        return mean([
            self.recode(13),
            self.recode(14),
            self.recode(15),
            self.recode(16)
        ])

    def score_role_limitations_emotional(self) -> Optional[float]:
        return mean([self.recode(17), self.recode(18), self.recode(19)])

    def score_energy(self) -> Optional[float]:
        return mean([
            self.recode(23),
            self.recode(27),
            self.recode(29),
            self.recode(31)
        ])

    def score_emotional_wellbeing(self) -> Optional[float]:
        return mean([
            self.recode(24),
            self.recode(25),
            self.recode(26),
            self.recode(28),
            self.recode(30)
        ])

    def score_social_functioning(self) -> Optional[float]:
        return mean([self.recode(20), self.recode(32)])

    def score_pain(self) -> Optional[float]:
        return mean([self.recode(21), self.recode(22)])

    def score_general_health(self) -> Optional[float]:
        return mean([
            self.recode(1),
            self.recode(33),
            self.recode(34),
            self.recode(35),
            self.recode(36)
        ])

    @staticmethod
    def format_float_for_display(val: Optional[float]) -> Optional[str]:
        if val is None:
            return None
        return "{:.1f}".format(val)

    def score_overall(self) -> Optional[float]:
        values = []
        for q in range(1, self.NQUESTIONS + 1):
            values.append(self.recode(q))
        return mean(values)

    @staticmethod
    def section_row_html(text: str) -> str:
        return tr_span_col(text, cols=3, tr_class=CssClass.SUBHEADING)

    def answer_text(self, req: CamcopsRequest, q: int,
                    v: Any) -> Optional[str]:
        if v is None:
            return None
        # wxstring has its own validity checking, so we can do:
        if q == 1 or q == 2 or (20 <= q <= 22) or q == 32:
            return self.wxstring(req, "q" + str(q) + "_option" + str(v))
        elif 3 <= q <= 12:
            return self.wxstring(req, "activities_option" + str(v))
        elif 13 <= q <= 19:
            return self.wxstring(req, "yesno_option" + str(v))
        elif 23 <= q <= 31:
            return self.wxstring(req, "last4weeks_option" + str(v))
        elif 33 <= q <= 36:
            return self.wxstring(req, "q33to36_option" + str(v))
        else:
            return None

    def answer_row_html(self, req: CamcopsRequest, q: int) -> str:
        qtext = self.wxstring(req, "q" + str(q))
        v = getattr(self, "q" + str(q))
        atext = self.answer_text(req, q, v)
        s = self.recode(q)
        return tr(qtext,
                  answer(v) + ": " + answer(atext),
                  answer(s, formatter_answer=identity))

    @staticmethod
    def scoreline(text: str, footnote_num: int, score: Optional[float]) -> str:
        return tr(text + " <sup>[{}]</sup>".format(footnote_num),
                  answer(score) + " / 100")

    def get_task_html(self, req: CamcopsRequest) -> str:
        h = """
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {tr_is_complete}
        """.format(
            CssClass=CssClass,
            tr_is_complete=self.get_is_complete_tr(req),
        )
        h += self.scoreline(
            self.wxstring(req, "score_overall"), 1,
            self.format_float_for_display(self.score_overall()))
        h += self.scoreline(
            self.wxstring(req, "score_physical_functioning"), 2,
            self.format_float_for_display(self.score_physical_functioning()))
        h += self.scoreline(
            self.wxstring(req, "score_role_limitations_physical"), 3,
            self.format_float_for_display(
                self.score_role_limitations_physical()))
        h += self.scoreline(
            self.wxstring(req, "score_role_limitations_emotional"), 4,
            self.format_float_for_display(
                self.score_role_limitations_emotional()))
        h += self.scoreline(self.wxstring(req, "score_energy"), 5,
                            self.format_float_for_display(self.score_energy()))
        h += self.scoreline(
            self.wxstring(req, "score_emotional_wellbeing"), 6,
            self.format_float_for_display(self.score_emotional_wellbeing()))
        h += self.scoreline(
            self.wxstring(req, "score_social_functioning"), 7,
            self.format_float_for_display(self.score_social_functioning()))
        h += self.scoreline(self.wxstring(req, "score_pain"), 8,
                            self.format_float_for_display(self.score_pain()))
        h += self.scoreline(
            self.wxstring(req, "score_general_health"), 9,
            self.format_float_for_display(self.score_general_health()))
        h += """
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="60%">Question</th>
                    <th width="30%">Answer</th>
                    <th width="10%">Score</th>
                </tr>
        """.format(CssClass=CssClass, )
        for q in range(1, 2 + 1):
            h += self.answer_row_html(req, q)
        h += self.section_row_html(self.wxstring(req, "activities_q"))
        for q in range(3, 12 + 1):
            h += self.answer_row_html(req, q)
        h += self.section_row_html(
            self.wxstring(req, "work_activities_physical_q"))
        for q in range(13, 16 + 1):
            h += self.answer_row_html(req, q)
        h += self.section_row_html(
            self.wxstring(req, "work_activities_emotional_q"))
        for q in range(17, 19 + 1):
            h += self.answer_row_html(req, q)
        h += self.section_row_html("<br>")
        h += self.answer_row_html(req, 20)
        h += self.section_row_html("<br>")
        for q in range(21, 22 + 1):
            h += self.answer_row_html(req, q)
        h += self.section_row_html(
            self.wxstring(req, "last4weeks_q_a") + " " +
            self.wxstring(req, "last4weeks_q_b"))
        for q in range(23, 31 + 1):
            h += self.answer_row_html(req, q)
        h += self.section_row_html("<br>")
        for q in [32]:
            h += self.answer_row_html(req, q)
        h += self.section_row_html(self.wxstring(req, "q33to36stem"))
        for q in range(33, 36 + 1):
            h += self.answer_row_html(req, q)
        h += """
            </table>
            <div class="{CssClass.COPYRIGHT}">
                The RAND 36-Item Short Form Health Survey was developed at RAND
                as part of the Medical Outcomes Study. See
            http://www.rand.org/health/surveys_tools/mos/mos_core_36item.html
            </div>
            <div class="{CssClass.FOOTNOTES}">
                All questions are first transformed to a score in the range
                0–100. Higher scores are always better. Then:
                [1] Mean of all 36 questions.
                [2] Mean of Q3–12 inclusive.
                [3] Q13–16.
                [4] Q17–19.
                [5] Q23, 27, 29, 31.
                [6] Q24, 25, 26, 28, 30.
                [7] Q20, 32.
                [8] Q21, 22.
                [9] Q1, 33–36.
            </div>
        """.format(CssClass=CssClass, )
        return h
Example #19
0
class Sfmpq2(TaskHasPatientMixin, Task, metaclass=Sfmpq2Metaclass):
    __tablename__ = "sfmpq2"
    shortname = "SF-MPQ2"

    N_QUESTIONS = 22
    MAX_SCORE_PER_Q = 10
    ALL_QUESTIONS = strseq("q", 1, N_QUESTIONS)

    CONTINUOUS_PAIN_QUESTIONS = Task.fieldnames_from_list(
        "q", {1, 5, 6, 8, 9, 10})
    INTERMITTENT_PAIN_QUESTIONS = Task.fieldnames_from_list(
        "q", {2, 3, 4, 11, 16, 18})
    NEUROPATHIC_PAIN_QUESTIONS = Task.fieldnames_from_list(
        "q", {7, 17, 19, 20, 21, 22})
    AFFECTIVE_PAIN_QUESTIONS = Task.fieldnames_from_list("q", {12, 13, 14, 15})

    @staticmethod
    def longname(req: CamcopsRequest) -> str:
        _ = req.gettext
        return _("Short-Form McGill Pain Questionnaire 2")

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(
                name="total_pain",
                coltype=Float(),
                value=self.total_pain(),
                comment=f"Total pain (/{self.MAX_SCORE_PER_Q})",
            ),
            SummaryElement(
                name="continuous_pain",
                coltype=Float(),
                value=self.continuous_pain(),
                comment=f"Continuous pain (/{self.MAX_SCORE_PER_Q})",
            ),
            SummaryElement(
                name="intermittent_pain",
                coltype=Float(),
                value=self.intermittent_pain(),
                comment=f"Intermittent pain (/{self.MAX_SCORE_PER_Q})",
            ),
            SummaryElement(
                name="neuropathic_pain",
                coltype=Float(),
                value=self.neuropathic_pain(),
                comment=f"Neuropathic pain (/{self.MAX_SCORE_PER_Q})",
            ),
            SummaryElement(
                name="affective_pain",
                coltype=Float(),
                value=self.affective_pain(),
                comment=f"Affective pain (/{self.MAX_SCORE_PER_Q})",
            ),
        ]

    def is_complete(self) -> bool:
        if self.any_fields_none(self.ALL_QUESTIONS):
            return False
        if not self.field_contents_valid():
            return False
        return True

    def total_pain(self) -> float:
        return self.mean_fields(self.ALL_QUESTIONS)

    def continuous_pain(self) -> float:
        return self.mean_fields(self.CONTINUOUS_PAIN_QUESTIONS)

    def intermittent_pain(self) -> float:
        return self.mean_fields(self.INTERMITTENT_PAIN_QUESTIONS)

    def neuropathic_pain(self) -> float:
        return self.mean_fields(self.NEUROPATHIC_PAIN_QUESTIONS)

    def affective_pain(self) -> float:
        return self.mean_fields(self.AFFECTIVE_PAIN_QUESTIONS)

    def format_average(self, value) -> str:
        return "{} / {}".format(
            answer(ws.number_to_dp(value, 3, default="?")),
            self.MAX_SCORE_PER_Q,
        )

    def get_task_html(self, req: CamcopsRequest) -> str:
        rows = ""
        for q_num in range(1, self.N_QUESTIONS + 1):
            q_field = "q" + str(q_num)
            question_cell = "{}. {}".format(q_num, self.wxstring(req, q_field))

            score = getattr(self, q_field)

            rows += tr_qa(question_cell, score)

        html = """
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {tr_is_complete}
                    {total_pain}
                    {continuous_pain}
                    {intermittent_pain}
                    {neuropathic_pain}
                    {affective_pain}
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="60%">Question</th>
                    <th width="40%">Answer <sup>[6]</sup></th>
                </tr>
                {rows}
            </table>
            <div class="{CssClass.FOOTNOTES}">
                [1] Average of items 1–22.
                [2] Average of items 1, 5, 6, 8, 9, 10.
                [3] Average of items 2, 3, 4, 11, 16, 18.
                [4] Average of items 7, 17, 19, 20, 21, 22.
                [5] Average of items 12, 13, 14, 15.
                [6] All items are rated from “0 – none” to
                    “10 – worst possible”.
            </div>
        """.format(
            CssClass=CssClass,
            tr_is_complete=self.get_is_complete_tr(req),
            total_pain=tr(
                self.wxstring(req, "total_pain") + " <sup>[1]</sup>",
                self.format_average(self.total_pain()),
            ),
            continuous_pain=tr(
                self.wxstring(req, "continuous_pain") + " <sup>[2]</sup>",
                self.format_average(self.continuous_pain()),
            ),
            intermittent_pain=tr(
                self.wxstring(req, "intermittent_pain") + " <sup>[3]</sup>",
                self.format_average(self.intermittent_pain()),
            ),
            neuropathic_pain=tr(
                self.wxstring(req, "neuropathic_pain") + " <sup>[4]</sup>",
                self.format_average(self.neuropathic_pain()),
            ),
            affective_pain=tr(
                self.wxstring(req, "affective_pain") + " <sup>[5]</sup>",
                self.format_average(self.affective_pain()),
            ),
            rows=rows,
        )
        return html
Example #20
0
class Hamd7(TaskHasPatientMixin,
            TaskHasClinicianMixin,
            Task,
            metaclass=Hamd7Metaclass):
    """
    Server implementation of the HAMD-7 task.
    """

    __tablename__ = "hamd7"
    shortname = "HAMD-7"
    info_filename_stem = "hamd"
    provides_trackers = True

    NQUESTIONS = 7
    TASK_FIELDS = strseq("q", 1, NQUESTIONS)
    MAX_SCORE = 26

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Hamilton Rating Scale for Depression (7-item scale)")

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label="HAM-D-7 total score",
                axis_label=f"Total score (out of {self.MAX_SCORE})",
                axis_min=-0.5,
                axis_max=self.MAX_SCORE + 0.5,
                horizontal_lines=[19.5, 11.5, 3.5],
                horizontal_labels=[
                    TrackerLabel(23, self.wxstring(req, "severity_severe")),
                    TrackerLabel(15.5, self.wxstring(req,
                                                     "severity_moderate")),
                    TrackerLabel(7.5, self.wxstring(req, "severity_mild")),
                    TrackerLabel(1.75, self.wxstring(req, "severity_none")),
                ],
            )
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [
            CtvInfo(content=(f"HAM-D-7 total score "
                             f"{self.total_score()}/{self.MAX_SCORE} "
                             f"({self.severity(req)})"))
        ]

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(
                name="total",
                coltype=Integer(),
                value=self.total_score(),
                comment=f"Total score (/{self.MAX_SCORE})",
            ),
            SummaryElement(
                name="severity",
                coltype=SummaryCategoryColType,
                value=self.severity(req),
                comment="Severity",
            ),
        ]

    def is_complete(self) -> bool:
        return (self.all_fields_not_none(self.TASK_FIELDS)
                and self.field_contents_valid())

    def total_score(self) -> int:
        return self.sum_fields(self.TASK_FIELDS)

    def severity(self, req: CamcopsRequest) -> str:
        score = self.total_score()
        if score >= 20:
            return self.wxstring(req, "severity_severe")
        elif score >= 12:
            return self.wxstring(req, "severity_moderate")
        elif score >= 4:
            return self.wxstring(req, "severity_mild")
        else:
            return self.wxstring(req, "severity_none")

    def get_task_html(self, req: CamcopsRequest) -> str:
        score = self.total_score()
        severity = self.severity(req)
        answer_dicts = []
        for q in range(1, self.NQUESTIONS + 1):
            d = {None: None}
            for option in range(0, 5):
                if q == 6 and option > 2:
                    continue
                d[option] = self.wxstring(
                    req, "q" + str(q) + "_option" + str(option))
            answer_dicts.append(d)

        q_a = ""
        for q in range(1, self.NQUESTIONS + 1):
            q_a += tr_qa(
                self.wxstring(req, "q" + str(q) + "_s"),
                get_from_dict(answer_dicts[q - 1], getattr(self,
                                                           "q" + str(q))),
            )

        return """
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {tr_is_complete}
                    {total_score}
                    {severity}
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="30%">Question</th>
                    <th width="70%">Answer</th>
                </tr>
                {q_a}
            </table>
            <div class="{CssClass.FOOTNOTES}">
                [1] ≥20 severe, ≥12 moderate, ≥4 mild, &lt;4 none.
            </div>
        """.format(
            CssClass=CssClass,
            tr_is_complete=self.get_is_complete_tr(req),
            total_score=tr(
                req.sstring(SS.TOTAL_SCORE),
                answer(score) + " / {}".format(self.MAX_SCORE),
            ),
            severity=tr_qa(
                self.wxstring(req, "severity") + " <sup>[1]</sup>", severity),
            q_a=q_a,
        )
Example #21
0
class Mfi20(TaskHasPatientMixin, Task, metaclass=Mfi20Metaclass):
    __tablename__ = "mfi20"
    shortname = "MFI-20"

    prohibits_clinical = True
    prohibits_commercial = True

    N_QUESTIONS = 20
    MIN_SCORE_PER_Q = 1
    MAX_SCORE_PER_Q = 5
    MIN_SCORE = MIN_SCORE_PER_Q * N_QUESTIONS
    MAX_SCORE = MAX_SCORE_PER_Q * N_QUESTIONS
    N_Q_PER_SUBSCALE = 4  # always
    MIN_SUBSCALE = MIN_SCORE_PER_Q * N_Q_PER_SUBSCALE
    MAX_SUBSCALE = MAX_SCORE_PER_Q * N_Q_PER_SUBSCALE
    ALL_QUESTIONS = strseq("q", 1, N_QUESTIONS)
    REVERSE_QUESTIONS = Task.fieldnames_from_list(
        "q", {2, 5, 9, 10, 13, 14, 16, 17, 18, 19}
    )

    GENERAL_FATIGUE_QUESTIONS = Task.fieldnames_from_list("q", {1, 5, 12, 16})
    PHYSICAL_FATIGUE_QUESTIONS = Task.fieldnames_from_list("q", {2, 8, 14, 20})
    REDUCED_ACTIVITY_QUESTIONS = Task.fieldnames_from_list("q", {3, 6, 10, 17})
    REDUCED_MOTIVATION_QUESTIONS = Task.fieldnames_from_list(
        "q", {4, 9, 15, 18}
    )
    MENTAL_FATIGUE_QUESTIONS = Task.fieldnames_from_list("q", {7, 11, 13, 19})

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Multidimensional Fatigue Inventory")

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        subscale_range = f"[{self.MIN_SUBSCALE}–{self.MAX_SUBSCALE}]"
        return self.standard_task_summary_fields() + [
            SummaryElement(
                name="total",
                coltype=Integer(),
                value=self.total_score(),
                comment=f"Total score [{self.MIN_SCORE}–{self.MAX_SCORE}]",
            ),
            SummaryElement(
                name="general_fatigue",
                coltype=Integer(),
                value=self.general_fatigue_score(),
                comment=f"General fatigue {subscale_range}",
            ),
            SummaryElement(
                name="physical_fatigue",
                coltype=Integer(),
                value=self.physical_fatigue_score(),
                comment=f"Physical fatigue {subscale_range}",
            ),
            SummaryElement(
                name="reduced_activity",
                coltype=Integer(),
                value=self.reduced_activity_score(),
                comment=f"Reduced activity {subscale_range}",
            ),
            SummaryElement(
                name="reduced_motivation",
                coltype=Integer(),
                value=self.reduced_motivation_score(),
                comment=f"Reduced motivation {subscale_range}",
            ),
            SummaryElement(
                name="mental_fatigue",
                coltype=Integer(),
                value=self.mental_fatigue_score(),
                comment=f"Mental fatigue {subscale_range}",
            ),
        ]

    def is_complete(self) -> bool:
        if self.any_fields_none(self.ALL_QUESTIONS):
            return False
        if not self.field_contents_valid():
            return False
        return True

    def score_fields(self, fields: List[str]) -> int:
        total = 0
        for f in fields:
            value = getattr(self, f)
            if value is not None:
                if f in self.REVERSE_QUESTIONS:
                    value = self.MAX_SCORE_PER_Q + 1 - value

            total += value if value is not None else 0

        return total

    def total_score(self) -> int:
        return self.score_fields(self.ALL_QUESTIONS)

    def general_fatigue_score(self) -> int:
        return self.score_fields(self.GENERAL_FATIGUE_QUESTIONS)

    def physical_fatigue_score(self) -> int:
        return self.score_fields(self.PHYSICAL_FATIGUE_QUESTIONS)

    def reduced_activity_score(self) -> int:
        return self.score_fields(self.REDUCED_ACTIVITY_QUESTIONS)

    def reduced_motivation_score(self) -> int:
        return self.score_fields(self.REDUCED_MOTIVATION_QUESTIONS)

    def mental_fatigue_score(self) -> int:
        return self.score_fields(self.MENTAL_FATIGUE_QUESTIONS)

    def get_task_html(self, req: CamcopsRequest) -> str:
        fullscale_range = f"[{self.MIN_SCORE}–{self.MAX_SCORE}]"
        subscale_range = f"[{self.MIN_SUBSCALE}–{self.MAX_SUBSCALE}]"

        rows = ""
        for q_num in range(1, self.N_QUESTIONS + 1):
            q_field = "q" + str(q_num)
            question_cell = "{}. {}".format(q_num, self.wxstring(req, q_field))

            score = getattr(self, q_field)

            rows += tr_qa(question_cell, score)

        html = """
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {tr_is_complete}
                    {total_score}
                    {general_fatigue_score}
                    {physical_fatigue_score}
                    {reduced_activity_score}
                    {reduced_motivation_score}
                    {mental_fatigue_score}
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="60%">Question</th>
                    <th width="40%">Answer <sup>[8]</sup></th>
                </tr>
                {rows}
            </table>
            <div class="{CssClass.FOOTNOTES}">
                [1] Questions 2, 5, 9, 10, 13, 14, 16, 17, 18, 19
                    reverse-scored when summing.
                [2] Sum for questions 1–20.
                [3] General fatigue: Sum for questions 1, 5, 12, 16.
                [4] Physical fatigue: Sum for questions 2, 8, 14, 20.
                [5] Reduced activity: Sum for questions 3, 6, 10, 17.
                [6] Reduced motivation: Sum for questions 4, 9, 15, 18.
                [7] Mental fatigue: Sum for questions 7, 11, 13, 19.
                [8] All questions are rated from “1 – yes, that is true” to
                    “5 – no, that is not true”.
            </div>
        """.format(
            CssClass=CssClass,
            tr_is_complete=self.get_is_complete_tr(req),
            total_score=tr(
                req.sstring(SS.TOTAL_SCORE) + " <sup>[1][2]</sup>",
                f"{answer(self.total_score())} {fullscale_range}",
            ),
            general_fatigue_score=tr(
                self.wxstring(req, "general_fatigue") + " <sup>[1][3]</sup>",
                f"{answer(self.general_fatigue_score())} {subscale_range}",
            ),
            physical_fatigue_score=tr(
                self.wxstring(req, "physical_fatigue") + " <sup>[1][4]</sup>",
                f"{answer(self.physical_fatigue_score())} {subscale_range}",
            ),
            reduced_activity_score=tr(
                self.wxstring(req, "reduced_activity") + " <sup>[1][5]</sup>",
                f"{answer(self.reduced_activity_score())} {subscale_range}",
            ),
            reduced_motivation_score=tr(
                self.wxstring(req, "reduced_motivation")
                + " <sup>[1][6]</sup>",
                f"{answer(self.reduced_motivation_score())} {subscale_range}",
            ),
            mental_fatigue_score=tr(
                self.wxstring(req, "mental_fatigue") + " <sup>[1][7]</sup>",
                f"{answer(self.mental_fatigue_score())} {subscale_range}",
            ),
            rows=rows,
        )
        return html
Example #22
0
class Caps(TaskHasPatientMixin, Task, metaclass=CapsMetaclass):
    """
    Server implementation of the CAPS task.
    """
    __tablename__ = "caps"
    shortname = "CAPS"
    longname = "Cardiff Anomalous Perceptions Scale"
    provides_trackers = True

    NQUESTIONS = 32
    ENDORSE_FIELDS = strseq("endorse", 1, NQUESTIONS)

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        return [
            TrackerInfo(value=self.total_score(),
                        plot_label="CAPS total score",
                        axis_label="Total score (out of 32)",
                        axis_min=-0.5,
                        axis_max=32.5)
        ]

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(name="total",
                           coltype=Integer(),
                           value=self.total_score(),
                           comment="Total score (/32)"),
            SummaryElement(name="distress",
                           coltype=Integer(),
                           value=self.distress_score(),
                           comment="Distress score (/160)"),
            SummaryElement(name="intrusiveness",
                           coltype=Integer(),
                           value=self.intrusiveness_score(),
                           comment="Intrusiveness score (/160)"),
            SummaryElement(name="frequency",
                           coltype=Integer(),
                           value=self.frequency_score(),
                           comment="Frequency score (/160)"),
        ]

    def is_question_complete(self, q: int) -> bool:
        if getattr(self, "endorse" + str(q)) is None:
            return False
        if getattr(self, "endorse" + str(q)):
            if getattr(self, "distress" + str(q)) is None:
                return False
            if getattr(self, "intrusiveness" + str(q)) is None:
                return False
            if getattr(self, "frequency" + str(q)) is None:
                return False
        return True

    def is_complete(self) -> bool:
        if not self.field_contents_valid():
            return False
        for i in range(1, Caps.NQUESTIONS + 1):
            if not self.is_question_complete(i):
                return False
        return True

    def total_score(self) -> int:
        return self.count_booleans(self.ENDORSE_FIELDS)

    def distress_score(self) -> int:
        score = 0
        for q in range(1, Caps.NQUESTIONS + 1):
            if getattr(self, "endorse" + str(q)) \
                    and getattr(self, "distress" + str(q)) is not None:
                score += self.sum_fields(["distress" + str(q)])
        return score

    def intrusiveness_score(self) -> int:
        score = 0
        for q in range(1, Caps.NQUESTIONS + 1):
            if getattr(self, "endorse" + str(q)) \
                    and getattr(self, "intrusiveness" + str(q)) is not None:
                score += self.sum_fields(["intrusiveness" + str(q)])
        return score

    def frequency_score(self) -> int:
        score = 0
        for q in range(1, Caps.NQUESTIONS + 1):
            if getattr(self, "endorse" + str(q)) \
                    and getattr(self, "frequency" + str(q)) is not None:
                score += self.sum_fields(["frequency" + str(q)])
        return score

    def get_task_html(self, req: CamcopsRequest) -> str:
        total = self.total_score()
        distress = self.distress_score()
        intrusiveness = self.intrusiveness_score()
        frequency = self.frequency_score()

        q_a = ""
        for q in range(1, Caps.NQUESTIONS + 1):
            q_a += tr(
                self.wxstring(req, "q" + str(q)),
                answer(get_yes_no_none(req, getattr(self,
                                                    "endorse" + str(q)))),
                answer(
                    getattr(self, "distress" +
                            str(q)) if getattr(self, "endorse" +
                                               str(q)) else ""),
                answer(
                    getattr(self, "intrusiveness" +
                            str(q)) if getattr(self, "endorse" +
                                               str(q)) else ""),
                answer(
                    getattr(self, "frequency" +
                            str(q)) if getattr(self, "endorse" +
                                               str(q)) else ""))

        h = """
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {tr_is_complete}
                    {total_score}
                    {distress}
                    {intrusiveness}
                    {frequency}
                </table>
            </div>
            <div class="{CssClass.EXPLANATION}">
                Anchor points: DISTRESS {distress1}, {distress5}.
                INTRUSIVENESS {intrusiveness1}, {intrusiveness5}.
                FREQUENCY {frequency1}, {frequency5}.
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="60%">Question</th>
                    <th width="10%">Endorsed?</th>
                    <th width="10%">Distress (1–5)</th>
                    <th width="10%">Intrusiveness (1–5)</th>
                    <th width="10%">Frequency (1–5)</th>
                </tr>
            </table>
            <div class="{CssClass.FOOTNOTES}">
                [1] Total score: sum of endorsements (yes = 1, no = 0).
                Dimension scores: sum of ratings (0 if not endorsed).
                (Bell et al. 2006, PubMed ID 16237200)
            </div>
            <div class="{CssClass.COPYRIGHT}">
                CAPS: Copyright © 2005, Bell, Halligan & Ellis.
                Original article:
                    Bell V, Halligan PW, Ellis HD (2006).
                    The Cardiff Anomalous Perceptions Scale (CAPS): a new
                    validated measure of anomalous perceptual experience.
                    Schizophrenia Bulletin 32: 366–377.
                Published by Oxford University Press on behalf of the Maryland
                Psychiatric Research Center. All rights reserved. The online
                version of this article has been published under an open access
                model. Users are entitled to use, reproduce, disseminate, or
                display the open access version of this article for
                non-commercial purposes provided that: the original authorship
                is properly and fully attributed; the Journal and Oxford
                University Press are attributed as the original place of
                publication with the correct citation details given; if an
                article is subsequently reproduced or disseminated not in its
                entirety but only in part or as a derivative work this must be
                clearly indicated. For commercial re-use, please contact
                [email protected].<br>
                <b>This is a derivative work (partial reproduction, viz. the
                scale text).</b>
            </div>
        """.format(
            CssClass=CssClass,
            tr_is_complete=self.get_is_complete_tr(req),
            total_score=tr_qa(
                "{} <sup>[1]</sup> (0–32)".format(
                    req.wappstring("total_score")), total),
            distress=tr_qa("{} (0–160)".format(self.wxstring(req, "distress")),
                           distress),
            intrusiveness=tr_qa(
                "{} (0–160)".format(self.wxstring(req, "intrusiveness")),
                intrusiveness),
            frequency=tr_qa(
                "{} (0–160)".format(self.wxstring(req, "frequency")),
                frequency),
            distress1=self.wxstring(req, "distress_option1"),
            distress5=self.wxstring(req, "distress_option5"),
            intrusiveness1=self.wxstring(req, "intrusiveness_option1"),
            intrusiveness5=self.wxstring(req, "intrusiveness_option5"),
            frequency1=self.wxstring(req, "frequency_option1"),
            frequency5=self.wxstring(req, "frequency_option5"),
        )
        return h
Example #23
0
class Gad7(TaskHasPatientMixin, Task, metaclass=Gad7Metaclass):
    """
    Server implementation of the GAD-7 task.
    """

    __tablename__ = "gad7"
    shortname = "GAD-7"
    provides_trackers = True

    NQUESTIONS = 7
    TASK_FIELDS = strseq("q", 1, NQUESTIONS)
    MAX_SCORE = 21

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Generalized Anxiety Disorder Assessment")

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label="GAD-7 total score",
                axis_label="Total score (out of 21)",
                axis_min=-0.5,
                axis_max=self.MAX_SCORE + 0.5,
                horizontal_lines=[14.5, 9.5, 4.5],
                horizontal_labels=[
                    TrackerLabel(17, req.sstring(SS.SEVERE)),
                    TrackerLabel(12, req.sstring(SS.MODERATE)),
                    TrackerLabel(7, req.sstring(SS.MILD)),
                    TrackerLabel(2.25, req.sstring(SS.NONE)),
                ],
            )
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [
            CtvInfo(content=(
                f"GAD-7 total score {self.total_score()}/{self.MAX_SCORE} "
                f"({self.severity(req)})"))
        ]

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(
                name="total",
                coltype=Integer(),
                value=self.total_score(),
                comment=f"Total score (/{self.MAX_SCORE})",
            ),
            SummaryElement(
                name="severity",
                coltype=SummaryCategoryColType,
                value=self.severity(req),
                comment="Severity",
            ),
        ]

    def is_complete(self) -> bool:
        return (self.all_fields_not_none(self.TASK_FIELDS)
                and self.field_contents_valid())

    def total_score(self) -> int:
        return self.sum_fields(self.TASK_FIELDS)

    def severity(self, req: CamcopsRequest) -> str:
        score = self.total_score()
        if score >= 15:
            severity = req.sstring(SS.SEVERE)
        elif score >= 10:
            severity = req.sstring(SS.MODERATE)
        elif score >= 5:
            severity = req.sstring(SS.MILD)
        else:
            severity = req.sstring(SS.NONE)
        return severity

    def get_task_html(self, req: CamcopsRequest) -> str:
        score = self.total_score()
        severity = self.severity(req)
        answer_dict = {None: None}
        for option in range(0, 4):
            answer_dict[option] = (str(option) + " — " +
                                   self.wxstring(req, "a" + str(option)))

        q_a = ""
        for q in range(1, self.NQUESTIONS + 1):
            q_a += tr_qa(
                self.wxstring(req, "q" + str(q)),
                get_from_dict(answer_dict, getattr(self, "q" + str(q))),
            )

        return """
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {tr_is_complete}
                    {total_score}
                    {anxiety_severity}
                </table>
            </div>
            <div class="{CssClass.EXPLANATION}">
                Ratings are over the last 2 weeks.
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="50%">Question</th>
                    <th width="50%">Answer</th>
                </tr>
                {q_a}
            </table>
            <div class="{CssClass.FOOTNOTES}">
                [1] ≥15 severe, ≥10 moderate, ≥5 mild.
                Score ≥10 identifies: generalized anxiety disorder with
                sensitivity 89%, specificity 82% (Spitzer et al. 2006, PubMed
                ID 16717171);
                panic disorder with sensitivity 74%, specificity 81% (Kroenke
                et al. 2010, PMID 20633738);
                social anxiety with sensitivity 72%, specificity 80% (Kroenke
                et al. 2010);
                post-traumatic stress disorder with sensitivity 66%,
                specificity 81% (Kroenke et al. 2010).
                The majority of evidence contributing to these figures comes
                from primary care screening studies.
            </div>
        """.format(
            CssClass=CssClass,
            tr_is_complete=self.get_is_complete_tr(req),
            total_score=tr(
                req.sstring(SS.TOTAL_SCORE),
                answer(score) + " / {}".format(self.MAX_SCORE),
            ),
            anxiety_severity=tr(
                self.wxstring(req, "anxiety_severity") + " <sup>[1]</sup>",
                severity,
            ),
            q_a=q_a,
        )

    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        codes = [
            SnomedExpression(req.snomed(
                SnomedLookup.GAD7_PROCEDURE_ASSESSMENT))
        ]
        if self.is_complete():
            codes.append(
                SnomedExpression(
                    req.snomed(SnomedLookup.GAD7_SCALE),
                    {req.snomed(SnomedLookup.GAD7_SCORE): self.total_score()},
                ))
        return codes
Example #24
0
class Zbi12(TaskHasRespondentMixin, TaskHasPatientMixin, Task,
            metaclass=Zbi12Metaclass):
    """
    Server implementation of the ZBI-12 task.
    """
    __tablename__ = "zbi12"
    shortname = "ZBI-12"
    longname = "Zarit Burden Interview-12"

    MIN_PER_Q = 0
    MAX_PER_Q = 4
    NQUESTIONS = 12
    TASK_FIELDS = strseq("q", 1, NQUESTIONS)
    MAX_TOTAL = MAX_PER_Q * NQUESTIONS

    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="Total score (/ {})".format(self.MAX_TOTAL)
            ),
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [CtvInfo(content="ZBI-12 total score {}/{}".format(
            self.total_score(), self.MAX_TOTAL))]

    def total_score(self) -> int:
        return self.sum_fields(self.TASK_FIELDS)

    def is_complete(self) -> bool:
        return (
            self.field_contents_valid() and
            self.is_respondent_complete() and
            self.are_all_fields_complete(self.TASK_FIELDS)
        )

    def get_task_html(self, req: CamcopsRequest) -> str:
        option_dict = {None: None}
        for a in range(self.MIN_PER_Q, self.MAX_PER_Q + 1):
            option_dict[a] = req.wappstring("zbi_a" + str(a))
        h = """
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {complete_tr}
                    <tr>
                        <td>Total score (/ {maxtotal})</td>
                        <td>{total}</td>
                    </td>
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="75%">Question</th>
                    <th width="25%">Answer ({minq}–{maxq})</th>
                </tr>
        """.format(
            CssClass=CssClass,
            complete_tr=self.get_is_complete_tr(req),
            total=answer(self.total_score()),
            maxtotal=self.MAX_TOTAL,
            minq=self.MIN_PER_Q,
            maxq=self.MAX_PER_Q,
        )
        for q in range(1, self.NQUESTIONS + 1):
            a = getattr(self, "q" + str(q))
            fa = ("{}: {}".format(a, get_from_dict(option_dict, a))
                  if a is not None else None)
            h += tr(self.wxstring(req, "q" + str(q)), answer(fa))
        h += """
            </table>
        """ + DATA_COLLECTION_UNLESS_UPGRADED_DIV
        return h
Example #25
0
class Core10(TaskHasPatientMixin, Task):
    """
    Server implementation of the CORE-10 task.
    """

    __tablename__ = "core10"
    shortname = "CORE-10"
    provides_trackers = True

    COMMENT_NORMAL = " (0 not at all - 4 most or all of the time)"
    COMMENT_REVERSED = " (0 most or all of the time - 4 not at all)"

    q1 = CamcopsColumn(
        "q1",
        Integer,
        permitted_value_checker=ZERO_TO_FOUR_CHECKER,
        comment="Q1 (tension/anxiety)" + COMMENT_NORMAL,
    )
    q2 = CamcopsColumn(
        "q2",
        Integer,
        permitted_value_checker=ZERO_TO_FOUR_CHECKER,
        comment="Q2 (support)" + COMMENT_REVERSED,
    )
    q3 = CamcopsColumn(
        "q3",
        Integer,
        permitted_value_checker=ZERO_TO_FOUR_CHECKER,
        comment="Q3 (coping)" + COMMENT_REVERSED,
    )
    q4 = CamcopsColumn(
        "q4",
        Integer,
        permitted_value_checker=ZERO_TO_FOUR_CHECKER,
        comment="Q4 (talking is too much)" + COMMENT_NORMAL,
    )
    q5 = CamcopsColumn(
        "q5",
        Integer,
        permitted_value_checker=ZERO_TO_FOUR_CHECKER,
        comment="Q5 (panic)" + COMMENT_NORMAL,
    )
    q6 = CamcopsColumn(
        "q6",
        Integer,
        permitted_value_checker=ZERO_TO_FOUR_CHECKER,
        comment="Q6 (suicidality)" + COMMENT_NORMAL,
    )
    q7 = CamcopsColumn(
        "q7",
        Integer,
        permitted_value_checker=ZERO_TO_FOUR_CHECKER,
        comment="Q7 (sleep problems)" + COMMENT_NORMAL,
    )
    q8 = CamcopsColumn(
        "q8",
        Integer,
        permitted_value_checker=ZERO_TO_FOUR_CHECKER,
        comment="Q8 (despair/hopelessness)" + COMMENT_NORMAL,
    )
    q9 = CamcopsColumn(
        "q9",
        Integer,
        permitted_value_checker=ZERO_TO_FOUR_CHECKER,
        comment="Q9 (unhappy)" + COMMENT_NORMAL,
    )
    q10 = CamcopsColumn(
        "q10",
        Integer,
        permitted_value_checker=ZERO_TO_FOUR_CHECKER,
        comment="Q10 (unwanted images)" + COMMENT_NORMAL,
    )

    N_QUESTIONS = 10
    MAX_SCORE = 4 * N_QUESTIONS
    QUESTION_FIELDNAMES = strseq("q", 1, N_QUESTIONS)

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Clinical Outcomes in Routine Evaluation, 10-item measure")

    # noinspection PyMethodParameters
    @classproperty
    def minimum_client_version(cls) -> Version:
        return Version("2.2.8")

    def is_complete(self) -> bool:
        return self.all_fields_not_none(self.QUESTION_FIELDNAMES)

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        return [
            TrackerInfo(
                value=self.clinical_score(),
                plot_label="CORE-10 clinical score (rating distress)",
                axis_label=f"Clinical score (out of {self.MAX_SCORE})",
                axis_min=-0.5,
                axis_max=self.MAX_SCORE + 0.5,
                axis_ticks=[
                    TrackerAxisTick(40, "40"),
                    TrackerAxisTick(35, "35"),
                    TrackerAxisTick(30, "30"),
                    TrackerAxisTick(25, "25"),
                    TrackerAxisTick(20, "20"),
                    TrackerAxisTick(15, "15"),
                    TrackerAxisTick(10, "10"),
                    TrackerAxisTick(5, "5"),
                    TrackerAxisTick(0, "0"),
                ],
                horizontal_lines=[30, 20, 10],
            )
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [
            CtvInfo(
                content=(
                    f"CORE-10 clinical score "
                    f"{self.clinical_score()}/{self.MAX_SCORE}"
                )
            )
        ]
        # todo: CORE10: add suicidality to clinical text?

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(
                name="clinical_score",
                coltype=Integer(),
                value=self.clinical_score(),
                comment=f"Clinical score (/{self.MAX_SCORE})",
            )
        ]

    def total_score(self) -> int:
        return self.sum_fields(self.QUESTION_FIELDNAMES)

    def n_questions_complete(self) -> int:
        return self.n_fields_not_none(self.QUESTION_FIELDNAMES)

    def clinical_score(self) -> float:
        n_q_completed = self.n_questions_complete()
        if n_q_completed == 0:
            # avoid division by zero
            return 0
        return self.N_QUESTIONS * self.total_score() / n_q_completed

    def get_task_html(self, req: CamcopsRequest) -> str:
        normal_dict = {
            None: None,
            0: "0 — " + self.wxstring(req, "a0"),
            1: "1 — " + self.wxstring(req, "a1"),
            2: "2 — " + self.wxstring(req, "a2"),
            3: "3 — " + self.wxstring(req, "a3"),
            4: "4 — " + self.wxstring(req, "a4"),
        }
        reversed_dict = {
            None: None,
            0: "0 — " + self.wxstring(req, "a4"),
            1: "1 — " + self.wxstring(req, "a3"),
            2: "2 — " + self.wxstring(req, "a2"),
            3: "3 — " + self.wxstring(req, "a1"),
            4: "4 — " + self.wxstring(req, "a0"),
        }

        def get_tr_qa(qnum_: int, mapping: Dict[Optional[int], str]) -> str:
            nstr = str(qnum_)
            return tr_qa(
                self.wxstring(req, "q" + nstr),
                get_from_dict(mapping, getattr(self, "q" + nstr)),
            )

        q_a = get_tr_qa(1, normal_dict)
        for qnum in (2, 3):
            q_a += get_tr_qa(qnum, reversed_dict)
        for qnum in range(4, self.N_QUESTIONS + 1):
            q_a += get_tr_qa(qnum, normal_dict)

        tr_clinical_score = tr(
            "Clinical score <sup>[1]</sup>",
            answer(self.clinical_score()) + " / {}".format(self.MAX_SCORE),
        )
        return f"""
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {self.get_is_complete_tr(req)}
                    {tr_clinical_score}
                </table>
            </div>
            <div class="{CssClass.EXPLANATION}">
                Ratings are over the last week.
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="60%">Question</th>
                    <th width="40%">Answer</th>
                </tr>
                {q_a}
            </table>
            <div class="{CssClass.FOOTNOTES}">
                [1] Clinical score is: number of questions × total score
                    ÷ number of questions completed. If all questions are
                    completed, it's just the total score.
            </div>
        """

    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        codes = [
            SnomedExpression(
                req.snomed(SnomedLookup.CORE10_PROCEDURE_ASSESSMENT)
            )
        ]
        if self.is_complete():
            codes.append(
                SnomedExpression(
                    req.snomed(SnomedLookup.CORE10_SCALE),
                    {
                        req.snomed(
                            SnomedLookup.CORE10_SCORE
                        ): self.total_score()
                    },
                )
            )
        return codes
Example #26
0
class CbiR(TaskHasPatientMixin,
           TaskHasRespondentMixin,
           Task,
           metaclass=CbiRMetaclass):
    """
    Server implementation of the CBI-R task.
    """

    __tablename__ = "cbir"
    shortname = "CBI-R"

    confirm_blanks = CamcopsColumn(
        "confirm_blanks",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="Respondent confirmed that blanks are deliberate (N/A) "
        "(0/NULL no, 1 yes)",
    )
    comments = Column("comments", UnicodeText, comment="Additional comments")

    MIN_SCORE = 0
    MAX_SCORE = 4
    QNUMS_MEMORY = (1, 8)  # tuple: first, last
    QNUMS_EVERYDAY = (9, 13)
    QNUMS_SELF = (14, 17)
    QNUMS_BEHAVIOUR = (18, 23)
    QNUMS_MOOD = (24, 27)
    QNUMS_BELIEFS = (28, 30)
    QNUMS_EATING = (31, 34)
    QNUMS_SLEEP = (35, 36)
    QNUMS_STEREOTYPY = (37, 40)
    QNUMS_MOTIVATION = (41, 45)

    NQUESTIONS = 45
    TASK_FIELDS = strseq("frequency", 1, NQUESTIONS) + strseq(
        "distress", 1, NQUESTIONS)

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Cambridge Behavioural Inventory, Revised")

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(
                name="memory_frequency_pct",
                coltype=Float(),
                value=self.frequency_subscore(*self.QNUMS_MEMORY),
                comment="Memory/orientation: frequency score (% of max)",
            ),
            SummaryElement(
                name="memory_distress_pct",
                coltype=Float(),
                value=self.distress_subscore(*self.QNUMS_MEMORY),
                comment="Memory/orientation: distress score (% of max)",
            ),
            SummaryElement(
                name="everyday_frequency_pct",
                coltype=Float(),
                value=self.frequency_subscore(*self.QNUMS_EVERYDAY),
                comment="Everyday skills: frequency score (% of max)",
            ),
            SummaryElement(
                name="everyday_distress_pct",
                coltype=Float(),
                value=self.distress_subscore(*self.QNUMS_EVERYDAY),
                comment="Everyday skills: distress score (% of max)",
            ),
            SummaryElement(
                name="selfcare_frequency_pct",
                coltype=Float(),
                value=self.frequency_subscore(*self.QNUMS_SELF),
                comment="Self-care: frequency score (% of max)",
            ),
            SummaryElement(
                name="selfcare_distress_pct",
                coltype=Float(),
                value=self.distress_subscore(*self.QNUMS_SELF),
                comment="Self-care: distress score (% of max)",
            ),
            SummaryElement(
                name="behaviour_frequency_pct",
                coltype=Float(),
                value=self.frequency_subscore(*self.QNUMS_BEHAVIOUR),
                comment="Abnormal behaviour: frequency score (% of max)",
            ),
            SummaryElement(
                name="behaviour_distress_pct",
                coltype=Float(),
                value=self.distress_subscore(*self.QNUMS_BEHAVIOUR),
                comment="Abnormal behaviour: distress score (% of max)",
            ),
            SummaryElement(
                name="mood_frequency_pct",
                coltype=Float(),
                value=self.frequency_subscore(*self.QNUMS_MOOD),
                comment="Mood: frequency score (% of max)",
            ),
            SummaryElement(
                name="mood_distress_pct",
                coltype=Float(),
                value=self.distress_subscore(*self.QNUMS_MOOD),
                comment="Mood: distress score (% of max)",
            ),
            SummaryElement(
                name="beliefs_frequency_pct",
                coltype=Float(),
                value=self.frequency_subscore(*self.QNUMS_BELIEFS),
                comment="Beliefs: frequency score (% of max)",
            ),
            SummaryElement(
                name="beliefs_distress_pct",
                coltype=Float(),
                value=self.distress_subscore(*self.QNUMS_BELIEFS),
                comment="Beliefs: distress score (% of max)",
            ),
            SummaryElement(
                name="eating_frequency_pct",
                coltype=Float(),
                value=self.frequency_subscore(*self.QNUMS_EATING),
                comment="Eating habits: frequency score (% of max)",
            ),
            SummaryElement(
                name="eating_distress_pct",
                coltype=Float(),
                value=self.distress_subscore(*self.QNUMS_EATING),
                comment="Eating habits: distress score (% of max)",
            ),
            SummaryElement(
                name="sleep_frequency_pct",
                coltype=Float(),
                value=self.frequency_subscore(*self.QNUMS_SLEEP),
                comment="Sleep: frequency score (% of max)",
            ),
            SummaryElement(
                name="sleep_distress_pct",
                coltype=Float(),
                value=self.distress_subscore(*self.QNUMS_SLEEP),
                comment="Sleep: distress score (% of max)",
            ),
            SummaryElement(
                name="stereotypic_frequency_pct",
                coltype=Float(),
                value=self.frequency_subscore(*self.QNUMS_STEREOTYPY),
                comment="Stereotypic and motor behaviours: frequency "
                "score (% of max)",
            ),
            SummaryElement(
                name="stereotypic_distress_pct",
                coltype=Float(),
                value=self.distress_subscore(*self.QNUMS_STEREOTYPY),
                comment="Stereotypic and motor behaviours: distress "
                "score (% of max)",
            ),
            SummaryElement(
                name="motivation_frequency_pct",
                coltype=Float(),
                value=self.frequency_subscore(*self.QNUMS_MOTIVATION),
                comment="Motivation: frequency score (% of max)",
            ),
            SummaryElement(
                name="motivation_distress_pct",
                coltype=Float(),
                value=self.distress_subscore(*self.QNUMS_MOTIVATION),
                comment="Motivation: distress score (% of max)",
            ),
        ]

    def subscore(self, first: int, last: int,
                 fieldprefix: str) -> Optional[float]:
        score = 0
        n = 0
        for q in range(first, last + 1):
            value = getattr(self, fieldprefix + str(q))
            if value is not None:
                score += value / self.MAX_SCORE
                n += 1
        return 100 * score / n if n > 0 else None

    def frequency_subscore(self, first: int, last: int) -> Optional[float]:
        return self.subscore(first, last, "frequency")

    def distress_subscore(self, first: int, last: int) -> Optional[float]:
        return self.subscore(first, last, "distress")

    def is_complete(self) -> bool:
        if (not self.field_contents_valid()
                or not self.is_respondent_complete()):
            return False
        if self.confirm_blanks:
            return True
        return self.all_fields_not_none(self.TASK_FIELDS)

    def get_task_html(self, req: CamcopsRequest) -> str:
        freq_dict = {None: None}
        distress_dict = {None: None}
        for a in range(self.MIN_SCORE, self.MAX_SCORE + 1):
            freq_dict[a] = self.wxstring(req, "f" + str(a))
            distress_dict[a] = self.wxstring(req, "d" + str(a))

        heading_memory = self.wxstring(req, "h_memory")
        heading_everyday = self.wxstring(req, "h_everyday")
        heading_selfcare = self.wxstring(req, "h_selfcare")
        heading_behaviour = self.wxstring(req, "h_abnormalbehaviour")
        heading_mood = self.wxstring(req, "h_mood")
        heading_beliefs = self.wxstring(req, "h_beliefs")
        heading_eating = self.wxstring(req, "h_eating")
        heading_sleep = self.wxstring(req, "h_sleep")
        heading_motor = self.wxstring(req, "h_stereotypy_motor")
        heading_motivation = self.wxstring(req, "h_motivation")

        def get_question_rows(first, last):
            html = ""
            for q in range(first, last + 1):
                f = getattr(self, "frequency" + str(q))
                d = getattr(self, "distress" + str(q))
                fa = (f"{f}: {get_from_dict(freq_dict, f)}"
                      if f is not None else None)
                da = (f"{d}: {get_from_dict(distress_dict, d)}"
                      if d is not None else None)
                html += tr(self.wxstring(req, "q" + str(q)), answer(fa),
                           answer(da))
            return html

        h = f"""
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {self.get_is_complete_tr(req)}
                </table>
                <table class="{CssClass.SUMMARY}">
                    <tr>
                        <th>Subscale</th>
                        <th>Frequency (% of max)</th>
                        <th>Distress (% of max)</th>
                    </tr>
                    <tr>
                        <td>{heading_memory}</td>
                        <td>{answer(self.frequency_subscore(*self.QNUMS_MEMORY))}</td>
                        <td>{answer(self.distress_subscore(*self.QNUMS_MEMORY))}</td>
                    </tr>
                    <tr>
                        <td>{heading_everyday}</td>
                        <td>{answer(self.frequency_subscore(*self.QNUMS_EVERYDAY))}</td>
                        <td>{answer(self.distress_subscore(*self.QNUMS_EVERYDAY))}</td>
                    </tr>
                    <tr>
                        <td>{heading_selfcare}</td>
                        <td>{answer(self.frequency_subscore(*self.QNUMS_SELF))}</td>
                        <td>{answer(self.distress_subscore(*self.QNUMS_SELF))}</td>
                    </tr>
                    <tr>
                        <td>{heading_behaviour}</td>
                        <td>{answer(self.frequency_subscore(*self.QNUMS_BEHAVIOUR))}</td>
                        <td>{answer(self.distress_subscore(*self.QNUMS_BEHAVIOUR))}</td>
                    </tr>
                    <tr>
                        <td>{heading_mood}</td>
                        <td>{answer(self.frequency_subscore(*self.QNUMS_MOOD))}</td>
                        <td>{answer(self.distress_subscore(*self.QNUMS_MOOD))}</td>
                    </tr>
                    <tr>
                        <td>{heading_beliefs}</td>
                        <td>{answer(self.frequency_subscore(*self.QNUMS_BELIEFS))}</td>
                        <td>{answer(self.distress_subscore(*self.QNUMS_BELIEFS))}</td>
                    </tr>
                    <tr>
                        <td>{heading_eating}</td>
                        <td>{answer(self.frequency_subscore(*self.QNUMS_EATING))}</td>
                        <td>{answer(self.distress_subscore(*self.QNUMS_EATING))}</td>
                    </tr>
                    <tr>
                        <td>{heading_sleep}</td>
                        <td>{answer(self.frequency_subscore(*self.QNUMS_SLEEP))}</td>
                        <td>{answer(self.distress_subscore(*self.QNUMS_SLEEP))}</td>
                    </tr>
                    <tr>
                        <td>{heading_motor}</td>
                        <td>{answer(self.frequency_subscore(*self.QNUMS_STEREOTYPY))}</td>
                        <td>{answer(self.distress_subscore(*self.QNUMS_STEREOTYPY))}</td>
                    </tr>
                    <tr>
                        <td>{heading_motivation}</td>
                        <td>{answer(self.frequency_subscore(*self.QNUMS_MOTIVATION))}</td>
                        <td>{answer(self.distress_subscore(*self.QNUMS_MOTIVATION))}</td>
                    </tr>
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                {tr(
                    "Respondent confirmed that blanks are deliberate (N/A)",
                    answer(get_yes_no(req, self.confirm_blanks))
                )}
                {tr("Comments", answer(self.comments, default=""))}
            </table>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="50%">Question</th>
                    <th width="25%">Frequency (0–4)</th>
                    <th width="25%">Distress (0–4)</th>
                </tr>
                {subheading_spanning_three_columns(heading_memory)}
                {get_question_rows(*self.QNUMS_MEMORY)}
                {subheading_spanning_three_columns(heading_everyday)}
                {get_question_rows(*self.QNUMS_EVERYDAY)}
                {subheading_spanning_three_columns(heading_selfcare)}
                {get_question_rows(*self.QNUMS_SELF)}
                {subheading_spanning_three_columns(heading_behaviour)}
                {get_question_rows(*self.QNUMS_BEHAVIOUR)}
                {subheading_spanning_three_columns(heading_mood)}
                {get_question_rows(*self.QNUMS_MOOD)}
                {subheading_spanning_three_columns(heading_beliefs)}
                {get_question_rows(*self.QNUMS_BELIEFS)}
                {subheading_spanning_three_columns(heading_eating)}
                {get_question_rows(*self.QNUMS_EATING)}
                {subheading_spanning_three_columns(heading_sleep)}
                {get_question_rows(*self.QNUMS_SLEEP)}
                {subheading_spanning_three_columns(heading_motor)}
                {get_question_rows(*self.QNUMS_STEREOTYPY)}
                {subheading_spanning_three_columns(heading_motivation)}
                {get_question_rows(*self.QNUMS_MOTIVATION)}
            </table>
        """
        return h
Example #27
0
class Smast(TaskHasPatientMixin, Task, metaclass=SmastMetaclass):
    """
    Server implementation of the SMAST task.
    """
    __tablename__ = "smast"
    shortname = "SMAST"
    longname = "Short Michigan Alcohol Screening Test"
    provides_trackers = True

    NQUESTIONS = 13
    TASK_FIELDS = strseq("q", 1, NQUESTIONS)

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label="SMAST total score",
                axis_label="Total score (out of {})".format(self.NQUESTIONS),
                axis_min=-0.5,
                axis_max=self.NQUESTIONS + 0.5,
                horizontal_lines=[
                    2.5,
                    1.5,
                ],
                horizontal_labels=[
                    TrackerLabel(4, self.wxstring(req, "problem_probable")),
                    TrackerLabel(2, self.wxstring(req, "problem_possible")),
                    TrackerLabel(0.75, self.wxstring(req, "problem_unlikely")),
                ])
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [
            CtvInfo(content="SMAST total score {}/{} ({})".format(
                self.total_score(), self.NQUESTIONS, self.likelihood(req)))
        ]

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(name="total",
                           coltype=Integer(),
                           value=self.total_score(),
                           comment="Total score (/{})".format(
                               self.NQUESTIONS)),
            SummaryElement(name="likelihood",
                           coltype=SummaryCategoryColType,
                           value=self.likelihood(req),
                           comment="Likelihood of problem"),
        ]

    def is_complete(self) -> bool:
        return (self.are_all_fields_complete(self.TASK_FIELDS)
                and self.field_contents_valid())

    def get_score(self, q: int) -> int:
        yes = "Y"
        value = getattr(self, "q" + str(q))
        if value is None:
            return 0
        if q == 1 or q == 4 or q == 5:
            return 0 if value == yes else 1
        else:
            return 1 if value == yes else 0

    def total_score(self) -> int:
        total = 0
        for q in range(1, self.NQUESTIONS + 1):
            total += self.get_score(q)
        return total

    def likelihood(self, req: CamcopsRequest) -> str:
        score = self.total_score()
        if score >= 3:
            return self.wxstring(req, "problem_probable")
        elif score >= 2:
            return self.wxstring(req, "problem_possible")
        else:
            return self.wxstring(req, "problem_unlikely")

    def get_task_html(self, req: CamcopsRequest) -> str:
        score = self.total_score()
        likelihood = self.likelihood(req)
        main_dict = {
            None: None,
            "Y": req.wappstring("yes"),
            "N": req.wappstring("no")
        }
        q_a = ""
        for q in range(1, self.NQUESTIONS + 1):
            q_a += tr(
                self.wxstring(req, "q" + str(q)),
                answer(get_from_dict(main_dict, getattr(self, "q" + str(q)))) +
                " — " + str(self.get_score(q)))
        h = """
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {tr_is_complete}
                    {total_score}
                    {problem_likelihood}
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="80%">Question</th>
                    <th width="20%">Answer</th>
                </tr>
                {q_a}
            </table>
            <div class="{CssClass.FOOTNOTES}">
                [1] Total score ≥3 probable, ≥2 possible, 0–1 unlikely.
            </div>
        """.format(
            CssClass=CssClass,
            tr_is_complete=self.get_is_complete_tr(req),
            total_score=tr(req.wappstring("total_score"),
                           answer(score) + " / {}".format(self.NQUESTIONS)),
            problem_likelihood=tr_qa(
                self.wxstring(req, "problem_likelihood") + " <sup>[1]</sup>",
                likelihood),
            q_a=q_a,
        )
        return h

    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        if not self.is_complete():
            return []
        return [SnomedExpression(req.snomed(SnomedLookup.SMAST_SCALE))]
Example #28
0
class Honos65(HonosBase, metaclass=Honos65Metaclass):
    """
    Server implementation of the HoNOS 65+ task.
    """
    __tablename__ = "honos65"
    shortname = "HoNOS 65+"
    longname = "Health of the Nation Outcome Scales, older adults"

    q8problemtype = CamcopsColumn(
        "q8problemtype",
        CharColType,
        permitted_value_checker=PermittedValueChecker(
            permitted_values=PV_PROBLEMTYPE),
        comment="Q8: type of problem (A phobic; B anxiety; "
        "C obsessive-compulsive; D stress; "  # NB slight difference: D
        "E dissociative; F somatoform; G eating; H sleep; "
        "I sexual; J other, specify)")
    q8otherproblem = Column("q8otherproblem",
                            UnicodeText,
                            comment="Q8: other problem: specify")

    NQUESTIONS = 12
    QFIELDS = strseq("q", 1, NQUESTIONS)
    MAX_SCORE = 48

    # noinspection PyUnresolvedReferences
    def is_complete(self) -> bool:
        if not self.are_all_fields_complete(self.QFIELDS):
            return False
        if not self.field_contents_valid():
            return False
        if self.q8 != 0 and self.q8 != 9 and self.q8problemtype is None:
            return False
        if self.q8 != 0 and self.q8 != 9 and self.q8problemtype == "J" \
                and self.q8otherproblem is None:
            return False
        return self.period_rated is not None

    def get_task_html(self, req: CamcopsRequest) -> str:
        q8_problem_type_dict = {
            None: None,
            "A": self.wxstring(req, "q8problemtype_option_a"),
            "B": self.wxstring(req, "q8problemtype_option_b"),
            "C": self.wxstring(req, "q8problemtype_option_c"),
            "D": self.wxstring(req, "q8problemtype_option_d"),
            "E": self.wxstring(req, "q8problemtype_option_e"),
            "F": self.wxstring(req, "q8problemtype_option_f"),
            "G": self.wxstring(req, "q8problemtype_option_g"),
            "H": self.wxstring(req, "q8problemtype_option_h"),
            "I": self.wxstring(req, "q8problemtype_option_i"),
            "J": self.wxstring(req, "q8problemtype_option_j"),
        }
        one_to_eight = ""
        for i in range(1, 8 + 1):
            one_to_eight += tr_qa(
                self.get_q(req, i),
                self.get_answer(req, i, getattr(self, "q" + str(i))))
        nine_onwards = ""
        for i in range(9, Honos.NQUESTIONS + 1):
            nine_onwards += tr_qa(
                self.get_q(req, i),
                self.get_answer(req, i, getattr(self, "q" + str(i))))

        h = """
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {tr_is_complete}
                    {total_score}
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="50%">Question</th>
                    <th width="50%">Answer <sup>[1]</sup></th>
                </tr>
                {period_rated}
                {one_to_eight}
                {q8problemtype}
                {q8otherproblem}
                {nine_onwards}
            </table>
            <div class="{CssClass.FOOTNOTES}">
                {FOOTNOTE_SCORING}
            </div>
            {copyright_div}
        """.format(
            CssClass=CssClass,
            tr_is_complete=self.get_is_complete_tr(req),
            total_score=tr(
                req.wappstring("total_score"),
                answer(self.total_score()) + " / {}".format(self.MAX_SCORE)),
            period_rated=tr_qa(self.wxstring(req, "period_rated"),
                               self.period_rated),
            one_to_eight=one_to_eight,
            q8problemtype=tr_qa(
                self.wxstring(req, "q8problemtype_s"),
                get_from_dict(q8_problem_type_dict, self.q8problemtype)),
            q8otherproblem=tr_qa(self.wxstring(req, "q8otherproblem_s"),
                                 self.q8otherproblem),
            nine_onwards=nine_onwards,
            FOOTNOTE_SCORING=FOOTNOTE_SCORING,
            copyright_div=self.COPYRIGHT_DIV,
        )
        return h

    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        codes = [
            SnomedExpression(
                req.snomed(SnomedLookup.HONOS65_PROCEDURE_ASSESSMENT))
        ]  # noqa
        if self.is_complete():
            codes.append(
                SnomedExpression(req.snomed(SnomedLookup.HONOS65_SCALE), {
                    req.snomed(SnomedLookup.HONOS65_SCORE):
                    self.total_score(),
                }))
        return codes
Example #29
0
class Ace3(TaskHasPatientMixin,
           TaskHasClinicianMixin,
           Task,
           metaclass=Ace3Metaclass):
    """
    Server implementation of the ACE-III task.
    """

    __tablename__ = "ace3"
    shortname = "ACE-III"
    provides_trackers = True

    prohibits_commercial = True

    age_at_leaving_full_time_education = Column(
        "age_at_leaving_full_time_education",
        Integer,
        comment="Age at leaving full time education",
    )
    occupation = Column("occupation", UnicodeText, comment="Occupation")
    handedness = CamcopsColumn(
        "handedness",
        String(length=1),  # was Text
        comment="Handedness (L or R)",
        permitted_value_checker=PermittedValueChecker(
            permitted_values=["L", "R"]),
    )
    attn_num_registration_trials = Column(
        "attn_num_registration_trials",
        Integer,
        comment="Attention, repetition, number of trials (not scored)",
    )
    fluency_letters_score = CamcopsColumn(
        "fluency_letters_score",
        Integer,
        comment="Fluency, words beginning with P, score 0-7",
        permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7),
    )
    fluency_animals_score = CamcopsColumn(
        "fluency_animals_score",
        Integer,
        comment="Fluency, animals, score 0-7",
        permitted_value_checker=PermittedValueChecker(minimum=0, maximum=7),
    )
    lang_follow_command_practice = CamcopsColumn(
        "lang_follow_command_practice",
        Integer,
        comment="Language, command, practice trial (not scored)",
        permitted_value_checker=BIT_CHECKER,
    )
    lang_read_words_aloud = CamcopsColumn(
        "lang_read_words_aloud",
        Integer,
        comment="Language, read five irregular words (0 or 1)",
        permitted_value_checker=BIT_CHECKER,
    )
    vsp_copy_infinity = CamcopsColumn(
        "vsp_copy_infinity",
        Integer,
        comment="Visuospatial, copy infinity (0-1)",
        permitted_value_checker=BIT_CHECKER,
    )
    vsp_copy_cube = CamcopsColumn(
        "vsp_copy_cube",
        Integer,
        comment="Visuospatial, copy cube (0-2)",
        permitted_value_checker=PermittedValueChecker(minimum=0, maximum=2),
    )
    vsp_draw_clock = CamcopsColumn(
        "vsp_draw_clock",
        Integer,
        comment="Visuospatial, draw clock (0-5)",
        permitted_value_checker=PermittedValueChecker(minimum=0, maximum=5),
    )
    picture1_blobid = CamcopsColumn(
        "picture1_blobid",
        Integer,
        comment="Photo 1/2 PNG BLOB ID",
        is_blob_id_field=True,
        blob_relationship_attr_name="picture1",
    )
    picture1_rotation = Column(
        # DEFUNCT as of v2.0.0
        # IGNORED. REMOVE WHEN ALL PRE-2.0.0 TABLETS GONE
        "picture1_rotation",
        Integer,
        comment="Photo 1/2 rotation (degrees clockwise)",
    )
    picture2_blobid = CamcopsColumn(
        "picture2_blobid",
        Integer,
        comment="Photo 2/2 PNG BLOB ID",
        is_blob_id_field=True,
        blob_relationship_attr_name="picture2",
    )
    picture2_rotation = Column(
        # DEFUNCT as of v2.0.0
        # IGNORED. REMOVE WHEN ALL PRE-2.0.0 TABLETS GONE
        "picture2_rotation",
        Integer,
        comment="Photo 2/2 rotation (degrees clockwise)",
    )
    comments = Column("comments", UnicodeText, comment="Clinician's comments")

    picture1 = blob_relationship(
        "Ace3", "picture1_blobid")  # type: Optional[Blob]  # noqa
    picture2 = blob_relationship(
        "Ace3", "picture2_blobid")  # type: Optional[Blob]  # noqa

    ATTN_SCORE_FIELDS = (strseq("attn_time", 1, 5) +
                         strseq("attn_place", 1, 5) +
                         strseq("attn_repeat_word", 1, 3) +
                         strseq("attn_serial7_subtraction", 1, 5))
    MEM_NON_RECOG_SCORE_FIELDS = (strseq("mem_recall_word", 1, 3) +
                                  strseq("mem_repeat_address_trial3_", 1, 7) +
                                  strseq("mem_famous", 1, 4) +
                                  strseq("mem_recall_address", 1, 7))
    LANG_SIMPLE_SCORE_FIELDS = (strseq("lang_write_sentences_point", 1, 2) +
                                strseq("lang_repeat_sentence", 1, 2) +
                                strseq("lang_name_picture", 1, 12) +
                                strseq("lang_identify_concept", 1, 4))
    LANG_FOLLOW_CMD_FIELDS = strseq("lang_follow_command", 1, 3)
    LANG_REPEAT_WORD_FIELDS = strseq("lang_repeat_word", 1, 4)
    VSP_SIMPLE_SCORE_FIELDS = strseq("vsp_count_dots", 1, 4) + strseq(
        "vsp_identify_letter", 1, 4)
    BASIC_COMPLETENESS_FIELDS = (
        ATTN_SCORE_FIELDS + MEM_NON_RECOG_SCORE_FIELDS +
        ["fluency_letters_score", "fluency_animals_score"] +
        ["lang_follow_command_practice"] + LANG_SIMPLE_SCORE_FIELDS +
        LANG_REPEAT_WORD_FIELDS + [
            "lang_read_words_aloud",
            "vsp_copy_infinity",
            "vsp_copy_cube",
            "vsp_draw_clock",
        ] + VSP_SIMPLE_SCORE_FIELDS + strseq("mem_recall_address", 1, 7))

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Addenbrooke’s Cognitive Examination III")

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label="ACE-III total score",
                axis_label="Total score (out of 100)",
                axis_min=-0.5,
                axis_max=100.5,
                horizontal_lines=[82.5, 88.5],
            )
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        a = self.attn_score()
        m = self.mem_score()
        f = self.fluency_score()
        lang = self.lang_score()
        v = self.vsp_score()
        t = a + m + f + lang + v
        text = (f"ACE-III total: {t}/{TOTAL_MAX} "
                f"(attention {a}/{ATTN_MAX}, memory {m}/{MEMORY_MAX}, "
                f"fluency {f}/{FLUENCY_MAX}, language {lang}/{LANG_MAX}, "
                f"visuospatial {v}/{VSP_MAX})")
        return [CtvInfo(content=text)]

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(
                name="total",
                coltype=Integer(),
                value=self.total_score(),
                comment=f"Total score (/{TOTAL_MAX})",
            ),
            SummaryElement(
                name="attn",
                coltype=Integer(),
                value=self.attn_score(),
                comment=f"Attention (/{ATTN_MAX})",
            ),
            SummaryElement(
                name="mem",
                coltype=Integer(),
                value=self.mem_score(),
                comment=f"Memory (/{MEMORY_MAX})",
            ),
            SummaryElement(
                name="fluency",
                coltype=Integer(),
                value=self.fluency_score(),
                comment=f"Fluency (/{FLUENCY_MAX})",
            ),
            SummaryElement(
                name="lang",
                coltype=Integer(),
                value=self.lang_score(),
                comment=f"Language (/{LANG_MAX})",
            ),
            SummaryElement(
                name="vsp",
                coltype=Integer(),
                value=self.vsp_score(),
                comment=f"Visuospatial (/{VSP_MAX})",
            ),
        ]

    def attn_score(self) -> int:
        return self.sum_fields(self.ATTN_SCORE_FIELDS)

    @staticmethod
    def get_recog_score(recalled: Optional[int],
                        recognized: Optional[int]) -> int:
        if recalled == 1:
            return 1
        return score_zero_for_absent(recognized)

    @staticmethod
    def get_recog_text(recalled: Optional[int],
                       recognized: Optional[int]) -> str:
        if recalled:
            return "<i>1 (already recalled)</i>"
        return answer(recognized)

    # noinspection PyUnresolvedReferences
    def get_mem_recognition_score(self) -> int:
        score = 0
        score += self.get_recog_score(
            (self.mem_recall_address1 == 1 and self.mem_recall_address2 == 1),
            self.mem_recognize_address1,
        )
        score += self.get_recog_score((self.mem_recall_address3 == 1),
                                      self.mem_recognize_address2)
        score += self.get_recog_score(
            (self.mem_recall_address4 == 1 and self.mem_recall_address5 == 1),
            self.mem_recognize_address3,
        )
        score += self.get_recog_score((self.mem_recall_address6 == 1),
                                      self.mem_recognize_address4)
        score += self.get_recog_score((self.mem_recall_address7 == 1),
                                      self.mem_recognize_address5)
        return score

    def mem_score(self) -> int:
        return (self.sum_fields(self.MEM_NON_RECOG_SCORE_FIELDS) +
                self.get_mem_recognition_score())

    def fluency_score(self) -> int:
        return score_zero_for_absent(
            self.fluency_letters_score) + score_zero_for_absent(
                self.fluency_animals_score)

    def get_follow_command_score(self) -> int:
        if self.lang_follow_command_practice != 1:
            return 0
        return self.sum_fields(self.LANG_FOLLOW_CMD_FIELDS)

    def get_repeat_word_score(self) -> int:
        n = self.sum_fields(self.LANG_REPEAT_WORD_FIELDS)
        return 2 if n >= 4 else (1 if n == 3 else 0)

    def lang_score(self) -> int:
        return (self.sum_fields(self.LANG_SIMPLE_SCORE_FIELDS) +
                self.get_follow_command_score() +
                self.get_repeat_word_score() +
                score_zero_for_absent(self.lang_read_words_aloud))

    def vsp_score(self) -> int:
        return (self.sum_fields(self.VSP_SIMPLE_SCORE_FIELDS) +
                score_zero_for_absent(self.vsp_copy_infinity) +
                score_zero_for_absent(self.vsp_copy_cube) +
                score_zero_for_absent(self.vsp_draw_clock))

    def total_score(self) -> int:
        return (self.attn_score() + self.mem_score() + self.fluency_score() +
                self.lang_score() + self.vsp_score())

    # noinspection PyUnresolvedReferences
    def is_recognition_complete(self) -> bool:
        return (
            ((self.mem_recall_address1 == 1 and self.mem_recall_address2 == 1)
             or self.mem_recognize_address1 is not None)
            and (self.mem_recall_address3 == 1
                 or self.mem_recognize_address2 is not None) and
            ((self.mem_recall_address4 == 1 and self.mem_recall_address5 == 1)
             or self.mem_recognize_address3 is not None)
            and (self.mem_recall_address6 == 1
                 or self.mem_recognize_address4 is not None)
            and (self.mem_recall_address7 == 1
                 or self.mem_recognize_address5 is not None))

    def is_complete(self) -> bool:
        if self.any_fields_none(self.BASIC_COMPLETENESS_FIELDS):
            return False
        if not self.field_contents_valid():
            return False
        if self.lang_follow_command_practice == 1 and self.any_fields_none(
                self.LANG_FOLLOW_CMD_FIELDS):
            return False
        return self.is_recognition_complete()

    # noinspection PyUnresolvedReferences
    def get_task_html(self, req: CamcopsRequest) -> str:
        def percent(score: int, maximum: int) -> str:
            return ws.number_to_dp(100 * score / maximum, PERCENT_DP)

        a = self.attn_score()
        m = self.mem_score()
        f = self.fluency_score()
        lang = self.lang_score()
        v = self.vsp_score()
        t = a + m + f + lang + v
        if self.is_complete():
            figsize = (
                PlotDefaults.FULLWIDTH_PLOT_WIDTH / 3,
                PlotDefaults.FULLWIDTH_PLOT_WIDTH / 4,
            )
            width = 0.9
            fig = req.create_figure(figsize=figsize)
            ax = fig.add_subplot(1, 1, 1)
            scores = numpy.array([a, m, f, lang, v])
            maxima = numpy.array(
                [ATTN_MAX, MEMORY_MAX, FLUENCY_MAX, LANG_MAX, VSP_MAX])
            y = 100 * scores / maxima
            x_labels = ["Attn", "Mem", "Flu", "Lang", "VSp"]
            # noinspection PyTypeChecker
            n = len(y)
            xvar = numpy.arange(n)
            ax.bar(xvar, y, width, color="b")
            ax.set_ylabel("%", fontdict=req.fontdict)
            ax.set_xticks(xvar)
            x_offset = -0.5
            ax.set_xlim(0 + x_offset, len(scores) + x_offset)
            ax.set_xticklabels(x_labels, fontdict=req.fontdict)
            fig.tight_layout()  # or the ylabel drops off the figure
            # fig.autofmt_xdate()
            req.set_figure_font_sizes(ax)
            figurehtml = req.get_html_from_pyplot_figure(fig)
        else:
            figurehtml = "<i>Incomplete; not plotted</i>"
        return (
            self.get_standard_clinician_comments_block(req, self.comments) +
            f"""
                <div class="{CssClass.SUMMARY}">
                    <table class="{CssClass.SUMMARY}">
                        <tr>
                            {self.get_is_complete_td_pair(req)}
                            <td class="{CssClass.FIGURE}"
                                rowspan="7">{figurehtml}</td>
                        </tr>
            """ + tr("Total ACE-III score <sup>[1]</sup>",
                     answer(t) + " / 100") +
            tr(
                "Attention",
                answer(a) + f" / {ATTN_MAX} ({percent(a, ATTN_MAX)}%)",
            ) + tr(
                "Memory",
                answer(m) + f" / {MEMORY_MAX} ({percent(m, MEMORY_MAX)}%)",
            ) + tr(
                "Fluency",
                answer(f) + f" / {FLUENCY_MAX} ({percent(f, FLUENCY_MAX)}%)",
            ) + tr(
                "Language",
                answer(lang) + f" / {LANG_MAX} ({percent(lang, LANG_MAX)}%)",
            ) + tr(
                "Visuospatial",
                answer(v) + f" / {VSP_MAX} ({percent(v, VSP_MAX)}%)",
            ) + f"""
                    </table>
                </div>
                <table class="{CssClass.TASKDETAIL}">
                    <tr>
                        <th width="75%">Question</th>
                        <th width="25%">Answer/score</td>
                    </tr>
            """ + tr_qa(
                "Age on leaving full-time education",
                self.age_at_leaving_full_time_education,
            ) + tr_qa("Occupation", ws.webify(self.occupation)) +
            tr_qa("Handedness", ws.webify(self.handedness)) +
            subheading_spanning_two_columns("Attention") + tr(
                "Day? Date? Month? Year? Season?",
                ", ".join(
                    answer(x) for x in (
                        self.attn_time1,
                        self.attn_time2,
                        self.attn_time3,
                        self.attn_time4,
                        self.attn_time5,
                    )),
            ) + tr(
                "House number/floor? Street/hospital? Town? County? Country?",
                ", ".join(
                    answer(x) for x in (
                        self.attn_place1,
                        self.attn_place2,
                        self.attn_place3,
                        self.attn_place4,
                        self.attn_place5,
                    )),
            ) + tr(
                "Repeat: Lemon? Key? Ball?",
                ", ".join(
                    answer(x) for x in (
                        self.attn_repeat_word1,
                        self.attn_repeat_word2,
                        self.attn_repeat_word3,
                    )),
            ) + tr(
                "Repetition: number of trials <i>(not scored)</i>",
                answer(self.attn_num_registration_trials,
                       formatter_answer=italic),
            ) + tr(
                "Serial subtractions: First correct? Second? Third? Fourth? "
                "Fifth?",
                ", ".join(
                    answer(x) for x in (
                        self.attn_serial7_subtraction1,
                        self.attn_serial7_subtraction2,
                        self.attn_serial7_subtraction3,
                        self.attn_serial7_subtraction4,
                        self.attn_serial7_subtraction5,
                    )),
            ) + subheading_spanning_two_columns("Memory (1)") + tr(
                "Recall: Lemon? Key? Ball?",
                ", ".join(
                    answer(x) for x in (
                        self.mem_recall_word1,
                        self.mem_recall_word2,
                        self.mem_recall_word3,
                    )),
            ) + subheading_spanning_two_columns("Fluency") + tr(
                "Score for words beginning with ‘P’ <i>(≥18 scores 7, 14–17 "
                "scores 6, 11–13 scores 5, 8–10 scores 4, 6–7 scores 3, "
                "4–5 scores 2, 2–3 scores 1, 0–1 scores 0)</i>",
                answer(self.fluency_letters_score) + " / 7",
            ) + tr(
                "Score for animals <i>(≥22 scores 7, 17–21 scores 6, "
                "14–16 scores 5, 11–13 scores 4, 9–10 scores 3, "
                "7–8 scores 2, 5–6 scores 1, &lt;5 scores 0)</i>",
                answer(self.fluency_animals_score) + " / 7",
            ) + subheading_spanning_two_columns("Memory (2)") + tr(
                "Third trial of address registration: Harry? Barnes? 73? "
                "Orchard? Close? Kingsbridge? Devon?",
                ", ".join(
                    answer(x) for x in (
                        self.mem_repeat_address_trial3_1,
                        self.mem_repeat_address_trial3_2,
                        self.mem_repeat_address_trial3_3,
                        self.mem_repeat_address_trial3_4,
                        self.mem_repeat_address_trial3_5,
                        self.mem_repeat_address_trial3_6,
                        self.mem_repeat_address_trial3_7,
                    )),
            ) + tr(
                "Current PM? Woman who was PM? USA president? USA president "
                "assassinated in 1960s?",
                ", ".join(
                    answer(x) for x in (
                        self.mem_famous1,
                        self.mem_famous2,
                        self.mem_famous3,
                        self.mem_famous4,
                    )),
            ) + subheading_spanning_two_columns("Language") + tr(
                "<i>Practice trial (“Pick up the pencil and then the "
                "paper”)</i>",
                answer(self.lang_follow_command_practice,
                       formatter_answer=italic),
            ) + tr_qa(
                "“Place the paper on top of the pencil”",
                self.lang_follow_command1,
            ) + tr_qa(
                "“Pick up the pencil but not the paper”",
                self.lang_follow_command2,
            ) + tr_qa(
                "“Pass me the pencil after touching the paper”",
                self.lang_follow_command3,
            ) + tr(
                "Sentence-writing: point for ≥2 complete sentences about "
                "the one topic? Point for correct grammar and spelling?",
                ", ".join(
                    answer(x) for x in (
                        self.lang_write_sentences_point1,
                        self.lang_write_sentences_point2,
                    )),
            ) + tr(
                "Repeat: caterpillar? eccentricity? unintelligible? "
                "statistician? <i>(score 2 if all correct, 1 if 3 correct, "
                "0 if ≤2 correct)</i>",
                "<i>{}, {}, {}, {}</i> (score <b>{}</b> / 2)".format(
                    answer(self.lang_repeat_word1, formatter_answer=italic),
                    answer(self.lang_repeat_word2, formatter_answer=italic),
                    answer(self.lang_repeat_word3, formatter_answer=italic),
                    answer(self.lang_repeat_word4, formatter_answer=italic),
                    self.get_repeat_word_score(),
                ),
            ) + tr_qa(
                "Repeat: “All that glitters is not gold”?",
                self.lang_repeat_sentence1,
            ) + tr_qa(
                "Repeat: “A stitch in time saves nine”?",
                self.lang_repeat_sentence2,
            ) + tr(
                "Name pictures: spoon, book, kangaroo/wallaby",
                ", ".join(
                    answer(x) for x in (
                        self.lang_name_picture1,
                        self.lang_name_picture2,
                        self.lang_name_picture3,
                    )),
            ) + tr(
                "Name pictures: penguin, anchor, camel/dromedary",
                ", ".join(
                    answer(x) for x in (
                        self.lang_name_picture4,
                        self.lang_name_picture5,
                        self.lang_name_picture6,
                    )),
            ) + tr(
                "Name pictures: harp, rhinoceros/rhino, barrel/keg/tub",
                ", ".join(
                    answer(x) for x in (
                        self.lang_name_picture7,
                        self.lang_name_picture8,
                        self.lang_name_picture9,
                    )),
            ) + tr(
                "Name pictures: crown, alligator/crocodile, "
                "accordion/piano accordion/squeeze box",
                ", ".join(
                    answer(x) for x in (
                        self.lang_name_picture10,
                        self.lang_name_picture11,
                        self.lang_name_picture12,
                    )),
            ) + tr(
                "Identify pictures: monarchy? marsupial? Antarctic? nautical?",
                ", ".join(
                    answer(x) for x in (
                        self.lang_identify_concept1,
                        self.lang_identify_concept2,
                        self.lang_identify_concept3,
                        self.lang_identify_concept4,
                    )),
            ) + tr_qa(
                "Read all successfully: sew, pint, soot, dough, height",
                self.lang_read_words_aloud,
            ) + subheading_spanning_two_columns("Visuospatial") +
            tr("Copy infinity",
               answer(self.vsp_copy_infinity) + " / 1") +
            tr("Copy cube",
               answer(self.vsp_copy_cube) + " / 2") + tr(
                   "Draw clock with numbers and hands at 5:10",
                   answer(self.vsp_draw_clock) + " / 5",
               ) + tr(
                   "Count dots: 8, 10, 7, 9",
                   ", ".join(
                       answer(x) for x in (
                           self.vsp_count_dots1,
                           self.vsp_count_dots2,
                           self.vsp_count_dots3,
                           self.vsp_count_dots4,
                       )),
               ) + tr(
                   "Identify letters: K, M, A, T",
                   ", ".join(
                       answer(x) for x in (
                           self.vsp_identify_letter1,
                           self.vsp_identify_letter2,
                           self.vsp_identify_letter3,
                           self.vsp_identify_letter4,
                       )),
               ) + subheading_spanning_two_columns("Memory (3)") + tr(
                   "Recall address: Harry? Barnes? 73? Orchard? Close? "
                   "Kingsbridge? Devon?",
                   ", ".join(
                       answer(x) for x in (
                           self.mem_recall_address1,
                           self.mem_recall_address2,
                           self.mem_recall_address3,
                           self.mem_recall_address4,
                           self.mem_recall_address5,
                           self.mem_recall_address6,
                           self.mem_recall_address7,
                       )),
               ) +
            tr(
                "Recognize address: Jerry Barnes/Harry Barnes/Harry Bradford?",
                self.get_recog_text(
                    (self.mem_recall_address1 == 1
                     and self.mem_recall_address2 == 1),
                    self.mem_recognize_address1,
                ),
            ) + tr(
                "Recognize address: 37/73/76?",
                self.get_recog_text(
                    (self.mem_recall_address3 == 1),
                    self.mem_recognize_address2,
                ),
            ) + tr(
                "Recognize address: Orchard Place/Oak Close/Orchard "
                "Close?",
                self.get_recog_text(
                    (self.mem_recall_address4 == 1
                     and self.mem_recall_address5 == 1),
                    self.mem_recognize_address3,
                ),
            ) + tr(
                "Recognize address: Oakhampton/Kingsbridge/Dartington?",
                self.get_recog_text(
                    (self.mem_recall_address6 == 1),
                    self.mem_recognize_address4,
                ),
            ) + tr(
                "Recognize address: Devon/Dorset/Somerset?",
                self.get_recog_text(
                    (self.mem_recall_address7 == 1),
                    self.mem_recognize_address5,
                ),
            ) + subheading_spanning_two_columns("Photos of test sheet") +
            tr_span_col(get_blob_img_html(self.picture1),
                        td_class=CssClass.PHOTO) +
            tr_span_col(get_blob_img_html(self.picture2),
                        td_class=CssClass.PHOTO) + f"""
                </table>
                <div class="{CssClass.FOOTNOTES}">
                    [1] In the ACE-R (the predecessor of the ACE-III),
                    scores ≤82 had sensitivity 0.84 and specificity 1.0 for
                    dementia, and scores ≤88 had sensitivity 0.94 and
                    specificity 0.89 for dementia, in a context of patients
                    with AlzD, FTD, LBD, MCI, and controls
                    (Mioshi et al., 2006, PMID 16977673).
                </div>
                <div class="{CssClass.COPYRIGHT}">
                    ACE-III: Copyright © 2012, John Hodges.
                    “The ACE-III is available for free. The copyright is held
                    by Professor John Hodges who is happy for the test to be
                    used in clinical practice and research projects. There is
                    no need to contact us if you wish to use the ACE-III in
                    clinical practice.”
                    (ACE-III FAQ, 7 July 2013, www.neura.edu.au).
                </div>
            """)

    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        codes = [
            SnomedExpression(
                req.snomed(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT))
        ]
        # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_ATTENTION_ORIENTATION)  # noqa
        # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_MEMORY)
        # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_FLUENCY)
        # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_LANGUAGE)
        # add(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT_SUBSCALE_VISUOSPATIAL)
        if self.is_complete():  # could refine: is each subscale complete?
            a = self.attn_score()
            m = self.mem_score()
            f = self.fluency_score()
            lang = self.lang_score()
            v = self.vsp_score()
            t = a + m + f + lang + v
            codes.append(
                SnomedExpression(
                    req.snomed(SnomedLookup.ACE_R_SCALE),
                    {
                        req.snomed(SnomedLookup.ACE_R_SCORE):
                        t,
                        req.snomed(SnomedLookup.ACE_R_SUBSCORE_ATTENTION_ORIENTATION):
                        a,  # noqa
                        req.snomed(SnomedLookup.ACE_R_SUBSCORE_MEMORY):
                        m,
                        req.snomed(SnomedLookup.ACE_R_SUBSCORE_FLUENCY):
                        f,
                        req.snomed(SnomedLookup.ACE_R_SUBSCORE_LANGUAGE):
                        lang,
                        req.snomed(SnomedLookup.ACE_R_SUBSCORE_VISUOSPATIAL):
                        v,
                    },
                ))
        return codes
Example #30
0
class Honosca(HonosBase, metaclass=HonoscaMetaclass):
    """
    Server implementation of the HoNOSCA task.
    """
    __tablename__ = "honosca"
    shortname = "HoNOSCA"
    longname = "Health of the Nation Outcome Scales, Children and Adolescents"

    NQUESTIONS = 15
    QFIELDS = strseq("q", 1, NQUESTIONS)
    LAST_SECTION_A_Q = 13
    FIRST_SECTION_B_Q = 14
    SECTION_A_QFIELDS = strseq("q", 1, LAST_SECTION_A_Q)
    SECTION_B_QFIELDS = strseq("q", FIRST_SECTION_B_Q, NQUESTIONS)
    MAX_SCORE = 60
    MAX_SECTION_A = 4 * len(SECTION_A_QFIELDS)
    MAX_SECTION_B = 4 * len(SECTION_B_QFIELDS)
    TASK_FIELDS = QFIELDS + ["period_rated"]

    def is_complete(self) -> bool:
        return (self.are_all_fields_complete(self.TASK_FIELDS)
                and self.field_contents_valid())

    def section_a_score(self) -> int:
        return self._total_score_for_fields(self.SECTION_A_QFIELDS)

    def section_b_score(self) -> int:
        return self._total_score_for_fields(self.SECTION_B_QFIELDS)

    def get_task_html(self, req: CamcopsRequest) -> str:
        section_a = ""
        for i in range(1, 13 + 1):
            section_a += tr_qa(
                self.get_q(req, i),
                self.get_answer(req, i, getattr(self, "q" + str(i))))
        section_b = ""
        for i in range(14, self.NQUESTIONS + 1):
            section_b += tr_qa(
                self.get_q(req, i),
                self.get_answer(req, i, getattr(self, "q" + str(i))))

        h = """
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {tr_is_complete}
                    {total_score}
                    {section_a_total}
                    {section_b_total}
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="50%">Question</th>
                    <th width="50%">Answer <sup>[1]</sup></th>
                </tr>
                {period_rated}
                {section_a_subhead}
                {section_a}
                {section_b_subhead}
                {section_b}
            </table>
            <div class="{CssClass.FOOTNOTES}">
                {FOOTNOTE_SCORING}
            </div>
            {copyright_div}
        """.format(
            CssClass=CssClass,
            tr_is_complete=self.get_is_complete_tr(req),
            total_score=tr(
                req.wappstring("total_score"),
                answer(self.total_score()) + " / {}".format(self.MAX_SCORE)),
            section_a_total=tr(
                self.wxstring(req, "section_a_total"),
                answer(self.section_a_score()) +
                " / {}".format(self.MAX_SECTION_A)),
            section_b_total=tr(
                self.wxstring(req, "section_b_total"),
                answer(self.section_b_score()) +
                " / {}".format(self.MAX_SECTION_B)),
            period_rated=tr_qa(self.wxstring(req, "period_rated"),
                               self.period_rated),
            section_a_subhead=subheading_spanning_two_columns(
                self.wxstring(req, "section_a_title")),
            section_a=section_a,
            section_b_subhead=subheading_spanning_two_columns(
                self.wxstring(req, "section_b_title")),
            section_b=section_b,
            FOOTNOTE_SCORING=FOOTNOTE_SCORING,
            copyright_div=self.COPYRIGHT_DIV,
        )
        return h

    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        codes = [
            SnomedExpression(
                req.snomed(SnomedLookup.HONOSCA_PROCEDURE_ASSESSMENT))
        ]  # noqa
        if self.is_complete():
            a = self.section_a_score()
            b = self.section_b_score()
            total = a + b
            codes.append(
                SnomedExpression(
                    req.snomed(SnomedLookup.HONOSCA_SCALE),
                    {
                        req.snomed(SnomedLookup.HONOSCA_SCORE):
                        total,
                        req.snomed(SnomedLookup.HONOSCA_SECTION_A_SCORE):
                        a,
                        req.snomed(SnomedLookup.HONOSCA_SECTION_B_SCORE):
                        b,
                        req.snomed(SnomedLookup.HONOSCA_SECTION_A_PLUS_B_SCORE):
                        total,  # noqa
                    }))
        return codes