Source code for camcops_server.tasks.ifs

"""
camcops_server/tasks/ifs.py

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

    Copyright (C) 2012, University of Cambridge, Department of Psychiatry.
    Created by Rudolf Cardinal (rnc1001@cam.ac.uk).

    This file is part of CamCOPS.

    CamCOPS is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    CamCOPS is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with CamCOPS. If not, see <https://www.gnu.org/licenses/>.

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

"""

from typing import Any, Dict, List, Tuple, Type

from sqlalchemy.ext.declarative import DeclarativeMeta
from sqlalchemy.sql.sqltypes import Boolean, Float, Integer

from camcops_server.cc_modules.cc_constants import (
    CssClass,
    DATA_COLLECTION_UNLESS_UPGRADED_DIV,
    INVALID_VALUE,
)
from camcops_server.cc_modules.cc_ctvinfo import CTV_INCOMPLETE, CtvInfo
from camcops_server.cc_modules.cc_html import (
    answer,
    get_correct_incorrect_none,
    td,
    tr,
    tr_qa,
)
from camcops_server.cc_modules.cc_request import CamcopsRequest
from camcops_server.cc_modules.cc_sqla_coltypes import (
    BIT_CHECKER,
    CamcopsColumn,
    ZERO_TO_ONE_CHECKER,
    ZERO_TO_TWO_CHECKER,
    ZERO_TO_THREE_CHECKER,
)
from camcops_server.cc_modules.cc_summaryelement import SummaryElement
from camcops_server.cc_modules.cc_task import (
    Task,
    TaskHasClinicianMixin,
    TaskHasPatientMixin,
)
from camcops_server.cc_modules.cc_trackerhelpers import TrackerInfo


# =============================================================================
# IFS
# =============================================================================


class IfsMetaclass(DeclarativeMeta):
    # noinspection PyInitNewSignature
    def __init__(
        cls: Type["Ifs"],
        name: str,
        bases: Tuple[Type, ...],
        classdict: Dict[str, Any],
    ) -> None:
        for seqlen in cls.Q4_DIGIT_LENGTHS:
            fname1 = f"q4_len{seqlen}_1"
            fname2 = f"q4_len{seqlen}_2"
            setattr(
                cls,
                fname1,
                CamcopsColumn(
                    fname1,
                    Boolean,
                    permitted_value_checker=BIT_CHECKER,
                    comment=f"Q4. Digits backward, length {seqlen}, trial 1",
                ),
            )
            setattr(
                cls,
                fname2,
                CamcopsColumn(
                    fname2,
                    Boolean,
                    permitted_value_checker=BIT_CHECKER,
                    comment=f"Q4. Digits backward, length {seqlen}, trial 2",
                ),
            )
        for n in cls.Q6_SEQUENCE_NUMS:
            fname = f"q6_seq{n}"
            setattr(
                cls,
                fname,
                CamcopsColumn(
                    fname,
                    Integer,
                    permitted_value_checker=BIT_CHECKER,
                    comment=f"Q6. Spatial working memory, sequence {n}",
                ),
            )
        for n in cls.Q7_PROVERB_NUMS:
            fname = "q7_proverb{}".format(n)
            setattr(
                cls,
                fname,
                CamcopsColumn(
                    fname,
                    Float,
                    permitted_value_checker=ZERO_TO_ONE_CHECKER,
                    comment=f"Q7. Proverb {n} (1 = correct explanation, "
                    f"0.5 = example, 0 = neither)",
                ),
            )
        for n in cls.Q8_SENTENCE_NUMS:
            fname = "q8_sentence{}".format(n)
            setattr(
                cls,
                fname,
                CamcopsColumn(
                    fname,
                    Integer,
                    permitted_value_checker=ZERO_TO_TWO_CHECKER,
                    comment=f"Q8. Hayling, sentence {n}",
                ),
            )
        super().__init__(name, bases, classdict)


[docs]class Ifs( TaskHasPatientMixin, TaskHasClinicianMixin, Task, metaclass=IfsMetaclass ): """ Server implementation of the IFS task. """ __tablename__ = "ifs" shortname = "IFS" provides_trackers = True q1 = CamcopsColumn( "q1", Integer, permitted_value_checker=ZERO_TO_THREE_CHECKER, comment="Q1. Motor series (motor programming)", ) q2 = CamcopsColumn( "q2", Integer, permitted_value_checker=ZERO_TO_THREE_CHECKER, comment="Q2. Conflicting instructions (interference sensitivity)", ) q3 = CamcopsColumn( "q3", Integer, permitted_value_checker=ZERO_TO_THREE_CHECKER, comment="Q3. Go/no-go (inhibitory control)", ) q5 = CamcopsColumn( "q5", Integer, permitted_value_checker=ZERO_TO_TWO_CHECKER, comment="Q5. Verbal working memory", ) Q4_DIGIT_LENGTHS = list(range(2, 7 + 1)) Q6_SEQUENCE_NUMS = list(range(1, 4 + 1)) Q7_PROVERB_NUMS = list(range(1, 3 + 1)) Q8_SENTENCE_NUMS = list(range(1, 3 + 1)) SIMPLE_Q = ( ["q1", "q2", "q3", "q5"] + [f"q6_seq{n}" for n in Q6_SEQUENCE_NUMS] + [f"q7_proverb{n}" for n in Q7_PROVERB_NUMS] + [f"q8_sentence{n}" for n in Q8_SENTENCE_NUMS] ) MAX_TOTAL = 30 MAX_WM = 10
[docs] @staticmethod def longname(req: "CamcopsRequest") -> str: _ = req.gettext return _("INECO Frontal Screening")
[docs] def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]: scoredict = self.get_score() return [ TrackerInfo( value=scoredict["total"], plot_label="IFS total score (higher 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=scoredict["wm"], plot_label="IFS working memory index (higher is better)", axis_label=f"Total score (out of {self.MAX_WM})", axis_min=-0.5, axis_max=self.MAX_WM + 0.5, ), ]
[docs] def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]: scoredict = self.get_score() return self.standard_task_summary_fields() + [ SummaryElement( name="total", coltype=Float(), value=scoredict["total"], comment=f"Total (out of {self.MAX_TOTAL}, higher better)", ), SummaryElement( name="wm", coltype=Integer(), value=scoredict["wm"], comment=f"Working memory index (out of {self.MAX_WM}; " f"sum of Q4 + Q6", ), ]
[docs] def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]: scoredict = self.get_score() if not self.is_complete(): return CTV_INCOMPLETE return [ CtvInfo( content=( f"Total: {scoredict['total']}/{self.MAX_TOTAL}; " f"working memory index {scoredict['wm']}/{self.MAX_WM}" ) ) ]
def get_score(self) -> Dict: q1 = getattr(self, "q1", 0) or 0 q2 = getattr(self, "q2", 0) or 0 q3 = getattr(self, "q3", 0) or 0 q4 = 0 for seqlen in self.Q4_DIGIT_LENGTHS: val1 = getattr(self, f"q4_len{seqlen}_1") val2 = getattr(self, f"q4_len{seqlen}_2") if val1 or val2: q4 += 1 if not val1 and not val2: break q5 = getattr(self, "q5", 0) or 0 q6 = self.sum_fields(["q6_seq" + str(s) for s in range(1, 4 + 1)]) q7 = self.sum_fields(["q7_proverb" + str(s) for s in range(1, 3 + 1)]) q8 = self.sum_fields(["q8_sentence" + str(s) for s in range(1, 3 + 1)]) total = q1 + q2 + q3 + q4 + q5 + q6 + q7 + q8 wm = q4 + q6 # working memory index (though not verbal) return dict(total=total, wm=wm)
[docs] def is_complete(self) -> bool: if not self.field_contents_valid(): return False if self.any_fields_none(self.SIMPLE_Q): return False for seqlen in self.Q4_DIGIT_LENGTHS: val1 = getattr(self, f"q4_len{seqlen}_1") val2 = getattr(self, f"q4_len{seqlen}_2") if val1 is None or val2 is None: return False if not val1 and not val2: return True # all done return True
def get_simple_tr_qa(self, req: CamcopsRequest, qprefix: str) -> str: q = self.wxstring(req, qprefix + "_title") val = getattr(self, qprefix) if val is not None: a = self.wxstring(req, qprefix + "_a" + str(val)) else: a = None return tr_qa(q, a)
[docs] def get_task_html(self, req: CamcopsRequest) -> str: scoredict = self.get_score() # Q1 q_a = self.get_simple_tr_qa(req, "q1") # Q2 q_a += self.get_simple_tr_qa(req, "q2") # Q3 q_a += self.get_simple_tr_qa(req, "q3") # Q4 q_a += tr( td(self.wxstring(req, "q4_title")), td("", td_class=CssClass.SUBHEADING), literal=True, ) required = True for n in self.Q4_DIGIT_LENGTHS: val1 = getattr(self, f"q4_len{n}_1") val2 = getattr(self, f"q4_len{n}_2") q = ( "… " + self.wxstring(req, f"q4_seq_len{n}_1") + " / " + self.wxstring(req, f"q4_seq_len{n}_2") ) if required: score = 1 if val1 or val2 else 0 a = ( answer(get_correct_incorrect_none(val1)) + " / " + answer(get_correct_incorrect_none(val2)) + f" (scores {score})" ) else: a = "" q_a += tr(q, a) if not val1 and not val2: required = False # Q5 q_a += self.get_simple_tr_qa(req, "q5") # Q6 q_a += tr( td(self.wxstring(req, "q6_title")), td("", td_class=CssClass.SUBHEADING), literal=True, ) for n in self.Q6_SEQUENCE_NUMS: nstr = str(n) val = getattr(self, "q6_seq" + nstr) q_a += tr_qa("… " + self.wxstring(req, "q6_seq" + nstr), val) # Q7 q7map = { None: None, 1: self.wxstring(req, "q7_a_1"), 0.5: self.wxstring(req, "q7_a_half"), 0: self.wxstring(req, "q7_a_0"), } q_a += tr( td(self.wxstring(req, "q7_title")), td("", td_class=CssClass.SUBHEADING), literal=True, ) for n in self.Q7_PROVERB_NUMS: nstr = str(n) val = getattr(self, "q7_proverb" + nstr) a = q7map.get(val, INVALID_VALUE) q_a += tr_qa("… " + self.wxstring(req, "q7_proverb" + nstr), a) # Q8 q8map = { None: None, 2: self.wxstring(req, "q8_a2"), 1: self.wxstring(req, "q8_a1"), 0: self.wxstring(req, "q8_a0"), } q_a += tr( td(self.wxstring(req, "q8_title")), td("", td_class=CssClass.SUBHEADING), literal=True, ) for n in self.Q8_SENTENCE_NUMS: nstr = str(n) val = getattr(self, "q8_sentence" + nstr) a = q8map.get(val, INVALID_VALUE) q_a += tr_qa("… " + self.wxstring(req, "q8_sentence_" + nstr), a) return f""" <div class="{CssClass.SUMMARY}"> <table class="{CssClass.SUMMARY}"> {self.get_is_complete_tr(req)} <tr> <td>Total (higher better)</td> <td>{answer(scoredict['total'])} / {self.MAX_TOTAL}</td> </td> <tr> <td>Working memory index <sup>1</sup></td> <td>{answer(scoredict['wm'])} / {self.MAX_WM}</td> </td> </table> </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] Sum of scores for Q4 + Q6. </div> {DATA_COLLECTION_UNLESS_UPGRADED_DIV} """ # noqa: E501