Beispiel #1
0
 def get_all_plots_for_one_task_html(self, tasks: List[Task]) -> str:
     """
     HTML for all plots for a given task type.
     """
     html = ""
     ntasks = len(tasks)
     if ntasks == 0:
         return html
     if not tasks[0].provides_trackers:
         # ask the first of the task instances
         return html
     alltrackers = [task.get_trackers(self.req) for task in tasks]
     datetimes = [task.get_creation_datetime() for task in tasks]
     ntrackers = len(alltrackers[0])
     # ... number of trackers supplied by the first task (and all tasks)
     for tracker in range(ntrackers):
         values = [
             alltrackers[tasknum][tracker].value
             for tasknum in range(ntasks)
         ]
         html += self.get_single_plot_html(
             datetimes, values, specimen_tracker=alltrackers[0][tracker])
     for task in tasks:
         audit(
             self.req,
             "Tracker data accessed",
             table=task.tablename,
             server_pk=task.pk,
             patient_server_pk=task.get_patient_server_pk(),
         )
     return html
Beispiel #2
0
    def create_superuser(cls, req: "CamcopsRequest", username: str,
                         password: str) -> bool:
        """
        Creates a superuser.

        Will fail if the user already exists.

        Args:
            req: :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
            username: the new superuser's username
            password: the new superuser's password

        Returns:
            success?

        """
        assert username, "Can't create superuser with no name"
        assert username != USER_NAME_FOR_SYSTEM, (
            "Can't create user with name {!r}".format(USER_NAME_FOR_SYSTEM))
        dbsession = req.dbsession
        user = cls.get_user_by_name(dbsession, username)
        if user:
            # already exists!
            return False
        # noinspection PyArgumentList
        user = cls(username=username)  # does work!
        user.superuser = True
        audit(req, "SUPERUSER CREATED: " + user.username, from_console=True)
        user.set_password(req, password)  # will audit
        dbsession.add(user)
        return True
Beispiel #3
0
def task_collection_to_tsv_zip_response(req: "CamcopsRequest",
                                        collection: "TaskCollection",
                                        sort_by_heading: bool) -> Response:
    """
    Converts a set of tasks to a TSV (tab-separated value) response, as a set
    of TSV files (one per table) in a ZIP file.
    
    Args:
        req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
        collection: a :class:`camcops_server.cc_modules.cc_taskcollection.TaskCollection`
        sort_by_heading: sort columns within each page by heading name? 

    Returns:
        a :class:`pyramid.response.Response` object

    """  # noqa
    # -------------------------------------------------------------------------
    # Create memory file and ZIP file within it
    # -------------------------------------------------------------------------
    memfile = io.BytesIO()
    z = zipfile.ZipFile(memfile, "w")

    # -------------------------------------------------------------------------
    # Iterate through tasks
    # -------------------------------------------------------------------------
    audit_descriptions = []  # type: List[str]
    # Task may return >1 file for TSV output (e.g. for subtables).
    tsvcoll = TsvCollection()
    for cls in collection.task_classes():
        for task in gen_audited_tasks_for_task_class(collection, cls,
                                                     audit_descriptions):
            tsv_pages = task.get_tsv_pages(req)
            tsvcoll.add_pages(tsv_pages)

    if sort_by_heading:
        tsvcoll.sort_headings_within_all_pages()

    # Write to ZIP.
    # If there are no valid task instances, there'll be no TSV; that's OK.
    for filename_stem in tsvcoll.get_page_names():
        tsv_filename = filename_stem + ".tsv"
        tsv_contents = tsvcoll.get_tsv_file(filename_stem)
        z.writestr(tsv_filename, tsv_contents.encode("utf-8"))

    # -------------------------------------------------------------------------
    # Finish and serve
    # -------------------------------------------------------------------------
    z.close()

    # Audit
    audit(req, "Basic dump: {}".format("; ".join(audit_descriptions)))

    # Return the result
    zip_contents = memfile.getvalue()
    memfile.close()
    zip_filename = "CamCOPS_dump_{}.zip".format(
        format_datetime(req.now, DateFormat.FILENAME))
    return ZipResponse(body=zip_contents, filename=zip_filename)
Beispiel #4
0
 def set_password(self, req: "CamcopsRequest", new_password: str) -> None:
     """
     Set a user's password.
     """
     self.hashedpw = rnc_crypto.hash_password(new_password,
                                              BCRYPT_DEFAULT_LOG_ROUNDS)
     self.last_password_change_utc = req.now_utc
     self.must_change_password = False
     audit(req, "Password changed for user " + self.username)
Beispiel #5
0
    def enable_user(cls, req: "CamcopsRequest", username: str) -> None:
        """
        Unlock user and clear login failures.

        Args:
            req: :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
            username: the user's username
        """
        SecurityAccountLockout.unlock_user(req, username)
        cls.clear_login_failures(req, username)
        audit(req, "User {} re-enabled".format(username))
Beispiel #6
0
 def audit(self,
           req: "CamcopsRequest",
           details: str,
           from_console: bool = False) -> None:
     """
     Audits an action to this patient.
     """
     audit(req,
           details,
           patient_server_pk=self._pk,
           table=Patient.__tablename__,
           server_pk=self._pk,
           from_console=from_console)
Beispiel #7
0
def set_password_directly(req: "CamcopsRequest", username: str,
                          password: str) -> bool:
    """
    If the user exists, set its password. Returns Boolean success.
    Used from the command line.
    """
    dbsession = req.dbsession
    user = User.get_user_by_name(dbsession, username)
    if not user:
        return False
    user.set_password(req, password)
    user.enable(req)
    audit(req, "Password changed for user " + user.username, from_console=True)
    return True
Beispiel #8
0
    def lock_user_out(cls, req: "CamcopsRequest", username: str,
                      lockout_minutes: int) -> None:
        """
        Lock user out for a specified number of minutes.

        Args:
            req: :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
            username: the user's username
            lockout_minutes: number of minutes
        """
        dbsession = req.dbsession
        now = req.now_utc
        lock_until = now + datetime.timedelta(minutes=lockout_minutes)
        # noinspection PyArgumentList
        lock = cls(username=username, lock_until=lock_until)
        dbsession.add(lock)
        audit(
            req, "Account {} locked out for {} minutes".format(
                username, lockout_minutes))
Beispiel #9
0
    def act_on_login_failure(cls, req: "CamcopsRequest",
                             username: str) -> None:
        """
        Record login failure and lock out user if necessary.

        Args:
            req: :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
            username: the user's username
        """
        cfg = req.config
        audit(req, "Failed login as user: {}".format(username))
        cls.record_login_failure(req, username)
        nfailures = cls.how_many_login_failures(req, username)
        nlockouts = nfailures // cfg.lockout_threshold
        nfailures_since_last_lockout = nfailures % cfg.lockout_threshold
        if nlockouts >= 1 and nfailures_since_last_lockout == 0:
            # new lockout required
            lockout_minutes = nlockouts * \
                              cfg.lockout_duration_increment_minutes
            SecurityAccountLockout.lock_user_out(req, username,
                                                 lockout_minutes)
Beispiel #10
0
def task_collection_to_sqlite_response(req: "CamcopsRequest",
                                       collection: "TaskCollection",
                                       export_options: TaskExportOptions,
                                       as_sql_not_binary: bool) -> Response:
    """
    Converts a set of tasks to an SQLite export, either as binary or the SQL
    text to regenerate it.

    Args:
        req: a :class:`camcops_server.cc_modules.cc_request.CamcopsRequest`
        collection: a :class:`camcops_server.cc_modules.cc_taskcollection.TaskCollection`
        export_options: a :class:`TaskExportOptions` object
        as_sql_not_binary: provide SQL text, rather than SQLite binary? 

    Returns:
        a :class:`pyramid.response.Response` object

    """  # noqa

    # -------------------------------------------------------------------------
    # Create memory file, dumper, and engine
    # -------------------------------------------------------------------------

    # This approach failed:
    #
    #   memfile = io.StringIO()
    #
    #   def dump(querysql, *multiparams, **params):
    #       compsql = querysql.compile(dialect=engine.dialect)
    #       memfile.write("{};\n".format(compsql))
    #
    #   engine = create_engine('{dialect}://'.format(dialect=dialect_name),
    #                          strategy='mock', executor=dump)
    #   dst_session = sessionmaker(bind=engine)()  # type: SqlASession
    #
    # ... you get the error
    #   AttributeError: 'MockConnection' object has no attribute 'begin'
    # ... which is fair enough.
    #
    # Next best thing: SQLite database.
    # Two ways to deal with it:
    # (a) duplicate our C++ dump code (which itself duplicate the SQLite
    #     command-line executable's dump facility), then create the database,
    #     dump it to a string, serve the string; or
    # (b) offer the binary SQLite file.
    # Or... (c) both.
    # Aha! pymysqlite.iterdump does this for us.
    #
    # If we create an in-memory database using create_engine('sqlite://'),
    # can we get the binary contents out? Don't think so.
    #
    # So we should first create a temporary on-disk file, then use that.

    # -------------------------------------------------------------------------
    # Make temporary file (one whose filename we can know).
    # We use tempfile.mkstemp() for security, or NamedTemporaryFile,
    # which is a bit easier. However, you can't necessarily open the file
    # again under all OSs, so that's no good. The final option is
    # TemporaryDirectory, which is secure and convenient.
    #
    # https://docs.python.org/3/library/tempfile.html
    # https://security.openstack.org/guidelines/dg_using-temporary-files-securely.html  # noqa
    # https://stackoverflow.com/questions/3924117/how-to-use-tempfile-namedtemporaryfile-in-python  # noqa
    # -------------------------------------------------------------------------
    db_basename = "temp.sqlite3"
    with tempfile.TemporaryDirectory() as tmpdirname:
        db_filename = os.path.join(tmpdirname, db_basename)
        # ---------------------------------------------------------------------
        # Make SQLAlchemy session
        # ---------------------------------------------------------------------
        url = "sqlite:///" + db_filename
        engine = create_engine(url, echo=False)
        dst_session = sessionmaker(bind=engine)()  # type: SqlASession
        # ---------------------------------------------------------------------
        # Iterate through tasks, creating tables as we need them.
        # ---------------------------------------------------------------------
        audit_descriptions = []  # type: List[str]
        task_generator = gen_audited_tasks_by_task_class(
            collection, audit_descriptions)
        # ---------------------------------------------------------------------
        # Next bit very tricky. We're trying to achieve several things:
        # - a copy of part of the database structure
        # - a copy of part of the data, with relationships intact
        # - nothing sensitive (e.g. full User records) going through
        # - adding new columns for Task objects offering summary values
        # - Must treat tasks all together, because otherwise we will insert
        #   duplicate dependency objects like Group objects.
        # ---------------------------------------------------------------------
        copy_tasks_and_summaries(tasks=task_generator,
                                 dst_engine=engine,
                                 dst_session=dst_session,
                                 export_options=export_options,
                                 req=req)
        dst_session.commit()
        # ---------------------------------------------------------------------
        # Audit
        # ---------------------------------------------------------------------
        audit(req, "SQL dump: {}".format("; ".join(audit_descriptions)))
        # ---------------------------------------------------------------------
        # Fetch file contents, either as binary, or as SQL
        # ---------------------------------------------------------------------
        filename_stem = "CamCOPS_dump_{}".format(
            format_datetime(req.now, DateFormat.FILENAME))
        suggested_filename = filename_stem + (".sql" if as_sql_not_binary else
                                              ".sqlite3")

        if as_sql_not_binary:
            # SQL text
            con = sqlite3.connect(db_filename)
            with io.StringIO() as f:
                # noinspection PyTypeChecker
                for line in con.iterdump():
                    f.write(line + "\n")
                con.close()
                f.flush()
                sql_text = f.getvalue()
            return TextAttachmentResponse(body=sql_text,
                                          filename=suggested_filename)
        else:
            # SQLite binary
            with open(db_filename, 'rb') as f:
                binary_contents = f.read()
            return SqliteBinaryResponse(body=binary_contents,
                                        filename=suggested_filename)
Beispiel #11
0
    def _get_xml(
        self,
        audit_string: str,
        xml_name: str,
        indent_spaces: int = 4,
        eol: str = "\n",
        include_comments: bool = False,
    ) -> str:
        """
        Returns an XML document representing this object.

        Args:
            audit_string: description used to audit access to this information
            xml_name: name of the root XML element
            indent_spaces: number of spaces to indent formatted XML
            eol: end-of-line string
            include_comments: include comments describing each field?

        Returns:
            an XML UTF-8 document representing the task.
        """
        iddef = self.taskfilter.get_only_iddef()
        if not iddef:
            raise ValueError("Tracker/CTV doesn't have a single ID number "
                             "criterion")
        branches = [
            self.consistency_info.get_xml_root(),
            XmlElement(
                name="_search_criteria",
                value=[
                    XmlElement(
                        name="task_tablename_list",
                        value=",".join(self.taskfilter.task_tablename_list),
                    ),
                    XmlElement(
                        name=ViewParam.WHICH_IDNUM,
                        value=iddef.which_idnum,
                        datatype=XmlDataTypes.INTEGER,
                    ),
                    XmlElement(
                        name=ViewParam.IDNUM_VALUE,
                        value=iddef.idnum_value,
                        datatype=XmlDataTypes.INTEGER,
                    ),
                    XmlElement(
                        name=ViewParam.START_DATETIME,
                        value=format_datetime(self.taskfilter.start_datetime,
                                              DateFormat.ISO8601),
                        datatype=XmlDataTypes.DATETIME,
                    ),
                    XmlElement(
                        name=ViewParam.END_DATETIME,
                        value=format_datetime(self.taskfilter.end_datetime,
                                              DateFormat.ISO8601),
                        datatype=XmlDataTypes.DATETIME,
                    ),
                ],
            ),
        ]
        options = TaskExportOptions(
            xml_include_plain_columns=True,
            xml_include_calculated=True,
            include_blobs=False,
        )
        for t in self.collection.all_tasks:
            branches.append(t.get_xml_root(self.req, options))
            audit(
                self.req,
                audit_string,
                table=t.tablename,
                server_pk=t.pk,
                patient_server_pk=t.get_patient_server_pk(),
            )
        tree = XmlElement(name=xml_name, value=branches)
        return get_xml_document(
            tree,
            indent_spaces=indent_spaces,
            eol=eol,
            include_comments=include_comments,
        )