Ejemplo n.º 1
0
def export_task(req: "CamcopsRequest", recipient: ExportRecipient,
                task: Task) -> None:
    """
    Exports a single task, checking that it remains valid to do so.
    
    Args:
        req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
        recipient: an :class:`camcops_server.cc_modules.cc_exportmodels.ExportRecipient`
        task: a :class:`camcops_server.cc_modules.cc_task.Task` 
    """  # noqa

    # Double-check it's OK! Just in case, for example, an old backend task has
    # persisted, or someone's managed to get an iffy back-end request in some
    # other way.
    if not recipient.is_task_suitable(task):
        # Warning will already have been emitted.
        return

    cfg = req.config
    lockfilename = cfg.get_export_lockfilename_task(
        recipient_name=recipient.recipient_name,
        basetable=task.tablename,
        pk=task.get_pk(),
    )
    dbsession = req.dbsession
    try:
        with lockfile.FileLock(lockfilename, timeout=0):  # doesn't wait
            # We recheck the export status once we hold the lock, in case
            # multiple jobs are competing to export it.
            if ExportedTask.task_already_exported(
                    dbsession=dbsession,
                    recipient_name=recipient.recipient_name,
                    basetable=task.tablename,
                    task_pk=task.get_pk()):
                log.info(
                    "Task {!r} already exported to recipient {!r}; "
                    "ignoring", task, recipient)
                # Not a warning; it's normal to see these because it allows the
                # client API to skip some checks for speed.
                return
            # OK; safe to export now.
            et = ExportedTask(recipient, task)
            dbsession.add(et)
            et.export(req)
            dbsession.commit()  # so the ExportedTask is visible to others ASAP
    except lockfile.AlreadyLocked:
        log.warning(
            "Export logfile {!r} already locked by another process; "
            "aborting", lockfilename)
Ejemplo n.º 2
0
    def task_classes(self) -> List[Type[Task]]:
        """
        Return a list of task classes permitted by the filter.

        Uses caching, since the filter will be called repeatedly.
        """
        if self._task_classes is None:
            self._task_classes = []  # type: List[Type[Task]]
            if self.task_types:
                starting_classes = task_classes_from_table_names(
                    self.task_types)
            else:
                starting_classes = Task.all_subclasses_by_shortname()
            skip_anonymous_tasks = self.skip_anonymous_tasks()
            for cls in starting_classes:
                if (self.tasks_offering_trackers_only
                        and not cls.provides_trackers):
                    # Class doesn't provide trackers; skip
                    continue
                if skip_anonymous_tasks and not cls.has_patient:
                    # Anonymous task; skip
                    continue
                if self.text_contents and not cls.get_text_filter_columns():
                    # Text filter and task has no text columns; skip
                    continue
                self._task_classes.append(cls)
            sort_task_classes_in_place(self._task_classes, self.sort_method)
        return self._task_classes
Ejemplo n.º 3
0
    def rebuild_entire_task_index(
        cls,
        session: SqlASession,
        indexed_at_utc: Pendulum,
        skip_tasks_with_missing_tables: bool = False,
    ) -> None:
        """
        Rebuilds the entire index.

        Args:
            session: an SQLAlchemy Session
            indexed_at_utc: current time in UTC
            skip_tasks_with_missing_tables: should we skip over tasks if their
                tables are not in the database? (This is so we can rebuild an
                index from a database upgrade, but not crash because newer
                tasks haven't had their tables created yet.)
        """
        log.info("Rebuilding entire task index")
        # noinspection PyUnresolvedReferences
        idxtable = cls.__table__  # type: Table

        # Delete all entries
        with if_sqlserver_disable_constraints_triggers(session, idxtable.name):
            session.execute(idxtable.delete())

        # Now rebuild:
        for taskclass in Task.all_subclasses_by_tablename():
            if skip_tasks_with_missing_tables:
                basetable = taskclass.tablename
                engine = get_engine_from_session(session)
                if not table_exists(engine, basetable):
                    continue
            cls.rebuild_index_for_task_type(
                session, taskclass, indexed_at_utc, delete_first=False
            )
Ejemplo n.º 4
0
 def apply_standard_task_fields(self, task: Task) -> None:
     """
     Writes some default values to an SQLAlchemy ORM object representing
     a task.
     """
     self.apply_standard_db_fields(task)
     task.when_created = self.era_time
Ejemplo n.º 5
0
def all_tracker_task_classes() -> List[Type[Task]]:
    """
    Returns a list of all task classes that provide tracker information.
    """
    return [
        cls for cls in Task.all_subclasses_by_shortname()
        if cls.provides_trackers
    ]
Ejemplo n.º 6
0
    def add_tasks(self, patient_id: int):
        for cls in Task.all_subclasses_by_tablename():
            task = cls()
            task.id = self.next_id(cls.id)
            self.apply_standard_task_fields(task)
            if task.has_patient:
                task.patient_id = patient_id

            self.fill_in_task_fields(task)

            self.dbsession.add(task)
            self.dbsession.commit()
Ejemplo n.º 7
0
 def offers_all_non_anonymous_task_types(self) -> bool:
     """
     Does this filter offer every single non-anonymous task class? Used for
     efficiency when using indexes.
     """
     offered_task_classes = self.task_classes
     for taskclass in Task.all_subclasses_by_shortname():
         if taskclass.is_anonymous:
             continue
         if taskclass not in offered_task_classes:
             return False
     return True
Ejemplo n.º 8
0
    def _task_matches_python_parts_of_filter(self, task: Task) -> bool:
        """
        Does the task pass the Python parts of the filter?

        Only applicable to the direct (not "via index") route.
        """
        assert not self._via_index

        # "Is task complete" filter
        if self._filter.complete_only:
            if not task.is_complete():
                return False

        return True
Ejemplo n.º 9
0
    def make_from_task(
        cls, task: Task, indexed_at_utc: Pendulum
    ) -> "TaskIndexEntry":
        """
        Returns a task index entry for the specified
        :class:`camcops_server.cc_modules.cc_task.Task`. The
        returned index requires inserting into a database session.

        Args:
            task:
                a :class:`camcops_server.cc_modules.cc_task.Task`
            indexed_at_utc:
                current time in UTC
        """
        assert indexed_at_utc is not None, "Missing indexed_at_utc"

        index = cls()

        index.indexed_at_utc = indexed_at_utc

        index.task_table_name = task.tablename
        index.task_pk = task.pk

        patient = task.patient
        index.patient_pk = patient.pk if patient else None

        index.device_id = task.device_id
        index.era = task.era
        index.when_created_utc = task.get_creation_datetime_utc()
        index.when_created_iso = task.when_created
        # noinspection PyProtectedMember
        index.when_added_batch_utc = task._when_added_batch_utc
        index.adding_user_id = task.get_adding_user_id()
        index.group_id = task.group_id
        index.task_is_complete = task.is_complete()

        return index
Ejemplo n.º 10
0
    def rebuild_entire_task_index(cls, session: SqlASession,
                                  indexed_at_utc: Pendulum) -> None:
        """
        Rebuilds the entire index.

        Args:
            session: an SQLAlchemy Session
            indexed_at_utc: current time in UTC
        """
        log.info("Rebuilding entire task index")
        # noinspection PyUnresolvedReferences
        idxtable = cls.__table__  # type: Table
        # Delete all entries
        session.execute(
            idxtable.delete()
        )
        # Now rebuild:
        for taskclass in Task.all_subclasses_by_tablename():
            cls.rebuild_index_for_task_type(session, taskclass,
                                            indexed_at_utc,
                                            delete_first=False)
Ejemplo n.º 11
0
    def create_tasks(self) -> None:
        from camcops_server.cc_modules.cc_blob import Blob
        from camcops_server.tasks.photo import Photo
        from camcops_server.cc_modules.cc_task import Task

        patient_with_two_idnums = self.create_patient_with_two_idnums()
        patient_with_one_idnum = self.create_patient_with_one_idnum()

        for cls in Task.all_subclasses_by_tablename():
            t1 = cls()
            t1.id = 1
            self.apply_standard_task_fields(t1)
            if t1.has_patient:
                t1.patient_id = patient_with_two_idnums.id

            if isinstance(t1, Photo):
                b = Blob()
                b.id = 1
                self.apply_standard_db_fields(b)
                b.tablename = t1.tablename
                b.tablepk = t1.id
                b.fieldname = "photo_blobid"
                b.filename = "some_picture.png"
                b.mimetype = MimeType.PNG
                b.image_rotation_deg_cw = 0
                b.theblob = DEMO_PNG_BYTES
                self.dbsession.add(b)

                t1.photo_blobid = b.id

            self.dbsession.add(t1)

            t2 = cls()
            t2.id = 2
            self.apply_standard_task_fields(t2)
            if t2.has_patient:
                t2.patient_id = patient_with_one_idnum.id
            self.dbsession.add(t2)

        self.dbsession.commit()
Ejemplo n.º 12
0
    DemoDatabaseTestCase,
    DemoRequestTestCase,
    ExtendedTestCase,
)  # nopep8
from camcops_server.cc_modules.cc_user import (
    SecurityLoginFailure,
    set_password_directly,
    User,
)  # nopep8
from camcops_server.cc_modules.celery import (
    CELERY_APP_NAME,
    CELERY_SOFT_TIME_LIMIT_SEC,
)  # nopep8

log.info("Imports complete")
log.info("Using {} tasks", len(Task.all_subclasses_by_tablename()))

if TYPE_CHECKING:
    from pyramid.router import Router  # nopep8

# =============================================================================
# Other constants
# =============================================================================

WINDOWS = platform.system() == "Windows"

# =============================================================================
# Helper functions for web server launcher
# =============================================================================

Ejemplo n.º 13
0
    def setUp(self) -> None:
        super().setUp()
        from cardinal_pythonlib.datetimefunc import (
            convert_datetime_to_utc,
            format_datetime,
        )
        from camcops_server.cc_modules.cc_blob import Blob
        from camcops_server.cc_modules.cc_constants import DateFormat
        from camcops_server.cc_modules.cc_device import Device
        from camcops_server.cc_modules.cc_group import Group
        from camcops_server.cc_modules.cc_patient import Patient
        from camcops_server.cc_modules.cc_patientidnum import PatientIdNum
        from camcops_server.cc_modules.cc_task import Task
        from camcops_server.cc_modules.cc_user import User
        from camcops_server.tasks.photo import Photo

        Base.metadata.create_all(self.engine)

        self.era_time = pendulum.parse("2010-07-07T13:40+0100")
        self.era_time_utc = convert_datetime_to_utc(self.era_time)
        self.era = format_datetime(self.era_time, DateFormat.ISO8601)

        # Set up groups, users, etc.
        # ... ID number definitions
        iddef1 = IdNumDefinition(which_idnum=1,
                                 description="NHS number",
                                 short_description="NHS#",
                                 hl7_assigning_authority="NHS",
                                 hl7_id_type="NHSN")
        self.dbsession.add(iddef1)
        iddef2 = IdNumDefinition(which_idnum=2,
                                 description="RiO number",
                                 short_description="RiO",
                                 hl7_assigning_authority="CPFT",
                                 hl7_id_type="CPFT_RiO")
        self.dbsession.add(iddef2)
        # ... group
        self.group = Group()
        self.group.name = "testgroup"
        self.group.description = "Test group"
        self.group.upload_policy = "sex AND anyidnum"
        self.group.finalize_policy = "sex AND idnum1"
        self.dbsession.add(self.group)
        self.dbsession.flush()  # sets PK fields

        # ... users

        self.user = User.get_system_user(self.dbsession)
        self.user.upload_group_id = self.group.id
        self.req._debugging_user = self.user  # improve our debugging user

        # ... devices
        self.server_device = Device.get_server_device(self.dbsession)
        self.other_device = Device()
        self.other_device.name = "other_device"
        self.other_device.friendly_name = "Test device that may upload"
        self.other_device.registered_by_user = self.user
        self.other_device.when_registered_utc = self.era_time_utc
        self.other_device.camcops_version = CAMCOPS_SERVER_VERSION
        self.dbsession.add(self.other_device)

        self.dbsession.flush()  # sets PK fields

        # Populate database with two of everything
        p1 = Patient()
        p1.id = 1
        self._apply_standard_db_fields(p1)
        p1.forename = "Forename1"
        p1.surname = "Surname1"
        p1.dob = pendulum.parse("1950-01-01")
        self.dbsession.add(p1)
        p1_idnum1 = PatientIdNum()
        p1_idnum1.id = 1
        self._apply_standard_db_fields(p1_idnum1)
        p1_idnum1.patient_id = p1.id
        p1_idnum1.which_idnum = iddef1.which_idnum
        p1_idnum1.idnum_value = 333
        self.dbsession.add(p1_idnum1)
        p1_idnum2 = PatientIdNum()
        p1_idnum2.id = 2
        self._apply_standard_db_fields(p1_idnum2)
        p1_idnum2.patient_id = p1.id
        p1_idnum2.which_idnum = iddef2.which_idnum
        p1_idnum2.idnum_value = 444
        self.dbsession.add(p1_idnum2)

        p2 = Patient()
        p2.id = 2
        self._apply_standard_db_fields(p2)
        p2.forename = "Forename2"
        p2.surname = "Surname2"
        p2.dob = pendulum.parse("1975-12-12")
        self.dbsession.add(p2)
        p2_idnum1 = PatientIdNum()
        p2_idnum1.id = 3
        self._apply_standard_db_fields(p2_idnum1)
        p2_idnum1.patient_id = p2.id
        p2_idnum1.which_idnum = iddef1.which_idnum
        p2_idnum1.idnum_value = 555
        self.dbsession.add(p2_idnum1)

        self.dbsession.flush()

        for cls in Task.all_subclasses_by_tablename():
            t1 = cls()
            t1.id = 1
            self._apply_standard_task_fields(t1)
            if t1.has_patient:
                t1.patient_id = p1.id

            if isinstance(t1, Photo):
                b = Blob()
                b.id = 1
                self._apply_standard_db_fields(b)
                b.tablename = t1.tablename
                b.tablepk = t1.id
                b.fieldname = 'photo_blobid'
                b.filename = "some_picture.png"
                b.mimetype = MimeType.PNG
                b.image_rotation_deg_cw = 0
                b.theblob = DEMO_PNG_BYTES
                self.dbsession.add(b)

                t1.photo_blobid = b.id

            self.dbsession.add(t1)

            t2 = cls()
            t2.id = 2
            self._apply_standard_task_fields(t2)
            if t2.has_patient:
                t2.patient_id = p2.id
            self.dbsession.add(t2)

        self.dbsession.commit()
Ejemplo n.º 14
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
Ejemplo n.º 15
0
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__(
        cls: Type["Bdi"],
        name: str,
        bases: Tuple[Type, ...],
        classdict: Dict[str, Any],
    ) -> None:
        add_multiple_columns(
Ejemplo n.º 16
0
 def get_rows_colnames(self, req: "CamcopsRequest") -> PlainReportType:
     final_rows = []  # type: List[Sequence[Sequence[Any]]]
     colnames = []  # type: List[str]
     dbsession = req.dbsession
     group_ids = req.user.ids_of_groups_user_may_report_on
     superuser = req.user.superuser
     via_index = req.get_bool_param(ViewParam.VIA_INDEX, True)
     if via_index:
         # noinspection PyUnresolvedReferences
         query = (
             select([
                 TaskIndexEntry.task_table_name.label("task"),
                 extract_year(
                     TaskIndexEntry.when_created_utc).label("year"),  # noqa
                 extract_month(TaskIndexEntry.when_created_utc).label(
                     "month"),  # noqa
                 func.count().label("num_tasks_added"),
             ]).select_from(TaskIndexEntry.__table__).group_by(
                 "task", "year", "month").order_by("task", desc("year"),
                                                   desc("month")))
         if not superuser:
             # Restrict to accessible groups
             # noinspection PyProtectedMember
             query = query.where(TaskIndexEntry.group_id.in_(group_ids))
         rows, colnames = get_rows_fieldnames_from_query(dbsession, query)
         final_rows.extend(rows)
     else:
         classes = Task.all_subclasses_by_tablename()
         for cls in classes:
             # noinspection PyProtectedMember
             select_fields = [
                 literal(cls.__tablename__).label("task"),
                 # func.year() is specific to some DBs, e.g. MySQL
                 # so is func.extract();
                 # http://modern-sql.com/feature/extract
                 extract_year(isotzdatetime_to_utcdatetime(cls.when_created)
                              ).label("year"),
                 extract_month(
                     isotzdatetime_to_utcdatetime(
                         cls.when_created)).label("month"),
                 func.count().label("num_tasks_added"),
             ]
             # noinspection PyUnresolvedReferences
             select_from = cls.__table__
             # noinspection PyProtectedMember
             wheres = [cls._current == True]  # nopep8
             if not superuser:
                 # Restrict to accessible groups
                 # noinspection PyProtectedMember
                 wheres.append(cls._group_id.in_(group_ids))
             group_by = ["year", "month"]
             order_by = [desc("year"), desc("month")]
             # ... http://docs.sqlalchemy.org/en/latest/core/tutorial.html#ordering-or-grouping-by-a-label  # noqa
             query = select(select_fields) \
                 .select_from(select_from) \
                 .where(and_(*wheres)) \
                 .group_by(*group_by) \
                 .order_by(*order_by)
             rows, colnames = get_rows_fieldnames_from_query(
                 dbsession, query)
             final_rows.extend(rows)
     return PlainReportType(rows=final_rows, column_names=colnames)
Ejemplo n.º 17
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
Ejemplo n.º 18
0
    def check_index(
        cls, session: SqlASession, show_all_bad: bool = False
    ) -> bool:
        """
        Checks the index.

        Args:
            session:
                an SQLAlchemy Session
            show_all_bad:
                show all bad entries? (If false, return upon the first)

        Returns:
            bool: is the index OK?
        """
        ok = True

        log.info("Checking all task indexes represent valid entries")
        for taskclass in Task.all_subclasses_by_tablename():
            tasktablename = taskclass.tablename
            log.debug("Checking {}", tasktablename)
            # noinspection PyUnresolvedReferences,PyProtectedMember
            q_idx_without_original = session.query(TaskIndexEntry).filter(
                TaskIndexEntry.task_table_name == tasktablename,
                ~exists()
                .select_from(taskclass.__table__)
                .where(
                    and_(
                        TaskIndexEntry.task_pk == taskclass._pk,
                        taskclass._current == True,  # noqa: E712
                    )
                ),
            )
            # No check for a valid patient at this time.
            for index in q_idx_without_original:
                log.error("Task index without matching original: {!r}", index)
                ok = False
                if not show_all_bad:
                    return ok

        log.info("Checking all tasks have an index")
        for taskclass in Task.all_subclasses_by_tablename():
            tasktablename = taskclass.tablename
            log.debug("Checking {}", tasktablename)
            # noinspection PyUnresolvedReferences,PyProtectedMember
            q_original_with_idx = session.query(taskclass).filter(
                taskclass._current == True,  # noqa: E712
                ~exists()
                .select_from(TaskIndexEntry.__table__)
                .where(
                    and_(
                        TaskIndexEntry.task_pk == taskclass._pk,
                        TaskIndexEntry.task_table_name == tasktablename,
                    )
                ),
            )
            for orig in q_original_with_idx:
                log.error("Task without index entry: {!r}", orig)
                ok = False
                if not show_all_bad:
                    return ok

        return ok
Ejemplo n.º 19
0
    def test_all_tasks(self) -> None:
        self.announce("test_all_tasks")
        from datetime import date
        import hl7
        from sqlalchemy.sql.schema import Column
        from camcops_server.cc_modules.cc_ctvinfo import CtvInfo  # noqa: F811
        from camcops_server.cc_modules.cc_patient import Patient  # noqa: F811
        from camcops_server.cc_modules.cc_simpleobjects import IdNumReference
        from camcops_server.cc_modules.cc_snomed import (  # noqa: F811
            SnomedExpression, )
        from camcops_server.cc_modules.cc_string import APPSTRING_TASKNAME
        from camcops_server.cc_modules.cc_summaryelement import SummaryElement
        from camcops_server.cc_modules.cc_trackerhelpers import (  # noqa: F811
            TrackerInfo, )
        from camcops_server.cc_modules.cc_spreadsheet import (  # noqa: F811
            SpreadsheetPage, )
        from camcops_server.cc_modules.cc_xml import XmlElement

        subclasses = Task.all_subclasses_by_tablename()
        tables = [cls.tablename for cls in subclasses]
        log.info("Actual task table names: {!r} (n={})", tables, len(tables))
        req = self.req
        recipdef = self.recipdef
        dummy_data_factory = DummyDataInserter()
        for cls in subclasses:
            log.info("Testing {}", cls)
            assert cls.extrastring_taskname != APPSTRING_TASKNAME
            q = self.dbsession.query(cls)
            t = q.first()  # type: Task

            self.assertIsNotNone(t, "Missing task!")

            # Name validity
            validate_task_tablename(t.tablename)

            # Core functions
            self.assertIsInstance(t.is_complete(), bool)
            self.assertIsInstance(t.get_task_html(req), str)
            for trackerinfo in t.get_trackers(req):
                self.assertIsInstance(trackerinfo, TrackerInfo)
            ctvlist = t.get_clinical_text(req)
            if ctvlist is not None:
                for ctvinfo in ctvlist:
                    self.assertIsInstance(ctvinfo, CtvInfo)
            for est in t.get_all_summary_tables(req):
                self.assertIsInstance(est.get_spreadsheet_page(),
                                      SpreadsheetPage)
                self.assertIsInstance(est.get_xml_element(), XmlElement)

            self.assertIsInstance(t.has_patient, bool)
            self.assertIsInstance(t.is_anonymous, bool)
            self.assertIsInstance(t.has_clinician, bool)
            self.assertIsInstance(t.has_respondent, bool)
            self.assertIsInstance(t.tablename, str)
            for fn in t.get_fieldnames():
                self.assertIsInstance(fn, str)
            self.assertIsInstance(t.field_contents_valid(), bool)
            for msg in t.field_contents_invalid_because():
                self.assertIsInstance(msg, str)
            for fn in t.get_blob_fields():
                self.assertIsInstance(fn, str)

            self.assertIsInstance(t.pk,
                                  int)  # all our examples do have PKs  # noqa
            self.assertIsInstance(t.is_preserved(), bool)
            self.assertIsInstance(t.was_forcibly_preserved(), bool)
            self.assertIsInstanceOrNone(t.get_creation_datetime(), Pendulum)
            self.assertIsInstanceOrNone(t.get_creation_datetime_utc(),
                                        Pendulum)
            self.assertIsInstanceOrNone(
                t.get_seconds_from_creation_to_first_finish(), float)

            self.assertIsInstance(t.get_adding_user_id(), int)
            self.assertIsInstance(t.get_adding_user_username(), str)
            self.assertIsInstance(t.get_removing_user_username(), str)
            self.assertIsInstance(t.get_preserving_user_username(), str)
            self.assertIsInstance(t.get_manually_erasing_user_username(), str)

            # Summaries
            for se in t.standard_task_summary_fields():
                self.assertIsInstance(se, SummaryElement)

            # SNOMED-CT
            if req.snomed_supported:
                for snomed_code in t.get_snomed_codes(req):
                    self.assertIsInstance(snomed_code, SnomedExpression)

            # Clinician
            self.assertIsInstance(t.get_clinician_name(), str)

            # Respondent
            self.assertIsInstance(t.is_respondent_complete(), bool)

            # Patient
            self.assertIsInstanceOrNone(t.patient, Patient)
            self.assertIsInstance(t.is_female(), bool)
            self.assertIsInstance(t.is_male(), bool)
            self.assertIsInstanceOrNone(t.get_patient_server_pk(), int)
            self.assertIsInstance(t.get_patient_forename(), str)
            self.assertIsInstance(t.get_patient_surname(), str)
            dob = t.get_patient_dob()
            assert (dob is None or isinstance(dob, date)
                    or isinstance(dob, Date))
            self.assertIsInstanceOrNone(t.get_patient_dob_first11chars(), str)
            self.assertIsInstance(t.get_patient_sex(), str)
            self.assertIsInstance(t.get_patient_address(), str)
            for idnum in t.get_patient_idnum_objects():
                self.assertIsInstance(idnum.get_idnum_reference(),
                                      IdNumReference)
                self.assertIsInstance(idnum.is_superficially_valid(), bool)
                self.assertIsInstance(idnum.description(req), str)
                self.assertIsInstance(idnum.short_description(req), str)
                self.assertIsInstance(idnum.get_filename_component(req), str)

            # HL7 v2
            pidseg = t.get_patient_hl7_pid_segment(req, recipdef)
            assert isinstance(pidseg, str) or isinstance(pidseg, hl7.Segment)
            for dataseg in t.get_hl7_data_segments(req, recipdef):
                self.assertIsInstance(dataseg, hl7.Segment)
            for dataseg in t.get_hl7_extra_data_segments(recipdef):
                self.assertIsInstance(dataseg, hl7.Segment)

            # FHIR
            self.assertIsInstance(
                t.get_fhir_bundle(req, recipdef).as_json(),
                dict)  # the main test is not crashing!

            # Other properties
            self.assertIsInstance(t.is_erased(), bool)
            self.assertIsInstance(t.is_live_on_tablet(), bool)
            for attrname, col in t.gen_text_filter_columns():
                self.assertIsInstance(attrname, str)
                self.assertIsInstance(col, Column)

            # Views
            for page in t.get_spreadsheet_pages(req):
                self.assertIsInstance(page.get_tsv(), str)
            self.assertIsInstance(t.get_xml(req), str)
            self.assertIsInstance(t.get_html(req), str)
            self.assertIsInstance(t.get_pdf(req), bytes)
            self.assertIsInstance(t.get_pdf_html(req), str)
            self.assertIsInstance(t.suggested_pdf_filename(req), str)
            self.assertIsInstance(
                t.get_rio_metadata(
                    req,
                    which_idnum=1,
                    uploading_user_id=self.user.id,
                    document_type="some_doc_type",
                ),
                str,
            )

            # Help
            help_url = t.help_url()
            self.assertEqual(
                urllib.request.urlopen(help_url).getcode(),
                HttpStatus.OK,
                msg=f"Task help not found at {help_url}",
            )

            # Special operations
            t.apply_special_note(req,
                                 "Debug: Special note! (1)",
                                 from_console=True)
            t.apply_special_note(req,
                                 "Debug: Special note! (2)",
                                 from_console=False)
            self.assertIsInstance(t.special_notes, list)
            t.cancel_from_export_log(req, from_console=True)
            t.cancel_from_export_log(req, from_console=False)

            # Insert random data and check it doesn't crash.
            dummy_data_factory.fill_in_task_fields(t)
            self.assertIsInstance(t.get_html(req), str)

            # Destructive special operations
            self.assertFalse(t.is_erased())
            t.manually_erase(req)
            self.assertTrue(t.is_erased())
            t.delete_entirely(req)
Ejemplo n.º 20
0
    def get_rows_colnames(self, req: "CamcopsRequest") -> PlainReportType:
        dbsession = req.dbsession
        group_ids = req.user.ids_of_groups_user_may_report_on
        superuser = req.user.superuser

        by_year = req.get_bool_param(ViewParam.BY_YEAR, DEFAULT_BY_YEAR)
        by_month = req.get_bool_param(ViewParam.BY_MONTH, DEFAULT_BY_MONTH)
        by_task = req.get_bool_param(ViewParam.BY_TASK, DEFAULT_BY_TASK)
        by_user = req.get_bool_param(ViewParam.BY_USER, DEFAULT_BY_USER)
        via_index = req.get_bool_param(ViewParam.VIA_INDEX, True)

        label_year = "year"
        label_month = "month"
        label_task = "task"
        label_user = "******"
        label_n = "num_tasks_added"

        final_rows = []  # type: List[Sequence[Sequence[Any]]]
        colnames = []  # type: List[str]  # for type checker

        if via_index:
            # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            # Indexed method (preferable)
            # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            selectors = []  # type: List[FunctionElement]
            groupers = []  # type: List[str]
            sorters = []  # type: List[Union[str, UnaryExpression]]
            if by_year:
                selectors.append(
                    extract_year(
                        TaskIndexEntry.when_created_utc).label(label_year))
                groupers.append(label_year)
                sorters.append(desc(label_year))
            if by_month:
                selectors.append(
                    extract_month(
                        TaskIndexEntry.when_created_utc).label(label_month))
                groupers.append(label_month)
                sorters.append(desc(label_month))
            if by_task:
                selectors.append(
                    TaskIndexEntry.task_table_name.label(label_task))
                groupers.append(label_task)
                sorters.append(label_task)
            if by_user:
                selectors.append(User.username.label(label_user))
                groupers.append(label_user)
                sorters.append(label_user)
            # Regardless:
            selectors.append(func.count().label(label_n))

            # noinspection PyUnresolvedReferences
            query = (
                select(selectors).select_from(
                    TaskIndexEntry.__table__).group_by(*groupers).order_by(
                        *sorters)
                # ... https://docs.sqlalchemy.org/en/latest/core/tutorial.html#ordering-or-grouping-by-a-label  # noqa
            )
            if by_user:
                # noinspection PyUnresolvedReferences
                query = query.select_from(User.__table__).where(
                    TaskIndexEntry.adding_user_id == User.id)
            if not superuser:
                # Restrict to accessible groups
                # noinspection PyProtectedMember
                query = query.where(TaskIndexEntry.group_id.in_(group_ids))
            rows, colnames = get_rows_fieldnames_from_query(dbsession, query)
            # noinspection PyTypeChecker
            final_rows = rows
        else:
            # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            # Without using the server method (worse)
            # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
            groupers = []  # type: List[str]
            sorters = []  # type: List[Tuple[str, bool]]
            # ... (key, reversed/descending)

            if by_year:
                groupers.append(label_year)
                sorters.append((label_year, True))
            if by_month:
                groupers.append(label_month)
                sorters.append((label_month, True))
            if by_task:
                groupers.append(label_task)
                # ... redundant in the SQL, which involves multiple queries
                # (one per task type), but useful for the Python
                # aggregation.
                sorters.append((label_task, False))
            if by_user:
                groupers.append(label_user)
                sorters.append((label_user, False))

            classes = Task.all_subclasses_by_tablename()
            counter = Counter()
            for cls in classes:
                selectors = []  # type: List[FunctionElement]

                if by_year:
                    selectors.append(
                        # func.year() is specific to some DBs, e.g. MySQL
                        # so is func.extract();
                        # http://modern-sql.com/feature/extract
                        extract_year(
                            isotzdatetime_to_utcdatetime(cls.when_created)
                        ).label(label_year))
                if by_month:
                    selectors.append(
                        extract_month(
                            isotzdatetime_to_utcdatetime(
                                cls.when_created)).label(label_month))
                if by_task:
                    selectors.append(
                        literal(cls.__tablename__).label(label_task))
                if by_user:
                    selectors.append(User.username.label(label_user))
                # Regardless:
                selectors.append(func.count().label(label_n))

                # noinspection PyUnresolvedReferences
                query = (
                    select(selectors).select_from(cls.__table__).where(
                        cls._current == True)  # noqa: E712
                    .group_by(*groupers))
                if by_user:
                    # noinspection PyUnresolvedReferences
                    query = query.select_from(
                        User.__table__).where(cls._adding_user_id == User.id)
                if not superuser:
                    # Restrict to accessible groups
                    # noinspection PyProtectedMember
                    query = query.where(cls._group_id.in_(group_ids))
                rows, colnames = get_rows_fieldnames_from_query(
                    dbsession, query)
                if by_task:
                    final_rows.extend(rows)
                else:
                    for row in rows:  # type: RowProxy
                        key = tuple(row[keyname] for keyname in groupers)
                        count = row[label_n]
                        counter.update({key: count})
            if not by_task:
                PseudoRow = namedtuple("PseudoRow", groupers + [label_n])
                for key, total in counter.items():
                    values = list(key) + [total]
                    final_rows.append(PseudoRow(*values))
            # Complex sorting:
            # https://docs.python.org/3/howto/sorting.html#sort-stability-and-complex-sorts  # noqa
            for key, descending in reversed(sorters):
                final_rows.sort(key=attrgetter(key), reverse=descending)

        return PlainReportType(rows=final_rows, column_names=colnames)
Ejemplo n.º 21
0
class Iesr(TaskHasPatientMixin, Task, metaclass=IesrMetaclass):
    """
    Server implementation of the IES-R task.
    """

    __tablename__ = "iesr"
    shortname = "IES-R"
    provides_trackers = True

    event = Column("event", UnicodeText, comment="Relevant event")

    NQUESTIONS = 22
    MIN_SCORE = 0  # per question
    MAX_SCORE = 4  # per question

    MAX_TOTAL = 88
    MAX_AVOIDANCE = 32
    MAX_INTRUSION = 28
    MAX_HYPERAROUSAL = 28

    QUESTION_FIELDS = strseq("q", 1, NQUESTIONS)
    AVOIDANCE_QUESTIONS = [5, 7, 8, 11, 12, 13, 17, 22]
    AVOIDANCE_FIELDS = Task.fieldnames_from_list("q", AVOIDANCE_QUESTIONS)
    INTRUSION_QUESTIONS = [1, 2, 3, 6, 9, 16, 20]
    INTRUSION_FIELDS = Task.fieldnames_from_list("q", INTRUSION_QUESTIONS)
    HYPERAROUSAL_QUESTIONS = [4, 10, 14, 15, 18, 19, 21]
    HYPERAROUSAL_FIELDS = Task.fieldnames_from_list("q",
                                                    HYPERAROUSAL_QUESTIONS)

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Impact of Events Scale – Revised")

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label="IES-R 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.avoidance_score(),
                plot_label="IES-R avoidance score",
                axis_label=f"Avoidance score (out of {self.MAX_AVOIDANCE})",
                axis_min=-0.5,
                axis_max=self.MAX_AVOIDANCE + 0.5,
            ),
            TrackerInfo(
                value=self.intrusion_score(),
                plot_label="IES-R intrusion score",
                axis_label=f"Intrusion score (out of {self.MAX_INTRUSION})",
                axis_min=-0.5,
                axis_max=self.MAX_INTRUSION + 0.5,
            ),
            TrackerInfo(
                value=self.hyperarousal_score(),
                plot_label="IES-R hyperarousal score",
                axis_label=
                f"Hyperarousal score (out of {self.MAX_HYPERAROUSAL})",  # noqa
                axis_min=-0.5,
                axis_max=self.MAX_HYPERAROUSAL + 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="avoidance_score",
                coltype=Integer(),
                value=self.avoidance_score(),
                comment=f"Avoidance score (/ {self.MAX_AVOIDANCE})",
            ),
            SummaryElement(
                name="intrusion_score",
                coltype=Integer(),
                value=self.intrusion_score(),
                comment=f"Intrusion score (/ {self.MAX_INTRUSION})",
            ),
            SummaryElement(
                name="hyperarousal_score",
                coltype=Integer(),
                value=self.hyperarousal_score(),
                comment=f"Hyperarousal score (/ {self.MAX_HYPERAROUSAL})",
            ),
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        t = self.total_score()
        a = self.avoidance_score()
        i = self.intrusion_score()
        h = self.hyperarousal_score()
        return [
            CtvInfo(content=(f"IES-R total score {t}/{self.MAX_TOTAL} "
                             f"(avoidance {a}/{self.MAX_AVOIDANCE} "
                             f"intrusion {i}/{self.MAX_INTRUSION}, "
                             f"hyperarousal {h}/{self.MAX_HYPERAROUSAL})"))
        ]

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

    def avoidance_score(self) -> int:
        return self.sum_fields(self.AVOIDANCE_FIELDS)

    def intrusion_score(self) -> int:
        return self.sum_fields(self.INTRUSION_FIELDS)

    def hyperarousal_score(self) -> int:
        return self.sum_fields(self.HYPERAROUSAL_FIELDS)

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

    def get_task_html(self, req: CamcopsRequest) -> str:
        option_dict = {None: None}
        for a in range(self.MIN_SCORE, self.MAX_SCORE + 1):
            option_dict[a] = req.wappstring(AS.IESR_A_PREFIX + str(a))
        h = 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())} / {self.MAX_TOTAL}</td>
                    </td>
                    <tr>
                        <td>Avoidance score</td>
                        <td>{answer(self.avoidance_score())} / {self.MAX_AVOIDANCE}</td>
                    </td>
                    <tr>
                        <td>Intrusion score</td>
                        <td>{answer(self.intrusion_score())} / {self.MAX_INTRUSION}</td>
                    </td>
                    <tr>
                        <td>Hyperarousal score</td>
                        <td>{answer(self.hyperarousal_score())} / {self.MAX_HYPERAROUSAL}</td>
                    </td>
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                {tr_qa(req.sstring(SS.EVENT), self.event)}
            </table>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="75%">Question</th>
                    <th width="25%">Answer (0–4)</th>
                </tr>
        """  # noqa
        for q in range(1, self.NQUESTIONS + 1):
            a = getattr(self, "q" + str(q))
            fa = (f"{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

    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        codes = [
            SnomedExpression(req.snomed(
                SnomedLookup.IESR_PROCEDURE_ASSESSMENT))
        ]
        if self.is_complete():
            codes.append(
                SnomedExpression(
                    req.snomed(SnomedLookup.IESR_SCALE),
                    {req.snomed(SnomedLookup.IESR_SCORE): self.total_score()},
                ))
        return codes
Ejemplo n.º 22
0
class Suppsp(TaskHasPatientMixin, Task, metaclass=SuppspMetaclass):
    __tablename__ = "suppsp"
    shortname = "SUPPS-P"

    N_QUESTIONS = 20
    MIN_SCORE_PER_Q = 1
    MAX_SCORE_PER_Q = 4
    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)
    NEGATIVE_URGENCY_QUESTIONS = Task.fieldnames_from_list("q", {6, 8, 13, 15})
    LACK_OF_PERSEVERANCE_QUESTIONS = Task.fieldnames_from_list(
        "q", {1, 4, 7, 11})
    LACK_OF_PREMEDITATION_QUESTIONS = Task.fieldnames_from_list(
        "q", {2, 5, 12, 19})
    SENSATION_SEEKING_QUESTIONS = Task.fieldnames_from_list(
        "q", {9, 14, 16, 18})
    POSITIVE_URGENCY_QUESTIONS = Task.fieldnames_from_list(
        "q", {3, 10, 17, 20})

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Short UPPS-P Impulsive Behaviour Scale")

    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="negative_urgency",
                coltype=Integer(),
                value=self.negative_urgency_score(),
                comment=f"Negative urgency {subscale_range}",
            ),
            SummaryElement(
                name="lack_of_perseverance",
                coltype=Integer(),
                value=self.lack_of_perseverance_score(),
                comment=f"Lack of perseverance {subscale_range}",
            ),
            SummaryElement(
                name="lack_of_premeditation",
                coltype=Integer(),
                value=self.lack_of_premeditation_score(),
                comment=f"Lack of premeditation {subscale_range}",
            ),
            SummaryElement(
                name="sensation_seeking",
                coltype=Integer(),
                value=self.sensation_seeking_score(),
                comment=f"Sensation seeking {subscale_range}",
            ),
            SummaryElement(
                name="positive_urgency",
                coltype=Integer(),
                value=self.positive_urgency_score(),
                comment=f"Positive urgency {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 total_score(self) -> int:
        return self.sum_fields(self.ALL_QUESTIONS)

    def negative_urgency_score(self) -> int:
        return self.sum_fields(self.NEGATIVE_URGENCY_QUESTIONS)

    def lack_of_perseverance_score(self) -> int:
        return self.sum_fields(self.LACK_OF_PERSEVERANCE_QUESTIONS)

    def lack_of_premeditation_score(self) -> int:
        return self.sum_fields(self.LACK_OF_PREMEDITATION_QUESTIONS)

    def sensation_seeking_score(self) -> int:
        return self.sum_fields(self.SENSATION_SEEKING_QUESTIONS)

    def positive_urgency_score(self) -> int:
        return self.sum_fields(self.POSITIVE_URGENCY_QUESTIONS)

    def get_task_html(self, req: CamcopsRequest) -> str:
        normal_score_dict = {
            None: None,
            1: "1 — " + self.wxstring(req, "a0"),
            2: "2 — " + self.wxstring(req, "a1"),
            3: "3 — " + self.wxstring(req, "a2"),
            4: "4 — " + self.wxstring(req, "a3"),
        }
        reverse_score_dict = {
            None: None,
            4: "4 — " + self.wxstring(req, "a0"),
            3: "3 — " + self.wxstring(req, "a1"),
            2: "2 — " + self.wxstring(req, "a2"),
            1: "1 — " + self.wxstring(req, "a3"),
        }
        reverse_q_nums = {3, 6, 8, 9, 10, 13, 14, 15, 16, 17, 18, 20}
        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)
            score_dict = normal_score_dict

            if q_num in reverse_q_nums:
                score_dict = reverse_score_dict

            answer_cell = get_from_dict(score_dict, score)

            rows += tr_qa(question_cell, answer_cell)

        html = """
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {tr_is_complete}
                    {total_score}
                    {negative_urgency_score}
                    {lack_of_perseverance_score}
                    {lack_of_premeditation_score}
                    {sensation_seeking_score}
                    {positive_urgency_score}
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="60%">Question</th>
                    <th width="40%">Score</th>
                </tr>
                {rows}
            </table>
            <div class="{CssClass.FOOTNOTES}">
                [1] Sum for questions 1–20.
                [2] Sum for questions 6, 8, 13, 15.
                [3] Sum for questions 1, 4, 7, 11.
                [4] Sum for questions 2, 5, 12, 19.
                [5] Sum for questions 9, 14, 16, 18.
                [6] Sum for questions 3, 10, 17, 20.
            </div>
        """.format(
            CssClass=CssClass,
            tr_is_complete=self.get_is_complete_tr(req),
            total_score=tr(
                req.sstring(SS.TOTAL_SCORE) + " <sup>[1]</sup>",
                f"{answer(self.total_score())} {fullscale_range}",
            ),
            negative_urgency_score=tr(
                self.wxstring(req, "negative_urgency") + " <sup>[2]</sup>",
                f"{answer(self.negative_urgency_score())} {subscale_range}",
            ),
            lack_of_perseverance_score=tr(
                self.wxstring(req, "lack_of_perseverance") + " <sup>[3]</sup>",
                f"{answer(self.lack_of_perseverance_score())} {subscale_range}",  # noqa: E501
            ),
            lack_of_premeditation_score=tr(
                self.wxstring(req, "lack_of_premeditation") +
                " <sup>[4]</sup>",
                f"{answer(self.lack_of_premeditation_score())} {subscale_range}",  # noqa: E501
            ),
            sensation_seeking_score=tr(
                self.wxstring(req, "sensation_seeking") + " <sup>[5]</sup>",
                f"{answer(self.sensation_seeking_score())} {subscale_range}",
            ),
            positive_urgency_score=tr(
                self.wxstring(req, "positive_urgency") + " <sup>[6]</sup>",
                f"{answer(self.positive_urgency_score())} {subscale_range}",
            ),
            rows=rows,
        )
        return html