def nameEmail(self, user=None): """Provide a string representation of the name and email of the user. !!! note Care will be taken that to unauthenticated users only limited information about users will be shown. Parameters ---------- user: dict, optional `None` The user whose identity must be represented. If absent, the currently logged in user will be taken. Returns ------- string """ if user is None: user = self.user name = G(user, N.name) or E if not name: firstName = G(user, N.firstName) or E lastName = G(user, N.lastName) or E name = firstName + (BLANK if firstName and lastName else E) + lastName group = self.groupRep(user=user) isAuth = group != UNAUTH email = (G(user, N.email) or E) if isAuth else E return (name, email)
def icon(icon, asChar=False, **atts): """icon. Pseudo element for an icon. !!! warning The `icon` names must be listed in the web.yaml config file under the key `icons`. The icon itself is a Unicode character. Parameters ---------- icon: string Name of the icon. asChar: boolean, optional, `False` If `True`, just output the icon character. Otherwise, wrap it in a `<span>` with all attributes that might have been passed. Returns ------- string(html) """ iconChar = G(ICONS, icon, default=ICONS[N.noicon]) if asChar: return G(ICONS, icon, default=ICONS[N.noicon]) addClass = f"{N.symbol} i-{icon} " return HtmlElement(N.span).wrap(iconChar, addClass=addClass, **atts)
def getValueIds(self, valueTable, constrain): """Fetch a set of ids from a value table. The ids are taken from the value reocrds that satisfy a constraint. but with members restricted by a constraint. Parameters ---------- valueTable: string The table that contains the records. constrain: 2-tuple, optional `None` A custom constraint. If present, it should be a tuple `(fieldName, value)`. Only records with that value in that field will be delivered. Returns ------- set of ObjectId """ records = ( r for r in getattr(self, valueTable, {}).values() if G(r, constrain[0]) == constrain[1] ) return {G(r, N._id) for r in records}
def getWf(self, table, kind=None): """Select a source of attributes within a workflow item. Parameters ---------- table: string We must specify the kind of record for which we want the attributes: contrib, assessment, or review. kind: string {`expert`, `final`}, optional `None` Only if we want review attributes Returns ------- dict """ data = self.data if table == N.contrib: return data data = G(data, N.assessment) if table in {N.assessment, N.criteriaEntry}: return data if table in {N.review, N.reviewEntry}: data = G(G(data, N.reviews), kind) return data return None
def main(): client = MongoClient() sys.stdout.write(f"ADD TEST USERS to DATABASE {DATABASE} ... ") db = client[DATABASE] idFromIso = { G(record, N.iso): G(record, N._id) for record in db.country.find() } idFromGroup = { G(record, N.rep): G(record, N._id) for record in db.permissionGroup.find() } users = [] for user in USER: u = dict(x for x in user.items()) u[N.group] = idFromGroup[u[N.group]] if N.country in u: u[N.country] = idFromIso[u[N.country]] users.append(u) db.user.insert_many(users) sys.stdout.write("DONE\n")
def getValueInv(self, valueTable, constrain): """Fetch a mapping from values to ids from a value table. The mapping is like the *valueTable*`Inv` attribute of `Db`, but with members restricted by a constraint. !!! caution This only works properly if the valueTable has a field `rep`. Parameters ---------- valueTable: string The table that contains the records. constrain: 2-tuple, optional `None` A custom constraint. If present, it should be a tuple `(fieldName, value)`. Only records with that value in that field will be delivered. Returns ------- dict Keyed by values, valued by ids. """ records = ( r for r in getattr(self, valueTable, {}).values() if G(r, constrain[0]) == constrain[1] ) eids = {G(r, N._id) for r in records} return { value: eid for (value, eid) in getattr(self, f"""{valueTable}Inv""", {}).items() if eid in eids }
def myReviewerKind(self, reviewer=None): """Determine whether the current user is `expert` or `final`. Parameters ---------- reviewer: dict, optional `None` If absent, the assessment in the workflow info will be inspected to get a dict of its reviewers by kind. Otherwise, it should be a dict of user ids keyed by `expert` and `final`. Returns ------- string {`expert`, `final`} | `None` Depending on whether the current user is such a reviewer of the assessment of this contribution. Or `None` if (s)he is not a reviewer at all. """ uid = self.uid if reviewer is None: reviewer = G(self.getWf(N.assessment), N.reviewer) return (N.expert if G(reviewer, N.expert) == uid else N.final if G(reviewer, N.final) == uid else None)
def execute(context, task, eid): """Executes a workflow task. First a table object is constructed, based on the `table` property of the task, using `context`. Then a record object is constructed in that table, based on the `eid` parameter. If that all succeeds, all information is at hand to verify permissions and perform the task. Parameters ---------- context: object A `control.context.Context` singleton task: string The name of the task eid: string(objectId) The id of the relevant record """ taskInfo = G(TASKS, task) acro = G(taskInfo, N.acro) table = G(taskInfo, N.table) if table not in ALL_TABLES: flash(f"""Workflow {acro} operates on wrong table: "{table or E}""", "error") return (False, None) return mkTable(context, table).record(eid=eid).task(task)
def assertIt(cl, exp): assertModifyField(cl, CONTRIB, eid, TITLE, TITLE2, exp[CONTRIB]) assertModifyField(cl, ASSESS, aId, TITLE, TITLE_A2, exp[ASSESS]) reviewerFId = G(users, FINAL) reviewerEId = G(users, EXPERT) assertModifyField(cl, ASSESS, aId, REVIEWER_E, (reviewerFId, FINAL), exp["assign"]) assertModifyField(cl, ASSESS, aId, REVIEWER_E, (reviewerEId, EXPERT), exp["assign"]) assertModifyField( cl, CRITERIA_ENTRY, cIdFirst, EVIDENCE, ([EVIDENCE1], EVIDENCE1), exp[CRITERIA_ENTRY], ) for (rId, remarks, kind) in ( (rExpertId, REMARKS_E, EXPERT), (rFinalId, REMARKS_F, FINAL), ): assertModifyField(cl, REVIEW, rId, REMARKS, ([remarks], remarks), exp[f"{REVIEW}_{kind}"]) for (kind, reid) in renId.items(): comments = COMMENTS_E if kind == EXPERT else COMMENTS_F assertModifyField( cl, REVIEW_ENTRY, reid, COMMENTS, ([comments], comments), exp[f"{REVIEW_ENTRY}_{kind}"], )
def bodyCompact(self, **kwargs): critId = self.critId critRecord = self.critRecord perm = self.perm critData = critRecord.record actual = G(critData, N.actual, default=False) msg = E if actual else G(MESSAGES, N.legacyCriterion) critKey = f"""{N.criteria}/{critId}/help""" (infoShow, infoHide, infoBody) = H.detailx( (N.info, N.dismiss), critRecord.wrapHelp(), critKey, openAtts=dict(cls="button small", title="Explanation and scoring guide"), closeAtts=dict(cls="button small", title="Hide criteria explanation"), ) score = H.div(self.field(N.score).wrap(asEdit=G(perm, N.isEdit))) evidence = H.div(self.field(N.evidence).wrap(asEdit=G(perm, N.isEdit))) entry = H.div([ H.div(H.he(msg), cls="heavy") if msg else E, infoShow, infoHide, infoBody, score, evidence, ], ) return entry
def test_addAssessment2(clientOwner): recordId = startInfo["recordId"] recordInfo = startInfo["recordInfo"] ids = startInfo["ids"] eid = G(recordId, CONTRIB) aId = G(recordId, ASSESS) assessInfo = getItem(clientOwner, ASSESS, aId) recordInfo[ASSESS] = assessInfo fields = assessInfo["fields"] aTitle = G(fields, TITLE) assertModifyField(clientOwner, CONTRIB, eid, TYPE, (ids["TYPE2"], TYPE2), True) assessInfo = getItem(clientOwner, ASSESS, aId) text = assessInfo["text"] assert checkWarning(text, aTitle) assertStartTask(clientOwner, START_ASSESSMENT, eid, True) aIds = getEid(clientOwner, ASSESS, multiple=True) assert len(aIds) == 2 newAId = [i for i in aIds if i != aId][0] assertDelItem(clientOwner, ASSESS, newAId, True) assertModifyField(clientOwner, CONTRIB, eid, TYPE, (ids["TYPE1"], TYPE1), True) assessInfo = getItem(clientOwner, ASSESS, aId) text = assessInfo["text"] assert not checkWarning(text, aTitle)
def test_reviewEntries(clients): recordId = startInfo["recordId"] cIds = recordId[CRITERIA_ENTRY] reviewId = G(recordId, REVIEW) rEid = G(reviewId, EXPERT) rFid = G(reviewId, FINAL) assert rEid is not None assert rFid is not None def assertIt(cl, exp): user = cl.user for crId in cIds: criteriaEntryInfo = getItem(cl, CRITERIA_ENTRY, crId) text = criteriaEntryInfo["text"] reviewEntries = findReviewEntries(text) if exp: if user in {EXPERT, FINAL}: assert user in reviewEntries assert COMMENTS in reviewEntries[user][1] assert reviewEntries[user][1][COMMENTS] == E else: assert EXPERT in reviewEntries assert FINAL in reviewEntries expect = {user: False for user in USERS} expect.update({user: True for user in POWER_USERS | {EXPERT, FINAL}}) forall(clients, expect, assertIt)
def countryRep(self, user=None): """Provide a short representation of the country of a user. Parameters ---------- user: dict, optional `None` The user whose country must be represented. If absent, the currently logged in user will be taken. Returns ------- string The representation consists of the 2-letter country code plus a derived two letter unicode character combination that will be turned into a flag of that country. """ db = self.db country = db.country if user is None: user = self.user countryId = G(user, N.country) countryInfo = G(country, countryId) iso = G(countryInfo, N.iso, default=E) flag = shiftRegional(iso) if iso else Qc countryShort = iso + flag return countryShort
def makeCrit(self, mainTable, conditions): """Translate conditons into a MongoDb criterion. The conditions come from the options on the interface: whether to constrain to items that have assessments and or reviews. The result can be fed into an other Mongo query. It can also be used to filter a list of record that has already been fetched. !!! hint `{'assessment': '1'}` means: only those things that have an assessment. `'-1'`: means: not having an assessment. `'0'`: means: don't care. !!! hint See also `Db.getList`. Parameters ---------- mainTable: string The name of the table that is being filtered. conditions: dict keyed by a table name (such as assessment or review) and valued by -1, 0 or 1 (as strings). Result ------ dict keyed by the same table name as `conditions` and valued by a set of mongo ids of items that satisfy the criterion. Only for the criteria that do care! """ activeOptions = { G(G(OPTIONS, cond), N.table): crit == ONE for (cond, crit) in conditions.items() if crit in {ONE, MINONE} } if None in activeOptions: del activeOptions[None] criterion = {} for (table, crit) in activeOptions.items(): eids = { G(record, mainTable) for record in self.mongoCmd( N.makeCrit, table, N.find, {mainTable: {M_EX: True}}, {mainTable: True}, ) } if crit in criterion: criterion[crit] |= eids else: criterion[crit] = eids return criterion
def test_assignReviewers(clients, field, user): valueTables = startInfo["valueTables"] recordId = startInfo["recordId"] aId = G(recordId, ASSESS) users = G(valueTables, USER) expect = {user: False for user in USERS} assignReviewers(clients, users, aId, field, user, True, expect)
def titleStr(self, record): """The title string is a suitable icon plus the participle field.""" decision = G(record, N.participle) if decision is None: return Qq sign = G(record, N.sign) decision = f"""{sign}{NBSP}{decision}""" return decision
def startAssign(): aId = recordId[ASSESS] users = G(valueTables, USER) for (field, user) in ((REVIEWER_E, EXPERT), (REVIEWER_F, FINAL)): value = G(users, user) assertModifyField(clientOffice, ASSESS, aId, field, (value, user), True) if review: startReviews()
def titleStr(self, record): """Put the score and the level in the title.""" score = G(record, N.score) if score is None: return Qq score = H.he(score) level = H.he(G(record, N.level)) or Qq return f"""{score} - {level}"""
def assertIt(cl, exp): user = cl.user for kind in [EXPERT, FINAL]: rId = G(reviewId, kind) expStatus = kind == user if exp else exp for decision in [REJECT, REVISE, ACCEPT, REVOKE]: decisionStr = G(G(REVIEW_DECISION, decision), kind) url = f"/api/task/{decisionStr}/{rId}" assertStatus(cl, url, expStatus)
def __init__(self, regime, test=False): """## Initialization Pick up the connection to MongoDb. !!! note Parameters ---------- regime: {"production", "development"} See below test: boolean See below. """ self.regime = regime """*string* Whether the app runs in production or in development.""" self.test = test """*boolean* Whether to connect to the test database.""" database = G(DATABASE, N.test) if test else G(DATABASE, regime) self.database = database mode = f"""regime = {regime} {"test" if test else E}""" if not self.database: serverprint(f"""MONGO: no database configured for {mode}""") sys.exit(1) self.client = None """*object* The MongoDb client.""" self.mongo = None """*object* The connection to the MongoDb database. The connnection exists before the Db singleton is initialized. """ self.collected = {} """*dict* For each value table, the time that this worker last collected it. In the database there is a table which holds the last time for each value table that a worker updated a value in it. """ self.collect() creator = [ G(record, N._id) for record in self.user.values() if G(record, N.eppn) == CREATOR ] if not creator: serverprint(f"""DATABASE: no creator user found in {database}.user""") sys.exit(1) self.creatorId = creator[0] """*ObjectId* System user.
def titleStr(self, record): """The title is a sequence number plus the short criterion text.""" context = self.context types = context.types seq = H.he(G(record, N.seq)) or Qn eid = G(record, N.criteria) title = Qq if eid is None else types.criteria.title(eid=eid) return f"""{seq}. {title}"""
def titleStr(self, record): """Put the main type and the sub type in the title.""" if not record: return Qq mainType = G(record, N.mainType) or E subType = G(record, N.subType) or E sep = WHYPHEN if mainType and subType else E return H.he(f"""{mainType}{sep}{subType}""")
def __init__(self, db, regime): """## Initialization Include a handle to `control.db.Db` into the attributes. Parameters ---------- db: object See below. """ self.db = db """*object* The `control.db.Db` singleton Provides methods to retrieve user info from the database and store user info there. """ permissionGroupInv = db.permissionGroupInv # determine production or devel self.isDevel = regime == N.development """*boolean* Whether the server runs in production or in development. In production we use the DARIAH Identity provider, while in development we use a simple, console-based way of logging a few test users in. """ self.authority = N.local if self.isDevel else N.DARIAH """*string* The name of the authority that identifies users. In production it is "DARIAH", which stands for the DARIAH Identity Provider. In development it is "local". """ self.authId = G(permissionGroupInv, AUTH) """*string* The groupId of the `auth` permission group. """ self.authUser = {N.group: self.authId} """*string* Info of the `auth` permission group. """ self.unauthId = G(permissionGroupInv, UNAUTH) """*string* The groupId of the `public` permission group. """ self.unauthUser = {N.group: self.unauthId} """*string* Info of the `public` permission group. """ self.user = {} """*dict* The attributes of the currently logged in user."""
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)
def title(self, *args, **kwargs): uid = self.uid record = self.record creatorId = G(record, N.creator) youRep = f""" ({N.you})""" if creatorId == uid else E lastModified = G(record, N.modified) lastModifiedRep = (self.field(N.modified).value[-1].rsplit( DOT, maxsplit=1)[0] if lastModified else Qu) return H.span(f"""{lastModifiedRep}{youRep}""", cls=f"rentrytitle")
def insert(self, force=False, masterTable=None, masterId=None): mayInsert = force or self.mayInsert if not mayInsert: return None if not masterTable or not masterId: return None context = self.context db = context.db uid = self.uid eppn = self.eppn table = self.table masterOid = castObjectId(masterId) masterRecord = context.getItem(N.assessment, masterOid) contribId = G(masterRecord, N.contrib) if not contribId: return None wfitem = context.getWorkflowItem(contribId) if not wfitem.permission(N.startReview): return contribId (contribType, ) = wfitem.info(N.contrib, N.type) (assessmentTitle, ) = wfitem.info(N.assessment, N.title) fields = { N.contrib: contribId, masterTable: masterOid, N.reviewType: contribType, N.title: f"review of {assessmentTitle}", } reviewId = db.insertItem(table, uid, eppn, False, **fields) criteriaEntries = db.getDetails( N.criteriaEntry, N.assessment, masterOid, sortKey=lambda r: G(r, N.seq, default=0), ) records = [{ N.seq: G(critEntry, N.seq, default=0), N.criteria: G(critEntry, N.criteria), N.criteriaEntry: G(critEntry, N._id), N.assessment: masterOid, N.review: reviewId, } for critEntry in criteriaEntries] db.insertMany(N.reviewEntry, uid, eppn, records) self.adjustWorkflow(contribId, new=False) return contribId
def checkPerm( require, perm, ): """Verify whether user's credentials match the requirements. Parameters ---------- require: string The required permission level (see the perm.yaml configuration file under the key `roles`). perm: dict User attributes, in paticular `group` which is the role a user can play on the basis of his/her identity. But it also contains attributes that links a user to ceertain records, e.g. the records of which (s)he is creator/editor, or National Coordinator. Returns ------- boolean """ if require == UNAUTH: return True group = G(perm, N.group) if require == NOBODY: return False if require == AUTH: return group != UNAUTH isSuper = group in {OFFICE, SYSTEM, ROOT} if require == OFFICE: return isSuper if require == SYSTEM: return group in {SYSTEM, ROOT} if require == ROOT: return group == ROOT if require == EDIT: return group != UNAUTH and (G(perm, N.isEdit) or isSuper) if require == OWN: return group != UNAUTH and (G(perm, N.isOwn) or isSuper) if require == COORD: return group == COORD and G(perm, N.sameCountry) or isSuper
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
def insert(self, force=False, masterTable=None, masterId=None): mayInsert = force or self.mayInsert if not mayInsert: return None if not masterTable or not masterId: return None context = self.context db = context.db uid = self.uid eppn = self.eppn table = self.table typeCriteria = db.typeCriteria masterOid = castObjectId(masterId) wfitem = context.getWorkflowItem(masterOid) if not wfitem.permission(N.startAssessment): return None (contribType, contribTitle) = wfitem.info(N.contrib, N.type, N.title) if not contribType: flash("You cannot assess a contribution without a type", "error") return None isActual = G(G(db.typeContribution, contribType), N.actual) if not isActual: flash("You cannot assess a contribution with a legacy type", "error") return None fields = { masterTable: masterOid, N.assessmentType: contribType, N.title: f"assessment of {contribTitle}", } criteria = G(typeCriteria, contribType, default=[]) if not criteria: flash("This contribution type has no criteria", "error") return None assessmentId = db.insertItem(table, uid, eppn, False, **fields) records = [ {N.seq: seq, N.criteria: crit, N.assessment: assessmentId} for (seq, crit) in enumerate(criteria) ] db.insertMany(N.criteriaEntry, uid, eppn, records) self.adjustWorkflow(masterOid, new=False) return assessmentId
def authenticate(self, login=False): """Verify the authenticated status of the current user. This function is called for every request that requires authentication. Whether a user is authenticated or not depends on whether a session for that user is present. And that depends on whether the identity provider has sent attributes (eppn and others) to the server. The data in the `user` attribute will be cleared if there is an authenticated user. Subsequent methods that ask for the uid of the currennt user will get nothing if there is no authenticated user. If there is an authenticated user, and `login=False`, his/her data are not loaded into the `user` attribute. Parameters ---------- login: boolean, optional `False` Pass `True` in order to verify/update a user that has just logged in. The data in the `user` attribute will be updated with his/her data. The user table in the database will be updated if the identity provider has given updated attributed for that user. Returns ------- boolean Whether the current user is authenticated. """ user = self.user # if login=True we want to log the user in # if login=False we only want the current user information if login: session.pop(N.eppn, None) if self.checkLogin(): # in this case there is an eppn session[N.eppn] = G(user, N.eppn) return True return False eppn = G(session, N.eppn) if eppn: if not self.getUser(eppn, mayCreate=False): self.clearUser() return False return True self.clearUser() return False