Index: openacs-4/packages/workflow/www/doc/specification.adp =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/www/doc/specification.adp,v diff -u -r1.2 -r1.3 --- openacs-4/packages/workflow/www/doc/specification.adp 26 Aug 2015 18:03:36 -0000 1.2 +++ openacs-4/packages/workflow/www/doc/specification.adp 12 Sep 2016 06:10:32 -0000 1.3 @@ -11,29 +11,29 @@

Overview

I recently built a typical workflow-based application, bug-tracker, and decided against using the acs-workflow package that I myself built. -That's not a good recommendation. We need to fix that.

+That's not a good recommendation. We need to fix that.

Goals

The goal is to implement a workflow package that:

Gripes with the current acs-workflow:

Finite State Machine

Take bug-tracker as an example. The bug-tracker workflow and @@ -50,75 +50,77 @@ state. -

I've finally come to the realization that we'll be better off in -the short to medium term with just a well-functioning +

I've finally come to the realization that we'll be +better off in the short to medium term with just a well-functioning implementation of a finite state machine based workflow module. In general, a workflow consists of a finite set of states, and a finite set of actions. Each action has a set of states in which -it's enabled, or it can be always enabled in all states. And each -action can cause the workflow case to move into a new state, or it -can leave the state unaltered.

+it's enabled, or it can be always enabled in all states. And +each action can cause the workflow case to move into a new state, +or it can leave the state unaltered.

Note that the ability to have an action enabled in more than one state is a convenience, and not part of the mathematical model of -finite state machines. Likeways with actions that don't change the -state. But it's mighty convenient, as you've seen illustrated by -the bug-tracker example above.

+finite state machines. Likeways with actions that don't change +the state. But it's mighty convenient, as you've seen +illustrated by the bug-tracker example above.

Workflows

A workflow is a set of roles, actions, and states, and their relations.

A workflow is associated with an object, which would typically be one of the following:

    -
  1. A package type: This is the default workflow for the -bug-tracker package.
  2. A package instance: This is the default workflow for a -particular instance of the bug-tracker package.
  3. A single case: Future versions could allow you to -customize your workflow for a particular bug or content story.
  4. +
  5. A package type: This is the default workflow +for the bug-tracker package.
  6. A package instance: This is the default +workflow for a particular instance of the bug-tracker package.
  7. A single case: Future versions could allow you +to customize your workflow for a particular bug or content +story.
-

There's also a short_name, so you can easily distinguish between -multiple workflows for the same package, e.g., one for handling the -bug, and another for approving creation of new versions or -components in the bug-tracker.

-

A workflow is also associated with an object type. The -reason for this is that assignments will frequently depend on -attributes of the specific object for the case. In bug-tracker, for -example, the default assignee for a bug will be the maintainer of -the component in which the bug has been found. The bug-tracker will -provide one or more assignment service contract implementations, -which, given the bug_id will give you the component maintainer, or -the project maintainer. These can be used to set up automatic -assignment through a nice web-based user interface.

+

There's also a short_name, so you can easily distinguish +between multiple workflows for the same package, e.g., one for +handling the bug, and another for approving creation of new +versions or components in the bug-tracker.

+

A workflow is also associated with an object +type. The reason for this is that assignments will +frequently depend on attributes of the specific object for the +case. In bug-tracker, for example, the default assignee for a bug +will be the maintainer of the component in which the bug has been +found. The bug-tracker will provide one or more assignment service +contract implementations, which, given the bug_id will give you the +component maintainer, or the project maintainer. These can be used +to set up automatic assignment through a nice web-based user +interface.

When you create a new workflow case for a specific object, we will check that this object descends from the object type for which the workflow is for. If your workflow is general enough to work for all object types, then you can simply associate it with the common ancestor of all objects, 'acs_object'.

When you create a new instance of the bug-tracker, we would -make a copy of the default bug-tracker workflow for your -particular package, so that you can make local changes to the -workflow, to the assignments, etc.

-

A workflow can have side-effects, which fire when -any action is triggered on that workflow. These fire after -the specific actions. See more under action side-effects. These are -declared as a standard "Action_SideEffect" service contract -implementation.

+make a copy of the default bug-tracker workflow +for your particular package, so that you can make local changes to +the workflow, to the assignments, etc.

+

A workflow can have side-effects, which fire +when any action is triggered on that workflow. +These fire after the specific actions. See more under action +side-effects. These are declared as a standard +"Action_SideEffect" service contract implementation.

Another service contract on the workflow level is the -activity log entry title formatting contract. Using a -side-effect callback, you can store additional key/value pairs in -the activity log. You can use the title formatting service contract -to pull these out, along with any other data you like, and use them -to format the title of the log entry for display.

+activity log entry title formatting contract. +Using a side-effect callback, you can store additional key/value +pairs in the activity log. You can use the title formatting service +contract to pull these out, along with any other data you like, and +use them to format the title of the log entry for display.

Roles

A workflow has a set of roles. For bug-tracker, this is Submitter, and Assignee. More complex bug-tracker workflows, could -add Triager and Tester. For a typical pulication workflow, you'd -have Author, Editor, and Publisher. Normally, you'd always include -an 'Administrator' role.

+add Triager and Tester. For a typical pulication workflow, +you'd have Author, Editor, and Publisher. Normally, you'd +always include an 'Administrator' role.

Each role is associated wtih one or more actions in the -workflow. The assignee is assigned to the 'Resolve' action, but -also has permission to perform the Edit, Comment and Reassign -actions. The submitter is assigned to the 'Close' action, but also -has permission to 'Reopen', 'Edit', 'Comment', and possibly -'Reassign'.

+workflow. The assignee is assigned to the 'Resolve' action, +but also has permission to perform the Edit, Comment and Reassign +actions. The submitter is assigned to the 'Close' action, +but also has permission to 'Reopen', 'Edit', +'Comment', and possibly 'Reassign'.

The idea behind introducing roles is that you do not want to go through the bother of assigning each action individually, when normally they are grouped together.

@@ -127,57 +129,60 @@ reassigned at any time.

Default Assignment

The tricky part, however, is the rules saying who should be -assigned by default, or who can be assigned to this role. -First, let's look at how the default assignees can be -determined.

+assigned by default, or who can be assigned to this role. +First, let's look at how the default assignees +can be determined.

  1. If you only have one publisher, then you simply want to assign -the Publisher role to that publisher always. That's called a -static assignment, and the information about that current -assignee is kept in the workflow data model.
  2. The Submitter role in bug-tracker, you want to assign to -whoever opened the bug, namely the object creation -user.
  3. The Assignee role in bug-tracker is given to the maintainer of +the Publisher role to that publisher always. That's called a +static assignment, and the information about that +current assignee is kept in the workflow data model.
  4. The Submitter role in bug-tracker, you want to assign to +whoever opened the bug, namely the object creation +user.
  5. The Assignee role in bug-tracker is given to the maintainer of the component in which the bug was found. This is an example of a -completely application-specific assignment, one which is -only relevant for bug-tracker bug objects, because we need to know -the particular bug-tracker data model for this to work.
  6. +completely application-specific assignment, one +which is only relevant for bug-tracker bug objects, because we need +to know the particular bug-tracker data model for this to +work.

These different options are supplied by programmers as implementations of a particular service contract (see below under service contracts).

-

In the definition of a workflow, you can select an ordered -list of default assignment methods Each will be tried in the -order you specify. The first to return a non-empty list of -assignees is the one which will be used, and the rest won't get -called. So for example you can say "first try component maintainer, -and if non is specified, use the project maintainer".

+

In the definition of a workflow, you can select an +ordered list of default assignment methods Each will be +tried in the order you specify. The first to return a non-empty +list of assignees is the one which will be used, and the rest +won't get called. So for example you can say "first try +component maintainer, and if non is specified, use the project +maintainer".

The workflow package will supply a few standard implementations:

-

Default assignment is done in a lazy fashion, in that we don't -try to find the default assignees until we need to. We need to the -first time an action assigned to that role is enabled. This allows -your default assignment to depend on things that happened in prior -tasks.

+

Default assignment is done in a lazy fashion, in that we +don't try to find the default assignees until we need to. We +need to the first time an action assigned to that role is enabled. +This allows your default assignment to depend on things that +happened in prior tasks.

Reassignment

-

Now, let's look at what happens when you want to reassign a role -to someone else. the

+

Now, let's look at what happens when you want to reassign a +role to someone else. the

  1. If you want to reassign the role, the user interface offers a -pick-list of the users and groups which you're most likely -to want to reassign the role to. Who they are will depend on the -particular application. One common idea is to display the users who -are currently assigned to this role in other cases.
  2. If the desired person or group wasn't in the pick-list, you can -search. The search is conducted among all the users who -could possibly be assigned to this role, which, again, will depend -on the application. It could be all registered users on the site, -it could be all members of the nearest surrounding subsite, it -could be all members of a particular named group, or it could be -some other calculation based on the application.
  3. +pick-list of the users and groups which you're +most likely to want to reassign the role to. Who they are will +depend on the particular application. One common idea is to display +the users who are currently assigned to this role in other +cases.
  4. If the desired person or group wasn't in the pick-list, you +can search. The search is conducted among all the +users who could possibly be assigned to this role, which, again, +will depend on the application. It could be all registered users on +the site, it could be all members of the nearest surrounding +subsite, it could be all members of a particular named group, or it +could be some other calculation based on the application.

A couple of default implementations will be supplied by the workflow package. For the pick-list:

@@ -192,41 +197,45 @@ parties in the workflow_role_allowed_parties table.

Actions

-

In order to determine who are supposed to perform an -action, and who are allowed to perform the action, we let +

In order to determine who are supposed to perform an +action, and who are allowed to perform the action, we let you specify these three things for each action:

-

Actions can also have side-effects, which simply means -that whenever an action is triggered, one or more specified service -contract implementations will get executed. These side-effects are -executed after all other updates, both to the case object, -and to the workflow tables, have been completed.

+

Actions can also have side-effects, which +simply means that whenever an action is triggered, one or more +specified service contract implementations will get executed. These +side-effects are executed after all other updates, +both to the case object, and to the workflow tables, have been +completed.

States

This is specific to the FSM-model. A workflow has a finite set -of states, for example "open", "resolved", and "closed". A case -will always be in exactly one such state. When you perform an -action, the workflow can be pushed into a new state.

+of states, for example "open", "resolved", and +"closed". A case will always be in exactly one such +state. When you perform an action, the workflow can be pushed into +a new state.

There will be one initial state, which the workflow will start out in. This will be the first state according to the sort order from workflow_fsm_states

-

States have almost no information associated with them, they're -simply used to govern which actions are available.

+

States have almost no information associated with them, +they're simply used to govern which actions are available.

Cases

A case is the term for a workflow in action. A case always revolves around a specific object. and we currently only allow one @@ -404,52 +413,52 @@

Service Contracts

-workflow.Role_DefaultAssignees:
+workflow.Role_DefaultAssignees:
   GetObjectType -> string
   GetPrettyName -> string
   GetAssignees (case_id, object_id, role_id) -> { list of party_id }
 
-workflow.Role_AssigneePickList
+workflow.Role_AssigneePickList
   GetObjectType -> string
   GetPrettyName -> string
   GetPickList (case_id, object_id, role_id) -> { list of party_id }
 
-workflow.Role_AssigneeSubQuery
+workflow.Role_AssigneeSubQuery
   GetObjectType -> string
   GetPrettyName -> string
   GetSubQueryName (case_id, object_id, role_id) -> { subquery_name { bind variable list } }
 
-workflow.Action_SideEffect
+workflow.Action_SideEffect
   GetObjectType -> string
   GetPrettyName -> string
   DoSideEffect (case_id, object_id, action_id, entry_id) -> (none)
 
-workflow.ActivityLog_FormatTitle
+workflow.ActivityLog_FormatTitle
   GetObjectType -> string
   GetPrettyName -> string
   GetTitle (entry_id) -> title
 
-

The GetObjectType method is used for the service contract -implementation to tell which object types it is valid for. For -example, a DefaultAssignee implementation can look at a bug, find -out which component it is found in, then look up the component -definition to find the default maintainer. This implementation, -though, is only valid for objects of type 'bt_bugs', or any -descendants thereof. Thus, this is what the GetObjectType call -would return for this implementation. If your implementation is -valid for any ACS Object, then simply return 'acs_object', as this -is the mother of all objects.

-

The GetPrettyName method will be run through a +

The GetObjectType method is used for the +service contract implementation to tell which object types it is +valid for. For example, a DefaultAssignee implementation can look +at a bug, find out which component it is found in, then look up the +component definition to find the default maintainer. This +implementation, though, is only valid for objects of type +'bt_bugs', or any descendants thereof. Thus, this is what +the GetObjectType call would return for this implementation. If +your implementation is valid for any ACS Object, then simply return +'acs_object', as this is the mother of all objects.

+

The GetPrettyName method will be run through a localization filter, meaning that any occurance of the -#message-key# notation will be replaced with a -message catalog lookup for the current domain.

-

The AssigneeQuery service contract probably needs a -little explanation. You're supposed to supply a valid subquery, -which will select the columns party_id, name, email, and +#message-key# notation will be replaced with +a message catalog lookup for the current domain.

+

The AssigneeQuery service contract probably +needs a little explanation. You're supposed to supply a valid +subquery, which will select the columns party_id, name, email, and screen_name (nulls are okay) of all the parties that a role can possibly be assigned to. A simple version could simply be "cc_users". Another would be:

@@ -466,17 +475,17 @@ select distinct q.party_id, q.name || ' (' || u.email || ')' as name_and_email -from (your subquery goes here) q +from (your subquery goes here) q where upper(coalesce(q.name, '') || q.email || ' ' || coalesce(q.screen_name, '')) like upper('%'||:value||'%') order by name_and_email

Now, one little caveat is that you have to return the query -dispatcher query name, not the actual query. The query name -will then get passed to db_map to produce the actual -subquery.

+dispatcher query name, not the actual query. The +query name will then get passed to db_map to +produce the actual subquery.

Workflow will supply these service contract implementations by default:

@@ -496,23 +505,25 @@

Notifications

You can sign up for notifications at several levels:

    -
  1. Notify me of all actions to which I'm assigned. You don't have -to manually go sign up for these notifications, but you should be -able to change the delivery method and frequency.
  2. Notify me of all activity on ... +
  3. Notify me of all actions to which I'm assigned. You +don't have to manually go sign up for these notifications, but +you should be able to change the delivery method and +frequency.
  4. Notify me of all activity on ...
      -
    1. Any case where I'm assigned to some role.
    2. A particular case (one bug-tracker bug)
    3. All cases in the particular workflow (entire bug-tracker +
    4. Any case where I'm assigned to some role.
    5. A particular case (one bug-tracker bug)
    6. All cases in the particular workflow (entire bug-tracker project)

You should always receive at most one notification per activity. -They're sent out in the order in which they're listed here, and if -you get the first, you won't get the second, third or fourth; if -you get the second, you won't get the third or fourth, etc.

-

A special case is that the first notification isn't optional. -You don't have to manually go sign up for those notifications, and -you can't turn them off entirely. You can still change the delivery -method and the frequency, though.

+They're sent out in the order in which they're listed here, +and if you get the first, you won't get the second, third or +fourth; if you get the second, you won't get the third or +fourth, etc.

+

A special case is that the first notification isn't +optional. You don't have to manually go sign up for those +notifications, and you can't turn them off entirely. You can +still change the delivery method and the frequency, though.

In order to implement this, we need to make three fairly trivial enhancements to the notifications package.

    @@ -522,17 +533,18 @@ -already_notified and to not notify those again, and likewise, to return the list of users notified by the given notification.
  1. We need to be able to limit notifications to only a subset of -the subscribed base. If you have a subscription on "any case where -I'm assigned to some role", that's a dynamic relationship. So the -call to notification::new would take as a parameter -the list of people who are assigned to some role on this particular -case. Only people who are subscribed and on that list will -get notified. I can't think of a good name for such a parameter, -perhaps -positive_list.
  2. Finally, we need to force people on the positive list above to -get notifications even though they don't currently have a +the subscribed base. If you have a subscription on "any case +where I'm assigned to some role", that's a dynamic +relationship. So the call to notification::new would +take as a parameter the list of people who are assigned to some +role on this particular case. Only people who are subscribed +and on that list will get notified. I can't think of a +good name for such a parameter, perhaps +-positive_list.
  3. Finally, we need to force people on the positive list above to +get notifications even though they don't currently have a subscription. This could be a -force:boolean parameter which works in conjunction with the positive list, so that people -on the positive list who aren't subscribers get a default +on the positive list who aren't subscribers get a default email/instant subscription automatically. They can then go back and change their delivery method and frequency later.
@@ -732,19 +744,20 @@ label name } }

The enabled actions which the current user has permission to perform.

case::action::get_editable_fields(case_id, action) -> { list of field names }

Which fields should we edit, depending on the current action. -NOTE! We probably won't be able to support this in the first +NOTE! We probably won't be able to support this in the first version.

case::state::get_hidden_fields(case_id) -> { list of field names }

Which fields should we hide, depending on the state. NOTE! We -probably won't be able to support this in the first version.

case::action::available_p(case_id, user_id, action_id) -> +probably won't be able to support this in the first +version.

case::action::available_p(case_id, user_id, action_id) -> (boolean)

Is this action enabled and allowed for this user?

case::action::new_state(case_id, action_id) -> (state_id)

The new state which the case will have after this action has -been performed (if action doesn't change state, returns the current -state again.

case::action::execute(case_id, action_id, comment, +been performed (if action doesn't change state, returns the +current state again.

case::action::execute(case_id, action_id, comment, comment_format) -> (state_id)

Perform the action, updating the workflow state, etc. This should be called from inside a db_transaction where the case object has just been updated.

-

Here's what the form page would look like:

+

Here's what the form page would look like:

 ad_page_contract { ... } {
     bug_id:integer,notnull  
@@ -801,39 +814,39 @@
 
-

Nice-to-haves that aren't entirely pie-in-the-sky +

Nice-to-haves that aren't entirely pie-in-the-sky include:

Appendix A. Pluggable Models

-

I've looked into pluggable models before, and it's not too -complicated. The trick is that you have four areas where the +

I've looked into pluggable models before, and it's not +too complicated. The trick is that you have four areas where the generic workflow framework/engine will interface with the plugin model:

-

Hence, we've concluded that a rewrite is in fact the most +

Hence, we've concluded that a rewrite is in fact the most productive strategy.