Module control.field
Fields of records of tables.
- Display readonly/editable
- Refresh buttons
- Saving values
Expand source code
"""Fields of records of tables.
* Display readonly/editable
* Refresh buttons
* Saving values
"""
from flask import request, abort
from config import Config as C, Names as N
from control.html import HtmlElements as H
from control.utils import pick as G, bencode, cap1, E, BLANK, ONE, COMMA
from control.perm import getPermField
from control.typ.value import ConversionError
CT = C.tables
CW = C.web
CF = C.workflow
DEFAULT_TYPE = CT.defaultType
CONSTRAINED = CT.constrained
WITH_NOW = CT.withNow
WORKFLOW_TABLES = set(CT.userTables) | set(CT.userEntryTables)
CASCADE_SPECS = CT.cascade
WORKFLOW_FIELDS = CF.fields
REFRESH = CW.messages[N.refresh]
LIMIT_JSON = CW.limitJson
class Field:
"""Deals with fields."""
inheritProps = (
N.context,
N.uid,
N.eppn,
N.table,
N.record,
N.eid,
N.perm,
N.readonly,
N.mayRead,
)
def __init__(
self,
recordObj,
field,
asMaster=False,
readonly=None,
mayRead=None,
mayEdit=None,
):
"""## Initialization
Store the incoming information.
A number of properties will be inherited from the record object
that spawns a field object.
The value of the field will be looked up from the record and saved into the attribute
`value`.
Set the attribute `fieldTypeObj` to a suitable derived class of
`control.typ.base.TypeBase`.
!!! caution
Fields that point to master records are never editable in this app.
!!! hint
The parameters `readonly`, `mayRead`, `mayEdit` are optional.
If they are not passed or `None`, values for these will be taken
from the `recordObj` that has spawned this field object.
!!! caution
Whether a field is readable or editable, depends first on how it
is configured in the .yaml file in `tables` that correspnds to the table.
This can be overriden by setting the `mayRead` and `mayEdit` attributes
in the `recordObj`, and it can be overridden again by explicitly
passing values for them here.
Parameters
----------
recordObj: object
See below.
field: string
See below.
asMaster: boolean, optional `False`
See below.
readonly: boolean | `None`, optional `None`
Whether the field must be presented readonly.
mayRead: boolean | `None`, optional `None`
If passed, overrides the configured read permission for this field.
mayEdit: boolean | `None`, optional `None`
If passed, overrides the configured write permission for this field.
"""
for prop in Field.inheritProps:
setattr(self, prop, getattr(recordObj, prop, None))
self.recordObj = recordObj
"""*object* A `control.record.Record` object (or one of a derived class)
"""
self.field = field
"""*string* The name of the field
"""
self.asMaster = asMaster
"""*boolean* Whether this field points to a master record.
"""
table = self.table
withNow = G(WITH_NOW, table)
if withNow:
nowFields = []
for info in withNow.values():
if type(info) is str:
nowFields.append(info)
else:
nowFields.extend(info)
nowFields = set(nowFields)
else:
nowFields = set()
self.withRefresh = field == N.modified or field in nowFields
"""*boolean* Whether the field needs a refresh button.
"""
self.withNow = G(withNow, field)
"""*dict* Which field updates need a timestamp?
The info comes from tables.yaml, under key `withNow`.
It is keyed by table name, then by field name, and the value
is a single field or lists of two fields with the names of
corresponding timestamp fields.
When a (boolean) field has two timestamp fields, the first one is used if
he value is a list, the first one will be used if the modification
sets the field to `True` and the second one when the field becomes `False`.
!!! hint
In assessments, when `submitted` becomes `True`, `dateSubmitted` receives
a timestamp. When `submitted` becomes `False`, it is `dateWithdrawn` that
receives the timestamp.
"""
fieldSpecs = recordObj.fields
fieldSpec = G(fieldSpecs, field)
record = self.record
self.value = G(record, field)
"""*mixed* The value of the field.
"""
require = G(fieldSpec, N.perm, default={})
self.require = require
"""*dict* The required permissions for this field.
Keys are `read` and `edit`, the values are `True` or `False`.
"""
self.label = G(fieldSpec, N.label, default=cap1(field))
"""*string* A label to display in front of the field.
"""
self.tp = G(fieldSpec, N.type, default=DEFAULT_TYPE)
"""*string* The data type of the field."""
self.multiple = G(fieldSpec, N.multiple, default=False)
"""*boolean* Whether the field value consists of multiple values or a single one.
"""
self.extensible = G(fieldSpec, N.extensible, default=False)
"""*boolean* Whether the user may add new values to the value table of this field.
"""
context = self.context
perm = self.perm
table = self.table
eid = self.eid
tp = self.tp
types = context.types
fieldTypeObj = getattr(types, tp, None)
self.fieldTypeObj = fieldTypeObj
"""*object* The type object by which the value of this field can be interpreted.
"""
self.widgetType = fieldTypeObj.widgetType
"""*string* The type of widget for presenting an edit view on this field.
"""
readonly = self.readonly if readonly is None else readonly
(self.mayRead, self.mayEdit) = getPermField(
table, perm, require, **self.getActualMinimum()
)
if mayRead is not None:
self.mayRead = mayRead
if mayEdit is not None:
self.mayEdit = mayEdit
if readonly or asMaster:
self.mayEdit = False
self.atts = dict(table=table, eid=eid, field=field)
"""*dict* Identification of this field value: table, id of record, name of field.
!!! hint
To be used to pass to buttons in widgets for this field.
"""
def getActualMinimum(self):
tp = self.tp
if tp == N.permissionGroup:
context = self.context
db = context.db
auth = context.auth
user = auth.user
userGroup = G(user, N.group)
minimum = G(G(db.permissionGroup, userGroup), N.rep)
value = self.value
actual = G(G(db.permissionGroup, value), N.rep)
return dict(actual=actual, minimum=minimum)
return {}
def save(self, data):
"""Save a new value for this field to MongoDb.
Before saving, permissions and workflow conditions will be checked.
After saving, workflow information will be adjusted.
!!! caution
If the `editors` field of an assessment or review is modified,
the same value should be put in the detail records, the
`criteriaEntry` and `reviewEntry` records respectively.
Parameters
----------
data: mixed
The new value(s) for this field.
It comes straight from the client, and will be converted to proper
Python/MongoDb values
"""
mayEdit = self.mayEdit
if not mayEdit:
return False
context = self.context
db = context.db
uid = self.uid
eppn = self.eppn
table = self.table
eid = self.eid
field = self.field
extensible = self.extensible
record = self.record
recordObj = self.recordObj
require = self.require
constrain = None
constrainField = G(CONSTRAINED, field)
if constrainField:
constrainValue = G(record, constrainField)
if constrainValue:
constrain = (constrainField, constrainValue)
multiple = self.multiple
fieldTypeObj = self.fieldTypeObj
conversion = fieldTypeObj.fromStr if fieldTypeObj else None
kwargs = dict(uid=uid, eppn=eppn, extensible=extensible) if extensible else {}
if constrain:
kwargs[N.constrain] = constrain
if conversion is not None:
try:
if multiple:
data = [conversion(d, **kwargs) for d in data or []]
else:
data = conversion(data, **kwargs)
except ConversionError:
return False
modified = G(record, N.modified)
nowFields = []
if data is not None:
withNow = self.withNow
if withNow:
withNowField = (
withNow
if type(withNow) is str
else withNow[0]
if data
else withNow[1]
)
nowFields.append(withNowField)
result = db.updateField(
table,
eid,
field,
data,
eppn,
modified,
nowFields=nowFields,
)
if not result:
return False
(updates, deletions) = result
record = context.getItem(table, eid, requireFresh=True)
recordObj.reload(record)
self.value = G(record, field)
self.perm = recordObj.perm
perm = self.perm
(self.mayRead, self.mayEdit) = getPermField(
table, perm, require, **self.getActualMinimum()
)
good = True
if field == N.editors and table in CASCADE_SPECS:
for dtable in CASCADE_SPECS[table]:
drecords = db.getDetails(dtable, table, eid)
for drecord in drecords:
deid = G(drecord, N._id)
result = db.updateField(
dtable,
deid,
N.editors,
data,
eppn,
modified,
nowFields=nowFields,
)
if not result:
good = False
if table in WORKFLOW_TABLES and field in WORKFLOW_FIELDS:
recordObj.adjustWorkflow()
return good
def isEmpty(self):
"""Whether the value(s) is/are empty.
A single value is empty if it is `None`.
A mutliple value is empty if it is `None` or `[]`.
Returns
-------
boolean
"""
value = self.value
multiple = self.multiple
return value is None or multiple and value == []
def isBlank(self):
"""Whether the value(s) is/are blank.
A single value is empty if it is `None` or `''`.
A mutliple value is empty if it is `None` or `[]`, or all its
component values are blank.
Returns
-------
boolean
"""
value = self.value
multiple = self.multiple
return (
value is None
or value == E
or multiple
and (value == [] or all(v is None or v == E for v in value))
)
def wrapBare(self, markup=True):
"""Produce the bare field value.
This is the result of calling the
`control.typ.base.TypeBase.toDisplay` method on the derived
type class that matches the type of the field.
Returns
-------
string(html)
"""
context = self.context
types = context.types
tp = self.tp
value = self.value
multiple = self.multiple
fieldTypeObj = getattr(types, tp, None)
method = fieldTypeObj.toDisplay
return (
(
[method(val, markup=markup) for val in (value or [])]
if multiple
else method(value, markup=markup)
)
if markup is None
else (
BLANK.join(method(val, markup=markup) for val in (value or []))
if multiple
else method(value, markup=markup)
)
)
def wrap(self, action=None, asEdit=False, empty=False, withLabel=True, cls=E):
"""Wrap the field into HTML.
If there is an `action`, data from the request is picked up and
`Field.save` is called to save that data to the MongoDB.
Depending on the `action`, the field is then rendered as follows:
action | effect
--- | ---
`save` | no rendering
`view`| a read only rendering
`edit` | an edit widget
Whether a field is presented as an editable field depends on a number of factors:
factor | story
--- | ---
is master | a field pointing to a master will not be edited
attribute `mayEdit` | does the current user have permission to edit the field?
action `edit` or parameter `asEdit` | do we want to present an editable widget?
Parameters
----------
action: {save, edit, view}, optional `None`
If present, data will be saved to the database first.
asEdit: boolean, optional `False`
No data will be saved.
The field will rendered editable, if permitted.
empty: boolean, optional `False`
Only relevant for readonly views: if the value is empty, just present the
empty string and nothing else.
withLabel: boolean, optional `True`
Whether to precede the value with a field label.
The label is specified in the field specs which are in the table's .yaml
file in `control/tables`.
cls: string, optional `''`
A CSS class to append to the outer `<div>` of the result.
Returns
-------
string(html)
If there was an `action`, the bare representation of the field value is returned.
Otherwise, and if `withLabel`, a label is added.
"""
mayRead = self.mayRead
if mayRead is False:
return E
asMaster = self.asMaster
mayEdit = self.mayEdit
if action is not None and not asMaster:
contentLength = request.content_length
if contentLength is not None and contentLength > LIMIT_JSON:
abort(400)
data = request.get_json()
if data is not None and N.save in data:
if mayEdit:
good = self.save(data[N.save])
else:
good = False
if not good:
abort(400)
if action == N.save:
return E
editable = mayEdit and (action == N.edit or asEdit) and not asMaster
widget = self.wrapWidget(editable, cls=cls)
if action is not None:
return H.join(widget)
if empty and self.isEmpty():
return E
label = self.label
editClass = " edit" if editable else E
return (
H.div(
[
H.div(f"""{label}:""", cls="record-label"),
H.div(widget, cls=f"record-value{editClass}"),
],
cls="record-row",
)
if withLabel
else H.div(widget, cls=f"record-value{editClass}")
)
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 wrapValue(self, editable, cls=E):
"""Wraps the value of a field.
!!! hint "field=value in comments"
In the result we include a string
``` html
<!-- title=My contribution -->
```
but then with the field name instead of `title`
and the unmarked-up value instead of `My contribution`.
This makes it easier for the test suite
to spot the fields and their values.
Parameters
----------
editable: boolean
Whether the field should be presented in editable form
cls
See `Field.wrap()`
Returns
-------
string(html)
"""
context = self.context
types = context.types
fieldTypeObj = self.fieldTypeObj
field = self.field
value = self.value
tp = self.tp
multiple = self.multiple
extensible = self.extensible
widgetType = self.widgetType
baseCls = "tags" if widgetType == N.related else "values"
isSelectWidget = widgetType == N.related
args = []
if isSelectWidget and editable:
record = self.record
constrain = None
constrainField = G(CONSTRAINED, field)
if constrainField:
constrainValue = G(record, constrainField)
if constrainValue:
constrain = (constrainField, constrainValue)
args.append(multiple)
args.append(extensible)
args.append(constrain)
atts = dict(wtype=widgetType)
if editable:
typeObj = getattr(types, tp, None)
method = typeObj.toOrig
origStr = [method(v) for v in value or []] if multiple else method(value)
atts[N.orig] = bencode(origStr)
if multiple:
atts[N.multiple] = ONE
if extensible:
atts[N.extensible] = ONE
method = fieldTypeObj.widget if editable else fieldTypeObj.toDisplay
extraCls = E if editable else cls
valueBare = (
(COMMA.join(val for val in value or []) if multiple else value)
if tp == N.markdown
else (
COMMA.join(
fieldTypeObj.toDisplay(val, markup=False) for val in value or []
)
if multiple
else fieldTypeObj.toDisplay(value, markup=False)
)
)
return f"<!-- {field}={valueBare} -->" + (
H.div(
[
method(val, *args)
for val in (value or []) + ([E] if editable else [])
],
**atts,
cls=baseCls,
)
if multiple and not (editable and isSelectWidget)
else H.div(method(value, *args), **atts, cls=f"value {extraCls}")
)
Classes
class Field (recordObj, field, asMaster=False, readonly=None, mayRead=None, mayEdit=None)
-
Deals with fields.
Initialization
Store the incoming information.
A number of properties will be inherited from the record object that spawns a field object.
The value of the field will be looked up from the record and saved into the attribute
value
.Set the attribute
fieldTypeObj
to a suitable derived class ofTypeBase
.Caution
Fields that point to master records are never editable in this app.
Hint
The parameters
readonly
,mayRead
,mayEdit
are optional. If they are not passed orNone
, values for these will be taken from therecordObj
that has spawned this field object.Caution
Whether a field is readable or editable, depends first on how it is configured in the .yaml file in
tables
that correspnds to the table. This can be overriden by setting themayRead
andmayEdit
attributes in therecordObj
, and it can be overridden again by explicitly passing values for them here.Parameters
recordObj
:object
- See below.
field
:string
- See below.
asMaster
:boolean
, optionalFalse
- See below.
readonly
:boolean |
None, optional
None``- Whether the field must be presented readonly.
mayRead
:boolean |
None, optional
None``- If passed, overrides the configured read permission for this field.
mayEdit
:boolean |
None, optional
None``- If passed, overrides the configured write permission for this field.
Expand source code
class Field: """Deals with fields.""" inheritProps = ( N.context, N.uid, N.eppn, N.table, N.record, N.eid, N.perm, N.readonly, N.mayRead, ) def __init__( self, recordObj, field, asMaster=False, readonly=None, mayRead=None, mayEdit=None, ): """## Initialization Store the incoming information. A number of properties will be inherited from the record object that spawns a field object. The value of the field will be looked up from the record and saved into the attribute `value`. Set the attribute `fieldTypeObj` to a suitable derived class of `control.typ.base.TypeBase`. !!! caution Fields that point to master records are never editable in this app. !!! hint The parameters `readonly`, `mayRead`, `mayEdit` are optional. If they are not passed or `None`, values for these will be taken from the `recordObj` that has spawned this field object. !!! caution Whether a field is readable or editable, depends first on how it is configured in the .yaml file in `tables` that correspnds to the table. This can be overriden by setting the `mayRead` and `mayEdit` attributes in the `recordObj`, and it can be overridden again by explicitly passing values for them here. Parameters ---------- recordObj: object See below. field: string See below. asMaster: boolean, optional `False` See below. readonly: boolean | `None`, optional `None` Whether the field must be presented readonly. mayRead: boolean | `None`, optional `None` If passed, overrides the configured read permission for this field. mayEdit: boolean | `None`, optional `None` If passed, overrides the configured write permission for this field. """ for prop in Field.inheritProps: setattr(self, prop, getattr(recordObj, prop, None)) self.recordObj = recordObj """*object* A `control.record.Record` object (or one of a derived class) """ self.field = field """*string* The name of the field """ self.asMaster = asMaster """*boolean* Whether this field points to a master record. """ table = self.table withNow = G(WITH_NOW, table) if withNow: nowFields = [] for info in withNow.values(): if type(info) is str: nowFields.append(info) else: nowFields.extend(info) nowFields = set(nowFields) else: nowFields = set() self.withRefresh = field == N.modified or field in nowFields """*boolean* Whether the field needs a refresh button. """ self.withNow = G(withNow, field) """*dict* Which field updates need a timestamp? The info comes from tables.yaml, under key `withNow`. It is keyed by table name, then by field name, and the value is a single field or lists of two fields with the names of corresponding timestamp fields. When a (boolean) field has two timestamp fields, the first one is used if he value is a list, the first one will be used if the modification sets the field to `True` and the second one when the field becomes `False`. !!! hint In assessments, when `submitted` becomes `True`, `dateSubmitted` receives a timestamp. When `submitted` becomes `False`, it is `dateWithdrawn` that receives the timestamp. """ fieldSpecs = recordObj.fields fieldSpec = G(fieldSpecs, field) record = self.record self.value = G(record, field) """*mixed* The value of the field. """ require = G(fieldSpec, N.perm, default={}) self.require = require """*dict* The required permissions for this field. Keys are `read` and `edit`, the values are `True` or `False`. """ self.label = G(fieldSpec, N.label, default=cap1(field)) """*string* A label to display in front of the field. """ self.tp = G(fieldSpec, N.type, default=DEFAULT_TYPE) """*string* The data type of the field.""" self.multiple = G(fieldSpec, N.multiple, default=False) """*boolean* Whether the field value consists of multiple values or a single one. """ self.extensible = G(fieldSpec, N.extensible, default=False) """*boolean* Whether the user may add new values to the value table of this field. """ context = self.context perm = self.perm table = self.table eid = self.eid tp = self.tp types = context.types fieldTypeObj = getattr(types, tp, None) self.fieldTypeObj = fieldTypeObj """*object* The type object by which the value of this field can be interpreted. """ self.widgetType = fieldTypeObj.widgetType """*string* The type of widget for presenting an edit view on this field. """ readonly = self.readonly if readonly is None else readonly (self.mayRead, self.mayEdit) = getPermField( table, perm, require, **self.getActualMinimum() ) if mayRead is not None: self.mayRead = mayRead if mayEdit is not None: self.mayEdit = mayEdit if readonly or asMaster: self.mayEdit = False self.atts = dict(table=table, eid=eid, field=field) """*dict* Identification of this field value: table, id of record, name of field. !!! hint To be used to pass to buttons in widgets for this field. """ def getActualMinimum(self): tp = self.tp if tp == N.permissionGroup: context = self.context db = context.db auth = context.auth user = auth.user userGroup = G(user, N.group) minimum = G(G(db.permissionGroup, userGroup), N.rep) value = self.value actual = G(G(db.permissionGroup, value), N.rep) return dict(actual=actual, minimum=minimum) return {} def save(self, data): """Save a new value for this field to MongoDb. Before saving, permissions and workflow conditions will be checked. After saving, workflow information will be adjusted. !!! caution If the `editors` field of an assessment or review is modified, the same value should be put in the detail records, the `criteriaEntry` and `reviewEntry` records respectively. Parameters ---------- data: mixed The new value(s) for this field. It comes straight from the client, and will be converted to proper Python/MongoDb values """ mayEdit = self.mayEdit if not mayEdit: return False context = self.context db = context.db uid = self.uid eppn = self.eppn table = self.table eid = self.eid field = self.field extensible = self.extensible record = self.record recordObj = self.recordObj require = self.require constrain = None constrainField = G(CONSTRAINED, field) if constrainField: constrainValue = G(record, constrainField) if constrainValue: constrain = (constrainField, constrainValue) multiple = self.multiple fieldTypeObj = self.fieldTypeObj conversion = fieldTypeObj.fromStr if fieldTypeObj else None kwargs = dict(uid=uid, eppn=eppn, extensible=extensible) if extensible else {} if constrain: kwargs[N.constrain] = constrain if conversion is not None: try: if multiple: data = [conversion(d, **kwargs) for d in data or []] else: data = conversion(data, **kwargs) except ConversionError: return False modified = G(record, N.modified) nowFields = [] if data is not None: withNow = self.withNow if withNow: withNowField = ( withNow if type(withNow) is str else withNow[0] if data else withNow[1] ) nowFields.append(withNowField) result = db.updateField( table, eid, field, data, eppn, modified, nowFields=nowFields, ) if not result: return False (updates, deletions) = result record = context.getItem(table, eid, requireFresh=True) recordObj.reload(record) self.value = G(record, field) self.perm = recordObj.perm perm = self.perm (self.mayRead, self.mayEdit) = getPermField( table, perm, require, **self.getActualMinimum() ) good = True if field == N.editors and table in CASCADE_SPECS: for dtable in CASCADE_SPECS[table]: drecords = db.getDetails(dtable, table, eid) for drecord in drecords: deid = G(drecord, N._id) result = db.updateField( dtable, deid, N.editors, data, eppn, modified, nowFields=nowFields, ) if not result: good = False if table in WORKFLOW_TABLES and field in WORKFLOW_FIELDS: recordObj.adjustWorkflow() return good def isEmpty(self): """Whether the value(s) is/are empty. A single value is empty if it is `None`. A mutliple value is empty if it is `None` or `[]`. Returns ------- boolean """ value = self.value multiple = self.multiple return value is None or multiple and value == [] def isBlank(self): """Whether the value(s) is/are blank. A single value is empty if it is `None` or `''`. A mutliple value is empty if it is `None` or `[]`, or all its component values are blank. Returns ------- boolean """ value = self.value multiple = self.multiple return ( value is None or value == E or multiple and (value == [] or all(v is None or v == E for v in value)) ) def wrapBare(self, markup=True): """Produce the bare field value. This is the result of calling the `control.typ.base.TypeBase.toDisplay` method on the derived type class that matches the type of the field. Returns ------- string(html) """ context = self.context types = context.types tp = self.tp value = self.value multiple = self.multiple fieldTypeObj = getattr(types, tp, None) method = fieldTypeObj.toDisplay return ( ( [method(val, markup=markup) for val in (value or [])] if multiple else method(value, markup=markup) ) if markup is None else ( BLANK.join(method(val, markup=markup) for val in (value or [])) if multiple else method(value, markup=markup) ) ) def wrap(self, action=None, asEdit=False, empty=False, withLabel=True, cls=E): """Wrap the field into HTML. If there is an `action`, data from the request is picked up and `Field.save` is called to save that data to the MongoDB. Depending on the `action`, the field is then rendered as follows: action | effect --- | --- `save` | no rendering `view`| a read only rendering `edit` | an edit widget Whether a field is presented as an editable field depends on a number of factors: factor | story --- | --- is master | a field pointing to a master will not be edited attribute `mayEdit` | does the current user have permission to edit the field? action `edit` or parameter `asEdit` | do we want to present an editable widget? Parameters ---------- action: {save, edit, view}, optional `None` If present, data will be saved to the database first. asEdit: boolean, optional `False` No data will be saved. The field will rendered editable, if permitted. empty: boolean, optional `False` Only relevant for readonly views: if the value is empty, just present the empty string and nothing else. withLabel: boolean, optional `True` Whether to precede the value with a field label. The label is specified in the field specs which are in the table's .yaml file in `control/tables`. cls: string, optional `''` A CSS class to append to the outer `<div>` of the result. Returns ------- string(html) If there was an `action`, the bare representation of the field value is returned. Otherwise, and if `withLabel`, a label is added. """ mayRead = self.mayRead if mayRead is False: return E asMaster = self.asMaster mayEdit = self.mayEdit if action is not None and not asMaster: contentLength = request.content_length if contentLength is not None and contentLength > LIMIT_JSON: abort(400) data = request.get_json() if data is not None and N.save in data: if mayEdit: good = self.save(data[N.save]) else: good = False if not good: abort(400) if action == N.save: return E editable = mayEdit and (action == N.edit or asEdit) and not asMaster widget = self.wrapWidget(editable, cls=cls) if action is not None: return H.join(widget) if empty and self.isEmpty(): return E label = self.label editClass = " edit" if editable else E return ( H.div( [ H.div(f"""{label}:""", cls="record-label"), H.div(widget, cls=f"record-value{editClass}"), ], cls="record-row", ) if withLabel else H.div(widget, cls=f"record-value{editClass}") ) 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 wrapValue(self, editable, cls=E): """Wraps the value of a field. !!! hint "field=value in comments" In the result we include a string ``` html <!-- title=My contribution --> ``` but then with the field name instead of `title` and the unmarked-up value instead of `My contribution`. This makes it easier for the test suite to spot the fields and their values. Parameters ---------- editable: boolean Whether the field should be presented in editable form cls See `Field.wrap()` Returns ------- string(html) """ context = self.context types = context.types fieldTypeObj = self.fieldTypeObj field = self.field value = self.value tp = self.tp multiple = self.multiple extensible = self.extensible widgetType = self.widgetType baseCls = "tags" if widgetType == N.related else "values" isSelectWidget = widgetType == N.related args = [] if isSelectWidget and editable: record = self.record constrain = None constrainField = G(CONSTRAINED, field) if constrainField: constrainValue = G(record, constrainField) if constrainValue: constrain = (constrainField, constrainValue) args.append(multiple) args.append(extensible) args.append(constrain) atts = dict(wtype=widgetType) if editable: typeObj = getattr(types, tp, None) method = typeObj.toOrig origStr = [method(v) for v in value or []] if multiple else method(value) atts[N.orig] = bencode(origStr) if multiple: atts[N.multiple] = ONE if extensible: atts[N.extensible] = ONE method = fieldTypeObj.widget if editable else fieldTypeObj.toDisplay extraCls = E if editable else cls valueBare = ( (COMMA.join(val for val in value or []) if multiple else value) if tp == N.markdown else ( COMMA.join( fieldTypeObj.toDisplay(val, markup=False) for val in value or [] ) if multiple else fieldTypeObj.toDisplay(value, markup=False) ) ) return f"<!-- {field}={valueBare} -->" + ( H.div( [ method(val, *args) for val in (value or []) + ([E] if editable else []) ], **atts, cls=baseCls, ) if multiple and not (editable and isSelectWidget) else H.div(method(value, *args), **atts, cls=f"value {extraCls}") )
Class variables
var inheritProps
Instance variables
var asMaster
-
boolean Whether this field points to a master record.
var atts
-
dict Identification of this field value: table, id of record, name of field.
Hint
To be used to pass to buttons in widgets for this field.
var extensible
-
boolean Whether the user may add new values to the value table of this field.
var field
-
string The name of the field
var fieldTypeObj
-
object The type object by which the value of this field can be interpreted.
var label
-
string A label to display in front of the field.
var multiple
-
boolean Whether the field value consists of multiple values or a single one.
var recordObj
-
object A
Record
object (or one of a derived class) var require
-
dict The required permissions for this field.
Keys are
read
andedit
, the values areTrue
orFalse
. var tp
-
string The data type of the field.
var value
-
mixed The value of the field.
var widgetType
-
string The type of widget for presenting an edit view on this field.
var withNow
-
dict Which field updates need a timestamp?
The info comes from tables.yaml, under key
withNow
. It is keyed by table name, then by field name, and the value is a single field or lists of two fields with the names of corresponding timestamp fields.When a (boolean) field has two timestamp fields, the first one is used if he value is a list, the first one will be used if the modification sets the field to
True
and the second one when the field becomesFalse
.Hint
In assessments, when
submitted
becomesTrue
,dateSubmitted
receives a timestamp. Whensubmitted
becomesFalse
, it isdateWithdrawn
that receives the timestamp. var withRefresh
-
boolean Whether the field needs a refresh button.
Methods
def getActualMinimum(self)
-
Expand source code
def getActualMinimum(self): tp = self.tp if tp == N.permissionGroup: context = self.context db = context.db auth = context.auth user = auth.user userGroup = G(user, N.group) minimum = G(G(db.permissionGroup, userGroup), N.rep) value = self.value actual = G(G(db.permissionGroup, value), N.rep) return dict(actual=actual, minimum=minimum) return {}
def isBlank(self)
-
Whether the value(s) is/are blank.
A single value is empty if it is
None
or''
.A mutliple value is empty if it is
None
or[]
, or all its component values are blank.Returns
boolean
Expand source code
def isBlank(self): """Whether the value(s) is/are blank. A single value is empty if it is `None` or `''`. A mutliple value is empty if it is `None` or `[]`, or all its component values are blank. Returns ------- boolean """ value = self.value multiple = self.multiple return ( value is None or value == E or multiple and (value == [] or all(v is None or v == E for v in value)) )
def isEmpty(self)
-
Whether the value(s) is/are empty.
A single value is empty if it is
None
.A mutliple value is empty if it is
None
or[]
.Returns
boolean
Expand source code
def isEmpty(self): """Whether the value(s) is/are empty. A single value is empty if it is `None`. A mutliple value is empty if it is `None` or `[]`. Returns ------- boolean """ value = self.value multiple = self.multiple return value is None or multiple and value == []
def save(self, data)
-
Save a new value for this field to MongoDb.
Before saving, permissions and workflow conditions will be checked.
After saving, workflow information will be adjusted.
Caution
If the
editors
field of an assessment or review is modified, the same value should be put in the detail records, thecriteriaEntry
andreviewEntry
records respectively.Parameters
data
:mixed
- The new value(s) for this field. It comes straight from the client, and will be converted to proper Python/MongoDb values
Expand source code
def save(self, data): """Save a new value for this field to MongoDb. Before saving, permissions and workflow conditions will be checked. After saving, workflow information will be adjusted. !!! caution If the `editors` field of an assessment or review is modified, the same value should be put in the detail records, the `criteriaEntry` and `reviewEntry` records respectively. Parameters ---------- data: mixed The new value(s) for this field. It comes straight from the client, and will be converted to proper Python/MongoDb values """ mayEdit = self.mayEdit if not mayEdit: return False context = self.context db = context.db uid = self.uid eppn = self.eppn table = self.table eid = self.eid field = self.field extensible = self.extensible record = self.record recordObj = self.recordObj require = self.require constrain = None constrainField = G(CONSTRAINED, field) if constrainField: constrainValue = G(record, constrainField) if constrainValue: constrain = (constrainField, constrainValue) multiple = self.multiple fieldTypeObj = self.fieldTypeObj conversion = fieldTypeObj.fromStr if fieldTypeObj else None kwargs = dict(uid=uid, eppn=eppn, extensible=extensible) if extensible else {} if constrain: kwargs[N.constrain] = constrain if conversion is not None: try: if multiple: data = [conversion(d, **kwargs) for d in data or []] else: data = conversion(data, **kwargs) except ConversionError: return False modified = G(record, N.modified) nowFields = [] if data is not None: withNow = self.withNow if withNow: withNowField = ( withNow if type(withNow) is str else withNow[0] if data else withNow[1] ) nowFields.append(withNowField) result = db.updateField( table, eid, field, data, eppn, modified, nowFields=nowFields, ) if not result: return False (updates, deletions) = result record = context.getItem(table, eid, requireFresh=True) recordObj.reload(record) self.value = G(record, field) self.perm = recordObj.perm perm = self.perm (self.mayRead, self.mayEdit) = getPermField( table, perm, require, **self.getActualMinimum() ) good = True if field == N.editors and table in CASCADE_SPECS: for dtable in CASCADE_SPECS[table]: drecords = db.getDetails(dtable, table, eid) for drecord in drecords: deid = G(drecord, N._id) result = db.updateField( dtable, deid, N.editors, data, eppn, modified, nowFields=nowFields, ) if not result: good = False if table in WORKFLOW_TABLES and field in WORKFLOW_FIELDS: recordObj.adjustWorkflow() return good
def wrap(self, action=None, asEdit=False, empty=False, withLabel=True, cls='')
-
Wrap the field into HTML.
If there is an
action
, data from the request is picked up andField.save()
is called to save that data to the MongoDB. Depending on theaction
, the field is then rendered as follows:action effect save
no rendering view
a read only rendering edit
an edit widget Whether a field is presented as an editable field depends on a number of factors:
factor story is master a field pointing to a master will not be edited attribute mayEdit
does the current user have permission to edit the field? action edit
or parameterasEdit
do we want to present an editable widget? Parameters
action
:{save, edit, view}
, optionalNone
- If present, data will be saved to the database first.
asEdit
:boolean
, optionalFalse
- No data will be saved. The field will rendered editable, if permitted.
empty
:boolean
, optionalFalse
- Only relevant for readonly views: if the value is empty, just present the empty string and nothing else.
withLabel
:boolean
, optionalTrue
- Whether to precede the value with a field label.
The label is specified in the field specs which are in the table's .yaml
file in
control/tables
. cls
:string
, optional''
- A CSS class to append to the outer
<div>
of the result.
Returns
string(html)
- If there was an
action
, the bare representation of the field value is returned. Otherwise, and ifwithLabel
, a label is added.
Expand source code
def wrap(self, action=None, asEdit=False, empty=False, withLabel=True, cls=E): """Wrap the field into HTML. If there is an `action`, data from the request is picked up and `Field.save` is called to save that data to the MongoDB. Depending on the `action`, the field is then rendered as follows: action | effect --- | --- `save` | no rendering `view`| a read only rendering `edit` | an edit widget Whether a field is presented as an editable field depends on a number of factors: factor | story --- | --- is master | a field pointing to a master will not be edited attribute `mayEdit` | does the current user have permission to edit the field? action `edit` or parameter `asEdit` | do we want to present an editable widget? Parameters ---------- action: {save, edit, view}, optional `None` If present, data will be saved to the database first. asEdit: boolean, optional `False` No data will be saved. The field will rendered editable, if permitted. empty: boolean, optional `False` Only relevant for readonly views: if the value is empty, just present the empty string and nothing else. withLabel: boolean, optional `True` Whether to precede the value with a field label. The label is specified in the field specs which are in the table's .yaml file in `control/tables`. cls: string, optional `''` A CSS class to append to the outer `<div>` of the result. Returns ------- string(html) If there was an `action`, the bare representation of the field value is returned. Otherwise, and if `withLabel`, a label is added. """ mayRead = self.mayRead if mayRead is False: return E asMaster = self.asMaster mayEdit = self.mayEdit if action is not None and not asMaster: contentLength = request.content_length if contentLength is not None and contentLength > LIMIT_JSON: abort(400) data = request.get_json() if data is not None and N.save in data: if mayEdit: good = self.save(data[N.save]) else: good = False if not good: abort(400) if action == N.save: return E editable = mayEdit and (action == N.edit or asEdit) and not asMaster widget = self.wrapWidget(editable, cls=cls) if action is not None: return H.join(widget) if empty and self.isEmpty(): return E label = self.label editClass = " edit" if editable else E return ( H.div( [ H.div(f"""{label}:""", cls="record-label"), H.div(widget, cls=f"record-value{editClass}"), ], cls="record-row", ) if withLabel else H.div(widget, cls=f"record-value{editClass}") )
def wrapBare(self, markup=True)
-
Produce the bare field value.
This is the result of calling the
TypeBase.toDisplay()
method on the derived type class that matches the type of the field.Returns
string(html)
Expand source code
def wrapBare(self, markup=True): """Produce the bare field value. This is the result of calling the `control.typ.base.TypeBase.toDisplay` method on the derived type class that matches the type of the field. Returns ------- string(html) """ context = self.context types = context.types tp = self.tp value = self.value multiple = self.multiple fieldTypeObj = getattr(types, tp, None) method = fieldTypeObj.toDisplay return ( ( [method(val, markup=markup) for val in (value or [])] if multiple else method(value, markup=markup) ) if markup is None else ( BLANK.join(method(val, markup=markup) for val in (value or [])) if multiple else method(value, markup=markup) ) )
def wrapValue(self, editable, cls='')
-
Wraps the value of a field.
field=value in comments
In the result we include a string
html <!-- title=My contribution -->
but then with the field name instead of
title
and the unmarked-up value instead ofMy contribution
. This makes it easier for the test suite to spot the fields and their values.Parameters
editable
:boolean
- Whether the field should be presented in editable form
cls
- See
Field.wrap()
Returns
string(html)
Expand source code
def wrapValue(self, editable, cls=E): """Wraps the value of a field. !!! hint "field=value in comments" In the result we include a string ``` html <!-- title=My contribution --> ``` but then with the field name instead of `title` and the unmarked-up value instead of `My contribution`. This makes it easier for the test suite to spot the fields and their values. Parameters ---------- editable: boolean Whether the field should be presented in editable form cls See `Field.wrap()` Returns ------- string(html) """ context = self.context types = context.types fieldTypeObj = self.fieldTypeObj field = self.field value = self.value tp = self.tp multiple = self.multiple extensible = self.extensible widgetType = self.widgetType baseCls = "tags" if widgetType == N.related else "values" isSelectWidget = widgetType == N.related args = [] if isSelectWidget and editable: record = self.record constrain = None constrainField = G(CONSTRAINED, field) if constrainField: constrainValue = G(record, constrainField) if constrainValue: constrain = (constrainField, constrainValue) args.append(multiple) args.append(extensible) args.append(constrain) atts = dict(wtype=widgetType) if editable: typeObj = getattr(types, tp, None) method = typeObj.toOrig origStr = [method(v) for v in value or []] if multiple else method(value) atts[N.orig] = bencode(origStr) if multiple: atts[N.multiple] = ONE if extensible: atts[N.extensible] = ONE method = fieldTypeObj.widget if editable else fieldTypeObj.toDisplay extraCls = E if editable else cls valueBare = ( (COMMA.join(val for val in value or []) if multiple else value) if tp == N.markdown else ( COMMA.join( fieldTypeObj.toDisplay(val, markup=False) for val in value or [] ) if multiple else fieldTypeObj.toDisplay(value, markup=False) ) ) return f"<!-- {field}={valueBare} -->" + ( H.div( [ method(val, *args) for val in (value or []) + ([E] if editable else []) ], **atts, cls=baseCls, ) if multiple and not (editable and isSelectWidget) else H.div(method(value, *args), **atts, cls=f"value {extraCls}") )
def wrapWidget(self, editable, cls='')
-
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.
Expand source code
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))