Ejemplo n.º 1
0
class NewsContent(Base):
    __table__ = Table(
        'news_content', Base.metadata,
        Column('id', Integer, primary_key=True, autoincrement=True),
        Column('WEBSITE_ID', Integer), Column('CRAWL_URL', VARCHAR(100)),
        Column('NEWS_NAME', VARCHAR(100)), Column('NEWS_URL', VARCHAR(100)),
        Column('NEWS_IMAGE', VARCHAR(100)), Column('NEWS_DESC', TEXT),
        Column('KEYWORDS', VARCHAR(100)), Column('PUBLISH_TIME', DATE),
        Column('NEWS_RESOURCE', VARCHAR(50)), Column('NEWS_AUTHOR',
                                                     VARCHAR(50)),
        Column('COMMENT_NUM', Integer), Column('READ_NUM', Integer))
Ejemplo n.º 2
0
class ExportedTaskEmail(Base):
    """
    Represents an individual email export.
    """
    __tablename__ = "_exported_task_email"

    id = Column("id",
                BigInteger,
                primary_key=True,
                autoincrement=True,
                comment="Arbitrary primary key")
    exported_task_id = Column("exported_task_id",
                              BigInteger,
                              ForeignKey(ExportedTask.id),
                              nullable=False,
                              comment="FK to {}.{}".format(
                                  ExportedTask.__tablename__,
                                  ExportedTask.id.name))
    email_id = Column("email_id",
                      BigInteger,
                      ForeignKey(Email.id),
                      comment="FK to {}.{}".format(Email.__tablename__,
                                                   Email.id.name))

    exported_task = relationship(ExportedTask)
    email = relationship(Email)

    def __init__(self, exported_task: ExportedTask = None) -> None:
        """
        Args:
            exported_task: :class:`ExportedTask` object
        """
        self.exported_task = exported_task

    def export_task(self, req: "CamcopsRequest") -> None:
        """
        Exports the task itself to an email.

        Args:
            req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
        """
        exported_task = self.exported_task
        task = exported_task.task
        recipient = exported_task.recipient
        task_format = recipient.task_format
        task_filename = os.path.basename(recipient.get_filename(req, task))
        # ... we don't want a full path for e-mail!
        encoding = "utf8"

        # Export task
        attachments = []  # type: List[Tuple[str, bytes]]
        if task_format == FileType.PDF:
            binary = task.get_pdf(req)
        elif task_format == FileType.HTML:
            binary = task.get_html(req).encode(encoding)
        elif task_format == FileType.XML:
            binary = task.get_xml(req).encode(encoding)
        else:
            raise AssertionError("Unknown task_format")
        attachments.append((task_filename, binary))

        self.email = Email(
            from_addr=recipient.email_from,
            # date: automatic
            sender=recipient.email_sender,
            reply_to=recipient.email_reply_to,
            to=recipient.email_to,
            cc=recipient.email_cc,
            bcc=recipient.email_bcc,
            subject=recipient.get_email_subject(req, task),
            body=recipient.get_email_body(req, task),
            content_type=(CONTENT_TYPE_HTML if recipient.email_body_as_html
                          else CONTENT_TYPE_TEXT),
            charset=encoding,
            attachments_binary=attachments,
            save_msg_string=recipient.email_keep_message,
        )
        self.email.send(
            host=recipient.email_host,
            username=recipient.email_host_username,
            password=recipient.email_host_password,
            port=recipient.email_port,
            use_tls=recipient.email_use_tls,
        )
        if self.email.sent:
            exported_task.succeed()
        else:
            exported_task.abort("Failed to send e-mail")
Ejemplo n.º 3
0
class SimpleTask(Base, metaclass=SimpleTaskMetaclass):
    __tablename__ = "table_for_simple_task"
    some_pk = Column("some_pk", Integer, primary_key=True)
Ejemplo n.º 4
0
class CbiR(TaskHasPatientMixin,
           TaskHasRespondentMixin,
           Task,
           metaclass=CbiRMetaclass):
    """
    Server implementation of the CBI-R task.
    """
    __tablename__ = "cbir"
    shortname = "CBI-R"
    longname = "Cambridge Behavioural Inventory, Revised"

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

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

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

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

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

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

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

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

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

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

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

        h = """
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {complete_tr}
                </table>
                <table class="{CssClass.SUMMARY}">
                    <tr>
                        <th>Subscale</th>
                        <th>Frequency (% of max)</th>
                        <th>Distress (% of max)</th>
                    </tr>
                    <tr>
                        <td>{heading_memory}</td>
                        <td>{mem_f}</td>
                        <td>{mem_d}</td>
                    </tr>
                    <tr>
                        <td>{heading_everyday}</td>
                        <td>{everyday_f}</td>
                        <td>{everyday_d}</td>
                    </tr>
                    <tr>
                        <td>{heading_selfcare}</td>
                        <td>{self_f}</td>
                        <td>{self_d}</td>
                    </tr>
                    <tr>
                        <td>{heading_behaviour}</td>
                        <td>{behav_f}</td>
                        <td>{behav_d}</td>
                    </tr>
                    <tr>
                        <td>{heading_mood}</td>
                        <td>{mood_f}</td>
                        <td>{mood_d}</td>
                    </tr>
                    <tr>
                        <td>{heading_beliefs}</td>
                        <td>{beliefs_f}</td>
                        <td>{beliefs_d}</td>
                    </tr>
                    <tr>
                        <td>{heading_eating}</td>
                        <td>{eating_f}</td>
                        <td>{eating_d}</td>
                    </tr>
                    <tr>
                        <td>{heading_sleep}</td>
                        <td>{sleep_f}</td>
                        <td>{sleep_d}</td>
                    </tr>
                    <tr>
                        <td>{heading_motor}</td>
                        <td>{motor_f}</td>
                        <td>{motor_d}</td>
                    </tr>
                    <tr>
                        <td>{heading_motivation}</td>
                        <td>{motivation_f}</td>
                        <td>{motivation_d}</td>
                    </tr>
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                {tr_blanks}
                {tr_comments}
            </table>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="50%">Question</th>
                    <th width="25%">Frequency (0–4)</th>
                    <th width="25%">Distress (0–4)</th>
                </tr>
        """.format(
            CssClass=CssClass,
            complete_tr=self.get_is_complete_tr(req),
            heading_memory=heading_memory,
            mem_f=answer(self.frequency_subscore(*self.QNUMS_MEMORY)),
            mem_d=answer(self.distress_subscore(*self.QNUMS_MEMORY)),
            heading_everyday=heading_everyday,
            everyday_f=answer(self.frequency_subscore(*self.QNUMS_EVERYDAY)),
            everyday_d=answer(self.distress_subscore(*self.QNUMS_EVERYDAY)),
            heading_selfcare=heading_selfcare,
            self_f=answer(self.frequency_subscore(*self.QNUMS_SELF)),
            self_d=answer(self.distress_subscore(*self.QNUMS_SELF)),
            heading_behaviour=heading_behaviour,
            behav_f=answer(self.frequency_subscore(*self.QNUMS_BEHAVIOUR)),
            behav_d=answer(self.distress_subscore(*self.QNUMS_BEHAVIOUR)),
            heading_mood=heading_mood,
            mood_f=answer(self.frequency_subscore(*self.QNUMS_MOOD)),
            mood_d=answer(self.distress_subscore(*self.QNUMS_MOOD)),
            heading_beliefs=heading_beliefs,
            beliefs_f=answer(self.frequency_subscore(*self.QNUMS_BELIEFS)),
            beliefs_d=answer(self.distress_subscore(*self.QNUMS_BELIEFS)),
            heading_eating=heading_eating,
            eating_f=answer(self.frequency_subscore(*self.QNUMS_EATING)),
            eating_d=answer(self.distress_subscore(*self.QNUMS_EATING)),
            heading_sleep=heading_sleep,
            sleep_f=answer(self.frequency_subscore(*self.QNUMS_SLEEP)),
            sleep_d=answer(self.distress_subscore(*self.QNUMS_SLEEP)),
            heading_motor=heading_motor,
            motor_f=answer(self.frequency_subscore(*self.QNUMS_STEREOTYPY)),
            motor_d=answer(self.distress_subscore(*self.QNUMS_STEREOTYPY)),
            heading_motivation=heading_motivation,
            motivation_f=answer(
                self.frequency_subscore(*self.QNUMS_MOTIVATION)),
            motivation_d=answer(
                self.distress_subscore(*self.QNUMS_MOTIVATION)),
            tr_blanks=tr(
                "Respondent confirmed that blanks are deliberate (N/A)",
                answer(get_yes_no(req, self.confirm_blanks))),
            tr_comments=tr("Comments", answer(self.comments, default="")),
        )
        h += subheading_spanning_three_columns(heading_memory)
        h += get_question_rows(*self.QNUMS_MEMORY)
        h += subheading_spanning_three_columns(heading_everyday)
        h += get_question_rows(*self.QNUMS_EVERYDAY)
        h += subheading_spanning_three_columns(heading_selfcare)
        h += get_question_rows(*self.QNUMS_SELF)
        h += subheading_spanning_three_columns(heading_behaviour)
        h += get_question_rows(*self.QNUMS_BEHAVIOUR)
        h += subheading_spanning_three_columns(heading_mood)
        h += get_question_rows(*self.QNUMS_MOOD)
        h += subheading_spanning_three_columns(heading_beliefs)
        h += get_question_rows(*self.QNUMS_BELIEFS)
        h += subheading_spanning_three_columns(heading_eating)
        h += get_question_rows(*self.QNUMS_EATING)
        h += subheading_spanning_three_columns(heading_sleep)
        h += get_question_rows(*self.QNUMS_SLEEP)
        h += subheading_spanning_three_columns(heading_motor)
        h += get_question_rows(*self.QNUMS_STEREOTYPY)
        h += subheading_spanning_three_columns(heading_motivation)
        h += get_question_rows(*self.QNUMS_MOTIVATION)
        h += """
            </table>
        """
        return h
Ejemplo n.º 5
0
class ExportedTaskHL7Message(Base):
    """
    Represents an individual HL7 message.
    """
    __tablename__ = "_exported_task_hl7msg"

    id = Column("id",
                BigInteger,
                primary_key=True,
                autoincrement=True,
                comment="Arbitrary primary key")
    exported_task_id = Column("exported_task_id",
                              BigInteger,
                              ForeignKey(ExportedTask.id),
                              nullable=False,
                              comment="FK to {}.{}".format(
                                  ExportedTask.__tablename__,
                                  ExportedTask.id.name))
    sent_at_utc = Column("sent_at_utc",
                         DateTime,
                         comment="Time message was sent at (UTC)")
    reply_at_utc = Column("reply_at_utc",
                          DateTime,
                          comment="Time message was replied to (UTC)")
    success = Column(
        "success",
        Boolean,
        comment="Message sent successfully and acknowledged by HL7 server")
    failure_reason = Column("failure_reason",
                            Text,
                            comment="Reason for failure")
    message = Column("message", LongText, comment="Message body, if kept")
    reply = Column("reply", Text, comment="Server's reply, if kept")

    exported_task = relationship(ExportedTask)

    def __init__(self,
                 exported_task: ExportedTask = None,
                 *args,
                 **kwargs) -> None:
        """
        Must support parameter-free construction, not least for
        :func:`merge_db`.
        """
        super().__init__(*args, **kwargs)
        self.exported_task = exported_task

        self._hl7_msg = None  # type: hl7.Message

    @reconstructor
    def init_on_load(self) -> None:
        """
        Called when SQLAlchemy recreates an object; see
        https://docs.sqlalchemy.org/en/latest/orm/constructors.html.
        """
        self._hl7_msg = None

    @staticmethod
    def task_acceptable_for_hl7(recipient: ExportRecipient,
                                task: "Task") -> bool:
        """
        Is the task valid for HL7 export. (For example, anonymous tasks and
        tasks missing key ID information may not be.)

        Args:
            recipient: an :class:`camcops_server.cc_modules.cc_exportrecipient.ExportRecipient`
            task: a :class:`camcops_server.cc_modules.cc_task.Task` object

        Returns:
            bool: valid?

        """  # noqa
        if not task:
            return False
        if task.is_anonymous:
            return False  # Cannot send anonymous tasks via HL7
        patient = task.patient
        if not patient:
            return False
        if not recipient.primary_idnum:
            return False  # required for HL7
        if not patient.has_idnum_type(recipient.primary_idnum):
            return False
        return True

    def valid(self) -> bool:
        """
        Checks for internal validity; returns a bool.
        """
        exported_task = self.exported_task
        task = exported_task.task
        recipient = exported_task.recipient
        return self.task_acceptable_for_hl7(recipient, task)

    def succeed(self, now: Pendulum = None) -> None:
        """
        Record that we succeeded, and so did our associated task export.
        """
        now = now or get_now_utc_datetime()
        self.success = True
        self.sent_at_utc = now
        self.exported_task.succeed()

    def abort(self, msg: str, diverted_not_sent: bool = False) -> None:
        """
        Record that we failed, and so did our associated task export.

        (Called ``abort`` not ``fail`` because PyCharm has a bug relating to
        functions named ``fail``:
        https://stackoverflow.com/questions/21954959/pycharm-unreachable-code.)

        Args:
            msg: reason for failure
            diverted_not_sent: deliberately diverted (and not counted as sent)
                rather than a sending failure?
        """
        self.success = False
        self.failure_reason = msg
        self.exported_task.abort(
            "HL7 message deliberately not sent; diverted to file"
            if diverted_not_sent else "HL7 sending failed")

    def export_task(self, req: "CamcopsRequest") -> None:
        """
        Exports the task itself to an HL7 message.

        Args:
            req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
        """
        if not self.valid():
            self.abort(
                "Unsuitable for HL7; should have been filtered out earlier")
            return
        self.make_hl7_message(req)
        recipient = self.exported_task.recipient
        if recipient.hl7_debug_divert_to_file:
            self.divert_to_file(req)
        else:
            # Proper HL7 message
            self.transmit_hl7()

    def divert_to_file(self, req: "CamcopsRequest") -> None:
        """
        Write an HL7 message to a file. For debugging.

        Args:
            req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
        """
        exported_task = self.exported_task
        recipient = exported_task.recipient
        filename = recipient.get_filename(req,
                                          exported_task.task,
                                          override_task_format="hl7")
        now_utc = get_now_utc_pendulum()
        log.info("Diverting HL7 message to file {!r}", filename)
        written = exported_task.export_file(filename=filename,
                                            text=str(self._hl7_msg))
        if not written:
            return

        if recipient.hl7_debug_treat_diverted_as_sent:
            self.sent_at_utc = now_utc
            self.succeed(now_utc)
        else:
            self.abort("Exported to file as requested but not sent via HL7",
                       diverted_not_sent=True)

    def make_hl7_message(self, req: "CamcopsRequest") -> None:
        """
        Makes an HL7 message and stores it in ``self._hl7_msg``.

        May also store it in ``self.message`` (which is saved to the database),
        if we're saving HL7 messages.

        See

        - http://python-hl7.readthedocs.org/en/latest/index.html
        """
        task = self.exported_task.task
        recipient = self.exported_task.recipient

        # ---------------------------------------------------------------------
        # Parts
        # ---------------------------------------------------------------------
        msh_segment = make_msh_segment(message_datetime=req.now,
                                       message_control_id=str(self.id))
        pid_segment = task.get_patient_hl7_pid_segment(req, recipient)
        other_segments = task.get_hl7_data_segments(req, recipient)

        # ---------------------------------------------------------------------
        # Whole message
        # ---------------------------------------------------------------------
        segments = [msh_segment, pid_segment] + other_segments
        self._hl7_msg = hl7.Message(SEGMENT_SEPARATOR, segments)
        if recipient.hl7_keep_message:
            self.message = str(self._hl7_msg)

    def transmit_hl7(self) -> None:
        """
        Sends the HL7 message over TCP/IP.

        - Default MLLP/HL7 port is 2575
        - MLLP = minimum lower layer protocol

          - http://www.cleo.com/support/byproduct/lexicom/usersguide/mllp_configuration.htm
          - http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=hl7
          - Essentially just a TCP socket with a minimal wrapper:
            http://stackoverflow.com/questions/11126918
            
        - http://python-hl7.readthedocs.org/en/latest/api.html; however,
          we've modified that
        """  # noqa
        recipient = self.exported_task.recipient

        if recipient.hl7_ping_first:
            pinged = self.ping_hl7_server(recipient)
            if not pinged:
                self.abort("Could not ping HL7 host")
                return

        try:
            log.info("Sending HL7 message to {}:{}", recipient.hl7_host,
                     recipient.hl7_port)
            with MLLPTimeoutClient(recipient.hl7_host, recipient.hl7_port,
                                   recipient.hl7_network_timeout_ms) as client:
                server_replied, reply = client.send_message(self._hl7_msg)
        except socket.timeout:
            self.abort("Failed to send message via MLLP: timeout")
            return
        except Exception as e:
            self.abort("Failed to send message via MLLP: {}".format(e))
            return

        if not server_replied:
            self.abort("No response from server")
            return

        self.reply_at_utc = get_now_utc_datetime()
        if recipient.hl7_keep_reply:
            self.reply = reply

        try:
            replymsg = hl7.parse(reply)
        except Exception as e:
            self.abort("Malformed reply: {}".format(e))
            return

        success, failure_reason = msg_is_successful_ack(replymsg)
        if success:
            self.succeed()
        else:
            self.abort(failure_reason)

    @staticmethod
    def ping_hl7_server(recipient: ExportRecipient) -> bool:
        """
        Performs a TCP/IP ping on our HL7 server; returns success. If we've
        already pinged successfully during this run, don't bother doing it
        again.

        (No HL7 PING method yet. Proposal is
        http://hl7tsc.org/wiki/index.php?title=FTSD-ConCalls-20081028
        So use TCP/IP ping.)
        
        Args:
            recipient: an :class:`camcops_server.cc_modules.cc_exportrecipient.ExportRecipient`

        Returns:
            bool: success

        """  # noqa
        timeout_s = min(recipient.hl7_network_timeout_ms // 1000, 1)
        if ping(hostname=recipient.hl7_host, timeout_s=timeout_s):
            return True
        else:
            log.error("Failed to ping {!r}", recipient.hl7_host)
            return False
Ejemplo n.º 6
0
class AppliancesBrand(Base):
    __tablename__ = 'appliances_brand'
    id = Column(Integer, primary_key=True)
    brand_name = Column(String, unique=True)
Ejemplo n.º 7
0
class Address(Base):
    __tablename__ = 'addresses'
    id = Column(Integer, primary_key=True)
    name = Column(String, unique=True)
Ejemplo n.º 8
0
class HonosBase(TaskHasPatientMixin, TaskHasClinicianMixin, Task):
    __abstract__ = True
    provides_trackers = True

    period_rated = Column("period_rated",
                          UnicodeText,
                          comment="Period being rated")

    COPYRIGHT_DIV = f"""
        <div class="{CssClass.COPYRIGHT}">
            Health of the Nation Outcome Scales:
            Copyright © Royal College of Psychiatrists.
            Used here with permission.
        </div>
    """

    QFIELDS = None  # type: List[str]  # must be overridden
    MAX_SCORE = None  # type: int  # must be overridden

    def get_trackers(self, req: CamcopsRequest) -> List[TrackerInfo]:
        return [
            TrackerInfo(
                value=self.total_score(),
                plot_label=f"{self.shortname} total score",
                axis_label=f"Total score (out of {self.MAX_SCORE})",
                axis_min=-0.5,
                axis_max=self.MAX_SCORE + 0.5,
            )
        ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        return [
            CtvInfo(content=(f"{self.shortname} 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})",
            )
        ]

    def _total_score_for_fields(self, fieldnames: List[str]) -> int:
        total = 0
        for qname in fieldnames:
            value = getattr(self, qname)
            if value is not None and 0 <= value <= 4:
                # i.e. ignore null values and 9 (= not known)
                total += value
        return total

    def total_score(self) -> int:
        return self._total_score_for_fields(self.QFIELDS)

    def get_q(self, req: CamcopsRequest, q: int) -> str:
        return self.wxstring(req, "q" + str(q) + "_s")

    def get_answer(self, req: CamcopsRequest, q: int, a: int) -> Optional[str]:
        if a == 9:
            return self.wxstring(req, "option9")
        if a is None or a < 0 or a > 4:
            return None
        return self.wxstring(req, "q" + str(q) + "_option" + str(a))
Ejemplo n.º 9
0
class CPFTLPSDischarge(TaskHasPatientMixin, TaskHasClinicianMixin, Task):
    """
    Server implementation of the CPFT_LPS_Discharge task.
    """

    __tablename__ = "cpft_lps_discharge"
    shortname = "CPFT_LPS_Discharge"
    info_filename_stem = "clinical"

    discharge_date = Column("discharge_date", Date)
    discharge_reason_code = CamcopsColumn("discharge_reason_code",
                                          UnicodeText,
                                          exempt_from_anonymisation=True)

    leaflet_or_discharge_card_given = BoolColumn(
        "leaflet_or_discharge_card_given",
        constraint_name="ck_cpft_lps_discharge_lodcg",
    )
    frequent_attender = BoolColumn("frequent_attender")
    patient_wanted_copy_of_letter = BoolColumn(
        # Was previously text! That wasn't right.
        "patient_wanted_copy_of_letter")
    gaf_at_first_assessment = CamcopsColumn(
        "gaf_at_first_assessment",
        Integer,
        permitted_value_checker=PermittedValueChecker(minimum=0, maximum=100),
    )
    gaf_at_discharge = CamcopsColumn(
        "gaf_at_discharge",
        Integer,
        permitted_value_checker=PermittedValueChecker(minimum=0, maximum=100),
    )

    referral_reason_self_harm_overdose = BoolColumn(
        "referral_reason_self_harm_overdose",
        constraint_name="ck_cpft_lps_discharge_rrshoverdose",
    )
    referral_reason_self_harm_other = BoolColumn(
        "referral_reason_self_harm_other",
        constraint_name="ck_cpft_lps_discharge_rrshother",
    )
    referral_reason_suicidal_ideas = BoolColumn(
        "referral_reason_suicidal_ideas",
        constraint_name="ck_cpft_lps_discharge_rrsuicidal",
    )
    referral_reason_behavioural_disturbance = BoolColumn(
        "referral_reason_behavioural_disturbance",
        constraint_name="ck_cpft_lps_discharge_behavdisturb",
    )
    referral_reason_low_mood = BoolColumn("referral_reason_low_mood")
    referral_reason_elevated_mood = BoolColumn("referral_reason_elevated_mood")
    referral_reason_psychosis = BoolColumn("referral_reason_psychosis")
    referral_reason_pre_transplant = BoolColumn(
        "referral_reason_pre_transplant",
        constraint_name="ck_cpft_lps_discharge_pretransplant",
    )
    referral_reason_post_transplant = BoolColumn(
        "referral_reason_post_transplant",
        constraint_name="ck_cpft_lps_discharge_posttransplant",
    )
    referral_reason_delirium = BoolColumn("referral_reason_delirium")
    referral_reason_anxiety = BoolColumn("referral_reason_anxiety")
    referral_reason_somatoform_mus = BoolColumn(
        "referral_reason_somatoform_mus",
        constraint_name="ck_cpft_lps_discharge_mus",
    )
    referral_reason_motivation_adherence = BoolColumn(
        "referral_reason_motivation_adherence",
        constraint_name="ck_cpft_lps_discharge_motivadherence",
    )
    referral_reason_capacity = BoolColumn("referral_reason_capacity")
    referral_reason_eating_disorder = BoolColumn(
        "referral_reason_eating_disorder",
        constraint_name="ck_cpft_lps_discharge_eatingdis",
    )
    referral_reason_safeguarding = BoolColumn("referral_reason_safeguarding")
    referral_reason_discharge_placement = BoolColumn(
        "referral_reason_discharge_placement",
        constraint_name="ck_cpft_lps_discharge_dcplacement",
    )
    referral_reason_cognitive_problem = BoolColumn(
        "referral_reason_cognitive_problem",
        constraint_name="ck_cpft_lps_discharge_cognitiveprob",
    )
    referral_reason_substance_alcohol = BoolColumn(
        "referral_reason_substance_alcohol",
        constraint_name="ck_cpft_lps_discharge_alcohol",
    )
    referral_reason_substance_other = BoolColumn(
        "referral_reason_substance_other",
        constraint_name="ck_cpft_lps_discharge_substanceother",
    )
    referral_reason_other = BoolColumn("referral_reason_other")
    referral_reason_transplant_organ = CamcopsColumn(
        "referral_reason_transplant_organ",
        UnicodeText,
        exempt_from_anonymisation=True,
    )
    referral_reason_other_detail = Column("referral_reason_other_detail",
                                          UnicodeText)

    diagnosis_no_active_mental_health_problem = BoolColumn(
        "diagnosis_no_active_mental_health_problem",
        constraint_name="ck_cpft_lps_discharge_nomhprob",
    )
    diagnosis_psych_1_icd10code = Column("diagnosis_psych_1_icd10code",
                                         DiagnosticCodeColType)
    diagnosis_psych_1_description = CamcopsColumn(
        "diagnosis_psych_1_description",
        UnicodeText,
        exempt_from_anonymisation=True,
    )
    diagnosis_psych_2_icd10code = Column("diagnosis_psych_2_icd10code",
                                         DiagnosticCodeColType)
    diagnosis_psych_2_description = CamcopsColumn(
        "diagnosis_psych_2_description",
        UnicodeText,
        exempt_from_anonymisation=True,
    )
    diagnosis_psych_3_icd10code = Column("diagnosis_psych_3_icd10code",
                                         DiagnosticCodeColType)
    diagnosis_psych_3_description = CamcopsColumn(
        "diagnosis_psych_3_description",
        UnicodeText,
        exempt_from_anonymisation=True,
    )
    diagnosis_psych_4_icd10code = Column("diagnosis_psych_4_icd10code",
                                         DiagnosticCodeColType)
    diagnosis_psych_4_description = CamcopsColumn(
        "diagnosis_psych_4_description",
        UnicodeText,
        exempt_from_anonymisation=True,
    )
    diagnosis_medical_1 = Column("diagnosis_medical_1", UnicodeText)
    diagnosis_medical_2 = Column("diagnosis_medical_2", UnicodeText)
    diagnosis_medical_3 = Column("diagnosis_medical_3", UnicodeText)
    diagnosis_medical_4 = Column("diagnosis_medical_4", UnicodeText)

    management_assessment_diagnostic = BoolColumn(
        "management_assessment_diagnostic",
        constraint_name="ck_cpft_lps_discharge_mx_ass_diag",
    )
    management_medication = BoolColumn("management_medication")
    management_specialling_behavioural_disturbance = BoolColumn(
        "management_specialling_behavioural_disturbance",
        # Constraint name too long for MySQL unless we do this:
        constraint_name="ck_cpft_lps_discharge_msbd",
    )
    management_supportive_patient = BoolColumn("management_supportive_patient")
    management_supportive_carers = BoolColumn("management_supportive_carers")
    management_supportive_staff = BoolColumn("management_supportive_staff")
    management_nursing_management = BoolColumn("management_nursing_management")
    management_therapy_cbt = BoolColumn("management_therapy_cbt")
    management_therapy_cat = BoolColumn("management_therapy_cat")
    management_therapy_other = BoolColumn("management_therapy_other")
    management_treatment_adherence = BoolColumn(
        "management_treatment_adherence",
        constraint_name="ck_cpft_lps_discharge_mx_rx_adhere",
    )
    management_capacity = BoolColumn("management_capacity")
    management_education_patient = BoolColumn("management_education_patient")
    management_education_carers = BoolColumn("management_education_carers")
    management_education_staff = BoolColumn("management_education_staff")
    management_accommodation_placement = BoolColumn(
        "management_accommodation_placement",
        constraint_name="ck_cpft_lps_discharge_accom",
    )
    management_signposting_external_referral = BoolColumn(
        "management_signposting_external_referral",
        constraint_name="ck_cpft_lps_discharge_mx_signpostrefer",
    )
    management_mha_s136 = BoolColumn("management_mha_s136")
    management_mha_s5_2 = BoolColumn("management_mha_s5_2")
    management_mha_s2 = BoolColumn("management_mha_s2")
    management_mha_s3 = BoolColumn("management_mha_s3")
    management_complex_case_conference = BoolColumn(
        "management_complex_case_conference",
        constraint_name="ck_cpft_lps_discharge_caseconf",
    )
    management_other = BoolColumn("management_other")
    management_other_detail = Column("management_other_detail", UnicodeText)

    outcome = CamcopsColumn("outcome",
                            UnicodeText,
                            exempt_from_anonymisation=True)
    outcome_hospital_transfer_detail = Column(
        "outcome_hospital_transfer_detail", UnicodeText)
    outcome_other_detail = Column("outcome_other_detail", UnicodeText)

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("CPFT LPS – discharge")

    def is_complete(self) -> bool:
        return bool(self.discharge_date and self.discharge_reason_code and
                    # self.outcome and  # v2.0.0
                    self.field_contents_valid())

    def get_discharge_reason(self, req: CamcopsRequest) -> Optional[str]:
        if self.discharge_reason_code == "F":
            return self.wxstring(req, "reason_code_F")
        elif self.discharge_reason_code == "A":
            return self.wxstring(req, "reason_code_A")
        elif self.discharge_reason_code == "O":
            return self.wxstring(req, "reason_code_O")
        elif self.discharge_reason_code == "C":
            return self.wxstring(req, "reason_code_C")
        else:
            return None

    def get_referral_reasons(self, req: CamcopsRequest) -> List[str]:
        potential_referral_reasons = [
            "referral_reason_self_harm_overdose",
            "referral_reason_self_harm_other",
            "referral_reason_suicidal_ideas",
            "referral_reason_behavioural_disturbance",
            "referral_reason_low_mood",
            "referral_reason_elevated_mood",
            "referral_reason_psychosis",
            "referral_reason_pre_transplant",
            "referral_reason_post_transplant",
            "referral_reason_delirium",
            "referral_reason_anxiety",
            "referral_reason_somatoform_mus",
            "referral_reason_motivation_adherence",
            "referral_reason_capacity",
            "referral_reason_eating_disorder",
            "referral_reason_safeguarding",
            "referral_reason_discharge_placement",
            "referral_reason_cognitive_problem",
            "referral_reason_substance_alcohol",
            "referral_reason_substance_other",
            "referral_reason_other",
        ]
        referral_reasons = []
        for r in potential_referral_reasons:
            if getattr(self, r):
                referral_reasons.append(self.wxstring(req, "" + r))
        return referral_reasons

    def get_managements(self, req: CamcopsRequest) -> List[str]:
        potential_managements = [
            "management_assessment_diagnostic",
            "management_medication",
            "management_specialling_behavioural_disturbance",
            "management_supportive_patient",
            "management_supportive_carers",
            "management_supportive_staff",
            "management_nursing_management",
            "management_therapy_cbt",
            "management_therapy_cat",
            "management_therapy_other",
            "management_treatment_adherence",
            "management_capacity",
            "management_education_patient",
            "management_education_carers",
            "management_education_staff",
            "management_accommodation_placement",
            "management_signposting_external_referral",
            "management_mha_s136",
            "management_mha_s5_2",
            "management_mha_s2",
            "management_mha_s3",
            "management_complex_case_conference",
            "management_other",
        ]
        managements = []
        for r in potential_managements:
            if getattr(self, r):
                managements.append(self.wxstring(req, "" + r))
        return managements

    def get_psychiatric_diagnoses(self, req: CamcopsRequest) -> List[str]:
        psychiatric_diagnoses = ([
            self.wxstring(req, "diagnosis_no_active_mental_health_problem")
        ] if self.diagnosis_no_active_mental_health_problem else [])
        for i in range(1, 4 + 1):  # magic number
            if getattr(self, "diagnosis_psych_" + str(i) + "_icd10code"):
                psychiatric_diagnoses.append(
                    ws.webify(
                        getattr(self, "diagnosis_psych_" + str(i) +
                                "_icd10code")) + " – " +
                    ws.webify(
                        getattr(self, "diagnosis_psych_" + str(i) +
                                "_description")))
        return psychiatric_diagnoses

    def get_medical_diagnoses(self) -> List[str]:
        medical_diagnoses = []
        for i in range(1, 4 + 1):  # magic number
            if getattr(self, "diagnosis_medical_" + str(i)):
                medical_diagnoses.append(
                    ws.webify(getattr(self, "diagnosis_medical_" + str(i))))
        return medical_diagnoses

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        diagnoses = (self.get_psychiatric_diagnoses(req) +
                     self.get_medical_diagnoses())
        return [
            CtvInfo(
                heading=ws.webify(self.wxstring(req, "discharge_reason")),
                content=self.get_discharge_reason(req),
            ),
            CtvInfo(
                heading=ws.webify(self.wxstring(req, "referral_reason_t")),
                content=", ".join(self.get_referral_reasons(req)),
            ),
            CtvInfo(
                heading=ws.webify(self.wxstring(req, "diagnoses_t")),
                content=", ".join(diagnoses),
            ),
            CtvInfo(
                heading=ws.webify(self.wxstring(req, "management_t")),
                content=", ".join(self.get_managements(req)),
            ),
            CtvInfo(
                heading=ws.webify(self.wxstring(req, "outcome_t")),
                content=self.outcome,
            ),
        ]

    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>
            <table class="{CssClass.TASKDETAIL}">
                <col width="40%">
                <col width="60%">
        """
        h += tr_qa(
            self.wxstring(req, "discharge_date"),
            format_datetime(
                self.discharge_date,
                DateFormat.LONG_DATE_WITH_DAY,
                default=None,
            ),
            "",
        )
        h += tr_qa(
            self.wxstring(req, "discharge_reason"),
            self.get_discharge_reason(req),
            "",
        )
        h += tr_qa(
            self.wxstring(req, "leaflet_or_discharge_card_given"),
            get_yes_no_none(req, self.leaflet_or_discharge_card_given),
            "",
        )
        h += tr_qa(
            self.wxstring(req, "frequent_attender"),
            get_yes_no_none(req, self.frequent_attender),
            "",
        )
        h += tr_qa(
            self.wxstring(req, "patient_wanted_copy_of_letter"),
            self.patient_wanted_copy_of_letter,
            "",
        )
        h += tr_qa(
            self.wxstring(req, "gaf_at_first_assessment"),
            self.gaf_at_first_assessment,
            "",
        )
        h += tr_qa(self.wxstring(req, "gaf_at_discharge"),
                   self.gaf_at_discharge, "")

        h += subheading_spanning_two_columns(
            self.wxstring(req, "referral_reason_t"))
        h += tr_span_col(answer(", ".join(self.get_referral_reasons(req))),
                         cols=2)
        h += tr_qa(
            self.wxstring(req, "referral_reason_transplant_organ"),
            self.referral_reason_transplant_organ,
            "",
        )
        h += tr_qa(
            self.wxstring(req, "referral_reason_other_detail"),
            self.referral_reason_other_detail,
            "",
        )

        h += subheading_spanning_two_columns(self.wxstring(req, "diagnoses_t"))
        h += tr_qa(
            self.wxstring(req, "psychiatric_t"),
            "\n".join(self.get_psychiatric_diagnoses(req)),
            "",
        )
        h += tr_qa(
            self.wxstring(req, "medical_t"),
            "\n".join(self.get_medical_diagnoses()),
            "",
        )

        h += subheading_spanning_two_columns(self.wxstring(
            req, "management_t"))
        h += tr_span_col(answer(", ".join(self.get_managements(req))), cols=2)
        h += tr_qa(
            self.wxstring(req, "management_other_detail"),
            self.management_other_detail,
            "",
        )

        h += subheading_spanning_two_columns(self.wxstring(req, "outcome_t"))
        h += tr_qa(self.wxstring(req, "outcome_t"), self.outcome, "")
        h += tr_qa(
            self.wxstring(req, "outcome_hospital_transfer_detail"),
            self.outcome_hospital_transfer_detail,
            "",
        )
        h += tr_qa(
            self.wxstring(req, "outcome_other_detail"),
            self.outcome_other_detail,
            "",
        )

        h += """
            </table>
        """
        return h
Ejemplo n.º 10
0
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from sqlalchemy.sql.schema import Column, ForeignKey, Table
from sqlalchemy.sql.sqltypes import Integer, String

from heliotrope.database.orm.base import mapper_registry

parody_table = Table(
    "parody",
    mapper_registry.metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("galleryinfo_id", Integer, ForeignKey("galleryinfo.id")),
    Column("parody", String, nullable=False),
    Column("url", String, nullable=False),
)
Ejemplo n.º 11
0
class Honos65(HonosBase, metaclass=Honos65Metaclass):
    """
    Server implementation of the HoNOS 65+ task.
    """

    __tablename__ = "honos65"
    shortname = "HoNOS 65+"
    info_filename_stem = "honos"

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

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

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("Health of the Nation Outcome Scales, older adults")

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

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

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

    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        codes = [
            SnomedExpression(
                req.snomed(SnomedLookup.HONOS65_PROCEDURE_ASSESSMENT))
        ]
        if self.is_complete():
            codes.append(
                SnomedExpression(
                    req.snomed(SnomedLookup.HONOS65_SCALE),
                    {
                        req.snomed(SnomedLookup.HONOS65_SCORE):
                        self.total_score()
                    },
                ))
        return codes
Ejemplo n.º 12
0
class Ace3(TaskHasPatientMixin,
           TaskHasClinicianMixin,
           Task,
           metaclass=Ace3Metaclass):
    """
    Server implementation of the ACE-III task.
    """
    __tablename__ = "ace3"
    shortname = "ACE-III"
    longname = "Addenbrooke’s Cognitive Examination III"
    provides_trackers = 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))

    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 = ("ACE-III total: {t}/{tmax} "
                "(attention {a}/{amax}, memory {m}/{mmax}, "
                "fluency {f}/{fmax}, language {lang}/{lmax}, "
                "visuospatial {v}/{vmax})").format(t=t,
                                                   a=a,
                                                   m=m,
                                                   f=f,
                                                   lang=lang,
                                                   v=v,
                                                   tmax=TOTAL_MAX,
                                                   amax=ATTN_MAX,
                                                   mmax=MEMORY_MAX,
                                                   fmax=FLUENCY_MAX,
                                                   lmax=LANG_MAX,
                                                   vmax=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="Total score (/{})".format(TOTAL_MAX)),
            SummaryElement(name="attn",
                           coltype=Integer(),
                           value=self.attn_score(),
                           comment="Attention (/{})".format(ATTN_MAX)),
            SummaryElement(name="mem",
                           coltype=Integer(),
                           value=self.mem_score(),
                           comment="Memory (/{})".format(MEMORY_MAX)),
            SummaryElement(name="fluency",
                           coltype=Integer(),
                           value=self.fluency_score(),
                           comment="Fluency (/{})".format(FLUENCY_MAX)),
            SummaryElement(name="lang",
                           coltype=Integer(),
                           value=self.lang_score(),
                           comment="Language (/{})".format(LANG_MAX)),
            SummaryElement(name="vsp",
                           coltype=Integer(),
                           value=self.vsp_score(),
                           comment="Visuospatial (/{})".format(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 not self.are_all_fields_complete(self.BASIC_COMPLETENESS_FIELDS):
            return False
        if not self.field_contents_valid():
            return False
        if (self.lang_follow_command_practice == 1 and
                not self.are_all_fields_complete(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 = (FULLWIDTH_PLOT_WIDTH / 3, 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) +
            """
                <div class="{CssClass.SUMMARY}">
                    <table class="{CssClass.SUMMARY}">
                        <tr>
                            {is_complete}
                            <td class="{CssClass.FIGURE}" rowspan="7">{figurehtml}</td>
                        </tr>
            """.format(  # noqa
                CssClass=CssClass,
                is_complete=self.get_is_complete_td_pair(req),
                figurehtml=figurehtml) +
            tr("Total ACE-III score <sup>[1]</sup>",
               answer(t) + " / 100") + tr(
                   "Attention",
                   answer(a) +
                   " / {} ({}%)".format(ATTN_MAX, percent(a, ATTN_MAX))) +
            tr(
                "Memory",
                answer(m) +
                " / {} ({}%)".format(MEMORY_MAX, percent(m, MEMORY_MAX))) +
            tr(
                "Fluency",
                answer(f) +
                " / {} ({}%)".format(FLUENCY_MAX, percent(f, FLUENCY_MAX))) +
            tr(
                "Language",
                answer(lang) +
                " / {} ({}%)".format(LANG_MAX, percent(lang, LANG_MAX))) +
            tr("Visuospatial",
               answer(v) + " / {} ({}%)".format(VSP_MAX, percent(v, VSP_MAX)))
            + """
                    </table>
                </div>
                <table class="{CssClass.TASKDETAIL}">
                    <tr>
                        <th width="75%">Question</th>
                        <th width="25%">Answer/score</td>
                    </tr>
            """.format(CssClass=CssClass) +
            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) + """
                </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>
            """.format(CssClass=CssClass))

    def get_snomed_codes(self, req: CamcopsRequest) -> List[SnomedExpression]:
        codes = [
            SnomedExpression(
                req.snomed(SnomedLookup.ACE_R_PROCEDURE_ASSESSMENT))
        ]  # noqa
        # 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
Ejemplo n.º 13
0
class Icd10Schizotypal(TaskHasClinicianMixin,
                       TaskHasPatientMixin,
                       Task,
                       metaclass=Icd10SchizotypalMetaclass):
    """
    Server implementation of the ICD10-SZTYP task.
    """
    __tablename__ = "icd10schizotypal"
    shortname = "ICD10-SZTYP"
    longname = "ICD-10 criteria for schizotypal disorder (F21)"

    date_pertains_to = Column("date_pertains_to",
                              Date,
                              comment="Date the assessment pertains to")
    comments = Column("comments", UnicodeText, comment="Clinician's comments")
    b = CamcopsColumn(
        "b",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Criterion (B). True if: the subject has never met "
        "the criteria for any disorder in F20 (Schizophrenia).")

    N_A = 9
    A_FIELDS = strseq("a", 1, N_A)

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        c = self.meets_criteria()
        if c is None:
            category = "Unknown if met or not met"
        elif c:
            category = "Met"
        else:
            category = "Not met"
        infolist = [
            CtvInfo(
                content=("Pertains to: {}. Criteria for schizotypal "
                         "disorder: {}.".format(
                             format_datetime(self.date_pertains_to,
                                             DateFormat.LONG_DATE), category)))
        ]
        if self.comments:
            infolist.append(CtvInfo(content=ws.webify(self.comments)))
        return infolist

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(name="meets_criteria",
                           coltype=Boolean(),
                           value=self.meets_criteria(),
                           comment="Meets criteria for schizotypal disorder?"),
        ]

    # Meets criteria? These also return null for unknown.
    def meets_criteria(self) -> Optional[bool]:
        if not self.is_complete():
            return None
        return self.count_booleans(self.A_FIELDS) >= 4 and self.b

    def is_complete(self) -> bool:
        return (self.date_pertains_to is not None
                and self.are_all_fields_complete(self.A_FIELDS)
                and self.b is not None and self.field_contents_valid())

    def text_row(self, req: CamcopsRequest, wstringname: str) -> str:
        return tr(td(self.wxstring(req, wstringname)),
                  td("", td_class=CssClass.SUBHEADING),
                  literal=True)

    def get_task_html(self, req: CamcopsRequest) -> str:
        q_a = self.text_row(req, "a")
        for i in range(1, self.N_A + 1):
            q_a += self.get_twocol_bool_row_true_false(
                req, "a" + str(i), self.wxstring(req, "a" + str(i)))
        q_a += self.get_twocol_bool_row_true_false(req, "b",
                                                   self.wxstring(req, "b"))
        h = """
            {clinician_comments}
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {tr_is_complete}
                    {date_pertains_to}
                    {meets_criteria}
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="80%">Question</th>
                    <th width="20%">Answer</th>
                </tr>
                {q_a}
            </table>
            {ICD10_COPYRIGHT_DIV}
        """.format(
            clinician_comments=self.get_standard_clinician_comments_block(
                req, self.comments),
            CssClass=CssClass,
            tr_is_complete=self.get_is_complete_tr(req),
            date_pertains_to=tr_qa(
                req.wappstring("date_pertains_to"),
                format_datetime(self.date_pertains_to,
                                DateFormat.LONG_DATE,
                                default=None)),
            meets_criteria=tr_qa(req.wappstring("meets_criteria"),
                                 get_yes_no_none(req, self.meets_criteria())),
            q_a=q_a,
            ICD10_COPYRIGHT_DIV=ICD10_COPYRIGHT_DIV,
        )
        return h
Ejemplo n.º 14
0
class User(BaseModel, DatedModel):
    """Holds users' data"""

    __tablename__ = "users"
    username = Column(String, nullable=False, unique=True, comment="User's identifier")
    active = Column(
        "is_active",
        BOOLEAN(),
        nullable=False,
        server_default=cast(1, BOOLEAN),
        comment="Denotes active users",
    )

    _password = Column("password", String, nullable=False, comment="Password hash")

    # User identifiers
    email = Column(
        String, nullable=True, unique=True, comment="User's personal unique email"
    )

    # meta data
    _photo = Column("photo", String, nullable=True, comment="User's avatar url")
    phone = Column(String, nullable=True, comment="Contact number")

    # User information
    first_name = Column(String, nullable=False, comment="First Name")
    last_name = Column(String, nullable=False, server_default="", comment="Last Name")

    manager_id = Column(Integer, ForeignKey("users.id"), nullable=True)
    manager: "User" = relationship(
        "User", foreign_keys=[manager_id], lazy=True, uselist=False
    )

    # Relationships

    # Define the relationship to Role via UserRoles
    roles = relationship("Role", secondary="user_roles")
    # user sessions
    sessions = relationship(
        "Session", order_by="Session.created_at.asc()", uselist=True
    )

    affiliation: "UserAffiliation" = relationship("UserAffiliation", uselist=False)

    token = None

    def set_password(self, val: str):
        regx = re.compile(current_app.config["PASSWORD_RULE"])
        if not regx.match(val):
            raise UserExceptions.password_check_invalid()
        self._password = generate_password_hash(val)

    def get_password(self):
        return PasswordHelper(self._password)

    password = property(get_password, set_password)

    def get_photo(self):
        return (
            self.__photo_handler
            if getattr(self, "__photo_handler", None) is not None
            else FileHandler(url=self._photo)
            if self._photo
            else None
        )

    def set_photo(self, val: FileHandler):
        self.__photo_handler = val
        self._photo = getattr(val, "url", None)

    photo = property(get_photo, set_photo)

    def __init__(
        self,
        username: str,
        password: str,
        password_check: str,
        active: bool = True,
        email: str = None,
        photo: "FileHandler" = None,
        phone: str = None,
        first_name: str = "",
        last_name: str = "",
        **kwargs,
    ) -> None:
        if password != password_check:
            raise UserExceptions.password_check_invalid()
        self.username = username
        self.password = password
        self.active = active
        self.email = email
        self.photo = photo
        self.phone = phone
        self.first_name = first_name
        self.last_name = last_name

    @hybrid_property
    def name(self) -> str:
        """concatenates user's name"""
        return f"{self.first_name} {self.last_name}"

    def add_roles(self, roles: Union[List["Role"], "Role"]):
        """add roles to user

        Args:
            roles: A list of or a single role instances
        """
        from ._UserRoles import UserRoles

        new_roles = [
            UserRoles(user=self, role=role)
            for role in (roles if isinstance(roles, list) else [roles])
        ]

        db.session.add_all(new_roles)

    def delete(self, persist=False):
        """Delete user's record"""
        if self.photo:
            self.photo.delete()
        super().delete(persist=persist)

    def add_entity(self, entity: "Entity", create: bool = False, edit: bool = False):
        from ._UserEntityPermission import UserEntityPermission

        permission = UserEntityPermission(
            entity=entity, user=self, create=create, edit=edit
        )
        db.session.add(permission)
        db.session.commit()

    @hybrid_property
    def employees(self) -> List["User"]:

        return User.query.filter(User.manager_id == User.id).all()

    @hybrid_property
    def assets(self) -> List[AssetStorage]:

        return AssetStorage.query.filter(AssetStorage.added_by_id == self.id).all()
Ejemplo n.º 15
0
from sqlalchemy import Integer, String
from sqlalchemy.orm import relationship
from sqlalchemy.sql.schema import Column, Table, ForeignKey
from .database import Base

admin_phones = Table('admin_phones', Base.metadata,
                     Column('admin_id', Integer, ForeignKey('admins.id')),
                     Column('phone_id', Integer, ForeignKey('phones.id')))

operator_phones = Table(
    'operator_phones', Base.metadata,
    Column('operator_id', Integer, ForeignKey('operators.id')),
    Column('phone_id', Integer, ForeignKey('phones.id')))

customer_phones = Table(
    'customer_phones', Base.metadata,
    Column('customer_id', Integer, ForeignKey('customers.id')),
    Column('phone_id', Integer, ForeignKey('phones.id')))

worker_phones = Table('worker_phones', Base.metadata,
                      Column('worker_id', Integer, ForeignKey('workers.id')),
                      Column('phone_id', Integer, ForeignKey('phones.id')))

worker_brands = Table(
    'worker_brands', Base.metadata,
    Column('worker_id', Integer, ForeignKey('workers.id')),
    Column('brand_id', Integer, ForeignKey('appliances_brand.id')))

worker_appliances_types = Table(
    'worker_appliances_types', Base.metadata,
    Column('worker_id', Integer, ForeignKey('workers.id')),
Ejemplo n.º 16
0
class CPFTLPSReferral(TaskHasPatientMixin, Task):
    """
    Server implementation of the CPFT_LPS_Referral task.
    """

    __tablename__ = "cpft_lps_referral"
    shortname = "CPFT_LPS_Referral"
    info_filename_stem = "clinical"

    referral_date_time = Column("referral_date_time",
                                PendulumDateTimeAsIsoTextColType)
    lps_division = CamcopsColumn("lps_division",
                                 UnicodeText,
                                 exempt_from_anonymisation=True)
    referral_priority = CamcopsColumn("referral_priority",
                                      UnicodeText,
                                      exempt_from_anonymisation=True)
    referral_method = CamcopsColumn("referral_method",
                                    UnicodeText,
                                    exempt_from_anonymisation=True)
    referrer_name = Column("referrer_name", UnicodeText)
    referrer_contact_details = Column("referrer_contact_details", UnicodeText)
    referring_consultant = Column("referring_consultant", UnicodeText)
    referring_specialty = CamcopsColumn("referring_specialty",
                                        UnicodeText,
                                        exempt_from_anonymisation=True)
    referring_specialty_other = Column("referring_specialty_other",
                                       UnicodeText)
    patient_location = Column("patient_location", UnicodeText)
    admission_date = Column("admission_date", Date)
    estimated_discharge_date = Column("estimated_discharge_date", Date)
    patient_aware_of_referral = BoolColumn("patient_aware_of_referral")
    interpreter_required = BoolColumn("interpreter_required")
    sensory_impairment = BoolColumn("sensory_impairment")
    marital_status_code = CamcopsColumn(
        "marital_status_code",
        CharColType,
        permitted_value_checker=PermittedValueChecker(
            permitted_values=PV_NHS_MARITAL_STATUS),
    )
    ethnic_category_code = CamcopsColumn(
        "ethnic_category_code",
        CharColType,
        permitted_value_checker=PermittedValueChecker(
            permitted_values=PV_NHS_ETHNIC_CATEGORY),
    )
    admission_reason_overdose = BoolColumn("admission_reason_overdose")
    admission_reason_self_harm_not_overdose = BoolColumn(
        "admission_reason_self_harm_not_overdose",
        constraint_name="ck_cpft_lps_referral_arshno",
    )
    admission_reason_confusion = BoolColumn("admission_reason_confusion")
    admission_reason_trauma = BoolColumn("admission_reason_trauma")
    admission_reason_falls = BoolColumn("admission_reason_falls")
    admission_reason_infection = BoolColumn("admission_reason_infection")
    admission_reason_poor_adherence = BoolColumn(
        "admission_reason_poor_adherence",
        constraint_name="ck_cpft_lps_referral_adpa",
    )
    admission_reason_other = BoolColumn("admission_reason_other")
    existing_psychiatric_teams = Column("existing_psychiatric_teams",
                                        UnicodeText)
    care_coordinator = Column("care_coordinator", UnicodeText)
    other_contact_details = Column("other_contact_details", UnicodeText)
    referral_reason = Column("referral_reason", UnicodeText)

    @staticmethod
    def longname(req: "CamcopsRequest") -> str:
        _ = req.gettext
        return _("CPFT LPS – referral")

    def is_complete(self) -> bool:
        return bool(self.patient_location and self.referral_reason
                    and self.field_contents_valid())

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        return [
            CtvInfo(
                heading=ws.webify(self.wxstring(req, "f_referral_reason_t")),
                content=self.referral_reason,
            )
        ]

    @staticmethod
    def four_column_row(q1: str,
                        a1: Any,
                        q2: str,
                        a2: Any,
                        default: str = "") -> str:
        return f"""
            <tr>
                <td>{q1}</td><td>{answer(a1, default=default)}</td>
                <td>{q2}</td><td>{answer(a2, default=default)}</td>
            </tr>
        """

    @staticmethod
    def tr_qa(q: str, a: Any, default: str = "") -> str:
        return f"""
            <tr>
                <td colspan="2">{q}</td>
                <td colspan="2"><b>{default if a is None else a}</b></td>
            </tr>
        """

    def get_task_html(self, req: CamcopsRequest) -> str:
        person_marital_status = get_nhs_dd_person_marital_status(req)
        ethnic_category_code = get_nhs_dd_ethnic_category_code(req)
        if self.lps_division == "G":
            banner_class = CssClass.BANNER_REFERRAL_GENERAL_ADULT
            division_name = self.wxstring(req, "service_G")
        elif self.lps_division == "O":
            banner_class = CssClass.BANNER_REFERRAL_OLD_AGE
            division_name = self.wxstring(req, "service_O")
        elif self.lps_division == "S":
            banner_class = CssClass.BANNER_REFERRAL_SUBSTANCE_MISUSE
            division_name = self.wxstring(req, "service_S")
        else:
            banner_class = ""
            division_name = None

        if self.referral_priority == "R":
            priority_name = self.wxstring(req, "priority_R")
        elif self.referral_priority == "U":
            priority_name = self.wxstring(req, "priority_U")
        elif self.referral_priority == "E":
            priority_name = self.wxstring(req, "priority_E")
        else:
            priority_name = None

        potential_admission_reasons = [
            "admission_reason_overdose",
            "admission_reason_self_harm_not_overdose",
            "admission_reason_confusion",
            "admission_reason_trauma",
            "admission_reason_falls",
            "admission_reason_infection",
            "admission_reason_poor_adherence",
            "admission_reason_other",
        ]
        admission_reasons = []
        for r in potential_admission_reasons:
            if getattr(self, r):
                admission_reasons.append(self.wxstring(req, "f_" + r))

        h = f"""
            <div class="{CssClass.BANNER} {banner_class}">
                {answer(division_name, default_for_blank_strings=True)}
                referral at {
                    answer(format_datetime(
                        self.referral_date_time,
                        DateFormat.SHORT_DATETIME_WITH_DAY_NO_TZ,
                        default=None))}
            </div>
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {self.get_is_complete_tr(req)}
                </table>
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <col width="25%">
                <col width="25%">
                <col width="25%">
                <col width="25%">
        """
        h += subheading_spanning_four_columns(
            self.wxstring(req, "t_about_referral"))
        h += """
            <tr>
                <td>{q_method}</td>
                <td>{a_method}</td>
                <td>{q_priority}</td>
                <td class="{CssClass.HIGHLIGHT}">{a_priority}</td>
            </tr>
        """.format(
            CssClass=CssClass,
            q_method=self.wxstring(req, "f_referral_method"),
            a_method=answer(self.referral_method),
            q_priority=self.wxstring(req, "f_referral_priority"),
            a_priority=(
                answer(self.referral_priority, default_for_blank_strings=True)
                + ": "  # noqa
                + answer(priority_name)),
        )
        h += self.four_column_row(
            self.wxstring(req, "f_referrer_name"),
            self.referrer_name,
            self.wxstring(req, "f_referring_specialty"),
            self.referring_specialty,
        )
        h += self.four_column_row(
            self.wxstring(req, "f_referrer_contact_details"),
            self.referrer_contact_details,
            self.wxstring(req, "f_referring_specialty_other"),
            self.referring_specialty_other,
        )
        h += self.four_column_row(
            self.wxstring(req, "f_referring_consultant"),
            self.referring_consultant,
            "",
            "",
        )
        h += subheading_spanning_four_columns(self.wxstring(req, "t_patient"))
        h += """
            <tr>
                <td>{q_when}</td>
                <td>{a_when}</td>
                <td>{q_where}</td>
                <td class="{CssClass.HIGHLIGHT}">{a_where}</td>
            </tr>
        """.format(
            CssClass=CssClass,
            q_when=self.wxstring(req, "f_admission_date"),
            a_when=answer(
                format_datetime(self.admission_date,
                                DateFormat.LONG_DATE,
                                default=None),
                "",
            ),
            q_where=self.wxstring(req, "f_patient_location"),
            a_where=answer(self.patient_location),
        )
        h += self.four_column_row(
            self.wxstring(req, "f_estimated_discharge_date"),
            format_datetime(self.estimated_discharge_date,
                            DateFormat.LONG_DATE, ""),
            self.wxstring(req, "f_patient_aware_of_referral"),
            get_yes_no_none(req, self.patient_aware_of_referral),
        )
        h += self.four_column_row(
            self.wxstring(req, "f_marital_status"),
            person_marital_status.get(self.marital_status_code, INVALID_VALUE),
            self.wxstring(req, "f_interpreter_required"),
            get_yes_no_none(req, self.interpreter_required),
        )
        h += self.four_column_row(
            self.wxstring(req, "f_ethnic_category"),
            ethnic_category_code.get(self.ethnic_category_code, INVALID_VALUE),
            self.wxstring(req, "f_sensory_impairment"),
            get_yes_no_none(req, self.sensory_impairment),
        )
        h += subheading_spanning_four_columns(
            self.wxstring(req, "t_admission_reason"))
        h += tr_span_col(answer(", ".join(admission_reasons), ""), cols=4)
        h += subheading_spanning_four_columns(
            self.wxstring(req, "t_other_people"))
        h += self.tr_qa(
            self.wxstring(req, "f_existing_psychiatric_teams"),
            self.existing_psychiatric_teams,
            "",
        )
        h += self.tr_qa(self.wxstring(req, "f_care_coordinator"),
                        self.care_coordinator, "")
        h += self.tr_qa(
            self.wxstring(req, "f_other_contact_details"),
            self.other_contact_details,
            "",
        )
        h += subheading_spanning_four_columns(
            self.wxstring(req, "t_referral_reason"))
        h += tr_span_col(answer(self.referral_reason, ""), cols=4)
        h += """
            </table>
        """
        return h
Ejemplo n.º 17
0
class Phone(Base):
    __tablename__ = 'phones'
    id = Column(Integer, primary_key=True)
    phone = Column(Integer, unique=True)
Ejemplo n.º 18
0
class Icd10Schizophrenia(TaskHasClinicianMixin, TaskHasPatientMixin, Task):
    """
    Server implementation of the ICD10-SZ task.
    """
    __tablename__ = "icd10schizophrenia"
    shortname = "ICD10-SZ"
    longname = "ICD-10 criteria for schizophrenia (F20)"

    passivity_bodily = CamcopsColumn(
        "passivity_bodily",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Passivity: delusions of control, influence, or "
        "passivity, clearly referred to body or limb movements...")
    passivity_mental = CamcopsColumn(
        "passivity_mental",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="(passivity) ... or to specific thoughts, actions, or "
        "sensations.")
    hv_commentary = CamcopsColumn(
        "hv_commentary",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Hallucinatory voices giving a running commentary on the "
        "patient's behaviour")
    hv_discussing = CamcopsColumn(
        "hv_discussing",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Hallucinatory voices discussing the patient among "
        "themselves")
    hv_from_body = CamcopsColumn(
        "hv_from_body",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Other types of hallucinatory voices coming from some "
        "part of the body")
    delusions = CamcopsColumn(
        "delusions",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Delusions: persistent delusions of other kinds that are "
        "culturally inappropriate and completely impossible, such as "
        "religious or political identity, or superhuman powers and "
        "abilities (e.g. being able to control the weather, or being "
        "in communication with aliens from another world).")
    delusional_perception = CamcopsColumn(
        "delusional_perception",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Delusional perception [a normal perception, "
        "delusionally interpreted]")
    thought_echo = CamcopsColumn(
        "thought_echo",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Thought echo [hearing one's own thoughts aloud, just "
        "before, just after, or simultaneously with the thought]")
    thought_withdrawal = CamcopsColumn(
        "thought_withdrawal",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Thought withdrawal [the feeling that one's thoughts "
        "have been removed by an outside agency]")
    thought_insertion = CamcopsColumn(
        "thought_insertion",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Thought insertion [the feeling that one's thoughts have "
        "been placed there from outside]")
    thought_broadcasting = CamcopsColumn(
        "thought_broadcasting",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Thought broadcasting [the feeling that one's thoughts "
        "leave oneself and are diffused widely, or are audible to "
        "others, or that others think the same thoughts in unison]")

    hallucinations_other = CamcopsColumn(
        "hallucinations_other",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Hallucinations: persistent hallucinations in any "
        "modality, when accompanied either by fleeting or half-formed "
        "delusions without clear affective content, or by persistent "
        "over-valued ideas, or when occurring every day for weeks or "
        "months on end.")
    thought_disorder = CamcopsColumn(
        "thought_disorder",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Thought disorder: breaks or interpolations in the train "
        "of thought, resulting in incoherence or irrelevant speech, "
        "or neologisms.")
    catatonia = CamcopsColumn(
        "catatonia",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Catatonia: catatonic behaviour, such as excitement, "
        "posturing, or waxy flexibility, negativism, mutism, and "
        "stupor.")

    negative = CamcopsColumn(
        "negative",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Negative symptoms: 'negative' symptoms such as marked "
        "apathy, paucity of speech, and blunting or incongruity of "
        "emotional responses, usually resulting in social withdrawal "
        "and lowering of social performance; it must be clear that "
        "these are not due to depression or to neuroleptic "
        "medication.")

    present_one_month = CamcopsColumn(
        "present_one_month",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Symptoms in groups A-C present for most of the time "
        "during an episode of psychotic illness lasting for at least "
        "one month (or at some time during most of the days).")

    also_manic = CamcopsColumn(
        "also_manic",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Also meets criteria for manic episode (F30)?")
    also_depressive = CamcopsColumn(
        "also_depressive",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Also meets criteria for depressive episode (F32)?")
    if_mood_psychosis_first = CamcopsColumn(
        "if_mood_psychosis_first",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="If the patient also meets criteria for manic episode "
        "(F30) or depressive episode (F32), the criteria listed above "
        "must have been met before the disturbance of mood developed.")

    not_organic_or_substance = CamcopsColumn(
        "not_organic_or_substance",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="The disorder is not attributable to organic brain "
        "disease (in the sense of F0), or to alcohol- or drug-related "
        "intoxication, dependence or withdrawal.")

    behaviour_change = CamcopsColumn(
        "behaviour_change",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="A significant and consistent change in the overall "
        "quality of some aspects of personal behaviour, manifest as "
        "loss of interest, aimlessness, idleness, a self-absorbed "
        "attitude, and social withdrawal.")
    performance_decline = CamcopsColumn(
        "performance_decline",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="Marked decline in social, scholastic, or occupational "
        "performance.")

    subtype_paranoid = CamcopsColumn(
        "subtype_paranoid",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="PARANOID (F20.0): dominated by delusions or hallucinations.")
    subtype_hebephrenic = CamcopsColumn(
        "subtype_hebephrenic",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="HEBEPHRENIC (F20.1): dominated by affective changes "
        "(shallow, flat, incongruous, or inappropriate affect) and "
        "either pronounced thought disorder or aimless, disjointed "
        "behaviour is present.")
    subtype_catatonic = CamcopsColumn(
        "subtype_catatonic",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="CATATONIC (F20.2): psychomotor disturbances dominate "
        "(such as stupor, mutism, excitement, posturing, negativism, "
        "rigidity, waxy flexibility, command automatisms, or verbal "
        "perseveration).")
    subtype_undifferentiated = CamcopsColumn(
        "subtype_undifferentiated",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="UNDIFFERENTIATED (F20.3): schizophrenia with active "
        "psychosis fitting none or more than one of the above three "
        "types.")
    subtype_postschizophrenic_depression = CamcopsColumn(
        "subtype_postschizophrenic_depression",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="POST-SCHIZOPHRENIC DEPRESSION (F20.4): in which a depressive "
        "episode has developed for at least 2 weeks following a "
        "schizophrenic episode within the last 12 months and in which "
        "schizophrenic symptoms persist but are not as prominent as "
        "the depression.")
    subtype_residual = CamcopsColumn(
        "subtype_residual",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="RESIDUAL (F20.5): in which previous psychotic episodes "
        "of schizophrenia have given way to a chronic condition with "
        "'negative' symptoms of schizophrenia for at least 1 year.")
    subtype_simple = CamcopsColumn(
        "subtype_simple",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="SIMPLE SCHIZOPHRENIA (F20.6), in which 'negative' "
        "symptoms (C) with a change in personal behaviour (D) develop "
        "for at least one year without any psychotic episodes (no "
        "symptoms from groups A or B or other hallucinations or "
        "well-formed delusions), and with a marked decline in social, "
        "scholastic, or occupational performance.")
    subtype_cenesthopathic = CamcopsColumn(
        "subtype_cenesthopathic",
        Boolean,
        permitted_value_checker=BIT_CHECKER,
        comment="CENESTHOPATHIC (within OTHER F20.8): body image "
        "aberration (e.g. desomatization, loss of bodily boundaries, "
        "feelings of body size change) or abnormal bodily sensations "
        "(e.g. numbness, stiffness, feeling strange, "
        "depersonalization, or sensations of pain, temperature, "
        "electricity, heaviness, lightness, or discomfort when "
        "touched) dominate.")

    date_pertains_to = Column("date_pertains_to",
                              Date,
                              comment="Date the assessment pertains to")
    comments = Column("comments", UnicodeText, comment="Clinician's comments")

    A_NAMES = [
        "passivity_bodily", "passivity_mental", "hv_commentary",
        "hv_discussing", "hv_from_body", "delusions", "delusional_perception",
        "thought_echo", "thought_withdrawal", "thought_insertion",
        "thought_broadcasting"
    ]
    B_NAMES = ["hallucinations_other", "thought_disorder", "catatonia"]
    C_NAMES = ["negative"]
    D_NAMES = ["present_one_month"]
    E_NAMES = ["also_manic", "also_depressive", "if_mood_psychosis_first"]
    F_NAMES = ["not_organic_or_substance"]
    G_NAMES = ["behaviour_change", "performance_decline"]
    H_NAMES = [
        "subtype_paranoid", "subtype_hebephrenic", "subtype_catatonic",
        "subtype_undifferentiated", "subtype_postschizophrenic_depression",
        "subtype_residual", "subtype_simple", "subtype_cenesthopathic"
    ]

    def get_clinical_text(self, req: CamcopsRequest) -> List[CtvInfo]:
        if not self.is_complete():
            return CTV_INCOMPLETE
        c = self.meets_general_criteria()
        if c is None:
            category = "Unknown if met or not met"
        elif c:
            category = "Met"
        else:
            category = "Not met"
        infolist = [
            CtvInfo(
                content=("Pertains to: {}. General criteria for "
                         "schizophrenia: {}.".format(
                             format_datetime(self.date_pertains_to,
                                             DateFormat.LONG_DATE), category)))
        ]
        if self.comments:
            infolist.append(CtvInfo(content=ws.webify(self.comments)))
        return infolist

    def get_summaries(self, req: CamcopsRequest) -> List[SummaryElement]:
        return self.standard_task_summary_fields() + [
            SummaryElement(
                name="meets_general_criteria",
                coltype=Boolean(),
                value=self.meets_general_criteria(),
                comment="Meets general criteria for paranoid/hebephrenic/"
                "catatonic/undifferentiated schizophrenia "
                "(F20.0-F20.3)?"),
        ]

    # Meets criteria? These also return null for unknown.
    def meets_general_criteria(self) -> Optional[bool]:
        t_a = self.count_booleans(Icd10Schizophrenia.A_NAMES)
        u_a = self.n_incomplete(Icd10Schizophrenia.A_NAMES)
        t_b = self.count_booleans(Icd10Schizophrenia.B_NAMES) + \
            self.count_booleans(Icd10Schizophrenia.C_NAMES)
        u_b = self.n_incomplete(Icd10Schizophrenia.B_NAMES) + \
            self.n_incomplete(Icd10Schizophrenia.C_NAMES)
        if t_a + u_a < 1 and t_b + u_b < 2:
            return False
        if self.present_one_month is not None and not self.present_one_month:
            return False
        if ((self.also_manic or self.also_depressive)
                and is_false(self.if_mood_psychosis_first)):
            return False
        if is_false(self.not_organic_or_substance):
            return False
        if ((t_a >= 1 or t_b >= 2) and self.present_one_month and
            ((is_false(self.also_manic) and is_false(self.also_depressive))
             or self.if_mood_psychosis_first)
                and self.not_organic_or_substance):
            return True
        return None

    def is_complete(self) -> bool:
        return (self.date_pertains_to is not None
                and self.meets_general_criteria() is not None
                and self.field_contents_valid())

    def heading_row(self,
                    req: CamcopsRequest,
                    wstringname: str,
                    extra: str = None) -> str:
        return heading_spanning_two_columns(
            self.wxstring(req, wstringname) + (extra or ""))

    def text_row(self, req: CamcopsRequest, wstringname: str) -> str:
        return subheading_spanning_two_columns(self.wxstring(req, wstringname))

    def row_true_false(self, req: CamcopsRequest, fieldname: str) -> str:
        return self.get_twocol_bool_row_true_false(
            req, fieldname, self.wxstring(req, fieldname))

    def row_present_absent(self, req: CamcopsRequest, fieldname: str) -> str:
        return self.get_twocol_bool_row_present_absent(
            req, fieldname, self.wxstring(req, fieldname))

    def get_task_html(self, req: CamcopsRequest) -> str:
        h = """
            {clinician_comments}
            <div class="{CssClass.SUMMARY}">
                <table class="{CssClass.SUMMARY}">
                    {tr_is_complete}
                    {date_pertains_to}
                    {meets_general_criteria}
                </table>
            </div>
            <div class="{CssClass.EXPLANATION}">
                {comments}
            </div>
            <table class="{CssClass.TASKDETAIL}">
                <tr>
                    <th width="80%">Question</th>
                    <th width="20%">Answer</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),
            date_pertains_to=tr_qa(
                req.wappstring("date_pertains_to"),
                format_datetime(self.date_pertains_to,
                                DateFormat.LONG_DATE,
                                default=None)),
            meets_general_criteria=tr_qa(
                self.wxstring(req, "meets_general_criteria") +
                " <sup>[1]</sup>",  # noqa
                get_true_false_none(req, self.meets_general_criteria())),
            comments=self.wxstring(req, "comments"),
        )

        h += self.heading_row(req, "core", " <sup>[2]</sup>")
        for x in Icd10Schizophrenia.A_NAMES:
            h += self.row_present_absent(req, x)

        h += self.heading_row(req, "other_positive")
        for x in Icd10Schizophrenia.B_NAMES:
            h += self.row_present_absent(req, x)

        h += self.heading_row(req, "negative_title")
        for x in Icd10Schizophrenia.C_NAMES:
            h += self.row_present_absent(req, x)

        h += self.heading_row(req, "other_criteria")
        for x in Icd10Schizophrenia.D_NAMES:
            h += self.row_true_false(req, x)
        h += self.text_row(req, "duration_comment")
        for x in Icd10Schizophrenia.E_NAMES:
            h += self.row_true_false(req, x)
        h += self.text_row(req, "affective_comment")
        for x in Icd10Schizophrenia.F_NAMES:
            h += self.row_true_false(req, x)

        h += self.heading_row(req, "simple_title")
        for x in Icd10Schizophrenia.G_NAMES:
            h += self.row_present_absent(req, x)

        h += self.heading_row(req, "subtypes")
        for x in Icd10Schizophrenia.H_NAMES:
            h += self.row_present_absent(req, x)

        h += """
            </table>
            <div class="{CssClass.FOOTNOTES}">
                [1] All of:
                    (a) at least one core symptom, or at least two of the other
                        positive or negative symptoms;
                    (b) present for a month (etc.);
                    (c) if also manic/depressed, schizophreniform psychosis
                        came first;
                    (d) not attributable to organic brain disease or
                        psychoactive substances.
                [2] Symptom definitions from:
                    (a) Oyebode F (2008). Sims’ Symptoms in the Mind: An
                        Introduction to Descriptive Psychopathology. Fourth
                        edition, Saunders, Elsevier, Edinburgh.
                    (b) Pawar AV &amp; Spence SA (2003), PMID 14519605.
            </div>
        """.format(CssClass=CssClass) + ICD10_COPYRIGHT_DIV
        return h
Ejemplo n.º 19
0
class AppliancesType(Base):
    __tablename__ = 'appliances_type'
    id = Column(Integer, primary_key=True)
    type_name = Column(String, unique=True)
class InventoryItems(Base):
    __tablename__ = "inventory_item_tbl"
    __table_args__ = {'schema': 'apps'}
    item_id = Column('item_id',
                     Integer,
                     Sequence('item_id_sequence', schema='apps'),
                     primary_key=True)
    item_number = Column('item_number', String)
    organization_id = Column('organization_id', Integer)
    enabled_flag = Column('enabled_flag', String)
    description = Column('description', String)
    buyer_id = Column('buyer_id', Integer)
    item_type = Column('item_type', String)
    long_description = Column('long_description', String)
    asset_flag = Column('asset_flag', String)
    asset_id = Column('asset_id', Integer)
    purchasing_enabled_flag = Column('purchasing_enabled_flag', String)
    customer_order_enabled_flag = Column('customer_order_enabled_flag', String)
    returnable_flag = Column('returnable_flag', String)
    inspection_required_flag = Column('inspection_required_flag', String)
    list_price_per_unit = Column('list_price_per_unit', String)
    shelf_life_days = Column('shelf_life_days', String)
    item_control_code = Column('item_control_code', String)
    min_order_quantity = Column('min_order_quantity', String)
    max_order_quantity = Column('max_order_quantity', String)
    planner_code = Column('planner_code', String)
    transaction_uom = Column('transaction_uom', String)
    conversion = Column('conversion', Float)
    base_uom = Column('base_uom', String)
    created_by = Column('created_by', Integer)
    creation_date = Column('creation_date',
                           DateTime,
                           default=datetime.datetime.utcnow)
    last_update_date = Column('last_update_date',
                              DateTime,
                              default=datetime.datetime.utcnow,
                              onupdate=datetime.datetime.utcnow)
    last_updated_by = Column('last_updated_by', Integer)
Ejemplo n.º 21
0
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
from sqlalchemy.sql.schema import Column, ForeignKey, Table
from sqlalchemy.sql.sqltypes import Integer, String

from heliotrope.database.orm.base import mapper_registry

tag_table = Table(
    "tag",
    mapper_registry.metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("galleyinfo_id", Integer, ForeignKey("galleryinfo.id")),
    Column("male", String(1)),
    Column("female", String(1)),
    Column("tag", String, nullable=False),
    Column("url", String, nullable=False),
)
Ejemplo n.º 22
0
class IdNumDefinition(Base):
    """
    Represents an ID number definition.
    """

    __tablename__ = "_idnum_definitions"

    which_idnum = Column(
        "which_idnum",
        Integer,
        primary_key=True,
        index=True,
        comment="Which of the server's ID numbers is this?",
    )
    description = Column(
        "description",
        IdDescriptorColType,
        comment="Full description of the ID number",
    )
    short_description = Column(
        "short_description",
        IdDescriptorColType,
        comment="Short description of the ID number",
    )
    hl7_id_type = Column(
        "hl7_id_type",
        HL7IdTypeType,
        comment="HL7: Identifier Type code: 'a code corresponding to the type "
        "of identifier. In some cases, this code may be used as a "
        'qualifier to the "Assigning Authority" component.\'',
    )
    hl7_assigning_authority = Column(
        "hl7_assigning_authority",
        HL7AssigningAuthorityType,
        comment="HL7: Assigning Authority for ID number (unique name of the "
        "system/organization/agency/department that creates the data).",
    )
    validation_method = Column(
        "validation_method",
        String(length=ID_NUM_VALIDATION_METHOD_MAX_LEN),
        comment="Optional validation method",
    )
    fhir_id_system = Column(
        "fhir_id_system", UrlColType, comment="FHIR external ID 'system' URL"
    )

    def __init__(
        self,
        which_idnum: int = None,
        description: str = "",
        short_description: str = "",
        hl7_id_type: str = "",
        hl7_assigning_authority: str = "",
        validation_method: str = "",
        fhir_id_system: str = "",
    ):
        # We permit a "blank" constructor for automatic copying, e.g. merge_db.
        self.which_idnum = which_idnum
        self.description = description
        self.short_description = short_description
        self.hl7_id_type = hl7_id_type
        self.hl7_assigning_authority = hl7_assigning_authority
        self.validation_method = validation_method
        self.fhir_id_system = fhir_id_system

    def __repr__(self) -> str:
        return simple_repr(
            self,
            ["which_idnum", "description", "short_description"],
            with_addr=False,
        )

    def _camcops_default_fhir_id_system(self, req: "CamcopsRequest") -> str:
        """
        The built-in FHIR ID system URL that we'll use if the user hasn't
        specified one for the selected ID number type.
        """
        return req.route_url(
            Routes.FHIR_PATIENT_ID_SYSTEM, which_idnum=self.which_idnum
        )  # path will be e.g. /fhir_patient_id_system/3

    def effective_fhir_id_system(self, req: "CamcopsRequest") -> str:
        """
        If the user has set a FHIR ID system, return that. Otherwise, return
        a CamCOPS default.
        """
        return self.fhir_id_system or self._camcops_default_fhir_id_system(req)

    def verbose_fhir_id_system(self, req: "CamcopsRequest") -> str:
        """
        Returns a human-readable description of the FHIR ID system in effect,
        in HTML form.
        """
        _ = req.gettext
        if self.fhir_id_system:
            prefix = ""
            url = self.fhir_id_system
        else:
            prefix = _("Default:") + " "
            url = self._camcops_default_fhir_id_system(req)
        return f'{prefix} <a href="{url}">{url}</a>'
Ejemplo n.º 23
0
class ExportedTask(Base):
    """
    Class representing an attempt to exported a task (as part of a
    :class:`ExportRun` to a specific
    :class:`camcops_server.cc_modules.cc_exportrecipient.ExportRecipient`.
    """
    __tablename__ = "_exported_tasks"

    id = Column("id",
                BigInteger,
                primary_key=True,
                autoincrement=True,
                comment="Arbitrary primary key")
    recipient_id = Column("recipient_id",
                          BigInteger,
                          ForeignKey(ExportRecipient.id),
                          nullable=False,
                          comment="FK to {}.{}".format(
                              ExportRecipient.__tablename__,
                              ExportRecipient.id.name))
    basetable = Column("basetable",
                       TableNameColType,
                       nullable=False,
                       index=True,
                       comment="Base table of task concerned")
    task_server_pk = Column(
        "task_server_pk",
        Integer,
        nullable=False,
        index=True,
        comment="Server PK of task in basetable (_pk field)")
    start_at_utc = Column("start_at_utc",
                          DateTime,
                          nullable=False,
                          index=True,
                          comment="Time export was started (UTC)")
    finish_at_utc = Column("finish_at_utc",
                           DateTime,
                           comment="Time export was finished (UTC)")
    success = Column("success",
                     Boolean,
                     default=False,
                     nullable=False,
                     comment="Task exported successfully?")
    failure_reasons = Column("failure_reasons",
                             StringListType,
                             comment="Reasons for failure")
    cancelled = Column(
        "cancelled",
        Boolean,
        default=False,
        nullable=False,
        comment=
        "Export subsequently cancelled/invalidated (may trigger resend)"  # noqa
    )
    cancelled_at_utc = Column("cancelled_at_utc",
                              DateTime,
                              comment="Time export was cancelled at (UTC)")

    recipient = relationship(ExportRecipient)

    hl7_messages = relationship("ExportedTaskHL7Message")
    filegroups = relationship("ExportedTaskFileGroup")
    emails = relationship("ExportedTaskEmail")

    def __init__(self,
                 recipient: ExportRecipient = None,
                 task: "Task" = None,
                 basetable: str = None,
                 task_server_pk: int = None,
                 *args,
                 **kwargs) -> None:
        """
        Can initialize with a task, or a basetable/task_server_pk combination.

        Args:
            recipient: an :class:`camcops_server.cc_modules.cc_exportrecipient.ExportRecipient`
            task: a :class:`camcops_server.cc_modules.cc_task.Task` object
            basetable: base table name of the task
            task_server_pk: server PK of the task

        (However, we must also support a no-parameter constructor, not least
        for our :func:`merge_db` function.)
        """  # noqa
        super().__init__(*args, **kwargs)
        self.recipient = recipient
        self.start_at_utc = get_now_utc_datetime()
        if task:
            assert (not basetable) and task_server_pk is None, (
                "Task specified; mustn't specify basetable/task_server_pk")
            self.basetable = task.tablename
            self.task_server_pk = task.get_pk()
            self._task = task
        else:
            self.basetable = basetable
            self.task_server_pk = task_server_pk
            self._task = None  # type: Task

    @reconstructor
    def init_on_load(self) -> None:
        """
        Called when SQLAlchemy recreates an object; see
        https://docs.sqlalchemy.org/en/latest/orm/constructors.html.
        """
        self._task = None  # type: Task

    @property
    def task(self) -> "Task":
        """
        Returns the associated task.
        """
        if self._task is None:
            dbsession = SqlASession.object_session(self)
            try:
                self._task = task_factory_no_security_checks(
                    dbsession, self.basetable, self.task_server_pk)
            except KeyError:
                log.warning(
                    "Failed to retrieve task for basetable={!r}, PK={!r}",
                    self.basetable, self.task_server_pk)
                self._task = None
        return self._task

    def succeed(self) -> None:
        """
        Register success.
        """
        self.success = True
        self.finish()

    def abort(self, msg: str) -> None:
        """
        Record failure, and why.

        (Called ``abort`` not ``fail`` because PyCharm has a bug relating to
        functions named ``fail``:
        https://stackoverflow.com/questions/21954959/pycharm-unreachable-code.)

        Args:
            msg: why
        """
        self.success = False
        log.error("Task export failed: {}", msg)
        self._add_failure_reason(msg)
        self.finish()

    def _add_failure_reason(self, msg: str) -> None:
        """
        Writes to our ``failure_reasons`` list in a way that (a) obviates the
        need to create an empty list via ``__init__()``, and (b) will
        definitely mark it as dirty, so it gets saved to the database.

        See :class:`cardinal_pythonlib.sqlalchemy.list_types.StringListType`.

        Args:
            msg: the message
        """
        if self.failure_reasons is None:
            self.failure_reasons = [msg]
        else:
            # Do not use .append(); that won't mark the record as dirty.
            # Don't use "+="; similarly, that calls list.__iadd__(), not
            # InstrumentedAttribute.__set__().
            # noinspection PyAugmentAssignment
            self.failure_reasons = self.failure_reasons + [msg]

    def finish(self) -> None:
        """
        Records the finish time.
        """
        self.finish_at_utc = get_now_utc_datetime()

    def export(self, req: "CamcopsRequest") -> None:
        """
        Performs an export of the specific task.

        Args:
            req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
        """
        dbsession = req.dbsession
        recipient = self.recipient
        transmission_method = recipient.transmission_method
        log.info("Exporting task {!r} to recipient {}", self.task, recipient)

        if transmission_method == ExportTransmissionMethod.EMAIL:
            email = ExportedTaskEmail(self)
            dbsession.add(email)
            email.export_task(req)

        elif transmission_method == ExportTransmissionMethod.FILE:
            efg = ExportedTaskFileGroup(self)
            dbsession.add(efg)
            efg.export_task(req)

        elif transmission_method == ExportTransmissionMethod.HL7:
            ehl7 = ExportedTaskHL7Message(self)
            if ehl7.valid():
                dbsession.add(ehl7)
                ehl7.export_task(req)
            else:
                self.abort("Task not valid for HL7 export")

        else:
            raise AssertionError("Bug: bad transmission_method")

    @property
    def filegroup(self) -> "ExportedTaskFileGroup":
        """
        Returns a :class:`ExportedTaskFileGroup`, creating it if necessary.
        """
        if self.filegroups:
            # noinspection PyUnresolvedReferences
            filegroup = self.filegroups[0]  # type: ExportedTaskFileGroup
        else:
            filegroup = ExportedTaskFileGroup(self)
            # noinspection PyUnresolvedReferences
            self.filegroups.append(filegroup)
        return filegroup

    def export_file(self,
                    filename: str,
                    text: str = None,
                    binary: bytes = None,
                    text_encoding: str = UTF8) -> bool:
        """
        Exports a file.

        Args:
            filename:
            text: text contents (specify this XOR ``binary``)
            binary: binary contents (specify this XOR ``text``)
            text_encoding: encoding to use when writing text

        Returns: was it exported?
        """
        filegroup = self.filegroup
        return filegroup.export_file(filename=filename,
                                     text=text,
                                     binary=binary,
                                     text_encoding=text_encoding)

    def cancel(self) -> None:
        """
        Marks the task export as cancelled/invalidated.

        May trigger a resend (which is the point).
        """
        self.cancelled = True
        self.cancelled_at_utc = get_now_utc_datetime()

    @classmethod
    def task_already_exported(cls, dbsession: SqlASession, recipient_name: str,
                              basetable: str, task_pk: int) -> bool:
        """
        Has the specified task already been successfully exported?

        Args:
            dbsession: a :class:`sqlalchemy.orm.session.Session`
            recipient_name:
            basetable: name of the task's base table
            task_pk: server PK of the task

        Returns:
            does a successful export record exist for this task?

        """
        exists_q = (
            dbsession.query(cls).join(cls.recipient).filter(
                ExportRecipient.recipient_name == recipient_name).filter(
                    cls.basetable == basetable).filter(
                        cls.task_server_pk == task_pk).filter(
                            cls.success == True)  # nopep8
            .filter(cls.cancelled == False)  # nopep8
            .exists())
        return bool_from_exists_clause(dbsession, exists_q)
Ejemplo n.º 24
0
class URLDBRecord(DBBase):
    __tablename__ = 'urls'
    id = Column(Integer, primary_key=True, autoincrement=True)
    url_str_id = Column(Integer,
                        ForeignKey('url_strings.id'),
                        nullable=False,
                        index=True)
    url_str_record = relationship('URLStrDBRecord',
                                  uselist=False,
                                  foreign_keys=[url_str_id])
    url = association_proxy('url_str_record', 'url')
    status = Column(
        Enum(
            Status.done,
            Status.error,
            Status.in_progress,
            Status.skipped,
            Status.todo,
        ),
        index=True,
        default=Status.todo,
        nullable=False,
    )
    try_count = Column(Integer, nullable=False, default=0)
    level = Column(Integer, nullable=False, default=0)
    top_url_str_id = Column(Integer, ForeignKey('url_strings.id'))
    top_url_record = relationship('URLStrDBRecord',
                                  uselist=False,
                                  foreign_keys=[top_url_str_id])
    top_url = association_proxy('top_url_record', 'url')
    status_code = Column(Integer)
    referrer_id = Column(Integer, ForeignKey('url_strings.id'))
    referrer_record = relationship('URLStrDBRecord',
                                   uselist=False,
                                   foreign_keys=[referrer_id])
    referrer = association_proxy('referrer_record', 'url')
    inline = Column(Boolean)
    link_type = Column(String)
    url_encoding = Column(String)
    post_data = Column(String)

    def to_plain(self):
        return URLRecord(
            self.url,
            self.status,
            self.try_count,
            self.level,
            self.top_url,
            self.status_code,
            self.referrer,
            self.inline,
            self.link_type,
            self.url_encoding,
            self.post_data,
        )
Ejemplo n.º 25
0
class ExportedTaskFileGroup(Base):
    """
    Represents a small set of files exported in relation to a single task.
    """
    __tablename__ = "_exported_task_filegroup"

    id = Column("id",
                BigInteger,
                primary_key=True,
                autoincrement=True,
                comment="Arbitrary primary key")
    exported_task_id = Column("exported_task_id",
                              BigInteger,
                              ForeignKey(ExportedTask.id),
                              nullable=False,
                              comment="FK to {}.{}".format(
                                  ExportedTask.__tablename__,
                                  ExportedTask.id.name))
    filenames = Column("filenames",
                       StringListType,
                       comment="List of filenames exported")
    script_called = Column(
        "script_called",
        Boolean,
        default=False,
        nullable=False,
        comment="Was the {} script called?".format(
            ConfigParamExportRecipient.FILE_SCRIPT_AFTER_EXPORT))
    script_retcode = Column(
        "script_retcode",
        Integer,
        comment="Return code from the {} script".format(
            ConfigParamExportRecipient.FILE_SCRIPT_AFTER_EXPORT))
    script_stdout = Column(
        "script_stdout",
        UnicodeText,
        comment="stdout from the {} script".format(
            ConfigParamExportRecipient.FILE_SCRIPT_AFTER_EXPORT))
    script_stderr = Column(
        "script_stderr",
        UnicodeText,
        comment="stderr from the {} script".format(
            ConfigParamExportRecipient.FILE_SCRIPT_AFTER_EXPORT))

    exported_task = relationship(ExportedTask)

    def __init__(self, exported_task: ExportedTask = None) -> None:
        """
        Args:
            exported_task: :class:`ExportedTask` object
        """
        self.exported_task = exported_task

    def export_file(self,
                    filename: str,
                    text: str = None,
                    binary: bytes = None,
                    text_encoding: str = UTF8) -> False:
        """
        Exports the file.

        Args:
            filename:
            text: text contents (specify this XOR ``binary``)
            binary: binary contents (specify this XOR ``text``)
            text_encoding: encoding to use when writing text

        Returns:
            bool: was it exported?
        """
        assert bool(text) != bool(binary), "Specify text XOR binary"
        exported_task = self.exported_task
        filename = os.path.abspath(filename)
        directory = os.path.dirname(filename)
        recipient = exported_task.recipient

        if not recipient.file_overwrite_files and os.path.isfile(filename):
            self.abort("File already exists: {!r}".format(filename))
            return False

        if recipient.file_make_directory:
            try:
                mkdir_p(directory)
            except Exception as e:
                self.abort("Couldn't make directory {!r}: {}".format(
                    directory, e))
                return False

        try:
            log.debug("Writing to {!r}", filename)
            if text:
                with open(filename, mode="w", encoding=text_encoding) as f:
                    f.write(text)
            else:
                with open(filename, mode="wb") as f:
                    f.write(binary)
        except Exception as e:
            self.abort("Failed to open or write file {!r}: {}".format(
                filename, e))
            return False

        self.note_exported_file(filename)
        return True

    def note_exported_file(self, *filenames: str) -> None:
        """
        Records a filename that has been exported, or several.

        Args:
            *filenames: filenames
        """
        if self.filenames is None:
            self.filenames = list(filenames)
        else:
            # See ExportedTask._add_failure_reason() above:
            # noinspection PyAugmentAssignment,PyTypeChecker
            self.filenames = self.filenames + list(filenames)

    def export_task(self, req: "CamcopsRequest") -> None:
        """
        Exports the task itself to a file.

        Args:
            req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
        """
        exported_task = self.exported_task
        task = exported_task.task
        recipient = exported_task.recipient
        task_format = recipient.task_format
        task_filename = recipient.get_filename(req, task)
        rio_metadata_filename = change_filename_ext(task_filename,
                                                    ".metadata").replace(
                                                        " ", "")
        # ... in case we use it. No spaces in its filename.

        # Before we calculate the PDF, etc., we can pre-check for existing
        # files.
        if not recipient.file_overwrite_files:
            target_filenames = [task_filename]
            if recipient.file_export_rio_metadata:
                target_filenames.append(rio_metadata_filename)
            for fname in target_filenames:
                if os.path.isfile(os.path.abspath(fname)):
                    self.abort("File already exists: {!r}".format(fname))
                    return

        # Export task
        if task_format == FileType.PDF:
            binary = task.get_pdf(req)
            text = None
        elif task_format == FileType.HTML:
            binary = None
            text = task.get_html(req)
        elif task_format == FileType.XML:
            binary = None
            text = task.get_xml(req)
        else:
            raise AssertionError("Unknown task_format")
        written = self.export_file(task_filename,
                                   text=text,
                                   binary=binary,
                                   text_encoding=UTF8)
        if not written:
            return

        # RiO metadata too?
        if recipient.file_export_rio_metadata:

            metadata = task.get_rio_metadata(recipient.rio_idnum,
                                             recipient.rio_uploading_user,
                                             recipient.rio_document_type)
            # We're going to write in binary mode, to get the newlines right.
            # One way is:
            #     with codecs.open(filename, mode="w", encoding="ascii") as f:
            #         f.write(metadata.replace("\n", DOS_NEWLINE))
            # Here's another.
            metadata = metadata.replace("\n", DOS_NEWLINE)
            # ... Servelec say CR = "\r", but DOS is \r\n.
            metadata_binary = metadata.encode("ascii")
            # UTF-8 is NOT supported by RiO for metadata.
            written_metadata = self.export_file(rio_metadata_filename,
                                                binary=metadata_binary)
            if not written_metadata:
                return

        self.finish_run_script_if_necessary()

    def succeed(self) -> None:
        """
        Register success.
        """
        self.exported_task.succeed()

    def abort(self, msg: str) -> None:
        """
        Record failure, and why.

        (Called ``abort`` not ``fail`` because PyCharm has a bug relating to
        functions named ``fail``:
        https://stackoverflow.com/questions/21954959/pycharm-unreachable-code.)

        Args:
            msg: why
        """
        self.exported_task.abort(msg)

    def finish_run_script_if_necessary(self) -> None:
        """
        Completes the file export by running the external script, if required.
        """
        recipient = self.exported_task.recipient
        if self.filenames and recipient.file_script_after_export:
            args = [recipient.file_script_after_export] + self.filenames
            try:
                encoding = sys.getdefaultencoding()
                p = subprocess.Popen(args,
                                     stdout=subprocess.PIPE,
                                     stderr=subprocess.PIPE)
                out, err = p.communicate()
                self.script_called = True
                self.script_stdout = out.decode(encoding)
                self.script_stderr = err.decode(encoding)
                self.script_retcode = p.returncode
            except Exception as e:
                self.script_called = False
                self.script_stdout = ""
                self.script_stderr = str(e)
                self.abort("Failed to run script")
                return
        self.succeed()
Ejemplo n.º 26
0
class BzAttachment(GenericTable):
    __tablename__ = "bzattachments"
    __lobs__ = {"content": 1 << 24}

    id = Column(Integer, primary_key=True)
    bug_id = Column(Integer, ForeignKey("{0}.id".format(BzBug.__tablename__)), nullable=False, index=True)
    user_id = Column(Integer, ForeignKey("{0}.id".format(BzUser.__tablename__)), nullable=False, index=True)
    mimetype = Column(String(256), nullable=False)
    description = Column(String(256), nullable=False)
    creation_time = Column(DateTime, nullable=False)
    last_change_time = Column(DateTime, nullable=False)
    is_private = Column(Boolean, nullable=False)
    is_patch = Column(Boolean, nullable=False)
    is_obsolete = Column(Boolean, nullable=False)
    filename = Column(String(256), nullable=False)

    bug = relationship(BzBug, backref="attachments")
    user = relationship(BzUser)
Ejemplo n.º 27
0
    ID_SHORT_DESCRIPTION_PREFIX = (
        "idShortDescription"  # text; apply suffixes 1-8
    )


StoredVarNameColTypeDefunct = String(length=255)
StoredVarTypeColTypeDefunct = String(length=255)
_ssv_metadata = MetaData()

server_stored_var_table_defunct = Table(
    "_server_storedvars",  # table name
    _ssv_metadata,  # metadata separate from everything else
    Column(
        "name",
        StoredVarNameColTypeDefunct,
        primary_key=True,
        index=True,
        comment="Variable name",
    ),
    Column(
        "type",
        StoredVarTypeColTypeDefunct,
        nullable=False,
        comment="Variable type ('integer', 'real', 'text')",
    ),
    Column("valueInteger", Integer, comment="Value of an integer variable"),
    Column("valueText", UnicodeText, comment="Value of a text variable"),
    Column("valueReal",
           Float,
           comment="Value of a real (floating-point) variable"),
)
Ejemplo n.º 28
0
class BzBug(GenericTable):
    __tablename__ = "bzbugs"

    id = Column(Integer, primary_key=True)
    summary = Column(String(256), nullable=False)
    status = Column(Enum(*BUG_STATES, name="bzbug_status"), nullable=False)
    resolution = Column(Enum(*BUG_RESOLUTIONS, name="bzbug_resolution"), nullable=True)
    duplicate = Column(Integer, ForeignKey("{0}.id".format(__tablename__)), nullable=True, index=True)
    creation_time = Column(DateTime, nullable=False)
    last_change_time = Column(DateTime, nullable=False)
    private = Column(Boolean, nullable=False)
    tracker_id = Column(Integer, ForeignKey("{0}.id".format(Bugtracker.__tablename__)), nullable=False, index=True)
    opsysrelease_id = Column(Integer, ForeignKey("{0}.id".format(OpSysRelease.__tablename__)),
                             nullable=False, index=True)
    component_id = Column(Integer, ForeignKey("{0}.id".format(OpSysComponent.__tablename__)),
                          nullable=False, index=True)
    whiteboard = Column(String(256), nullable=False)
    creator_id = Column(Integer, ForeignKey("{0}.id".format(BzUser.__tablename__)), nullable=False, index=True)

    tracker = relationship(Bugtracker, backref="bugs")
    opsysrelease = relationship(OpSysRelease)
    component = relationship(OpSysComponent)
    creator = relationship(BzUser)

    def __str__(self) -> str:
        return 'BZ#{0}'.format(self.id)

    def order(self) -> int:
        return BUG_STATES.index(self.status)

    @property
    def url(self) -> str:
        return "{0}{1}".format(self.tracker.web_url, self.id)

    @property
    def serialize(self) -> Dict[str, Any]:
        return {
            'id': self.id,
            'summary': self.summary,
            'status': self.status,
            'resolution': self.resolution,
            'duplicate': self.duplicate,
            'creation_time': self.creation_time,
            'last_change_time': self.last_change_time,
            'private': self.private,
            'tracker_id': self.tracker_id,
            'opsysrelease_id': self.opsysrelease_id,
            'component_id': self.component_id,
            'whiteboard': self.whiteboard,
            'creator_id': self.creator_id,
            'type': 'BUGZILLA'
        }
Ejemplo n.º 29
0
class ExtraMixin(object):
    has_extra_bits = True
    x = Column("x", Integer)
    y = Column("y", Integer)
    z = Column("z", Integer)
Ejemplo n.º 30
0
class Character(BaseContent, Base):
    __tablename__ = 'charactersheet'
    id = Column(Integer, primary_key=True)
    title = Column(String(256))
    body = Column(JSON)
    timestamp = Column(DateTime, index=True, default=datetime.utcnow,
                       onupdate=datetime.utcnow)
    user_id = Column(Integer, ForeignKey('user_profile.id'))
    player = relationship('UserProfile', backref=backref(
        'characters', lazy='dynamic'))

    folder = relationship('Folder', backref='characters')

    def __repr__(self):
        return '<Character {}>'.format(self.title)

    def __init__(self,
                 mechanics: Type[CharacterMechanics] = CharacterMechanics,
                 *args, **kwargs):
        super(Character, self).__init__(*args, **kwargs)
        self._data = None
        # Add a subclass or something that
        # has the mechanics of the character.
        self.mechanics = mechanics(self)

    @reconstructor
    def init_on_load(self):
        system = self.data.get('system', '')
        logger.debug(f"Loading character of type {system}")
        system = self.system
        self.mechanics = MECHANICS.get(system, CharacterMechanics)(self)

    @property
    def data(self) -> Dict[str, Any]:
        if isinstance(self.body, dict):
            return self.body
        raise TypeError("Body is not a dictionary")

    @property
    def system(self) -> str:
        s = self.data.get('system', None)
        if s is not None:
            return s

        logger.warning("Deprecation: Outdated character data")
        default = "Call of Cthulhu TM"
        if self.data.get('meta', {}).get('GameName') == default:
            logger.warning("Trying old CoC stuff.")
            return "coc7e"

        return "Unknown"

    @property
    def version(self):
        v = self.data.get('version', None)
        return v

    @property
    def game(self):
        return self.mechanics.game()

    def validate(self):
        return self.mechanics.validate()

    def to_dict(self):
        return {
            'id': self.id,
            'title': self.title,
            'body': self.data,
            'timestamp': self.timestamp,
            'user_id': self.user_id
        }

    def get_sheet(self):
        return self.data

    @property
    def name(self):
        return self.mechanics.name

    @property
    def age(self):
        return self.mechanics.age

    @property
    def portrait(self):
        return self.mechanics.portrait()

    @property
    def description(self):
        return self.mechanics.description

    def attribute(self, *args):

        path = args[0]

        val = reduce(lambda x, y: x.get(y, None) if x is not None else None,
                     path.split("."),
                     self.data)

        return val

    def set_attribute(self, attribute: Dict):
        """Set a specific attribute."""

        if attribute.get('category', None) == 'skill':
            logger.debug("Set a skill")
            datatype = attribute.get('type', 'string')
            skill = attribute['field']
            subfield = attribute.get('subfield', None)
            value = attribute.get('value')

            if datatype == 'number' and not isinstance(value, int):
                value = None

            skill = self.skill(skill, subfield)
            skill['value'] = value

        elif attribute.get('type', None) == 'skillcheck':
            logger.debug("Check a skill")
            skill = attribute['field']
            subfield = attribute.get('subfield', None)
            check = attribute.get('value', False)
            skill = self.skill(skill, subfield)
            skill['checked'] = check

        elif attribute.get('type', None) == 'occupationcheck':
            logger.debug("Mark occupation skill")
            skill = attribute['field']
            subfield = attribute.get('subfield', None)
            check = attribute.get('value', False)
            skill = self.skill(skill, subfield)
            skill['occupation'] = check

        elif attribute.get('type', None) == 'portrait':
            logger.debug("Set portrait")
            data = attribute.get('value', None)
            if data is not None:
                self.mechanics.set_portrait(fix_image(data))
        else:
            logger.debug(
                f"Set '{attribute['field']}' to '{attribute['value']}'")
            s = reduce(lambda x, y: x[y], attribute['field'].split(".")[:-1],
                       self.data)
            s[attribute['field'].split(".")[-1]] = attribute['value']

    def store_data(self):
        """Mark data as modified."""
        flag_modified(self, "body")

    def skill(self, *args, **kwargs):
        return self.mechanics.skill(*args, **kwargs)

    def skills(self, *args):
        """Return a list of skills."""
        return self.data['skills']

    def add_skill(self, skillname: str, value: int = 1):
        if self.skill(skillname) is not None:
            raise ValueError(f"Skill {skillname} already exists.")

        self.data['skills'].append({"name": skillname,
                                    "value": value,
                                    "start_value": value})
        if isinstance(self.data['skills'], list):
            self.data['skills'].sort(key=lambda x: x['name'])

    def add_subskill(self, name: str, parent: str):
        value = self.skill(parent)['value']
        start_value = self.skill(parent)['start_value']
        logger.debug("Try to add subskill")
        logger.debug(f"Name: {name}, parent {parent}, value {value}")
        if self.skill(parent, name) is not None:
            raise ValueError(f"Subskill {name} in {parent} already exists.")

        skill = self.skill(parent)
        if 'subskills' not in skill:
            skill['subskills'] = []
        skill['subskills'].append({
            'name': name,
            'value': value,
            'start_value': start_value
        })

    @property
    def schema_version(self):
        return self.data['meta']['Version']