Beispiel #1
0
    def insertMany(self, table, uid, eppn, records):
        """Insert several records at once.

        Typically used for inserting criteriaEntry en reviewEntry records.

        Parameters
        ----------
        table: string
            The table in which the record will be inserted.
        uid: ObjectId
            The user that creates the record, typically the logged in user.
        eppn: string
            The `eppn` of that same user. This is the unique identifier that comes from
            the DARIAH authentication service.
        records: iterable of dict
            The records (as dicts) to insert.
        """

        justNow = now()
        newRecords = [
            {
                N.dateCreated: justNow,
                N.creator: uid,
                N.modified: [MOD_FMT.format(eppn, justNow)],
                **record,
            }
            for record in records
        ]
        self.mongoCmd(N.insertMany, table, N.insert_many, newRecords)
Beispiel #2
0
    def insertUser(self, record):
        """Insert a user record, i.e. a record corresponding to a user.

        NB: the creator of this record is the system, by name of the
        `creatorId` attribute.

        Parameters
        ----------
        record: dict
            The user information to be stored, as a dictionary.

        Returns
        -------
        None
            But note that the new _id and the generated field values are added to the
            record.
        """

        creatorId = self.creatorId

        justNow = now()
        record.update(
            {
                N.dateLastLogin: justNow,
                N.statusLastLogin: N.Approved,
                N.mayLogin: True,
                N.creator: creatorId,
                N.dateCreated: justNow,
                N.modified: [MOD_FMT.format(CREATOR, justNow)],
            }
        )
        result = self.mongoCmd(N.insertUser, N.user, N.insert_one, record)
        self.recollect(N.user)
        record[N._id] = result.inserted_id
Beispiel #3
0
    def updateUser(self, record):
        """Updates user information.

        When users log in, or when they are assigned an other status,
        some of their attributes will change.

        Parameters
        ----------
        record: dict
            The new user information as a dict.
        """

        if N.isPristine in record:
            del record[N.isPristine]
        justNow = now()
        record.update(
            {
                N.dateLastLogin: justNow,
                N.statusLastLogin: N.Approved,
                N.modified: [MOD_FMT.format(CREATOR, justNow)],
            }
        )
        criterion = {N._id: G(record, N._id)}
        updates = {k: v for (k, v) in record.items() if k != N._id}
        instructions = {M_SET: updates, M_UNSET: {N.isPristine: E}}
        self.mongoCmd(N.updateUser, N.user, N.update_one, criterion, instructions)
        self.recollect(N.user)
Beispiel #4
0
    def collectActualItems(self, tables=None):
        """Determines which items are "actual".

        Actual items are those types and criteria that are specified in a
        package record that is itself actual.
        A package record is actual if the current data is between its start
        and end days.

        !!! caution
            If only value table needs to be collected that are not
            involved in the concept of "actual", nothing will be done.

        Parameters
        ----------
        tables: set of string, optional `None`
        """
        if tables is not None and not (tables & ACTUAL_TABLES):
            return

        justNow = now()

        packageActual = {
            G(record, N._id)
            for record in self.mongoCmd(
                N.collectActualItems,
                N.package,
                N.find,
                {N.startDate: {M_LTE: justNow}, N.endDate: {M_GTE: justNow}},
            )
        }
        for record in self.package.values():
            record[N.actual] = G(record, N._id) in packageActual

        typeActual = set(
            chain.from_iterable(
                G(record, N.typeContribution) or []
                for (_id, record) in self.package.items()
                if _id in packageActual
            )
        )
        for record in self.typeContribution.values():
            record[N.actual] = G(record, N._id) in typeActual

        criteriaActual = {
            _id
            for (_id, record) in self.criteria.items()
            if G(record, N.package) in packageActual
        }
        for record in self.criteria.values():
            record[N.actual] = G(record, N._id) in criteriaActual

        self.typeCriteria = {}
        for (_id, record) in self.criteria.items():
            if _id in criteriaActual:
                for tp in G(record, N.typeContribution) or []:
                    self.typeCriteria.setdefault(tp, set()).add(_id)

        if DEBUG_SYNCH:
            serverprint(f"""UPDATED {", ".join(ACTUAL_TABLES)}""")
Beispiel #5
0
def getDefaultDate():
    today = now()
    return (
        today.year,
        today.month,
        today.day,
        today.hour,
        today.minute,
        today.second,
    )
Beispiel #6
0
 def fromStr(self, editVal):
     if not editVal:
         return None
     if editVal == N.now:
         return now()
     normalParts = self.partition(editVal)
     if normalParts is None:
         return None
     cast = self.rawType
     return cast(*normalParts)
Beispiel #7
0
    def tasks(self, table, kind=None):
        """Present the currently available tasks as buttons on the interface.

        !!! hint "easy comments"
            We also include a comment `<!-- task~!taskName:eid -->
            for the ease of testing.

        Parameters
        ----------
        table: string
            We must specify the table for which we want to present the
            tasks: contrib, assessment, or review.
        kind: string {`expert`, `final`}, optional `None`
            Only if we want review attributes

        Returns
        -------
        string(html)
        """

        uid = self.uid

        if not uid or table not in USER_TABLES:
            return E

        eid = list(self.info(table, N._id, kind=kind))[0]
        taskParts = []

        allowedTasks = sorted((task, taskInfo)
                              for (task, taskInfo) in TASKS.items()
                              if G(taskInfo, N.table) == table)
        justNow = now()

        for (task, taskInfo) in allowedTasks:
            permitted = self.permission(task, kind=kind)
            if not permitted:
                continue

            remaining = type(permitted) is timedelta and permitted
            taskUntil = E
            if remaining:
                remainingRep = datetime.toDisplay(justNow + remaining)
                taskUntil = H.span(f""" before {remainingRep}""", cls="datex")
            taskMsg = G(taskInfo, N.msg)
            taskCls = G(taskInfo, N.cls)

            taskPart = (H.a(
                [taskMsg, taskUntil],
                f"""/api/task/{task}/{eid}""",
                cls=f"large task {taskCls}",
            ) + f"""<!-- task!{task}:{eid} -->""")
            taskParts.append(taskPart)

        return H.join(taskParts)
Beispiel #8
0
    def insertItem(self, table, uid, eppn, onlyIfNew, **fields):
        """Inserts a new record in a table, possibly only if it is new.

        The record will be filled with the specified fields, but also with
        provenance fields.

        The provenance fields are the creation date, the creator,
        and the start of the trail of modifiers.

        Parameters
        ----------
        table: string
            The table in which the record will be inserted.
        uid: ObjectId
            The user that creates the record, typically the logged in user.
        onlyIfNew: boolean
            If `True`, it will be checked whether a record with the specified fields
            already exists. If so, no record will be inserted.
        eppn: string
            The eppn of that same user. This is the unique identifier that comes from
            the DARIAH authentication service.
        **fields: dict
            The field names and their contents to populate the new record with.

        Returns
        -------
        ObjectId
            The id of the newly inserted record, or the id of the first existing
            record found, if `onlyIfNew` is true.
        """

        if onlyIfNew:
            existing = [
                G(rec, N._id)
                for rec in getattr(self, table, {}).values()
                if all(G(rec, k) == v for (k, v) in fields.items())
            ]
            if existing:
                return existing[0]

        justNow = now()
        newRecord = {
            N.dateCreated: justNow,
            N.creator: uid,
            N.modified: [MOD_FMT.format(eppn, justNow)],
            **fields,
        }
        result = self.mongoCmd(N.insertItem, table, N.insert_one, newRecord)
        if table in VALUE_TABLES:
            self.recollect(table)
        return result.inserted_id
Beispiel #9
0
def makeUserRoot(database, eppn, only=False):
    """Gives a specific user the role of root.

    Parameters
    ----------
    database: string
        The name of the database in which the user lives
    eppn: string
        The `eppn` attribute of the user
    only: boolean, optional `False`
        If true, all other users with root role will be demoted to
        office power.
    """
    client = MongoClient()
    mongo = client[database]
    perms = {
        perm: mongo.permissionGroup.find({"rep": perm},
                                         {"_id": True})[0]["_id"]
        for perm in {"root", "office"}
    }
    if only:
        mongo.user.update_many(
            {"group": perms["root"]},
            {"$set": {
                "group": perms["office"]
            }},
        )
    mongo.user.update_one(
        {"eppn": eppn},
        {"$set": {
            "group": perms["root"]
        }},
    )
    mongo.collect.update_one({"table": "user"},
                             {"$set": {
                                 "dateCollected": now()
                             }},
                             upsert=True)
Beispiel #10
0
    def collect(self):
        """Collect the contents of the value tables.

        Value tables have content that is needed almost all the time.
        All value tables will be completely cached within Db.

        !!! note
            This is meant to run at start up, before the workers start.
            After that, this worker will not execute it again.
            See also `recollect`.

        !!! warning
            We must take other workers into account. They need a signal
            to recollect. See `recollect`.
            We store the time that this worker has collected each table
            in attribute `collected`.

        !!! caution
            If you change the MongoDb from without, an you forget to
            put an appropriate time stamp, the app will not see it untill it
            is restarted.
            See for example how `root.makeUserRoot` handles this.

        !!! warning
            This is a complicated app.
            Some tables have records that specify whether other records are "actual".
            After collecting a value table, the "actual" items will be recomputed.
        """

        collected = self.collected

        for valueTable in VALUE_TABLES:
            self.cacheValueTable(valueTable)
            collected[valueTable] = now()

        self.collectActualItems()
        if DEBUG_SYNCH:
            serverprint(f"""COLLECTED {COMMA.join(sorted(VALUE_TABLES))}""")
Beispiel #11
0
    def permission(self, task, kind=None):
        """Checks whether a workflow task is permitted.

        Note that the tasks are listed per kind of record they apply to:
        contrib, assessment, review.
        They are typically triggered by big workflow buttons on the interface.

        When the request to execute such a task reaches the server, it will
        check whether the current user is allowed to execute this task
        on the records in question.

        !!! hint
            See above for explanation of the properties of the tasks.

        !!! note
            If you try to run a task on a kind of record that it is not
            designed for, it will be detected and no permission will be given.

        !!! note
            Some tasks are designed to set a field to a value.
            If that field already has that value, the task will not be permitted.
            This already rules out a lot of things and relieves the burden of
            prohibiting non-sensical tasks.

        It may be that the task is only permitted for some limited time from now on.
        Then a timedelta object with the amount of time left is returned.

        Parameters
        ----------
        table: string
            In order to check permissions, we must specify the kind of record that
            the task acts on: contrib, assessment, or review.
        task: string
            An string consisting of the name of a task.
        kind: string {`expert`, `final`}, optional `None`
            Only if we want review attributes

        Returns
        -------
        boolean | timedelta
        """

        db = self.db
        auth = self.auth
        uid = self.uid

        if task not in TASKS:
            return False

        taskInfo = TASKS[task]
        table = G(taskInfo, N.table)

        if uid is None or table not in USER_TABLES:
            return False

        taskField = (N.selected if table == N.contrib else
                     N.submitted if table == N.assessment else
                     N.decision if table == N.review else None)
        myKind = self.myKind

        (
            locked,
            done,
            frozen,
            mayAdd,
            stage,
            stageDate,
            creators,
            countryId,
            taskValue,
        ) = self.info(
            table,
            N.locked,
            N.done,
            N.frozen,
            N.mayAdd,
            N.stage,
            N.stageDate,
            N.creators,
            N.country,
            taskField,
            kind=kind,
        )

        operator = G(taskInfo, N.operator)
        value = G(taskInfo, N.value)
        if operator == N.set:
            if taskField == N.decision:
                value = G(db.decisionInv, value)

        (contribId, ) = self.info(N.contrib, N._id)

        isOwn = uid in creators
        isCoord = countryId and auth.coordinator(countryId=countryId)
        isSuper = auth.superuser()

        decisionDelay = G(taskInfo, N.delay)
        if decisionDelay:
            decisionDelay = timedelta(hours=decisionDelay)

        justNow = now()
        remaining = False
        if decisionDelay and stageDate:
            remaining = stageDate + decisionDelay - justNow
            if remaining <= timedelta(hours=0):
                remaining = False

        forbidden = frozen or done

        if forbidden and not remaining:
            return False

        if table == N.contrib:
            if not isOwn and not isCoord and not isSuper:
                return False

            if task == N.startAssessment:
                return not forbidden and isOwn and mayAdd

            if value == taskValue:
                return False

            if not isCoord:
                return False

            answer = not frozen or remaining

            if task == N.selectContrib:
                return stage != N.selectYes and answer

            if task == N.deselectContrib:
                return stage != N.selectNo and answer

            if task == N.unselectContrib:
                return stage != N.selectNone and answer

            return False

        if table == N.assessment:
            forbidden = frozen or done
            if forbidden:
                return False

            if task == N.startReview:
                return not forbidden and G(mayAdd, myKind)

            if value == taskValue:
                return False

            if uid not in creators:
                return False

            answer = not locked or remaining
            if not answer:
                return False

            if task == N.submitAssessment:
                return stage == N.complete and answer

            if task == N.resubmitAssessment:
                return stage == N.completeWithdrawn and answer

            if task == N.submitRevised:
                return stage == N.completeRevised and answer

            if task == N.withdrawAssessment:
                return (stage in {N.submitted, N.submittedRevised} and stage
                        not in {N.incompleteWithdrawn, N.completeWithdrawn}
                        and answer)

            return False

        if table == N.review:
            if frozen:
                return False

            if done and not remaining:
                return False

            taskKind = G(taskInfo, N.kind)
            if not kind or kind != taskKind or kind != myKind:
                return False

            answer = remaining or not done or remaining
            if not answer:
                return False

            (aStage, aStageDate) = self.info(N.assessment, N.stage,
                                             N.stageDate)
            (finalStage, ) = self.info(table, N.stage, kind=N.final)
            (expertStage, expertStageDate) = self.info(table,
                                                       N.stage,
                                                       N.stageDate,
                                                       kind=N.expert)
            xExpertStage = N.expertReviewRevoke if expertStage is None else expertStage
            xFinalStage = N.finalReviewRevoke if finalStage is None else finalStage
            revision = finalStage == N.reviewRevise
            zFinalStage = finalStage and not revision
            submitted = aStage == N.submitted
            submittedRevised = aStage == N.submittedRevised
            mayDecideExpert = (submitted and not finalStage
                               or submittedRevised and revision)

            if value == taskValue:
                if not revision:
                    return False

            if task in {
                    N.expertReviewRevise,
                    N.expertReviewAccept,
                    N.expertReviewReject,
                    N.expertReviewRevoke,
            } - {xExpertStage}:
                return (kind == N.expert and not zFinalStage
                        and mayDecideExpert and answer)

            if task in {
                    N.finalReviewRevise,
                    N.finalReviewAccept,
                    N.finalReviewReject,
                    N.finalReviewRevoke,
            } - {xFinalStage}:
                return (kind == N.final and not not expertStage
                        and (not aStageDate or expertStageDate > aStageDate)
                        and (((not finalStage and submitted) or
                              (revision and submittedRevised)) or remaining)
                        and answer)

            return False

        return False
Beispiel #12
0
    def recollect(self, table=None):
        """Collect the contents of the value tables if they have changed.

        For each value table it will be checked if they have been
        collected (by another worker) after this worker has started and if so,
        those tables and those tables only will be recollected.

        !!! caution
            Although the initial `collect` is done before workers start
            (`gunicorn --preload`), individual workers will end up with their
            own copy of the value table cache.
            So when we need to recollect values for our cache, we must notify
            in some way that other workers also have to recollect this table.

        ### Global recollection

        Whenever we recollect a value table, we insert the time of recollection
        in a record in the MongoDb.

        Somewhere at the start of each request, these records will be checked,
        and if needed, recollections will be done before the request processing.

        There is a table `collect`, with records having fields `table` and
        `dateCollected`. After each (re)collect of a table, the `dateCollected` of
        the appropriate record will be set to the current time.

        !!! note "recollect()"
            A `recollect()` without arguments should be done at the start of each
            request.

        !!! note "recollect(table)"
            A `recollect(table)` should be done whenever this worker has changed
            something in that value table.

        Parameters
        ----------
        table: string, optional `None`
            A recollect() without arguments collects *all* value tables that need
            collecting based on the times of change as recorded in the `collect`
            table.

            A recollect of a single table means that this worker has made a change.
            After the recollect, a timestamp will go into the `collect` table,
            so that other workers can pick it up.

            If table is `True`, all timestamps in the `collect` table will be set
            to now, so that each worker will refresh its value cache.
        """

        collected = self.collected

        if table is None:
            affected = set()
            for valueTable in VALUE_TABLES:
                record = self.mongoCmd(
                    N.recollect, N.collect, N.find_one, {RECOLLECT_NAME: valueTable}
                )
                lastChangedGlobally = G(record, RECOLLECT_DATE)
                lastChangedHere = G(collected, valueTable)
                if lastChangedGlobally and (
                    not lastChangedHere or lastChangedHere < lastChangedGlobally
                ):
                    self.cacheValueTable(valueTable)
                    collected[valueTable] = now()
                    affected.add(valueTable)
        elif table is True:
            affected = set()
            for valueTable in VALUE_TABLES:
                self.cacheValueTable(valueTable)
                collected[valueTable] = now()
                affected.add(valueTable)
        else:
            self.cacheValueTable(table)
            collected[table] = now()
            affected = {table}
        if affected:
            justNow = now()
            for aTable in affected:
                self.mongoCmd(
                    N.collect,
                    N.collect,
                    N.update_one,
                    {RECOLLECT_NAME: aTable},
                    {M_SET: {RECOLLECT_DATE: justNow}},
                    upsert=True,
                )

        self.collectActualItems(tables=affected)

        if affected:
            if DEBUG_SYNCH:
                serverprint(f"""COLLECTED {COMMA.join(sorted(affected))}""")
Beispiel #13
0
    def updateField(
        self, table, eid, field, data, actor, modified, nowFields=[],
    ):
        """Update a single field in a single record.

        !!! hint
            Whenever a field is updated in a record which has the field `isPristine`,
            this field will be deleted from the record.
            The rule is that pristine records are the ones that originate from the
            legacy data and have not changed since then.

        Parameters
        ----------
        table: string
            The table which holds the record to be updated.
        eid: ObjectId
            (Entity) id of the record to be updated.
        data: mixed
            The new value of for the updated field.
        actor: ObjectId
            The user that has triggered the update action.
        modified: list of string
            The current provenance trail of the record, which is a list of
            strings of the form "person on date".
            Here "person" is not an ID but a consolidated string representing
            the name of that person.
            The provenance trail will be trimmed in order to prevent excessively long
            trails. On each day, only the last action by each person will be recorded.
        nowFields: iterable of string, optional `[]`
            The names of additional fields in which the current datetime will be stored.
            For exampe, if `submitted` is modified, the current datetime will be saved in
            `dateSubmitted`.

        Returns
        -------
        dict | boolean
            The updated record, if the MongoDb operation was successful, else False
        """

        oid = castObjectId(eid)
        if oid is None:
            return False

        justNow = now()
        newModified = filterModified((modified or []) + [f"""{actor}{ON}{justNow}"""])
        criterion = {N._id: oid}
        nowItems = {nowField: justNow for nowField in nowFields}
        update = {
            field: data,
            N.modified: newModified,
            **nowItems,
        }
        delete = {N.isPristine: E}
        instructions = {
            M_SET: update,
            M_UNSET: delete,
        }

        status = self.mongoCmd(
            N.updateField, table, N.update_one, criterion, instructions
        )
        if not G(status.raw_result, N.ok, default=False):
            return False

        if table in VALUE_TABLES:
            self.recollect(table)
        return (
            update,
            set(delete.keys()),
        )
def test_sidebar2(clients):
    amounts = {
        "All contributions": [1],
        "My contributions": [({OWNER}, 1)],
        f"{BELGIUM} contributions": [1],
        "Contributions to be selected": [({MYCOORD}, 1)],
    }
    sidebar(clients, amounts)


@pytest.mark.parametrize(
    ("field", "value"),
    (
        (TITLE, TITLE1),
        (YEAR, str(now().year)),
        (COUNTRY, BELGIUM),
        (CONTACT_PERSON_NAME, OWNER_NAME),
        (CONTACT_PERSON_EMAIL, OWNER_EMAIL),
    ),
)
def test_fields(clientOwner, field, value):
    recordInfo = startInfo["recordInfo"]
    contribInfo = recordInfo[CONTRIB]
    fields = G(contribInfo, "fields")

    assert G(fields, field) == value


def test_makeEditorAll(clients):
    valueTables = startInfo["valueTables"]