diff --git a/src/doc/index.rst b/src/doc/index.rst index 4f63fe07f37a4432a442040ff2f614f01c959472..b5ce9f3235277b613b357dca5dfc3334d80aeb3f 100644 --- a/src/doc/index.rst +++ b/src/doc/index.rst @@ -10,6 +10,7 @@ Welcome to caosdb-server's documentation! Getting started <README_SETUP> Concepts <concepts> + tutorials Query Language <CaosDB-Query-Language> administration Development <development/devel> diff --git a/src/doc/tutorials.rst b/src/doc/tutorials.rst new file mode 100644 index 0000000000000000000000000000000000000000..27aacfb3eb9a4f04ab261b12f904e652c4daf3d3 --- /dev/null +++ b/src/doc/tutorials.rst @@ -0,0 +1,10 @@ +Tutorials +============== + +.. toctree:: + :maxdepth: 1 + :glob: + + tutorials/* + + diff --git a/src/doc/tutorials/setup_state_model.py b/src/doc/tutorials/setup_state_model.py new file mode 100755 index 0000000000000000000000000000000000000000..0a1a7daa3b14d7c3e7a5b8eac093857bddfad330 --- /dev/null +++ b/src/doc/tutorials/setup_state_model.py @@ -0,0 +1,379 @@ +#!/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) diff --git a/src/doc/tutorials/statemachine.rst b/src/doc/tutorials/statemachine.rst new file mode 100644 index 0000000000000000000000000000000000000000..317620423e6221ccaf29297fc1f71f0c008f78a0 --- /dev/null +++ b/src/doc/tutorials/statemachine.rst @@ -0,0 +1,34 @@ + +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()