Example #1
0
class PhotoSequenceSinglePhoto(GenericTabletRecordMixin, Base):
    __tablename__ = "photosequence_photos"

    photosequence_id = Column("photosequence_id",
                              Integer,
                              nullable=False,
                              comment="Tablet FK to photosequence")
    seqnum = Column("seqnum",
                    Integer,
                    nullable=False,
                    comment="Sequence number of this photo "
                    "(consistently 1-based as of 2018-12-01)")
    description = Column("description",
                         UnicodeText,
                         comment="Description of the photograph")
    photo_blobid = CamcopsColumn(
        "photo_blobid",
        Integer,
        is_blob_id_field=True,
        blob_relationship_attr_name="photo",
        comment="ID of the BLOB (foreign key to blobs.id, given "
        "matching device and current/frozen record status)")
    # IGNORED. REMOVE WHEN ALL PRE-2.0.0 TABLETS GONE:
    rotation = Column(  # DEFUNCT as of v2.0.0
        "rotation",
        Integer,
        comment="(DEFUNCT COLUMN) "
        "Rotation (clockwise, in degrees) to be applied for viewing")

    photo = blob_relationship("PhotoSequenceSinglePhoto", "photo_blobid")

    def get_html_table_rows(self) -> str:
        # noinspection PyTypeChecker
        return """
            <tr class="{CssClass.SUBHEADING}">
                <td>Photo {num}: <b>{description}</b></td>
            </tr>
            <tr><td>{photo}</td></tr>
        """.format(CssClass=CssClass,
                   num=self.seqnum,
                   description=ws.webify(self.description),
                   photo=get_blob_img_html(self.photo))
Example #2
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 #3
0
class Slums(TaskHasClinicianMixin, TaskHasPatientMixin, Task):
    """
    Server implementation of the SLUMS task.
    """

    __tablename__ = "slums"
    shortname = "SLUMS"
    provides_trackers = True

    alert = CamcopsColumn(
        "alert",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="Is the patient alert? (0 no, 1 yes)",
    )
    highschooleducation = CamcopsColumn(
        "highschooleducation",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="Does that patient have at least a high-school level of "
        "education? (0 no, 1 yes)",
    )

    q1 = CamcopsColumn(
        "q1",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="Q1 (day) (0-1)",
    )
    q2 = CamcopsColumn(
        "q2",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="Q2 (year) (0-1)",
    )
    q3 = CamcopsColumn(
        "q3",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="Q3 (state) (0-1)",
    )
    q5a = CamcopsColumn(
        "q5a",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="Q5a (money spent) (0-1)",
    )
    q5b = CamcopsColumn(
        "q5b",
        Integer,
        permitted_value_checker=ZERO_OR_TWO_CHECKER,
        comment="Q5b (money left) (0 or 2)",
    )  # worth 2 points
    q6 = CamcopsColumn(
        "q6",
        Integer,
        permitted_value_checker=ZERO_TO_THREE_CHECKER,
        comment="Q6 (animal naming) (0-3)",
    )  # from 0 to 3 points
    q7a = CamcopsColumn(
        "q7a",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="Q7a (recall apple) (0-1)",
    )
    q7b = CamcopsColumn(
        "q7b",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="Q7b (recall pen) (0-1)",
    )
    q7c = CamcopsColumn(
        "q7c",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="Q7c (recall tie) (0-1)",
    )
    q7d = CamcopsColumn(
        "q7d",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="Q7d (recall house) (0-1)",
    )
    q7e = CamcopsColumn(
        "q7e",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="Q7e (recall car) (0-1)",
    )
    q8b = CamcopsColumn(
        "q8b",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="Q8b (reverse 648) (0-1)",
    )
    q8c = CamcopsColumn(
        "q8c",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="Q8c (reverse 8537) (0-1)",
    )
    q9a = CamcopsColumn(
        "q9a",
        Integer,
        permitted_value_checker=ZERO_OR_TWO_CHECKER,
        comment="Q9a (clock - hour markers) (0 or 2)",
    )  # worth 2 points
    q9b = CamcopsColumn(
        "q9b",
        Integer,
        permitted_value_checker=ZERO_OR_TWO_CHECKER,
        comment="Q9b (clock - time) (0 or 2)",
    )  # worth 2 points
    q10a = CamcopsColumn(
        "q10a",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="Q10a (X in triangle) (0-1)",
    )
    q10b = CamcopsColumn(
        "q10b",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="Q10b (biggest figure) (0-1)",
    )
    q11a = CamcopsColumn(
        "q11a",
        Integer,
        permitted_value_checker=ZERO_OR_TWO_CHECKER,
        comment="Q11a (story - name) (0 or 2)",
    )  # worth 2 points
    q11b = CamcopsColumn(
        "q11b",
        Integer,
        permitted_value_checker=ZERO_OR_TWO_CHECKER,
        comment="Q11b (story - occupation) (0 or 2)",
    )  # worth 2 points
    q11c = CamcopsColumn(
        "q11c",
        Integer,
        permitted_value_checker=ZERO_OR_TWO_CHECKER,
        comment="Q11c (story - back to work) (0 or 2)",
    )  # worth 2 points
    q11d = CamcopsColumn(
        "q11d",
        Integer,
        permitted_value_checker=ZERO_OR_TWO_CHECKER,
        comment="Q11d (story - state) (0 or 2)",
    )  # worth 2 points

    clockpicture_blobid = CamcopsColumn(
        "clockpicture_blobid",
        Integer,
        is_blob_id_field=True,
        blob_relationship_attr_name="clockpicture",
        comment="BLOB ID of clock picture",
    )
    shapespicture_blobid = CamcopsColumn(
        "shapespicture_blobid",
        Integer,
        is_blob_id_field=True,
        blob_relationship_attr_name="shapespicture",
        comment="BLOB ID of shapes picture",
    )
    comments = Column("comments", UnicodeText, comment="Clinician's comments")

    clockpicture = blob_relationship(
        "Slums", "clockpicture_blobid")  # type: Optional[Blob]  # noqa
    shapespicture = blob_relationship(
        "Slums", "shapespicture_blobid")  # type: Optional[Blob]  # noqa

    PREAMBLE_FIELDS = ["alert", "highschooleducation"]
    SCORED_FIELDS = [
        "q1",
        "q2",
        "q3",
        "q5a",
        "q5b",
        "q6",
        "q7a",
        "q7b",
        "q7c",
        "q7d",
        "q7e",
        "q8b",
        "q8c",
        "q9a",
        "q9b",
        "q10a",
        "q10b",
        "q11a",
        "q11b",
        "q11c",
        "q11d",
    ]
    MAX_SCORE = 30

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("St Louis University Mental Status")

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        if self.highschooleducation == 1:
            hlines = [26.5, 20.5]
            y_upper = 28.25
            y_middle = 23.5
        else:
            hlines = [24.5, 19.5]
            y_upper = 27.25
            y_middle = 22
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label="SLUMS 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=hlines,
                horizontal_labels=[
                    TrackerLabel(y_upper, req.sstring(SS.NORMAL)),
                    TrackerLabel(y_middle, self.wxstring(req, "category_mci")),
                    TrackerLabel(17, self.wxstring(req, "category_dementia")),
                ],
            )
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [
            CtvInfo(content=f"SLUMS total score "
                    f"{self.total_score()}/{self.MAX_SCORE} "
                    f"({self.category(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="category",
                coltype=SummaryCategoryColType,
                value=self.category(req),
                comment="Category",
            ),
        ]

    def is_complete(self) -> bool:
        return (self.all_fields_not_none(self.PREAMBLE_FIELDS +
                                         self.SCORED_FIELDS)
                and self.field_contents_valid())

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

    def category(self, req: CamcopsRequest) -> str:
        score = self.total_score()
        if self.highschooleducation == 1:
            if score >= 27:
                return req.sstring(SS.NORMAL)
            elif score >= 21:
                return self.wxstring(req, "category_mci")
            else:
                return self.wxstring(req, "category_dementia")
        else:
            if score >= 25:
                return req.sstring(SS.NORMAL)
            elif score >= 20:
                return self.wxstring(req, "category_mci")
            else:
                return self.wxstring(req, "category_dementia")

    def get_task_html(self, req: CamcopsRequest) -> str:
        score = self.total_score()
        category = self.category(req)
        h = """
            {clinician_comments}
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {tr_is_complete}
                    {total_score}
                    {category}
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="80%">Question</th>
                    <th width="20%">Score</th>
                </tr>
        """.format(
            clinician_comments=self.get_standard_clinician_comments_block(
                req, self.comments),
            CssClass=CssClass,
            tr_is_complete=self.get_is_complete_tr(req),
            total_score=tr(
                req.sstring(SS.TOTAL_SCORE),
                answer(score) + f" / {self.MAX_SCORE}",
            ),
            category=tr_qa(
                req.sstring(SS.CATEGORY) + " <sup>[1]</sup>", category),
        )
        h += tr_qa(self.wxstring(req, "alert_s"),
                   get_yes_no_none(req, self.alert))
        h += tr_qa(
            self.wxstring(req, "highschool_s"),
            get_yes_no_none(req, self.highschooleducation),
        )
        h += tr_qa(self.wxstring(req, "q1_s"), self.q1)
        h += tr_qa(self.wxstring(req, "q2_s"), self.q2)
        h += tr_qa(self.wxstring(req, "q3_s"), self.q3)
        h += tr(
            "Q5 <sup>[2]</sup> (money spent, money left "
            "[<i>scores 2</i>]",
            ", ".join(answer(x) for x in (self.q5a, self.q5b)),
        )
        h += tr_qa(
            "Q6 (animal fluency) [<i>≥15 scores 3, 10–14 scores 2, "
            "5–9 scores 1, 0–4 scores 0</i>]",
            self.q6,
        )
        h += tr(
            "Q7 (recall: apple, pen, tie, house, car)",
            ", ".join(
                answer(x)
                for x in (self.q7a, self.q7b, self.q7c, self.q7d, self.q7e)),
        )
        h += tr(
            "Q8 (backwards: 648, 8537)",
            ", ".join(answer(x) for x in (self.q8b, self.q8c)),
        )
        h += tr(
            "Q9 (clock: hour markers, time [<i>score 2 each</i>]",
            ", ".join(answer(x) for x in (self.q9a, self.q9b)),
        )
        h += tr(
            "Q10 (X in triangle; which is biggest?)",
            ", ".join(answer(x) for x in (self.q10a, self.q10b)),
        )
        h += tr(
            "Q11 (story: Female’s name? Job? When back to work? "
            "State she lived in? [<i>score 2 each</i>])",
            ", ".join(
                answer(x)
                for x in (self.q11a, self.q11b, self.q11c, self.q11d)),
        )
        h += f"""
            </table>
            <table class="{CssClass.TASKDETAIL}">
        """
        h += subheading_spanning_two_columns("Images of tests: clock, shapes")
        # noinspection PyTypeChecker
        h += tr(
            td(
                get_blob_img_html(self.clockpicture),
                td_width="50%",
                td_class=CssClass.PHOTO,
            ),
            td(
                get_blob_img_html(self.shapespicture),
                td_width="50%",
                td_class=CssClass.PHOTO,
            ),
            literal=True,
        )
        h += f"""
            </table>
            <div class="{CssClass.FOOTNOTES}">
                [1] With high school education:
                ≥27 normal, ≥21 MCI, ≤20 dementia.
                Without high school education:
                ≥25 normal, ≥20 MCI, ≤19 dementia.
                (Tariq et al. 2006, PubMed ID 17068312.)
                [2] Q4 (learning the five words) isn’t scored.
            </div>
        """
        return h
class DemoQuestionnaire(Task, metaclass=DemoQuestionnaireMetaclass):
    """
    Server implementation of the demo questionnaire task.
    """

    __tablename__ = "demoquestionnaire"
    shortname = "Demo"
    is_anonymous = True

    mcqtext_1a = Column("mcqtext_1a", UnicodeText)
    mcqtext_1b = Column("mcqtext_1b", UnicodeText)
    mcqtext_2a = Column("mcqtext_2a", UnicodeText)
    mcqtext_2b = Column("mcqtext_2b", UnicodeText)
    mcqtext_3a = Column("mcqtext_3a", UnicodeText)
    mcqtext_3b = Column("mcqtext_3b", UnicodeText)
    typedvar_text = Column("typedvar_text", UnicodeText)
    typedvar_text_multiline = Column("typedvar_text_multiline", UnicodeText)
    typedvar_text_rich = Column("typedvar_text_rich", UnicodeText)  # v2
    typedvar_int = Column("typedvar_int", Integer)
    typedvar_real = Column("typedvar_real", Float)
    date_only = Column("date_only", Date)
    date_time = Column("date_time", PendulumDateTimeAsIsoTextColType)
    thermometer = Column("thermometer", Integer)
    diagnosticcode_code = Column("diagnosticcode_code", DiagnosticCodeColType)
    diagnosticcode_description = CamcopsColumn(
        "diagnosticcode_description",
        UnicodeText,
        exempt_from_anonymisation=True,
    )
    diagnosticcode2_code = Column("diagnosticcode2_code",
                                  DiagnosticCodeColType)  # v2
    diagnosticcode2_description = CamcopsColumn(
        "diagnosticcode2_description",
        UnicodeText,
        exempt_from_anonymisation=True,
    )  # v2
    photo_blobid = CamcopsColumn(
        "photo_blobid",
        Integer,
        is_blob_id_field=True,
        blob_relationship_attr_name="photo",
    )
    # IGNORED. REMOVE WHEN ALL PRE-2.0.0 TABLETS GONE:
    photo_rotation = Column("photo_rotation",
                            Integer)  # DEFUNCT as of v2.0.0  # noqa
    canvas_blobid = CamcopsColumn(
        "canvas_blobid",
        Integer,
        is_blob_id_field=True,
        blob_relationship_attr_name="canvas",
    )
    canvas2_blobid = CamcopsColumn(
        "canvas2_blobid",
        Integer,
        is_blob_id_field=True,
        blob_relationship_attr_name="canvas2",
    )
    spinbox_int = Column("spinbox_int", Integer)  # v2
    spinbox_real = Column("spinbox_real", Float)  # v2
    time_only = Column("time_only", Time)  # v2

    photo = blob_relationship("DemoQuestionnaire",
                              "photo_blobid")  # type: Optional[Blob]  # noqa
    canvas = blob_relationship("DemoQuestionnaire",
                               "canvas_blobid")  # type: Optional[Blob]  # noqa
    canvas2 = blob_relationship(
        "DemoQuestionnaire", "canvas2_blobid")  # type: Optional[Blob]  # noqa

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

    # noinspection PyMethodOverriding
    @staticmethod
    def is_complete() -> bool:
        return True

    def get_task_html(self, req: CamcopsRequest) -> str:
        h = f"""
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {self.get_is_complete_tr(req)}
                </table>
            </div>
            <div class="{CssClass.EXPLANATION}">
                This is a demo questionnaire, containing no genuine
                information.
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="50%">Question</th>
                    <th width="50%">Answer</th>
                </tr>
        """
        for i in range(1, N_MCQ + 1):
            h += self.get_twocol_val_row("mcq" + str(i))
        for i in range(1, N_MCQBOOL + 1):
            h += self.get_twocol_bool_row(req, "mcqbool" + str(i))
        for i in range(1, N_MULTIPLERESPONSE + 1):
            h += self.get_twocol_bool_row(req, "multipleresponse" + str(i))
        for i in range(1, N_BOOLTEXT + 1):
            h += self.get_twocol_bool_row(req, "booltext" + str(i))
        for i in range(1, N_BOOLIMAGE + 1):
            h += self.get_twocol_bool_row(req, "boolimage" + str(i))
        for i in range(1, N_PICKER + 1):
            h += self.get_twocol_val_row("picker" + str(i))
        for i in range(1, N_SLIDER + 1):
            h += self.get_twocol_val_row("slider" + str(i))
        h += self.get_twocol_string_row("mcqtext_1a")
        h += self.get_twocol_string_row("mcqtext_1b")
        h += self.get_twocol_string_row("mcqtext_2a")
        h += self.get_twocol_string_row("mcqtext_2b")
        h += self.get_twocol_string_row("mcqtext_3a")
        h += self.get_twocol_string_row("mcqtext_3b")
        h += self.get_twocol_string_row("typedvar_text")
        h += self.get_twocol_string_row("typedvar_text_multiline")
        h += self.get_twocol_string_row("typedvar_text_rich")
        h += self.get_twocol_val_row("typedvar_int")
        h += self.get_twocol_val_row("typedvar_real")
        h += self.get_twocol_val_row("date_only")
        h += self.get_twocol_val_row("date_time")
        h += self.get_twocol_val_row("thermometer")
        h += self.get_twocol_string_row("diagnosticcode_code")
        h += self.get_twocol_string_row("diagnosticcode_description")
        h += self.get_twocol_string_row("diagnosticcode2_code")
        h += self.get_twocol_string_row("diagnosticcode2_description")
        # noinspection PyTypeChecker
        h += self.get_twocol_picture_row(self.photo, "photo")
        # noinspection PyTypeChecker
        h += self.get_twocol_picture_row(self.canvas, "canvas")
        # noinspection PyTypeChecker
        h += self.get_twocol_picture_row(self.canvas2, "canvas2")
        h += ("""
            </table>

            <div>
                In addition to the data (above), this task demonstrates
                HTML/CSS styles used in the CamCOPS views.
            </div>

            <h1>Header 1</h1>
            <h2>Header 2</h2>
            <h3>Header 3</h3>

            <div>
                Plain div with <sup>superscript</sup> and <sub>subscript</sub>.
                <br>
                Answers look like this: """ + answer("Answer") + """<br>
                Missing answers look liks this: """ + answer(None) + """<br>
            </div>
        """)
        h += divtest(CssClass.BAD_ID_POLICY_MILD)
        h += divtest(CssClass.BAD_ID_POLICY_SEVERE)
        h += divtest(CssClass.CLINICIAN)
        h += divtest(CssClass.COPYRIGHT)
        h += divtest(CssClass.ERROR)
        h += divtest(CssClass.EXPLANATION)
        h += divtest(CssClass.FOOTNOTES)
        h += divtest(CssClass.FORMTITLE)
        h += divtest(CssClass.GREEN)
        h += divtest(CssClass.HEADING)
        h += divtest(CssClass.IMPORTANT)
        h += divtest(CssClass.INCOMPLETE)
        h += divtest(CssClass.INDENTED)
        h += divtest(CssClass.LIVE_ON_TABLET)
        h += divtest(CssClass.NAVIGATION)
        h += divtest(CssClass.OFFICE)
        h += divtest(CssClass.PATIENT)
        h += divtest(CssClass.RESPONDENT)
        h += divtest(CssClass.SMALLPRINT)
        h += divtest(CssClass.SPECIALNOTE)
        h += divtest(CssClass.SUBHEADING)
        h += divtest(CssClass.SUBSUBHEADING)
        h += divtest(CssClass.SUMMARY)
        h += divtest(CssClass.SUPERUSER)
        h += divtest(CssClass.TASKHEADER)
        h += divtest(CssClass.TRACKERHEADER)
        h += divtest(CssClass.TRACKER_ALL_CONSISTENT)
        h += divtest(CssClass.WARNING)
        h += """
            <table>
                <tr>
                    <th>Standard table heading; column 1</th><th>Column 2</th>
                </tr>
                <tr>
                    <td>Standard table row; column 1</td><td>Column 2</td>
                </tr>
            </table>

            <div>Inlined <code>code looks like this</code>.

            <div>There are some others, too.</div>
        """
        return h
Example #5
0
class Moca(TaskHasPatientMixin,
           TaskHasClinicianMixin,
           Task,
           metaclass=MocaMetaclass):
    """
    Server implementation of the MoCA task.
    """

    __tablename__ = "moca"
    shortname = "MoCA"
    provides_trackers = True

    prohibits_commercial = True
    prohibits_research = True

    education12y_or_less = CamcopsColumn(
        "education12y_or_less",
        Integer,
        permitted_value_checker=BIT_CHECKER,
        comment="<=12 years of education (0 no, 1 yes)",
    )
    trailpicture_blobid = CamcopsColumn(
        "trailpicture_blobid",
        Integer,
        is_blob_id_field=True,
        blob_relationship_attr_name="trailpicture",
        comment="BLOB ID of trail picture",
    )
    cubepicture_blobid = CamcopsColumn(
        "cubepicture_blobid",
        Integer,
        is_blob_id_field=True,
        blob_relationship_attr_name="cubepicture",
        comment="BLOB ID of cube picture",
    )
    clockpicture_blobid = CamcopsColumn(
        "clockpicture_blobid",
        Integer,
        is_blob_id_field=True,
        blob_relationship_attr_name="clockpicture",
        comment="BLOB ID of clock picture",
    )
    comments = Column("comments", UnicodeText, comment="Clinician's comments")

    trailpicture = blob_relationship(
        "Moca", "trailpicture_blobid")  # type: Optional[Blob]  # noqa
    cubepicture = blob_relationship(
        "Moca", "cubepicture_blobid")  # type: Optional[Blob]  # noqa
    clockpicture = blob_relationship(
        "Moca", "clockpicture_blobid")  # type: Optional[Blob]  # noqa

    NQUESTIONS = 28
    MAX_SCORE = 30

    QFIELDS = strseq("q", 1, NQUESTIONS)
    VSP_FIELDS = strseq("q", 1, 5)
    NAMING_FIELDS = strseq("q", 6, 8)
    ATTN_FIELDS = strseq("q", 9, 12)
    LANG_FIELDS = strseq("q", 13, 15)
    ABSTRACTION_FIELDS = strseq("q", 16, 17)
    MEM_FIELDS = strseq("q", 18, 22)
    ORIENTATION_FIELDS = strseq("q", 23, 28)

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Montreal Cognitive Assessment")

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label="MOCA 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=[25.5],
                horizontal_labels=[
                    TrackerLabel(26, req.sstring(SS.NORMAL),
                                 LabelAlignment.bottom),
                    TrackerLabel(25, req.sstring(SS.ABNORMAL),
                                 LabelAlignment.top),
                ],
            )
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [
            CtvInfo(content=f"MOCA 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="category",
                coltype=String(50),
                value=self.category(req),
                comment="Categorization",
            ),
        ]

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

    def total_score(self) -> int:
        score = self.sum_fields(self.QFIELDS)
        # Interpretation of the educational extra point: see moca.cpp; we have
        # a choice of allowing 31/30 or capping at 30. I think the instructions
        # imply a cap of 30.
        if score < self.MAX_SCORE:
            score += self.sum_fields(["education12y_or_less"])
            # extra point for this
        return score

    def score_vsp(self) -> int:
        return self.sum_fields(self.VSP_FIELDS)

    def score_naming(self) -> int:
        return self.sum_fields(self.NAMING_FIELDS)

    def score_attention(self) -> int:
        return self.sum_fields(self.ATTN_FIELDS)

    def score_language(self) -> int:
        return self.sum_fields(self.LANG_FIELDS)

    def score_abstraction(self) -> int:
        return self.sum_fields(self.ABSTRACTION_FIELDS)

    def score_memory(self) -> int:
        return self.sum_fields(self.MEM_FIELDS)

    def score_orientation(self) -> int:
        return self.sum_fields(self.ORIENTATION_FIELDS)

    def category(self, req: CamcopsRequest) -> str:
        totalscore = self.total_score()
        return (req.sstring(SS.NORMAL)
                if totalscore >= 26 else req.sstring(SS.ABNORMAL))

    # noinspection PyUnresolvedReferences
    def get_task_html(self, req: CamcopsRequest) -> str:
        vsp = self.score_vsp()
        naming = self.score_naming()
        attention = self.score_attention()
        language = self.score_language()
        abstraction = self.score_abstraction()
        memory = self.score_memory()
        orientation = self.score_orientation()
        totalscore = self.total_score()
        category = self.category(req)

        h = """
            {clinician_comments}
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {tr_is_complete}
                    {total_score}
                    {category}
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="69%">Question</th>
                    <th width="31%">Score</th>
                </tr>
        """.format(
            clinician_comments=self.get_standard_clinician_comments_block(
                req, self.comments),
            CssClass=CssClass,
            tr_is_complete=self.get_is_complete_tr(req),
            total_score=tr(
                req.sstring(SS.TOTAL_SCORE),
                answer(totalscore) + f" / {self.MAX_SCORE}",
            ),
            category=tr_qa(
                self.wxstring(req, "category") + " <sup>[1]</sup>", category),
        )

        h += tr(
            self.wxstring(req, "subscore_visuospatial"),
            answer(vsp) + " / 5",
            tr_class=CssClass.SUBHEADING,
        )
        h += tr(
            "Path, cube, clock/contour, clock/numbers, clock/hands",
            ", ".join(
                answer(x)
                for x in (self.q1, self.q2, self.q3, self.q4, self.q5)),
        )

        h += tr(
            self.wxstring(req, "subscore_naming"),
            answer(naming) + " / 3",
            tr_class=CssClass.SUBHEADING,
        )
        h += tr(
            "Lion, rhino, camel",
            ", ".join(answer(x) for x in (self.q6, self.q7, self.q8)),
        )

        h += tr(
            self.wxstring(req, "subscore_attention"),
            answer(attention) + " / 6",
            tr_class=CssClass.SUBHEADING,
        )
        h += tr(
            "5 digits forwards, 3 digits backwards, tapping, serial 7s "
            "[<i>scores 3</i>]",
            ", ".join(
                answer(x) for x in (self.q9, self.q10, self.q11, self.q12)),
        )

        h += tr(
            self.wxstring(req, "subscore_language"),
            answer(language) + " / 3",
            tr_class=CssClass.SUBHEADING,
        )
        h += tr(
            "Repeat sentence 1, repeat sentence 2, fluency to letter ‘F’",
            ", ".join(answer(x) for x in (self.q13, self.q14, self.q15)),
        )

        h += tr(
            self.wxstring(req, "subscore_abstraction"),
            answer(abstraction) + " / 2",
            tr_class=CssClass.SUBHEADING,
        )
        h += tr(
            "Means of transportation, measuring instruments",
            ", ".join(answer(x) for x in (self.q16, self.q17)),
        )

        h += tr(
            self.wxstring(req, "subscore_memory"),
            answer(memory) + " / 5",
            tr_class=CssClass.SUBHEADING,
        )
        h += tr(
            "Registered on first trial [<i>not scored</i>]",
            ", ".join(
                answer(x, formatter_answer=italic) for x in (
                    self.register_trial1_1,
                    self.register_trial1_2,
                    self.register_trial1_3,
                    self.register_trial1_4,
                    self.register_trial1_5,
                )),
        )
        h += tr(
            "Registered on second trial [<i>not scored</i>]",
            ", ".join(
                answer(x, formatter_answer=italic) for x in (
                    self.register_trial2_1,
                    self.register_trial2_2,
                    self.register_trial2_3,
                    self.register_trial2_4,
                    self.register_trial2_5,
                )),
        )
        h += tr(
            "Recall FACE, VELVET, CHURCH, DAISY, RED with no cue",
            ", ".join(
                answer(x)
                for x in (self.q18, self.q19, self.q20, self.q21, self.q22)),
        )
        h += tr(
            "Recall with category cue [<i>not scored</i>]",
            ", ".join(
                answer(x, formatter_answer=italic) for x in (
                    self.recall_category_cue_1,
                    self.recall_category_cue_2,
                    self.recall_category_cue_3,
                    self.recall_category_cue_4,
                    self.recall_category_cue_5,
                )),
        )
        h += tr(
            "Recall with multiple-choice cue [<i>not scored</i>]",
            ", ".join(
                answer(x, formatter_answer=italic) for x in (
                    self.recall_mc_cue_1,
                    self.recall_mc_cue_2,
                    self.recall_mc_cue_3,
                    self.recall_mc_cue_4,
                    self.recall_mc_cue_5,
                )),
        )

        h += tr(
            self.wxstring(req, "subscore_orientation"),
            answer(orientation) + " / 6",
            tr_class=CssClass.SUBHEADING,
        )
        h += tr(
            "Date, month, year, day, place, city",
            ", ".join(
                answer(x) for x in (
                    self.q23,
                    self.q24,
                    self.q25,
                    self.q26,
                    self.q27,
                    self.q28,
                )),
        )

        h += subheading_spanning_two_columns(self.wxstring(req, "education_s"))
        h += tr_qa("≤12 years’ education?", self.education12y_or_less)
        # noinspection PyTypeChecker
        h += """
            </table>
            <table class="{CssClass.TASKDETAIL}">
                {tr_subhead_images}
                {tr_images_1}
                {tr_images_2}
            </table>
            <div class="{CssClass.FOOTNOTES}">
                [1] Normal is ≥26 (Nasreddine et al. 2005, PubMed ID 15817019).
            </div>
            <div class="{CssClass.COPYRIGHT}">
                MoCA: Copyright © Ziad Nasreddine. In 2012, could be reproduced
                without permission for CLINICAL and EDUCATIONAL use (with
                permission from the copyright holder required for any other
                use), with no special restrictions on electronic versions.
                However, as of 2021, electronic versions are prohibited without
                specific authorization from the copyright holder; see <a
                href="https://camcops.readthedocs.io/en/latest/tasks/moca.html">
                https://camcops.readthedocs.io/en/latest/tasks/moca.html</a>.
            </div>
        """.format(
            CssClass=CssClass,
            tr_subhead_images=subheading_spanning_two_columns(
                "Images of tests: trail, cube, clock", th_not_td=True),
            tr_images_1=tr(
                td(
                    get_blob_img_html(self.trailpicture),
                    td_class=CssClass.PHOTO,
                    td_width="50%",
                ),
                td(
                    get_blob_img_html(self.cubepicture),
                    td_class=CssClass.PHOTO,
                    td_width="50%",
                ),
                literal=True,
            ),
            tr_images_2=tr(
                td(
                    get_blob_img_html(self.clockpicture),
                    td_class=CssClass.PHOTO,
                    td_width="50%",
                ),
                td("", td_class=CssClass.SUBHEADING),
                literal=True,
            ),
        )
        return h

    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        codes = [
            SnomedExpression(req.snomed(
                SnomedLookup.MOCA_PROCEDURE_ASSESSMENT))
        ]
        if self.is_complete():
            codes.append(
                SnomedExpression(
                    req.snomed(SnomedLookup.MOCA_SCALE),
                    {req.snomed(SnomedLookup.MOCA_SCORE): self.total_score()},
                ))
        return codes
Example #6
0
class Photo(TaskHasClinicianMixin, TaskHasPatientMixin, Task):
    """
    Server implementation of the Photo task.
    """

    __tablename__ = "photo"
    shortname = "Photo"
    info_filename_stem = "clinical"

    description = Column("description",
                         UnicodeText,
                         comment="Description of the photograph")
    photo_blobid = CamcopsColumn(
        "photo_blobid",
        Integer,
        is_blob_id_field=True,
        blob_relationship_attr_name="photo",
        comment="ID of the BLOB (foreign key to blobs.id, given "
        "matching device and current/frozen record status)",
    )
    # IGNORED. REMOVE WHEN ALL PRE-2.0.0 TABLETS GONE:
    rotation = Column(  # DEFUNCT as of v2.0.0
        "rotation",
        Integer,
        comment="Rotation (clockwise, in degrees) to be applied for viewing",
    )

    photo = blob_relationship("Photo", "photo_blobid")  # type: Optional[Blob]

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

    def is_complete(self) -> bool:
        return self.photo_blobid is not None

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        if not self.description:
            return []
        return [CtvInfo(content=self.description)]

    def get_task_html(self, req: CamcopsRequest) -> str:
        # noinspection PyTypeChecker
        return """
            <table class="{CssClass.TASKDETAIL}">
                <tr class="{CssClass.SUBHEADING}"><td>Description</td></tr>
                <tr><td>{description}</td></tr>
                <tr class="{CssClass.SUBHEADING}"><td>Photo</td></tr>
                <tr><td>{photo}</td></tr>
            </table>
        """.format(
            CssClass=CssClass,
            description=answer(
                ws.webify(self.description),
                default="(No description)",
                default_for_blank_strings=True,
            ),
            # ... xhtml2pdf crashes if the contents are empty...
            photo=get_blob_img_html(self.photo),
        )

    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        if not self.is_complete():
            return []
        return [
            SnomedExpression(req.snomed(SnomedLookup.PHOTOGRAPH_PROCEDURE)),
            SnomedExpression(
                req.snomed(SnomedLookup.PHOTOGRAPH_PHYSICAL_OBJECT)),
        ]