OArepo FSM library for record state transitions


Keywords
fsm, fsm-library, oarepo, records, state-management
License
MIT
Install
pip install oarepo-fsm==1.4.3

Documentation

oarepo-fsm

OArepo FSM library for record state transitions built on top of the https://pypi.org/project/sqlalchemy-fsm/ library.

Quickstart

Run the following commands to bootstrap your environment

git clone https://github.com/oarepo/oarepo-fsm
cd oarepo-fsm
pip install -e .[devel]

Configuration

Check that correct record_class is being used on the RECORDS_REST_ENDPOINT's item_route

item_route='/records/<pid(recid,record_class="yourapp.models:RecordModelFSM"):pid_value>',

To automatically add a link to the FSM endpoint to your record links, use the following links_factory_imp in your RECORDS_REST_ENDPOINTS config

links_factory_imp='oarepo_fsm.links:record_fsm_links_factory',

If you wish to activate FSM on a certain Record enpoints only, put in your config

OAREPO_FSM_ENABLED_REST_ENDPOINTS = ['recid']

Where recid is the prefix key into your RECORDS_REST_ENDPOINTS configuration. This library activates FSM on all endpoints using record_class inherited from FSMMixin otherwise.

Usage

In order to use this library, you need to define a Record model in your app, that inherits from a FSMMixin column

from invenio_records import Record
from oarepo_fsm.mixins import FSMMixin

class RecordModelFSM(FSMMixin, Record):
...

To define FSM transitions on this class, create methods decorated with @transition(..) e.g.

@transition(
    src=['open', 'archived'],
    dest='published',
    required=['id'],
    permissions=[editor_permission],
    commit_record=True)
def publish(self, **kwargs):
    print('record published')

Where decorator parameters mean:

  • src: record must be in one of the source states before transition could happen
  • dest: target state of the transition
  • required: a list of required **kwargs that must be passed to the @transition decorated function
  • permissions: currently logged user must have at least one of the permissions to execute the transition
  • commit_record: should the changes made in a record be commited after the function returns?

A transition-decorated function can optionally return a custom flask Response or a JSON-serializable dict to be provided to user in a JSON response.

"state" field name

To change state field name, set

class MyRecord(FSMMixin, ...):
    STATE_FIELD = 'administrative_props.state'

Note that the field name might be nested. If the container element does not exist, it will be created on the first transition.

Note: if you use your own state field, do not use marshamllow mixin, json schema or included mappings. Write/use your own.

REST API Usage

To get current record state and possible transitions (only transitions that you have permission to invoke will be returned)

GET <record_rest_item_endpoint>
>>>
{
    metadata: {
        state: <current state of the record>
        ... other record metadata
    }
    links: {
        self: ...,
        "transitions": {
            <fsm_transition1_name>: <transition_url>,
            <fsm_transition2_name>: <transition_url>,
        },
        ...
    }
}

To invoke a specific transition transition, do

POST <record_rest_endpoint>/<fsm_transition_name>

Further documentation is available on https://oarepo-fsm.readthedocs.io/

Permission factories

Sometimes access to records should be governed by the state of the record. For example, if the record is in state=editing, any editor can make changes. If it is state=approving, only the curator can modify the record.

On REST level, modification permissions are governed by permission factories

from invenio_records_rest.utils import allow_all, deny_all
RECORDS_REST_ENDPOINTS = dict(
    recid=dict(
       create_permission_factory_imp=deny_all,
       delete_permission_factory_imp=deny_all,
       update_permission_factory_imp=deny_all,
       read_permission_factory_imp=allow_all,
   )
)

This library provides the following factories and helpers:

  • transition_required(*transitions) allows user if he is entitled to perform any of the transitions ( method names) on the current record

  • states_required(*states, state_field="state" allows anyone if the record is in any of the states mentioned

  • require_all(*perms_or_factories) allows user only if all permissions allow. Use it with states_required as follows

    require_all(
        states_required('editing'),
        editing_user_permission_factory
    )
    

    where editing_user_permission_factory is a permission factory allowing only editing users.

  • require_any(*perms_or_factories) allows user if any of the permissions allow. Example

    require_any(
        require_all(
            states_required('editing'),
            editing_user_permission_factory
        ),
        require_all(
            states_required('editing', 'approving),
            curator_user_permission_factory
        ),
    )