Skip to content
Snippets Groups Projects
Commit fb366ac1 authored by Henrik tom Wörden's avatar Henrik tom Wörden
Browse files

DOC: add documentation on state machines

parent 910e031c
No related branches found
No related tags found
2 merge requests!58REL: prepare release 0.7.2,!49DOC: add documentation on state machines
Pipeline #17468 passed
...@@ -10,6 +10,7 @@ Welcome to caosdb-server's documentation! ...@@ -10,6 +10,7 @@ Welcome to caosdb-server's documentation!
Getting started <README_SETUP> Getting started <README_SETUP>
Concepts <concepts> Concepts <concepts>
tutorials
Query Language <CaosDB-Query-Language> Query Language <CaosDB-Query-Language>
administration administration
Development <development/devel> Development <development/devel>
......
Tutorials
==============
.. toctree::
:maxdepth: 1
:glob:
tutorials/*
#!/usr/bin/env python3
# encoding: utf-8
#
# This file is a part of the CaosDB Project.
#
# Copyright (C) 2021 Indiscale GmbH <info@indiscale.com>
# Copyright (C) 2021 Henrik tom Wörden <h.tomwoerden@indiscale.com>
# Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#
"""
This is a utility script to setup a publication process in LinkAhead using
states.
If you start from scratch you should perform the following actions in that
order:
1. setup_roles
2. setup_state_data_model
4. setup_model_publication_cycle
"""
from argparse import ArgumentParser, RawDescriptionHelpFormatter
import caosdb as db
from caosdb.common.administration import generate_password
def teardown(args):
"""fully clears the database"""
if "yes" != input(
"Are you really sure that you want to delete ALL "
"ENTITIES in LinkAhead? [yes/No]"
):
print("Nothing done.")
return
d = db.execute_query("FIND ENTITY WITH ID > 99")
if len(d) > 0:
d.delete(flags={"forceFinalState": "true"})
def soft_teardown(args):
""" allows to remove state data only """
recs = db.execute_query("FIND Entity WITH State")
for rec in recs:
rec.state = None
recs.update(flags={"forceFinalState": "true"})
db.execute_query("FIND StateModel").delete()
db.execute_query("FIND Transition").delete()
db.execute_query("FIND State").delete()
db.execute_query(
"FIND Property WITH name=from or name=to or name=initial or name=final or name=color").delete()
def setup_user(args):
"""Creates a user with given username and adds the given role.
If the user exists, it is deleted first. A random password is generated
and printed in clear text in the console output.
"""
username, role = args.username, args.role
try:
db.administration._delete_user(name=username)
except Exception:
pass
password = generate_password(10)
print("new password for {}:\n{}".format(username, password))
db.administration._insert_user(
name=username, password=password, status="ACTIVE")
db.administration._set_roles(username=username, roles=[role])
def remove_user(args):
"""deletes the given user"""
db.administration._delete_user(name=args.username)
def setup_role_permissions():
"""
Adds the appropriate permissions to the 'normal' and 'publisher' role.
The permissions are such that they suit the publication life cycle.
"""
db.administration._set_permissions(
role="normal",
permission_rules=[
db.administration.PermissionRule("Grant", "TRANSACTION:*"),
db.administration.PermissionRule(
"Grant", "ACM:USER:UPDATE_PASSWORD:?REALM?:?USERNAME?"
),
db.administration.PermissionRule("Grant", "STATE:TRANSITION:Edit"),
db.administration.PermissionRule("Grant", "UPDATE:PROPERTY:ADD"),
db.administration.PermissionRule(
"Grant", "UPDATE:PROPERTY:REMOVE"),
db.administration.PermissionRule(
"Grant", "STATE:TRANSITION:Start Review"),
db.administration.PermissionRule(
"Grant", "STATE:ASSIGN:Publish Life-cycle"
),
],
)
db.administration._set_permissions(
role="publisher",
permission_rules=[
db.administration.PermissionRule(
"Grant", "ACM:USER:UPDATE_PASSWORD:?REALM?:?USERNAME?"
),
db.administration.PermissionRule("Grant", "TRANSACTION:*"),
db.administration.PermissionRule("Grant", "UPDATE:PROPERTY:ADD"),
db.administration.PermissionRule(
"Grant", "UPDATE:PROPERTY:REMOVE"),
db.administration.PermissionRule("Grant", "STATE:*"),
],
)
def setup_roles(args):
"""Creates 'publisher' and 'normla' roles and assigns appropriate
permissions
If those roles exist they are deleted first.
"""
for role in ["publisher", "normal"]:
try:
db.administration._delete_role(name=role)
except Exception:
print("Could not delete role {}".format(role))
for role in ["publisher", "normal"]:
db.administration._insert_role(name=role, description="")
setup_role_permissions()
def setup_state_data_model(args):
"""Creates the data model for using states
RecordTypes: State, StateModel, Transition
Properties: from, to, initial, final, color
"""
cont = db.Container().extend(
[
db.RecordType("State"),
db.RecordType("StateModel"),
db.RecordType("Transition"),
db.Property(name="from", datatype="State"),
db.Property(name="to", datatype="State"),
db.Property(name="initial", datatype="State"),
db.Property(name="final", datatype="State"),
db.Property(name="color", datatype=db.TEXT),
]
)
cont.insert()
def setup_model_publication_cycle(args):
"""Creates States and Transitions for the Publication Life Cycle"""
unpublished_acl = db.ACL()
unpublished_acl.grant(role="publisher", permission="*")
unpublished_acl.grant(role="normal", permission="UPDATE:*")
unpublished_acl.grant(role="normal", permission="RETRIEVE:ENTITY")
unpublished_acl = db.State.create_state_acl(unpublished_acl)
unpublished_state = (
db.Record(
"Unpublished",
description="Unpublished entries are only visible to the team "
"and may be edited by any team member.",
)
.add_parent("State")
.add_property("color", "#5bc0de")
)
unpublished_state.acl = unpublished_acl
unpublished_state.insert()
review_acl = db.ACL()
review_acl.grant(role="publisher", permission="*")
review_acl.grant(role="normal", permission="RETRIEVE:ENTITY")
review_state = (
db.Record(
"Under Review",
description="Entries under review are not publicly available yet, "
"but they can only be edited by the members of the publisher "
"group.",
)
.add_parent("State")
.add_property("color", "#FFCC33")
)
review_state.acl = db.State.create_state_acl(review_acl)
review_state.insert()
published_acl = db.ACL()
published_acl.grant(role="guest", permission="RETRIEVE:ENTITY")
published_state = (
db.Record(
"Published",
description="Published entries are publicly available and "
"cannot be edited unless they are unpublished again.",
)
.add_parent("State")
.add_property("color", "#333333")
)
published_state.acl = db.State.create_state_acl(published_acl)
published_state.insert()
# 1->2
(
db.Record(
"Start Review",
description="This transitions denies the permissions to edit an "
"entry for anyone but the members of the publisher group. "
"However, the entry is not yet publicly available.",
)
.add_parent("Transition")
.add_property("from", "unpublished")
.add_property("to", "under review")
.add_property("color", "#FFCC33")
.insert()
)
# 2->3
(
db.Record(
"Publish",
description="Published entries are visible for the public and "
"cannot be changed unless they are unpublished again. Only members"
" of the publisher group can publish or unpublish entries.",
)
.add_parent("Transition")
.add_property("from", "under review")
.add_property("to", "published")
.add_property("color", "red")
.insert()
)
# 3->1
(
db.Record(
"Unpublish",
description="Unpublish this entry to hide it from "
"the public. Unpublished entries can be edited by any team "
"member.",
)
.add_parent("Transition")
.add_property("from", "published")
.add_property("to", "unpublished")
.insert()
)
# 2->1
(
db.Record(
"Reject",
description="Reject the publishing of this entity. Afterwards, "
"the entity is editable for any team member again.",
)
.add_parent("Transition")
.add_property("from", "under review")
.add_property("to", "unpublished")
.insert()
)
# 1->1
(
db.Record(
"Edit",
description="Edit this entity. The changes are not publicly "
"available until this entity will have been reviewed and "
"published.",
)
.add_parent(
"Transition",
)
.add_property("from", "unpublished")
.add_property("to", "unpublished")
.insert()
)
(
db.Record(
"Publish Life-cycle",
description="The publish life-cycle is a quality assurance tool. "
"Database entries can be edited without being publicly available "
"until the changes have been reviewed and explicitely published by"
" an eligible user.",
)
.add_parent("StateModel")
.add_property(
"Transition",
datatype=db.LIST("Transition"),
value=[
"Edit",
"Start Review",
"Reject",
"Publish",
"Unpublish",
],
)
.add_property("initial", "Unpublished")
.add_property("final", "Unpublished")
.insert()
)
def parse_args():
parser = ArgumentParser(
description=__doc__, formatter_class=RawDescriptionHelpFormatter
)
subparsers = parser.add_subparsers(
title="action",
metavar="ACTION",
description=(
"You can perform the following actions. "
"Print the detailed help for each command with "
"#> setup_state_model ACTION -h"
),
)
subparser = subparsers.add_parser(
"setup_state_data_model", help=setup_state_data_model.__doc__
)
subparser.set_defaults(call=setup_state_data_model)
subparser = subparsers.add_parser(
"setup_model_publication_cycle", help=setup_model_publication_cycle.__doc__
)
subparser.set_defaults(call=setup_model_publication_cycle)
subparser = subparsers.add_parser("setup_roles", help=setup_roles.__doc__)
subparser.set_defaults(call=setup_roles)
subparser = subparsers.add_parser("remove_user", help=remove_user.__doc__)
subparser.set_defaults(call=remove_user)
subparser.add_argument("username")
subparser = subparsers.add_parser("setup_user", help=setup_user.__doc__)
subparser.set_defaults(call=setup_user)
subparser.add_argument("username")
subparser.add_argument("role")
subparser = subparsers.add_parser(
"teardown", help="Removes ALL ENTITIES from LinkAhead!"
)
subparser.set_defaults(call=teardown)
subparser = subparsers.add_parser(
"soft_teardown", help=soft_teardown.__doc__
)
subparser.set_defaults(call=soft_teardown)
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
args.call(args)
State Machine
=============
Prerequisites
-------------
In order to use the state machine functionality you have to set the
corresponding server setting: ``EXT_ENTITY_STATE=ENABLED``.
Also, a few RecordTypes and Properties are required. You can use the
script `setup_state_model.py <https://gitlab.com/caosdb/caosdb-server/-/blob/dev/src/doc/tutorials/setup_state_model.py>`_
to create those or you may have a look at it to see what is needed (``setup_state_data_model`` function).
Defining the State Machine
--------------------------
Now you are setup to create your own state machine. You can define States and Transitions
and bundle it all to a StateModel. The above mentioned ``setup_state_model.py`` script defines
a publication cycle with the state "Unpublished", "UnderReview" and "Published".
Again, the ``setup_state_model.py`` script provides orientation on how this
can be setup (``setup_model_publication_cycle`` function).
Note, that you can provide ACL to the state definition which will be applied to an entity once
the state is reached. This is for example useful to change the visibility depending on a state change.
If you assign a state to a RecordType, this state will be the initial state
of Records that have that parent. For example by executing:
.. code-block:: Python
rt = db.RecordType("Article").retrieve()
rt.state = db.State(name="UnPublished", model="Publish Life-cycle")``
rt.update()
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment