= Record Approval = [[TOC]] The S3 '''Record Approval Framework''' hides records which have not yet been reviewed and approved by an authorized user. Status: under construction == Unapproved Records == Whether a record is approved or not is encoded as {{{approved_by}}} field in the record. The field is either None (unapproved record) or holds the user ID of the approving user (approved record). For tables which require approval, this field is mandatory (field type "integer" or "reference auth_user"). The default value for this field is None, i.e. all new records are unapproved by default. This field should not be editable, i.e. writable=False. == Workflow == The intended workflow for approval is: - user creates a new record => record is unapproved by default (i.e. approved_by=None) - unapproved records are treated as invalid and excluded from any kind of workflow (i.e. invisible) except for review/approval - user with approval role reviews the record and either approves or rejects it - approve sets the approved_by field of the record to the approving user's ID - reject deletes the record and all its dependencies - approved records are now available for all workflows, except for review/approval Developer notes: - ''it should not be possible to import pre-approved records, otherwise this would allow unauthorized users to circumvent the approval framework'' - ''unapproved records should be disregarded in Sync, because otherwise this introduces the same hole as above: the user could approve the record in a remote instance where he is Admin and synchronize it back. Apart from that, circulation of unapproved records turns the purpose of approval somewhat upside down.'' == Configuration == 1) The Record Approval Framework can be turned '''on/off globally''' by a deployment setting: {{{#!python settings.auth.record_approval = True }}} Default is False. 2) Whether record approval is required for a table can be configured '''per table''' like: {{{#!python s3db.configure(tablename, requires_approval=True) }}} Default is False. For a '''deployment-specific''' approval configuration, you can override all table-specific settings with: {{{#!python settings.auth.record_approval_required_for = [tablename, tablename] }}} Record approval will then only be applied to those tables in the list. If you set this to {{{None}}}, it will fall back to the table-specific settings. 3) In addition to activating record approval for a table, you will also need to give user roles permission to review/approve/reject records in this table. These permissions are encoded as auth.permission.REVIEW (permission to access unapproved records) and auth.permission.APPROVE (permission to approve or reject unapproved records). {{{#!python acl = current.auth.permission acl.update_acl(ROLE_XY, t="some_table", uacl=acl.READ|acl.CREATE|acl.REVIEW|acl.APPROVE, oacl=acl.READ|acl.UPDATE|acl.REVIEW|acl.APPROVE) }}} '''Note:''' that users can have review permission (=permission to ''see'' unapproved records) without need to also have approve/reject permission. '''Note:''' REVIEW permission alone doesn't give any access to unapproved records: it merely extends the READ/UPDATE permissions so they also apply for unapproved records. That means, if the user besides REVIEW has only READ permission in this table, then they can ''read'' unapproved records but not update them. To do that, they would need REVIEW+UPDATE permission. The permission to review/approve/reject records of this role is realm-limited (see [wiki:S3AAA/OrgAuth] for more details). '''Note:''' users can always only see approved records, unless they have review permissions and use the "review" method. For custom layouts, it is possible to implement custom views which include both groups of records by using the S3Resource API directly. == Methods to Approve or Reject Records == S3Resource implements two low-level methods for record approval: {{{#!python resource.approve() }}} ...to approve all records in a resource (and all its components), and {{{#!python resource.reject() }}} ...to reject (=delete) all unapproved records in a resource. '''Note:''' that S3Resource instances always exclude unapproved records, so these methods will always fail unless you define the resource with the unapproved=True parameter: {{{#!python resource = current.s3db.resource("my_table", 1, unapproved=True) }}} '''NOTE:''' resource.reject() is much more radical about record deletion than delete() - it will try to bypass any RESTRICTs and rigorously delete any dependency of the rejected records so they can not leave any undesired legacy behind. However, that means that reject() must always be properly authorized and strictly limited to unapproved records in tables which require approval. S3CRUD implements the '''review''' method to review/approve/reject records in a resource: {{{ /controller/function/review }}} ...to see all unapproved records in a resource, or: {{{ /controller/function/XY/review }}} ...to review/approve/reject a particular record. '''Note:''' For security reasons, both ''approve'' and ''reject'' via CRUD are only possible as POST requests which include the respective form keys from the ''review'' of a record. == Callbacks == For both, resource.approve() and resource.reject(), there are hooks which will be called for each record that gets approved/rejected: {{{#!python s3db.configure(tablename, onapprove=function) s3db.configure(tablename, onreject=function) }}} The hook function must take {{{(row)}}} as parameter, and doesn't return anything. == Imports == Imported data is assumed to be approved by the default approver. If you wish to import unapproved data then you need to set the attribute {{{@approved=false}}}