def makeCaption(self, label, entries, rule=False): """Produce the caption for a section of navigation items. Parameters ---------- label: string Points to a key in web.yaml, under `captions`, where the full text of the caption can be founnd. entries: iterable of (path, string(html)) The path is used to determine whether this entry is active; the string is the formatted html of the entry. rule: boolean Whether there should be a rule before the first entry. Returns ------- string(html) """ if not entries: return E refPath = self.path paths = {path for (path, rep) in entries} reps = [rep for (path, rep) in entries] active = any(refPath.startswith(f"""/{p}/""") for p in paths) navClass = " active" if active else E atts = dict(cls=f"nav {navClass}") if rule: atts[N.addClass] = " ruleabove" entriesRep = H.div(reps, cls="sidebarsec") return H.details(label, entriesRep, label, **atts)
def status(self, table, kind=None): """Present all workflow info and controls relevant to the record. Parameters ---------- table: string We must specify the kind of record for which we want to see the status: contrib, assessment, or review. kind: string {`expert`, `final`}, optional `None` Only if we want review attributes Returns ------- string(html) """ eid = list(self.info(table, N._id, kind=kind))[0] itemKey = f"""{table}/{eid}""" rButton = H.iconr(itemKey, "#workflow", msg=N.status) return H.div( [ rButton, self.statusOverview(table, kind=kind), self.tasks(table, kind=kind), ], cls="workflow", )
def widget(self, val): """Constructs and edit widget around for this type. Parameters ---------- val: string The initial value for the widget. Returns ------- string(html) Dependent on a batch of Javascript in `index.js`, look for `const widgets`. """ atts = {} if self.pattern: atts[N.pattern] = self.pattern validationMsg = TypeBase.validationMsg(self.name) widgetElem = H.input(self.toEdit(val), type=N.text, cls="wvalue", **atts) validationElem = H.span(E, valmsg=validationMsg) if validationMsg else E return H.join([widgetElem, validationElem])
def wrapTestUsers(self): """Present a widget to select a test user for login. !!! caution In production this will do nothing. Only in development mode one can select a test user. """ if not self.isDevel: return E db = self.db testUsers = { record[N.eppn]: record for record in db.user.values() if N.eppn in record and G(record, N.authority) == N.local } return H.join( [ H.div(H.a(u, href=f"/login?eppn={u}", cls="button small")) for u in testUsers ] + [ H.div( H.input( E, placeholder="email", onchange="window.location.href=`/login?email=${this.value}`", ) ) ] )
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 bodyCompact(self, **kwargs): perm = self.perm theTitle = self.title() comments = ("""<!-- begin review comment -->""" + H.div( self.field(N.comments).wrap(withLabel=False, asEdit=G(perm, N.isEdit)), ) + """<!-- end review comment -->""") return H.div([theTitle, comments], cls=f"reviewentry")
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 wrapWidget(self, editable, cls=E): """Wrap the field value. A widget shows the value and may have additional controls to * edit the value * refresh the value Refresh fields are those fields that change if other fields are updated, typically fields that record the moment on which something happened. These fields will get a refresh button automatically. Fields may have three conditions relevant for rendering: condition | rendering --- | --- not editable | readonly editable in readonly view | readonly with button for editable view editable in edit view | editable with button for readonly view Parameters ---------- editable: boolean Whether the field should be presented in editable form cls See `Field.wrap()` Returns ------- button: string(html) representation: string(html) They are packaged as a tuple. """ atts = self.atts mayEdit = self.mayEdit withRefresh = self.withRefresh button = (H.iconx(N.ok, cls="small", action=N.view, **atts) if editable else (H.iconx(N.edit, cls="small", action=N.edit, **atts) if mayEdit else H.iconx( N.refresh, cls="small", action=N.view, title=REFRESH, **atts, ) if withRefresh else E)) return (button, self.wrapValue(editable, cls=cls))
def body(self, myMasters=None, hideMasters=False): """Wrap the body of the record in HTML. This is the part without the provenance information and without the detail records. This method can be overridden by `body` methods in derived classes. Parameters ---------- myMasters: iterable of string, optional `None` A declaration of which fields must be treated as master fields. hideMaster: boolean, optional `False` If `True`, all master fields as declared in `myMasters` will be left out. Returns ------- string(html) """ fieldSpecs = self.fields provSpecs = self.prov return H.join( self.field(field, asMaster=field in myMasters).wrap() for field in fieldSpecs if (field not in provSpecs and not (hideMasters and field in myMasters)))
def wrap(self, readonly=False, showTable=None, showEid=None): """Wrap the details of all tables for this record into HTML. Parameters ---------- readonly: boolean, optional `False` Whether the records should be presented in readonly form. showTable: string Name of the detail table of which record `showEid` should be opened. showEid: ObjectId Id of the detail record that should be initially opened. Returns ------- string(html) """ table = self.table for detailTable in G(DETAILS, table, default=[]): self.fetchDetails(detailTable) details = self.details return H.join( self.wrapDetail( detailTable, readonly=readonly, showEid=showEid if detailTable == showTable else None, ) for detailTable in details)
def widget(self, val): values = G(BOOLEAN_TYPES, self.name) noneValue = False if len(values) == 2 else None refV = G(values, val, default=G(values, noneValue)) return H.div( [ H.iconx( values[w], bool=str(w).lower(), cls=(("active" if values[w] is refV else E) + " medium"), ) for w in values ], cls="wvalue", )
def titleRaw(obj, record, cls=E): """Generate a title for a different record. This is fast title generation. No record object will be created. The title will be based on the fields in the record, and its formatting is assisted by the appropriate type class in `control.typ.types`. !!! hint If the record is not "actual", its title will get a warning background color. See `control.db.Db.collect`. Parameters ---------- obj: object Any object that has a table and context attribute, e.g. a table object or a record object record: dict cls: string, optional, `''` A CSS class to add to the outer element of the result """ table = obj.table context = obj.context types = context.types typesObj = getattr(types, table, None) inActualCls = Record.inActualCls(obj, record) atts = dict(cls=f"{cls} {inActualCls}") return H.span(typesObj.title(record=record), **atts)
def widget(self, val, multiple, extensible, constrain): context = self.context db = context.db table = self.name valueRecords = db.getValueRecords(table, constrain=constrain) filterControl = ( [ H.input( E, type=N.text, placeholder=G(MESSAGES, N.filter, default=E), cls="wfilter", ), H.iconx(N.add, cls="small wfilter add", title="add value") if extensible else E, H.iconx(N.clear, cls="small wfilter clear", title="clear filter"), ] if len(valueRecords) > FILTER_THRESHOLD else [] ) atts = dict( markup=True, clickable=True, multiple=multiple, active=val, hideInActual=True, hideBlockedUsers=True, ) return H.div( filterControl + [ formatted for (text, formatted) in ( ([] if multiple else [self.title(record={}, **atts)]) + sorted( (self.title(record=record, **atts) for record in valueRecords), key=lambda x: x[0].lower(), ) ) ], cls="wvalue", )
def toDisplay(self, val, markup=True): """Turns a real value into a HTML code for readonly display. Parameters ---------- val: mixed A value of this type. Returns ------- string(html) Possibly with nice formatting depending on the nature of the value. """ if val is None: return QQ if markup else Qq valBare = H.he(self.normalize(str(val))) return H.span(valBare) if markup else valBare
def wrapHelp(self): info = H.join( self.field(field, readonly=True).wrap(action=N.view) for field in [N.typeContribution, N.remarks] if field != N.typeContribution ) detailsObj = self.DetailsClass(self) detailsObj.fetchDetails(N.score) details = detailsObj.wrapDetail( N.score, expanded=True, readonly=True, wrapMethod=N.wrapHelp, combineMethod=lambda x: [H.dl(x)], ) return H.div(info + details, cls="criteriahelp")
def statusOverview(self, table, kind=None): """Present the current status of a record on the interface. Parameters ---------- table: string We must specify the kind of record for which we want to present the stage: contrib, assessment, or review. kind: string {`expert`, `final`}, optional `None` Only if we want review attributes Returns ------- string(html) """ (stage, stageDate, locked, done, frozen, score, eid) = self.info( table, N.stage, N.stageDate, N.locked, N.done, N.frozen, N.score, N._id, kind=kind, ) stageInfo = G(STAGE_ATTS, stage) statusCls = G(stageInfo, N.cls) stageOn = (H.span(f""" on {datetime.toDisplay(stageDate)}""", cls="date") if stageDate else E) statusMsg = H.span([G(stageInfo, N.msg) or E, stageOn], cls=f"large status {statusCls}") lockedCls = N.locked if locked else E lockedMsg = (H.span(G(STATUS_REP, N.locked), cls=f"large status {lockedCls}") if locked else E) doneCls = N.done if done else E doneMsg = (H.span(G(STATUS_REP, N.done), cls=f"large status {doneCls}") if done else E) frozenCls = N.frozen if frozen else E frozenMsg = (H.span(G(STATUS_REP, N.frozen), cls="large status info") if frozen else E) statusRep = f"<!-- stage:{stage} -->" + H.div( [statusMsg, lockedMsg, doneMsg, frozenMsg], cls=frozenCls) scorePart = E if table == N.assessment: scoreParts = presentScore(score, eid) scorePart = (H.span(scoreParts) if table == N.assessment else (scoreParts[0] if scoreParts else E) if table == N.contrib else E) return H.div([statusRep, scorePart], cls="workflow-line")
def wrap(self, *args, **kwargs): wfitem = self.wfitem if not wfitem: return super().wrap(*args, **kwargs) details = self.details (reviewer, ) = wfitem.info(N.assessment, N.reviewer) self.fetchDetails( N.reviewEntry, sortKey=lambda r: G(r, N.dateCreated, default=0), ) (tableObj, records) = G(details, N.reviewEntry, default=(None, [])) if not tableObj: return E byReviewer = {N.expert: E, N.final: E} for dest in (N.expert, N.final): byReviewer[dest] = self.wrapDetail( N.reviewEntry, filterFunc=lambda r: G(r, N.creator) == G(reviewer, dest), bodyMethod=N.compact, expanded=True, withProv=False, withN=False, inner=False, ) if not byReviewer[dest]: byReviewer[dest] = H.span("""No review comment yet""", cls="info small") return H.div( [ f"""<!-- begin reviewer {dest} -->""" + H.div( [H.div(cap1(dest), cls="head"), G(byReviewer, dest)], cls=f"reviewentries {dest}", ) + f"""<!-- end reviewer {dest} -->""" for dest in reviewer ], cls="reviewers", )
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 wrap(self, *args, **kwargs): wfitem = self.wfitem if not wfitem: return super().wrap(*args, **kwargs) kind = self.kind statusRep = wfitem.status(N.review, kind=kind) return H.div(statusRep)
def title(self, *args, **kwargs): record = self.record critRecord = self.critRecord withEvidence = H.icon( N.missing if self.field(N.evidence).isBlank() else N.check) status = H.span(f"""evidence{NBSP}{withEvidence}""", cls="right small") seq = G(record, N.seq, default=Q) scoreRep = self.field(N.score).wrapBare() return H.span( [ H.span([f"""{seq}{DOT}{NBSP}""", critRecord.title()], cls="col1"), H.span(scoreRep, cls="col2"), status, ], cls=f"centrytitle criteria", )
def makeOptions(self): """Produce an options widget. The options are defined in web.yaml, under the key `options`. """ options = self.options filterRep = [ H.input(E, type=N.text, id="cfilter", placeholder="match title"), ] optionsRep = [ H.span( [H.checkbox(name, trival=value), G(G(OPTIONS, name), N.label)], cls=N.option, ) for (name, value) in options.items() ] return [("XXX", rep) for rep in filterRep + optionsRep]
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 title( self, record=None, eid=None, markup=False, active=None, ): """Generate a title for a rlated record. Parameters ---------- record: dict, optional `None` The record for which to generate a title. eid: ObjectId, optional `None` If `record` is not passed, use this to retrieve the full record. markup: boolean If true, generate the title in HTML markup, otherwise as a plain string. active: ObjectId, optional `None` If passed, is is the id of the currently *active* record, the one that the current user is interacting with. Returns ------- string(html) """ if record is None and eid is None: return (QQ, QQ) if markup else Qq table = self.name if record is None: context = self.context record = context.getItem(table, eid) titleStr = self.titleStr(record) titleHint = self.titleHint(record) if markup: if eid is None: eid = G(record, N._id) inActualCls = self.inActualCls(record) atts = dict(cls=f"tag medium {inActualCls}") if titleHint: atts[N.title] = titleHint href = f"""/{table}/item/{eid}""" titleFormatted = H.a(titleStr, href, target=N._blank, **atts) return (titleStr, titleFormatted) else: return titleStr
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 wrapHelp(self): term = self.title() definition = H.div( [ self.field(field).wrap( empty=True, action=N.view, cls="scoredesc" if field == N.description else E, ) for field in [N.description, N.remarks] ] ) return (term, definition)
def makeEntry(self, label, path, withOptions=False, asTask=False): """Produce an entry. !!! hint "easy comments" We also include a comment `<!-- caption^label --> for the ease of testing. Parameters ---------- label: string The text of the entry path: url The destination after the entry is clicked. withOptions: boolean, optional `False` Whether to include the options widget. asTask: boolean, optional `False` Display the entry as a big workflow task button or as a modest hyperlink. Returns ------- path: url The url that corresponds to this entry string(html) The wrapped entry """ options = self.options active = path == self.path task = "task info" if asTask else "button" navClass = f"{task} small nav" + (" active" if active else E) optionsRep = (AMP.join( f"""{name}={value}""" for (name, value) in options.items()) if withOptions else E) if optionsRep: optionSep = AMP if Q in path else Q optionsRep = optionSep + optionsRep atts = dict(cls=navClass, ) if withOptions: atts[N.hrefbase] = path comment = f"""<!-- caption^{label} -->""" return ( path, comment + H.a(label, path + optionsRep, **atts), )
def wrap(self, *args, **kwargs): wfitem = self.wfitem if not wfitem: return super().wrap(*args, **kwargs) self.fetchDetails( N.assessment, sortKey=lambda r: G(r, N.dateCreated, default=0), ) statusRep = wfitem.status(N.contrib) showEid = self.mustShow(N.assessment, kwargs) return H.div( [statusRep, self.wrapDetail(N.assessment, showEid=showEid)])
def insertButton(self): """Present an insert button on the interface. Only if the user has rights to insert new items in this table. """ mayInsert = self.mayInsert if not mayInsert: return E table = self.table itemSingle = self.itemLabels[0] return H.a( f"""New {itemSingle}""", f"""/api/{table}/{N.insert}""", cls="small task info", )
def titleStr(self, record): context = self.context auth = context.auth return H.he(auth.identity(record))