Index: openacs-4/packages/workflow/workflow.info =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/workflow.info,v diff -u -r1.4 -r1.5 --- openacs-4/packages/workflow/workflow.info 3 Feb 2003 12:22:24 -0000 1.4 +++ openacs-4/packages/workflow/workflow.info 5 Mar 2003 17:17:05 -0000 1.5 @@ -18,41 +18,58 @@ A Tcl API for creating workflows that support the Bug Tracker, CMS publication, simple approval, and much more. 2003-01-31 Collaboraid - This package lets you define the process that your tickets, articles, documents, reports, claims, change requests, or any other object of interest, must go through to ensure consistent quality and to avoid that any cases falls through the cracks. -<p> + This package lets you define the process that your tickets, articles, documents, reports, claims, change requests, or any other object of interest, must go through to ensure consistent quality and to avoid that any cases falls through the cracks. +<p> For more information, see: <a href="http://www.collaboraid.biz/developer/workflow-spec">the workflow specification</a>. + + + + + + + + + + + + + + + + + Index: openacs-4/packages/workflow/sql/oracle/workflow-data-drop.sql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/sql/oracle/workflow-data-drop.sql,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/workflow/sql/oracle/workflow-data-drop.sql 5 Mar 2003 17:17:18 -0000 1.1 @@ -0,0 +1,12 @@ +-- Convenient script for dropping all data (but not the datamodel) + +declare + foo integer; +begin + for row in (select workflow_id from workflows) + loop + foo := workflow.delete(row.workflow_id); + end loop; +end; +/ +show errors Index: openacs-4/packages/workflow/sql/oracle/workflow-procedural-create.sql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/sql/oracle/workflow-procedural-create.sql,v diff -u -r1.1 -r1.2 --- openacs-4/packages/workflow/sql/oracle/workflow-procedural-create.sql 12 Feb 2003 14:21:56 -0000 1.1 +++ openacs-4/packages/workflow/sql/oracle/workflow-procedural-create.sql 5 Mar 2003 17:17:18 -0000 1.2 @@ -10,7 +10,75 @@ --------------------------------- -- Workflow level, Generic Model --------------------------------- +create or replace package workflow_case +as + function get_pretty_state( + workflow_short_name in varchar, + object_id in integer + ) return varchar; + + function delete( + delete_case_id in integer + ) return integer; +end workflow_case; +/ +show errors + +create or replace package body workflow_case +as + function get_pretty_state( + workflow_short_name in varchar, + object_id in integer + ) return varchar + is + v_state_pretty varchar(4000); + v_object_id integer; + begin + v_object_id := object_id; + + select s.pretty_name + into v_state_pretty + from workflows w, + workflow_cases c, + workflow_case_fsm cfsm, + workflow_fsm_states s + where w.short_name = workflow_short_name + and c.object_id = v_object_id + and c.workflow_id = w.workflow_id + and cfsm.case_id = c.case_id + and s.state_id = cfsm.current_state; + + return v_state_pretty; + + end get_pretty_state; + + function delete( + delete_case_id in integer + ) return integer + is + begin + for rec in (select cr.item_id + from cr_items cr, workflow_case_log wcl + where cr.item_id = wcl.entry_id + and wcl.case_id = delete_case_id) + loop + delete from workflow_case_log where entry_id = rec.item_id; + content_item.delete(rec.item_id); + end loop; + + -- All workflow data cascades from the case id + delete from workflow_cases + where object_id = delete_case_id; + + return 0; + end delete; + +end workflow_case; +/ +show errors + + create or replace package workflow as function delete( @@ -32,16 +100,25 @@ / show errors --- package bodies create or replace package body workflow as function delete( delete_workflow_id in integer ) return integer is + foo integer; begin + -- Delete all cases first + for rec in (select case_id + from workflow_cases + where workflow_id = delete_workflow_id) + loop + foo := workflow_case.delete(rec.case_id); + end loop; + acs_object.delete(delete_workflow_id); + return 0; end delete; @@ -83,113 +160,95 @@ / show errors -create or replace package workflow_case -as function get_pretty_state( - workflow_short_name in varchar, - object_id in integer - ) return varchar; - -end workflow_case; +create or replace package workflow_case_log_entry +as + function new( + entry_id in integer, + case_id in integer, + action_id in integer, + comment in varchar, + comment_mime_type in varchar, + creation_user in integer, + creation_ip in varchar, + content_type in varchar default 'workflow_case_log_entry' + ) return integer; + +end workflow_case_log_entry; / show errors --- Function for getting the pretty state of a case -create or replace package body workflow_case +create or replace package body workflow_case_log_entry as - function get_pretty_state( - workflow_short_name in varchar, - object_id in integer - ) return varchar - is - v_state_pretty varchar(4000); - v_object_id integer; + function new( + entry_id in integer, + case_id in integer, + action_id in integer, + comment in varchar, + comment_mime_type in varchar, + creation_user in integer, + creation_ip in varchar, + content_type in varchar default 'workflow_case_log_entry' + ) return integer + is + v_name varchar2(4000); -- XXX aufflick fix this + v_action_short_name varchar2(4000); + v_action_pretty_past_tense varchar2(4000); + v_case_object_id integer; + v_item_id integer; + v_revision_id integer; begin - v_object_id := object_id; + select short_name, pretty_past_tense + into v_action_short_name, v_action_pretty_past_tense + from workflow_actions + where action_id = new.action_id; - select s.pretty_name - into v_state_pretty - from workflows w, - workflow_cases c, - workflow_case_fsm cfsm, - workflow_fsm_states s - where w.short_name = workflow_short_name - and c.object_id = v_object_id - and c.workflow_id = w.workflow_id - and cfsm.case_id = c.case_id - and s.state_id = cfsm.current_state; + -- use case object as context_id + select object_id + into v_case_object_id + from workflow_cases + where case_id = new.case_id; - return v_state_pretty; + -- build the unique name + if entry_id is not null then + v_item_id := entry_id; + else + select acs_object_id_seq.nextval into v_item_id from dual; + end if; + v_name := v_action_short_name || ' ' || v_item_id; - end get_pretty_state; + v_item_id := content_item.new ( + item_id => v_item_id, + name => v_name, + parent_id => 0, + title => v_action_pretty_past_tense, + creation_date => sysdate(), + creation_user => creation_user, + context_id => v_case_object_id, + creation_ip => creation_ip, + is_live => 't', + mime_type => comment_mime_type, + text => comment, + storage_type => 'text', + item_subtype => 'content_item', + content_type => content_type + ); -end workflow_case; -/ -show errors + -- insert the row into the single-column entry revision table + v_revision_id := content_item.get_live_revision (v_item_id); --- --create or replace package workflow_activity_log --- --as --- function new( --- case_id in integer, --- action_id in integer, --- comment_format in varchar --- ) return integer; + insert into workflow_case_log_rev (entry_rev_id) + values (v_revision_id); --- end workflow_activity_log; --- / --- show errors + -- insert into workflow-case-log + -- raise_application_error(-20000, 'about to insert ' || v_item_id || ',' || new.case_id || ',' || new.action_id); + insert into workflow_case_log (entry_id, case_id, action_id) + values (v_item_id, new.case_id, new.action_id); --- create or replace package body workflow_activity_log --- as --- function new( --- case_id in integer, --- action_id in integer, --- comment_format in varchar --- ) return integer --- is --- v_item_id cr_items.item_id%TYPE; --- v_revision_id cr_revisions.revision_id%TYPE; --- begin --- v_item_id := content_item.new ( --- name, --- parent_id, --- item_id, --- locale, --- creation_date, --- creation_user, --- context_id, --- creation_ip, --- 'content_item', --- content_type, --- null, --- null, --- null, --- null, --- null --- ); + -- return id of newly created item + return v_item_id; + end new; --- v_revision_id := content_revision.new ( --- title, --- description, --- publish_date, --- mime_type, --- nls_language, --- null, --- v_item_id, --- revision_id, --- creation_date, --- creation_user, --- creation_ip --- ); - --- insert into workflow_case_log --- (entry_id, case_id, action_id, comment_format) --- values (v_revision_id, case_id, action_id, comment_format); - --- content_item.set_live_revision (v_revision_id); - --- end new; - --- end workflow_activity_log; --- / ---show errors - +end workflow_case_log_entry; +/ +show errors + Index: openacs-4/packages/workflow/sql/oracle/workflow-procedural-drop.sql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/sql/oracle/workflow-procedural-drop.sql,v diff -u -r1.1 -r1.2 --- openacs-4/packages/workflow/sql/oracle/workflow-procedural-drop.sql 12 Feb 2003 14:21:56 -0000 1.1 +++ openacs-4/packages/workflow/sql/oracle/workflow-procedural-drop.sql 5 Mar 2003 17:17:18 -0000 1.2 @@ -12,6 +12,5 @@ --------------------------------- drop package workflow; drop package workflow_case; -drop package workflow_activity_log; +drop package workflow_case_log_entry; - Index: openacs-4/packages/workflow/sql/oracle/workflow-tables-create.sql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/sql/oracle/workflow-tables-create.sql,v diff -u -r1.1 -r1.2 --- openacs-4/packages/workflow/sql/oracle/workflow-tables-create.sql 12 Feb 2003 14:21:56 -0000 1.1 +++ openacs-4/packages/workflow/sql/oracle/workflow-tables-create.sql 5 Mar 2003 17:17:18 -0000 1.2 @@ -153,6 +153,8 @@ references workflow_roles(role_id) on delete set null, always_enabled_p char(1) default 'f' + constraint wf_acns_enabled_p_ck + check (always_enabled_p in ('t','f')) ); create sequence workflow_actions_seq; @@ -278,7 +280,15 @@ not null constraint wf_fsm_acn_enb_in_st_st_id_fk references workflow_fsm_states - on delete cascade + on delete cascade, + assigned_p char(1) default 'f' + constraint wf_fsm_acns_enabled_p_ck + check (assigned_p in ('t','f')) + -- The users in the role assigned to an action are only assigned to take action + -- in the enabled states that have the assigned_p flag + -- set to true. For example, in Bug Tracker, the resolve action is enabled + -- in both the open and resolved states but only has assigned_p set to true + -- in the open state. ); @@ -415,18 +425,7 @@ action_id integer constraint wf_case_log_acn_id_fk references workflow_actions(action_id) - on delete cascade, - user_id constraint workflow_case_log_user_id_fk - references users(user_id) - on delete cascade, - action_date date default sysdate - constraint workflow_case_log_acn_date_nn - not null, - comment_text varchar(4000), - comment_format varchar(50) - default 'text/plain' - constraint wf_clog_comment_format_nn - not null + on delete cascade ); create table workflow_case_log_data ( @@ -442,3 +441,16 @@ primary key (entry_id, key) ); +begin + content_type.create_type ( + content_type => 'workflow_case_log_entry', + supertype => 'content_revision', + pretty_name => 'Workflow Case Log Entry', + pretty_plural => 'Workflow Case Log Entries', + table_name => 'workflow_case_log_rev', + id_column => 'entry_rev_id', + name_method => null + ); +end; +/ +show errors Index: openacs-4/packages/workflow/sql/oracle/workflow-tables-drop.sql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/sql/oracle/workflow-tables-drop.sql,v diff -u -r1.1 -r1.2 --- openacs-4/packages/workflow/sql/oracle/workflow-tables-drop.sql 12 Feb 2003 14:21:56 -0000 1.1 +++ openacs-4/packages/workflow/sql/oracle/workflow-tables-drop.sql 5 Mar 2003 17:17:18 -0000 1.2 @@ -8,21 +8,30 @@ -- License. Full text of the license is available from the GNU Project: -- http://www.fsf.org/copyleft/gpl.html --- Drop all data in workflow tables by dropping the acs objects of all workflows in the system. --- This is sufficient since all workflow data ultimately --- hangs on workflow instances and will be dropped on cascade +declare + foo integer; begin - for row in (select object_id from acs_objects - where object_type = 'workflow_lite') + for row in (select workflow_id from workflows) loop - acs_object.delete(row.object_id); + foo := workflow.delete(row.workflow_id); end loop; acs_object_type.drop_type('workflow_lite', 't'); end; / show errors +begin + content_type.drop_type ( + content_type => 'workflow_case_log_entry', + drop_children_p => 't', + drop_table_p => 't' + ); +end; +/ +show errors + + -- Drop all tables drop table workflow_case_fsm; drop table workflow_case_role_party_map; Index: openacs-4/packages/workflow/sql/postgresql/drop-content-items.sql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/sql/postgresql/drop-content-items.sql,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/workflow/sql/postgresql/drop-content-items.sql 5 Mar 2003 17:17:33 -0000 1.1 @@ -0,0 +1,17 @@ +-- Convenient script for dropping all workflow data (but not the datamodel) + +-- Delete workflow data first +create function inline_0 () +returns integer as ' +declare + row record; +begin + for row in select item_id from cr_items where content_type = ''workflow_case_log_entry'' + loop + perform content_item__delete(row.item_id); + end loop; + + return 1; +end;' language 'plpgsql'; +select inline_0 (); +drop function inline_0(); Index: openacs-4/packages/workflow/sql/postgresql/workflow-data-drop.sql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/sql/postgresql/workflow-data-drop.sql,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/workflow/sql/postgresql/workflow-data-drop.sql 5 Mar 2003 17:17:33 -0000 1.1 @@ -0,0 +1,17 @@ +-- Convenient script for dropping all workflow data (but not the datamodel) + +-- Delete workflow data first +create function inline_0 () +returns integer as ' +declare + row record; +begin + for row in select workflow_id from workflows + loop + perform workflow__delete(row.workflow_id); + end loop; + + return 1; +end;' language 'plpgsql'; +select inline_0 (); +drop function inline_0(); Index: openacs-4/packages/workflow/sql/postgresql/workflow-procedural-create.sql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/sql/postgresql/workflow-procedural-create.sql,v diff -u -r1.5 -r1.6 --- openacs-4/packages/workflow/sql/postgresql/workflow-procedural-create.sql 12 Feb 2003 14:22:35 -0000 1.5 +++ openacs-4/packages/workflow/sql/postgresql/workflow-procedural-create.sql 5 Mar 2003 17:17:33 -0000 1.6 @@ -15,13 +15,44 @@ returns integer as ' declare delete_workflow_id alias for $1; + rec record; begin - select acs_object__delete(delete_workflow_id); + -- Delete all cases first + for rec in select case_id + from workflow_cases + where workflow_id = delete_workflow_id loop + perform workflow_case__delete (rec.case_id); + end loop; + + perform acs_object__delete(delete_workflow_id); + return 0; end;' language 'plpgsql'; +create or replace function workflow_case__delete (integer) +returns integer as ' +declare + delete_case_id alias for $1; + rec record; +begin + for rec in select cr.item_id + from cr_items cr, workflow_case_log wcl + where cr.item_id = wcl.entry_id + and wcl.case_id = delete_case_id loop + + delete from workflow_case_log where entry_id = rec.item_id; + perform content_item__delete(rec.item_id); + end loop; + + -- All workflow data cascades from the case id + delete from workflow_cases + where object_id = delete_case_id; + + return 0; +end;' language 'plpgsql'; + -- Function for creating a workflow create or replace function workflow__new ( varchar, -- short_name @@ -97,10 +128,10 @@ end; ' language 'plpgsql'; -select define_function_args ('workflow_case_log_entry__new','item_id,content_type;workflow_case_log_entry,case_id,action_id,comment,comment_mime_type,creation_user,creation_ip'); +select define_function_args ('workflow_case_log_entry__new','entry_id,content_type;workflow_case_log_entry,case_id,action_id,comment,comment_mime_type,creation_user,creation_ip'); create or replace function workflow_case_log_entry__new ( - integer, -- item_id + integer, -- entry_id varchar, -- content_type integer, -- case_id integer, -- action_id Index: openacs-4/packages/workflow/sql/postgresql/workflow-tables-create.sql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/sql/postgresql/workflow-tables-create.sql,v diff -u -r1.8 -r1.9 --- openacs-4/packages/workflow/sql/postgresql/workflow-tables-create.sql 12 Feb 2003 14:22:35 -0000 1.8 +++ openacs-4/packages/workflow/sql/postgresql/workflow-tables-create.sql 5 Mar 2003 17:17:33 -0000 1.9 @@ -304,7 +304,13 @@ not null constraint wf_fsm_acn_enb_in_st_st_id_fk references workflow_fsm_states - on delete cascade + on delete cascade, + assigned_p boolean default 't' + -- The users in the role assigned to an action are only assigned to take action + -- in the enabled states that have the assigned_p flag + -- set to true. For example, in Bug Tracker, the resolve action is enabled + -- in both the open and resolved states but only has assigned_p set to true + -- in the open state. ); @@ -361,7 +367,7 @@ create table workflow_cases ( case_id integer - constraint wf_cases_pk + constraint workflow_cases_pk primary key, workflow_id integer constraint wf_cases_workflow_id_nn @@ -380,6 +386,8 @@ -- the object which this case is about, e.g. the acs-object for a bug-tracker bug ); +create index workflow_cases_workflow_id on workflow_cases (workflow_id); + create table workflow_case_role_party_map ( case_id integer constraint wf_case_role_pty_map_case_id_nn @@ -388,9 +396,9 @@ references workflow_cases(case_id) on delete cascade, role_id integer - constraint wf_case_role_pty_map_case_id_nn + constraint wf_case_role_pty_map_role_id_nn not null - constraint wf_case_role_pty_map_case_id_fk + constraint wf_case_role_pty_map_role_id_fk references workflow_roles(role_id) on delete cascade, party_id integer @@ -441,6 +449,10 @@ on delete cascade ); +create index workflow_case_log_action_id on workflow_case_log (action_id); +create index workflow_case_log_case_id on workflow_case_log (case_id); + + create table workflow_case_log_data ( entry_id integer constraint wf_case_log_data_eid_nn Index: openacs-4/packages/workflow/sql/postgresql/workflow-tables-drop.sql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/sql/postgresql/workflow-tables-drop.sql,v diff -u -r1.5 -r1.6 --- openacs-4/packages/workflow/sql/postgresql/workflow-tables-drop.sql 12 Feb 2003 14:22:35 -0000 1.5 +++ openacs-4/packages/workflow/sql/postgresql/workflow-tables-drop.sql 5 Mar 2003 17:17:33 -0000 1.6 @@ -8,18 +8,15 @@ -- License. Full text of the license is available from the GNU Project: -- http://www.fsf.org/copyleft/gpl.html --- Drop all data in workflow tables by dropping the acs objects of all workflows in the system. --- This is sufficient since all workflow data ultimately --- hangs on workflow instances and will be dropped on cascade +-- Delete workflow data first create function inline_0 () returns integer as ' declare row record; begin - for row in select object_id from acs_objects - where object_type = ''workflow_lite'' + for row in select workflow_id from workflows loop - perform acs_object__delete(row.object_id); + perform workflow__delete(row.workflow_id); end loop; return 1; @@ -71,5 +68,3 @@ drop sequence workflow_actions_seq; drop sequence workflow_fsm_states_seq; drop sequence workflow_cases_seq; - - Index: openacs-4/packages/workflow/tcl/action-procs-oracle.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/action-procs-oracle.xql,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/workflow/tcl/action-procs-oracle.xql 5 Mar 2003 17:18:10 -0000 1.1 @@ -0,0 +1,55 @@ + + + oracle8.1.6 + + + + + select a.action_id, + a.workflow_id, + a.sort_order, + a.short_name, + a.pretty_name, + a.pretty_past_tense, + a.edit_fields, + a.assigned_role, + (select short_name from workflow_roles where role_id = a.assigned_role) as assigned_role_short_name, + a.always_enabled_p, + (select case when count(*) = 1 then 't' else 'f' end + from workflow_initial_action + where workflow_id = a.workflow_id + and action_id = a.action_id + ) as initial_action_p, + fa.new_state as new_state_id, + (select short_name from workflow_fsm_states where state_id = fa.new_state) as new_state + from workflow_actions a, + workflow_fsm_actions fa + where a.workflow_id = :workflow_id + and fa.action_id = a.action_id + and a.action_id = fa.action_id (+) + order by a.sort_order + + + + + + select nvl(max(sort_order),0) + 1 + from workflow_action_callbacks + where action_id = :action_id + + + + + + insert into workflow_action_allowed_roles + select :action_id, + (select role_id + from workflow_roles + where workflow_id = :workflow_id + and short_name = :allowed_role) as role_id + from dual + + + + + Index: openacs-4/packages/workflow/tcl/action-procs-postgresql.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/action-procs-postgresql.xql,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/workflow/tcl/action-procs-postgresql.xql 5 Mar 2003 17:18:10 -0000 1.1 @@ -0,0 +1,53 @@ + + + postgresql7.2 + + + + + select a.action_id, + a.workflow_id, + a.sort_order, + a.short_name, + a.pretty_name, + a.pretty_past_tense, + a.edit_fields, + a.assigned_role, + (select short_name from workflow_roles where role_id = a.assigned_role) as assigned_role_short_name, + a.always_enabled_p, + (select case when count(*) = 1 then 't' else 'f' end + from workflow_initial_action + where workflow_id = a.workflow_id + and action_id = a.action_id + ) as initial_action_p, + fa.new_state as new_state_id, + (select short_name from workflow_fsm_states where state_id = fa.new_state) as new_state + from workflow_actions a left outer join + workflow_fsm_actions fa on (a.action_id = fa.action_id) + where a.workflow_id = :workflow_id + and fa.action_id = a.action_id + order by a.sort_order + + + + + + select coalesce(max(sort_order),0) + 1 + from workflow_action_callbacks + where action_id = :action_id + + + + + + insert into workflow_action_allowed_roles + select :action_id, + (select role_id + from workflow_roles + where workflow_id = :workflow_id + and short_name = :allowed_role) as role_id + + + + + Index: openacs-4/packages/workflow/tcl/action-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/action-procs.tcl,v diff -u -r1.6 -r1.7 --- openacs-4/packages/workflow/tcl/action-procs.tcl 12 Feb 2003 14:23:14 -0000 1.6 +++ openacs-4/packages/workflow/tcl/action-procs.tcl 5 Mar 2003 17:18:10 -0000 1.7 @@ -124,6 +124,8 @@ } + # The cache is flushed in workflow::action::fsm::new + return $action_id } @@ -144,7 +146,7 @@ @param action_id The action_id of the action. @return List of role_id of the allowed roles } { - return [get_from_request_cache $action_id "allowed_roles"] + return [get_from_request_cache $action_id "allowed_role_ids"] } ad_proc -public workflow::action::get_privileges { @@ -267,7 +269,6 @@ db_dml insert_callback {} } - # Action related data changed - need to flush set workflow_id [workflow::action::get_workflow_id -action_id $action_id] workflow::action::flush_cache -workflow_id $workflow_id @@ -320,6 +321,7 @@ {-assigned_role {}} {-privileges {}} {-enabled_states {}} + {-assigned_states {}} {-new_state {}} {-callbacks {}} {-always_enabled_p f} @@ -363,16 +365,25 @@ } db_dml insert_fsm_action {} - # Record in which states the action is enabled + # Record in which states the action is enabled but not assigned foreach state_short_name $enabled_states { set enabled_state_id [workflow::state::fsm::get_id \ -workflow_id $workflow_id \ -short_name $state_short_name] + set assigned_p "f" db_dml insert_enabled_state {} } + + # Record where the action is both enabled and assigned + foreach state_short_name $assigned_states { + set enabled_state_id [workflow::state::fsm::get_id \ + -workflow_id $workflow_id \ + -short_name $state_short_name] + set assigned_p "t" + db_dml insert_enabled_state {} + } } - # Action info for this workflow changed, need to flush workflow::action::flush_cache -workflow_id $workflow_id } @@ -444,6 +455,7 @@ privileges {} always_enabled_p f enabled_states {} + assigned_states {} new_state {} initial_action_p f callbacks {} @@ -464,6 +476,7 @@ -privileges $action(privileges) \ -always_enabled_p $action(always_enabled_p) \ -enabled_states $action(enabled_states) \ + -assigned_states $action(assigned_states) \ -new_state $action(new_state) \ -callbacks $action(callbacks) \ -initial_action_p $action(initial_action_p) @@ -514,7 +527,8 @@ array unset row callbacks_array array unset row callback_ids array unset row allowed_roles_array - + array unset row allowed_role_ids + # Get rid of a few defaults array set defaults { initial_action_p f always_enabled_p f } @@ -523,7 +537,7 @@ array unset row $name } } - + # Get rid of empty strings foreach name [array names row] { if { [empty_string_p $row($name)] } { @@ -704,9 +718,11 @@ # Build an array for all allowed roles for all actions array set allowed_roles_array {} array set allowed_roles {} + array set allowed_role_ids {} db_foreach action_allowed_roles {} -column_array allowed_role_row { set allowed_roles_array($allowed_role_row(action_id),$allowed_role_row(role_id)) [array get allowed_role_row] lappend allowed_roles($allowed_role_row(action_id)) $allowed_role_row(short_name) + lappend allowed_role_ids($allowed_role_row(action_id)) $allowed_role_row(role_id) } # Build an array of privileges for all actions @@ -715,18 +731,23 @@ lappend privileges($action_id) $privilege } - # Build an erray of enabled state short names for all actions + # Build arrays of enabled and assigned state short names for all actions array set enabled_states {} + array set assigned_states {} db_foreach action_enabled_short_name {} { - lappend enabled_states($action_id) $short_name + if { [string equal $assigned_p "t"] } { + lappend assigned_states($action_id) $short_name + } else { + lappend enabled_states($action_id) $short_name + } } # For each action_id, add to the array of that action the contents of the - # sub arrays (callbacks, allowed_roles, privileges) + # sub arrays (callbacks, allowed_roles, allowed_role_ids, privileges) foreach action_id $action_ids { array set one_action $action_data($action_id) - foreach array_name { privileges enabled_states callbacks allowed_roles } { + foreach array_name { privileges enabled_states assigned_states callbacks allowed_roles allowed_role_ids } { if { [info exists ${array_name}($action_id)] } { set one_action(${array_name}) [set ${array_name}($action_id)] } else { Index: openacs-4/packages/workflow/tcl/action-procs.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/action-procs.xql,v diff -u -r1.6 -r1.7 --- openacs-4/packages/workflow/tcl/action-procs.xql 12 Feb 2003 14:23:14 -0000 1.6 +++ openacs-4/packages/workflow/tcl/action-procs.xql 5 Mar 2003 17:18:10 -0000 1.7 @@ -19,17 +19,6 @@ - - - insert into workflow_action_allowed_roles - select :action_id, - (select role_id - from workflow_roles - where workflow_id = :workflow_id - and short_name = :allowed_role) as role_id - - - insert into workflow_action_privileges @@ -64,36 +53,10 @@ workflow_actions a where a.action_id = p.action_id and a.workflow_id = :workflow_id + order by privilege - - - select a.action_id, - a.workflow_id, - a.sort_order, - a.short_name, - a.pretty_name, - a.pretty_past_tense, - a.edit_fields, - a.assigned_role, - (select short_name from workflow_roles where role_id = a.assigned_role) as assigned_role_short_name, - a.always_enabled_p, - (select case when count(*) = 1 then 't' else 'f' end - from workflow_initial_action - where workflow_id = a.workflow_id - and action_id = a.action_id - ) as initial_action_p, - fa.new_state as new_state_id, - (select short_name from workflow_fsm_states where state_id = fa.new_state) as new_state - from workflow_actions a left outer join - workflow_fsm_actions fa on (a.action_id = fa.action_id) - where a.workflow_id = :workflow_id - and fa.action_id = a.action_id - order by a.sort_order - - - select workflow_id @@ -132,30 +95,25 @@ workflow_action_allowed_roles aar where r.workflow_id = :workflow_id and r.role_id = aar.role_id + order by r.sort_order select s.short_name, - waeis.action_id + waeis.action_id, + waeis.assigned_p from workflow_fsm_action_en_in_st waeis, workflow_actions a, workflow_fsm_states s where waeis.action_id = a.action_id and a.workflow_id = :workflow_id and s.state_id = waeis.state_id + order by s.sort_order - - - select coalesce(max(sort_order),0) + 1 - from workflow_action_callbacks - where action_id = :action_id - - - insert into workflow_action_callbacks (action_id, acs_sc_impl_id, sort_order) @@ -175,9 +133,10 @@ insert into workflow_fsm_action_en_in_st - (action_id, state_id) - values (:action_id, :enabled_state_id) + (action_id, state_id, assigned_p) + values (:action_id, :enabled_state_id, :assigned_p) + Index: openacs-4/packages/workflow/tcl/case-procs-oracle.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/case-procs-oracle.xql,v diff -u -r1.1 -r1.2 --- openacs-4/packages/workflow/tcl/case-procs-oracle.xql 12 Feb 2003 14:23:14 -0000 1.1 +++ openacs-4/packages/workflow/tcl/case-procs-oracle.xql 5 Mar 2003 17:18:10 -0000 1.2 @@ -1,18 +1,26 @@ - oracle8.1.7 + oracle8.1.6 - + - select state_id - from workflow_fsm_states - where workflow_id = :workflow_id - order by sort_order - limit 1 + select c.case_id, + c.workflow_id, + c.object_id, + s.state_id, + s.short_name as state_short_name, + s.pretty_name as pretty_state, + s.hide_fields as state_hide_fields + from workflow_cases c, + workflow_case_fsm cfsm, + workflow_fsm_states s + where c.case_id = :case_id + and cfsm.case_id = c.case_id + and s.state_id (+) = cfsm.current_state - + select m.party_id, p.email, @@ -25,4 +33,65 @@ + + + select l.entry_id, + l.case_id, + l.action_id, + a.short_name as action_short_name, + a.pretty_past_tense as action_pretty_past_tense, + io.creation_user, + iou.first_names as user_first_names, + iou.last_name as user_last_name, + iou.email as user_email, + io.creation_date, + to_char(io.creation_date, 'fmMM/DDfm/YYYY') as creation_date_pretty, + blob_to_string(r.content) as comment_string, -- needs il8-ing... + r.mime_type as comment_mime_type, + d.key, + d.value + from workflow_case_log l, + workflow_actions a, + cr_items i, + acs_objects io, + cc_users iou, + cr_revisions r, + workflow_case_log_data d + where l.case_id = :case_id + and l.action_id = a.action_id + and i.item_id = l.entry_id + and io.object_id = i.item_id + and iou.user_id = io.creation_user + and r.revision_id = i.live_revision + and d.entry_id (+) = l.entry_id + order by creation_date + + + + + + + select distinct acs_object.name(p.party_id) || ' (' || p.email || ')' as label, p.party_id + from [ad_decode $subquery "" "cc_users" $subquery] p + where upper(nvl(acs_object.name(p.party_id) || ' ', '') || p.email) like upper('%'||:value||'%') + order by label + + + + + + select acs_object.name(p.party_id) || ' (' || p.email || ')' as label, p.party_id + from parties p + where p.party_id in ([join $party_id_list ", "]) + order by label + + + + + + select acs_object.name(:object_id) as name from dual + + + + Index: openacs-4/packages/workflow/tcl/case-procs-postgresql.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/case-procs-postgresql.xql,v diff -u -r1.2 -r1.3 --- openacs-4/packages/workflow/tcl/case-procs-postgresql.xql 12 Feb 2003 14:23:14 -0000 1.2 +++ openacs-4/packages/workflow/tcl/case-procs-postgresql.xql 5 Mar 2003 17:18:10 -0000 1.3 @@ -2,17 +2,24 @@ postgresql7.2 - + - select state_id - from workflow_fsm_states - where workflow_id = :workflow_id - order by sort_order - limit 1 + select c.case_id, + c.workflow_id, + c.object_id, + s.state_id, + s.short_name as state_short_name, + s.pretty_name as pretty_state, + s.hide_fields as state_hide_fields + from workflow_cases c, + workflow_case_fsm cfsm left outer join + workflow_fsm_states s on (s.state_id = cfsm.current_state) + where c.case_id = :case_id + and cfsm.case_id = c.case_id - + select m.party_id, p.email, @@ -25,31 +32,57 @@ - + select l.entry_id, l.case_id, l.action_id, a.short_name as action_short_name, - a.pretty_name as action_pretty_name, a.pretty_past_tense as action_pretty_past_tense, io.creation_user, iou.first_names as user_first_names, iou.last_name as user_last_name, iou.email as user_email, io.creation_date, to_char(io.creation_date, 'fmMM/DDfm/YYYY') as creation_date_pretty, - r.content as comment, - r.mime_type as comment_mime_type + r.content as comment_string, + r.mime_type as comment_mime_type, + d.key, + d.value from workflow_case_log l join workflow_actions a using (action_id) join cr_items i on (i.item_id = l.entry_id) join acs_objects io on (io.object_id = i.item_id) join cc_users iou on (iou.user_id = io.creation_user) join - cr_revisions r on (r.revision_id = i.live_revision) + cr_revisions r on (r.revision_id = i.live_revision) left outer join + workflow_case_log_data d using (entry_id) where l.case_id = :case_id order by creation_date + + + select acs_object__name(:object_id) as name + + + + + + select distinct acs_object__name(p.party_id) || ' (' || p.email || ')' as label, p.party_id + from [ad_decode $subquery "" "cc_users" $subquery] p + where upper(coalesce(acs_object__name(p.party_id) || ' ', '') || p.email) like upper('%'||:value||'%') + order by label + + + + + + select acs_object__name(p.party_id) || ' (' || p.email || ')' as label, p.party_id + from parties p + where p.party_id in ([join $party_id_list ", "]) + order by label + + + Index: openacs-4/packages/workflow/tcl/case-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/case-procs.tcl,v diff -u -r1.5 -r1.6 --- openacs-4/packages/workflow/tcl/case-procs.tcl 12 Feb 2003 14:23:14 -0000 1.5 +++ openacs-4/packages/workflow/tcl/case-procs.tcl 5 Mar 2003 17:18:10 -0000 1.6 @@ -162,6 +162,16 @@ if { ![exists_and_not_null user_id] } { set user_id [ad_conn user_id] } + return [util_memoize [list workflow::case::get_user_roles_not_cached $case_id $user_id] \ + [workflow::case::cache_timeout]] +} + +ad_proc -private workflow::case::get_user_roles_not_cached { case_id user_id } { + Used internally by the workflow Tcl API only. Goes to the database + to retrieve roles that user is assigned to. + + @author Peter Marklund +} { return [db_list select_user_roles {}] } @@ -176,6 +186,14 @@ @author Lars Pind (lars@collaboraid.biz) } { + return [util_memoize [list workflow::case::get_enabled_actions_not_cached $case_id] \ + [workflow::case::cache_timeout]] +} + +ad_proc -public workflow::case::get_enabled_actions_not_cached { case_id } { + Used internally by the workflow API only. Goes to the databaes to + get the enabled actions for the case. +} { return [db_list select_enabled_actions {}] } @@ -240,6 +258,8 @@ -role_id $role_id } } + + workflow::case::role::flush_cache $case_id } @@ -346,7 +366,7 @@ return $options } -ad_proc -public workflow::case::role::get_seach_query { +ad_proc -public workflow::case::role::get_search_query { {-case_id:required} {-role_id:required} } { @@ -374,18 +394,22 @@ -impl $impl_name \ -call_args [list $case_id $object_id $role_id]] + ns_log Notice "LARS: $contract_name, $impl_name = $subquery" + if { ![empty_string_p $subquery] } { # Return after the first non-empty list break } } - set query " - select distinct acs_object__name(p.party_id) || ' (' || p.email || ')' as label, p.party_id - from [ad_decode $subquery "" "cc_users" $subquery] p - where upper(coalesce(acs_object__name(p.party_id) || ' ', '') || p.email) like upper('%'||:value||'%') - order by label - " - return $query + + ns_log Notice "LARS: subquery = $subquery" + + ns_log Notice "LARS: returning [db_map select_search_results]" + + + return [db_map select_search_results] + + } ad_proc -public workflow::case::role::get_assignee_widget { @@ -405,9 +429,9 @@ workflow::role::get -role_id $role_id -array role set element "${prefix}$role(short_name)" - set query [workflow::case::role::get_seach_query -case_id $case_id -role_id $role_id] + set query [workflow::case::role::get_search_query -case_id $case_id -role_id $role_id] set picklist [workflow::case::role::get_picklist -case_id $case_id -role_id $role_id] - + return [list "${element}:search(search)" [list label $role(pretty_name)] [list mode display] \ [list search_query $query] [list options $picklist] optional] } @@ -445,7 +469,6 @@ } { set workflow_id [workflow::case::get_element -case_id $case_id -element workflow_id] - # LARS TODO: # Set role assignee values foreach role_id [workflow::get_roles -workflow_id $workflow_id] { workflow::role::get -role_id $role_id -array role @@ -490,16 +513,35 @@ @param case_id the ID of the case. @param role_id the ID of the role. + @return a list of + [array get]'s of party_id, email, name. @author Lars Pind (lars@collaboraid.biz) } { + return [util_memoize [list workflow::case::role::get_assignees_not_cached $case_id $role_id] \ + [workflow::case::cache_timeout]] +} + +ad_proc -private workflow::case::role::get_assignees_not_cached { case_id role_id } { + Proc used only internally by the workflow API. Retrieves role assignees + directly from the database. + + @author Peter Marklund +} { set result {} db_foreach select_assignees {} -column_array row { lappend result [array get row] } - return $result + return $result } +ad_proc -private workflow::case::role::flush_cache { case_id } { + Flush all role related info for a certain case. + +} { + util_memoize_flush_regexp [list workflow::case::role::get_assignees_not_cached $case_id] +} + ad_proc -public workflow::case::role::assignee_insert { {-case_id:required} {-role_id:required} @@ -531,6 +573,8 @@ } } } + + workflow::case::role::flush_cache $case_id } ad_proc -public workflow::case::role::assign { @@ -566,41 +610,153 @@ } } -ad_proc -public workflow::case::get_activity_html { - -case_id:required +ad_proc -private workflow::case::get_activity_html { + {-case_id:required} } { Get the activity log for a case as an HTML chunk + + @author Lars Pind } { - # LARS TODO: Template this + set log_html {} + set template { + @creation_date_pretty@ @action_pretty_past_tense@ @log_title@ by @community_member_link@ +
@comment_html@
+ } + + # Compile and evaluate the template + set code [template::adp_compile -string $template] + + foreach entry_arraylist [get_activity_log_info -case_id $case_id] { + foreach { var value } $entry_arraylist { + set $var $value + } + + set comment_html [ad_html_text_convert -from $comment_mime_type -to "text/html" -- $comment] + set community_member_link [acs_community_member_link -user_id $creation_user -label "$user_first_names $user_last_name"] + + append log_html [template::adp_eval code] + } + + return $log_html +} + +ad_proc -private workflow::case::get_activity_text { + {-case_id:required} +} { + Get the activity log for a case as a text chunk + + @author Lars Pind +} { + set log_text {} + + foreach entry_arraylist [get_activity_log_info -case_id $case_id] { + foreach { var value } $entry_arraylist { + set $var $value + } + + set entry_text "$creation_date_pretty $action_pretty_past_tense $log_title by $user_first_names $user_last_name ($user_email)" + + if { ![empty_string_p $comment] } { + append entry_text ":\n\n [join [split [ad_html_text_convert -from $comment_mime_type -to "text/plain" -maxlen 66 -- $comment] "\n"] "\n "]" + } + + lappend log_text $entry_text + + + } + return [join $log_text "\n\n"] +} + +ad_proc -private workflow::case::get_activity_log_info { + {-case_id:required} +} { + Get the data for the case activity log. + + @return a list of array-lists with the following entries: + comment comment_mime_type creation_date_pretty action_pretty_past_tense log_title + user_first_names user_last_name user_email creation_user data_arraylist + + @author Lars Pind +} { + global __cache__workflow__case__get_activity_log_info + if { ![info exists __cache__workflow__case__get_activity_log_info] } { + set __cache__workflow__case__get_activity_log_info [get_activity_log_info_not_cached -case_id $case_id] + } + return $__cache__workflow__case__get_activity_log_info +} + +ad_proc -private workflow::case::get_activity_log_info_not_cached { + {-case_id:required} +} { + Get the data for the case activity log. This version is cached for a single thread. + + @return a list of array-lists with the following entries: + comment comment_mime_type creation_date_pretty action_pretty_past_tense log_title + user_first_names user_last_name user_email creation_user data_arraylist + + @author Lars Pind +} { set workflow_id [workflow::case::get_element -case_id $case_id -element workflow_id] + set object_id [workflow::case::get_element -case_id $case_id -element object_id] set contract_name [workflow::service_contract::activity_log_format_title] # Get the name of any title Tcl callback proc set impl_names [workflow::get_callbacks \ -workflow_id $workflow_id \ -contract_name $contract_name] - # If there are more than one FormatLogTitle callback, we only use the first. - set impl_name [lindex $impl_names 0] + # First, we build up a multirow so we have all the data in memory, which lets us peek ahead at the contents + db_multirow -extend {comment} -local entries select_log {} { set comment $comment_string } - set log_html {} + + set rowcount [template::multirow -local size entries] + set counter 1 - db_foreach select_log {} { - if { ![empty_string_p $impl_name] } { - set log_title [acs_sc::invoke \ - -contract $contract_name \ - -operation "GetTitle" \ - -impl $impl_name \ - -call_args [list $entry_id]] - set log_title [ad_decode $log_title "" "" "($log_title)"] + set last_entry_id {} + set data_arraylist [list] + + # Then iterate over the multirow to build up the activity log HTML + # We need to peek ahead, because this is an outer join to get the rows in workflow_case_log_data + + set entries [list] + template::multirow -local foreach entries { + + if { ![empty_string_p $key] } { + lappend data_arraylist $key $value } - append log_html "$creation_date_pretty $action_pretty_past_tense $log_title by $user_first_names $user_last_name -
[ad_html_text_convert -from $comment_mime_type -to "text/html" -- $comment]
" + if { $counter == $rowcount || ![string equal $last_entry_id [set "entries:[expr $counter + 1](entry_id)"]] } { + + set log_title_elements [list] + foreach impl_name $impl_names { + set result [acs_sc::invoke \ + -contract $contract_name \ + -operation "GetTitle" \ + -impl $impl_name \ + -call_args [list $case_id $object_id $action_id $entry_id $data_arraylist]] + if { ![empty_string_p $result] } { + lappend log_title_elements $result + } + } + set log_title [ad_decode [llength $log_title_elements] 0 "" "([join $log_title_elements ", "])"] + + set row [list] + foreach var { + comment comment_mime_type creation_date_pretty action_pretty_past_tense log_title + user_first_names user_last_name user_email creation_user data_arraylist + } { + lappend row $var [set $var] + } + lappend entries $row + + set data_arraylist [list] + } + set last_entry_id $entry_id + incr counter } - - return $log_html + + return $entries } ad_proc workflow::case::get_notification_object { @@ -621,15 +777,12 @@ } return [workflow::case::get_element -case_id $case_id -element object_id] } - workflow { + default { if { ![exists_and_not_null workflow_id] } { return {} } return [workflow::get_element -workflow_id $workflow_id -element object_id] } - default { - return [apm_package_id_from_key [workflow::package_key]] - } } } @@ -750,18 +903,55 @@ upvar $array row if { [empty_string_p $action_id] } { - db_1row select_case_info {} -column_array row + array set row [util_memoize [list workflow::case::fsm::get_info_not_cached $case_id] \ + [workflow::case::cache_timeout]] set row(entry_id) {} } else { + # TODO: cache this query as well db_1row select_case_info_after_action {} -column_array row set row(entry_id) [db_nextval "acs_object_id_seq"] } } +ad_proc -private workflow::case::cache_timeout {} { + Number of seconds before we timeout the case level workflow cache. + @author Peter Marklund +} { + # 60 * 60 seconds is 1 hour + return 3600 +} +ad_proc -private workflow::case::fsm::get_info_not_cached { case_id } { + Used internally by the workflow id to get FSM case info from the + database. + @author Peter Marklund +} { + db_1row select_case_info {} -column_array row + return [array get row] +} + +ad_proc -private workflow::case::flush_cache { case_id } { + Flush all cached data for the given case. + + @author Peter Marklund +} { + # Flush scalar attributes (for fsm::get proc) + util_memoize_flush [list workflow::case::fsm::get_info_not_cached $case_id] + + # Flush role info (assignees etc) + workflow::case::role::flush_cache $case_id + + # Flush roles + util_memoize_flush_regexp [list workflow::case::get_user_roles_not_cached $case_id] + + # Flush enabled actions + util_memoize_flush [list workflow::case::get_enabled_actions_not_cached $case_id] +} + + ##### # # workflow::case::action @@ -952,12 +1142,14 @@ # Notifications notify \ - -case_id $case_id \ - -action_id $action_id \ - -entry_id $entry_id \ - -comment $comment \ - -comment_mime_type $comment_mime_type + -case_id $case_id \ + -action_id $action_id \ + -entry_id $entry_id \ + -comment $comment \ + -comment_mime_type $comment_mime_type } + + workflow::case::flush_cache $case_id return $entry_id } @@ -1012,69 +1204,167 @@ } { Send out notifications to relevant people. } { - # LARS TODO: - # Not implemented yet - return - # Get workflow_id workflow::case::get \ -case_id $case_id \ -array case workflow::get \ - -workflow_id $workflow_id \ + -workflow_id $case(workflow_id) \ -array workflow - # LARS TODO: - # we probably need a callback to format the message... - set subject "New notification" - set body "Here's the body" + set hr [string repeat "=" 70] - # LARS TODO: - # List of user_id's for people who are assigned to some task - # Don't forget to map parties to users - set assignee_list [list] + array set latest_action [lindex [workflow::case::get_activity_log_info -case_id $case_id] end] + + set latest_action_chunk "$latest_action(action_pretty_past_tense) $latest_action(log_title) by $latest_action(user_first_names) $latest_action(user_last_name) ($latest_action(user_email))" + + if { ![empty_string_p $latest_action(comment)] } { + append latest_action_chunk ":\n\n [join [split [ad_html_text_convert -from $latest_action(comment_mime_type) -to "text/plain" -maxlen 66 -- $latest_action(comment)] "\n"] "\n "]" + } + # Callback to get notification info + set contract_name [workflow::service_contract::notification_info] + set impl_names [workflow::get_callbacks \ + -workflow_id $case(workflow_id) \ + -contract_name $contract_name] + # We only use the first callback + set impl_name [lindex $impl_names 0] + + if { ![empty_string_p $impl_name] } { + set notification_info [acs_sc::invoke \ + -contract $contract_name \ + -operation "GetNotificationInfo" \ + -impl $impl_name \ + -call_args [list $case_id $case(object_id)]] + + } + + # Make sure the notification info list has at least 4 elements, so we can do below lindex's safely + lappend notification_info {} {} {} {} + + set object_url [lindex $notification_info 0] + set object_one_line [lindex $notification_info 1] + set object_details_list [lindex $notification_info 2] + set object_notification_tag [lindex $notification_info 3] + + if { [empty_string_p $object_one_line] } { + # Default: Case #$case_id: acs_object__name(case.object_id) + + set object_id $case(object_id) + db_1row select_object_name {} -column_array case_object + + set object_one_line "Case #$case_id: $case_object(name)" + } + + # Roles and their current assignees + foreach role_id [workflow::get_roles -workflow_id $case(workflow_id)] { + set label [workflow::role::get_element -role_id $role_id -element pretty_name] + foreach assignee_arraylist [workflow::case::role::get_assignees -case_id $case_id -role_id $role_id] { + array set assignee $assignee_arraylist + lappend object_details_list $label "$assignee(name) ($assignee(email))" + set label {} + } + } + + # Find the length of the longest label + set max_label_len 0 + foreach { label value } $object_details_list { + if { [string length $label] > $max_label_len } { + set max_label_len [string length $label] + } + } + + # Output notification info + set object_details_lines [list] + foreach { label value } $object_details_list { + if { ![empty_string_p $label] } { + lappend object_details_lines "$label[string repeat " " [expr $max_label_len - [string length $label]]] : $value" + } else { + lappend object_details_lines "[string repeat " " $max_label_len] $value" + } + } + set object_details_chunk [join $object_details_lines "\n"] + + set activity_log_chunk [workflow::case::get_activity_text -case_id $case_id] + + set the_subject "[ad_decode $object_notification_tag "" "" "\[$object_notification_tag\] "]$object_one_line: $latest_action(action_pretty_past_tense) $latest_action(log_title) by $latest_action(user_first_names) $latest_action(user_last_name)" + + # List of user_id's for people who are in the assigned_role to any enabled actions + set assignee_list [db_list enabled_action_assignees {}] + # List of users who play some role in this case - set case_player_list [list] + set case_player_list [db_list case_players {}] - # LARS TODO: - # We want the subject/body to be customized depending on the type of notification + # Get pretty_name and pretty_plural for the case's object type + set object_id $case(object_id) + db_1row select_object_type_info {} -column_array object_type + # Get name of the workflow's object + set object_id $workflow(object_id) + db_1row select_object_name {} -column_array workflow_object + + set next_action_chunk(workflow_assignee) "You are assigned to the next action." + + set next_action_chunk(workflow_my_cases) "You are a participant in this $object_type(pretty_name)." + + set next_action_chunk(workflow_case) "You have a watch on this $object_type(pretty_name)." + + set next_action_chunk(workflow) "You have requested to be notified about activity on all $object_type(pretty_plural) in this $workflow_object(name)." + + # Initialize stuff that depends on the notification type foreach type { workflow_assignee workflow_my_cases workflow_case workflow } { - set subject($type) $subject - set body($type) $body + set subject($type) $the_subject + set body($type) "$hr +$object_one_line +$hr + +$latest_action_chunk + +$hr + +$next_action_chunk($type)[ad_decode $object_url "" "" "\n\nPlease click here to visit this $object_type(pretty_name):\n\n$object_url"] + +$hr[ad_decode $object_details_chunk "" "" "\n$object_details_chunk\n$hr"] + +$activity_log_chunk + +$hr +" set force_p($type) 0 - set intersection($type) {} + set subset($type) {} } set force_p(workflow_assignee) 1 - set intersection(workflow_assignee) $assignee_list - set intersection(workflow_my_cases) $case_player_list - + set subset(workflow_assignee) $assignee_list + set subset(workflow_my_cases) $case_player_list set notified_list [list] foreach type { workflow_assignee workflow_my_cases workflow_case workflow } { - set object_id [get_notification_object \ + set object_id [workflow::case::get_notification_object \ -type $type \ - -workflow_id $workflow_id \ + -workflow_id $case(workflow_id) \ -case_id $case_id] if { ![empty_string_p $object_id] } { - set notified_list [notification::new \ + + ns_log Notice "LARS: $body($type)" + + set notified_list [concat $notified_list [notification::new \ -type_id [notification::type::get_type_id -short_name $type] \ -object_id $object_id \ + -action_id $entry_id \ -response_id $case(object_id) \ -notif_subject $subject($type) \ -notif_text $body($type) \ -already_notified $notified_list \ - -intersection $intersection($type) \ - -force=$force_p($type)] + -subset $subset($type) \ + -return_notified]] } } } Index: openacs-4/packages/workflow/tcl/case-procs.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/case-procs.xql,v diff -u -r1.5 -r1.6 --- openacs-4/packages/workflow/tcl/case-procs.xql 12 Feb 2003 14:23:15 -0000 1.5 +++ openacs-4/packages/workflow/tcl/case-procs.xql 5 Mar 2003 17:18:10 -0000 1.6 @@ -1,6 +1,16 @@ + + + select state_id + from workflow_fsm_states + where workflow_id = :workflow_id + order by sort_order + limit 1 + + + insert into workflow_cases ( @@ -32,7 +42,7 @@ - + select distinct rpm.role_id from workflow_case_role_party_map rpm, @@ -43,7 +53,7 @@ - + select a.action_id from workflow_cases c, @@ -90,15 +100,6 @@ - - - select acs_object__name(p.party_id) || ' (' || p.email || ')' as label, p.party_id - from parties p - where p.party_id in ([join $party_id_list ", "]) - order by label - - - delete from workflow_case_role_party_map @@ -134,24 +135,6 @@ - - - select c.case_id, - c.workflow_id, - c.object_id, - s.state_id, - s.short_name as state_short_name, - s.pretty_name as pretty_state, - s.hide_fields as state_hide_fields - from workflow_cases c, - workflow_case_fsm cfsm, - workflow_fsm_states s - where c.case_id = :case_id - and cfsm.case_id = c.case_id - and s.state_id = cfsm.current_state - - - select c.case_id, @@ -199,9 +182,57 @@ select count(*) - from cr_revisions - where revision_id = :entry_id + from cr_items + where item_id = :entry_id + + + select distinct u.user_id + from workflow_cases c, + workflow_actions a, + workflow_case_role_party_map rpm, + party_approved_member_map pmm, + users u + where c.case_id = :case_id + and a.workflow_id = c.workflow_id + and (a.always_enabled_p = 't' or + exists (select 1 + from workflow_fsm_action_en_in_st waeis, + workflow_case_fsm c_fsm + where waeis.action_id = a.action_id + and c_fsm.case_id = c.case_id + and waeis.state_id = c_fsm.current_state) + ) + and rpm.case_id = c.case_id + and rpm.role_id = a.assigned_role + and pmm.party_id = rpm.party_id + and u.user_id = pmm.member_id + + + + + + select distinct u.user_id + from workflow_case_role_party_map rpm, + party_approved_member_map pmm, + users u + where rpm.case_id = :case_id + and rpm.party_id = pmm.party_id + and pmm.member_id = u.user_id + + + + + + select lower(pretty_name) as pretty_name, + lower(pretty_plural) as pretty_plural + from acs_object_types ot, + acs_objects o + where o.object_id = :object_id + and ot.object_type = o.object_type + + + Index: openacs-4/packages/workflow/tcl/implementation-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/implementation-procs.tcl,v diff -u -r1.2 -r1.3 --- openacs-4/packages/workflow/tcl/implementation-procs.tcl 12 Feb 2003 14:23:15 -0000 1.2 +++ openacs-4/packages/workflow/tcl/implementation-procs.tcl 5 Mar 2003 17:18:10 -0000 1.3 @@ -115,6 +115,7 @@ } { Return a subquery for all registered users. } { + ns_log Notice "LARS: getsubquery returning [db_map cc_users]" return [db_map cc_users] } Index: openacs-4/packages/workflow/tcl/implementation-procs.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/implementation-procs.xql,v diff -u -r1.2 -r1.3 --- openacs-4/packages/workflow/tcl/implementation-procs.xql 12 Feb 2003 14:23:15 -0000 1.2 +++ openacs-4/packages/workflow/tcl/implementation-procs.xql 5 Mar 2003 17:18:10 -0000 1.3 @@ -28,7 +28,7 @@
- + cc_users Index: openacs-4/packages/workflow/tcl/install-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/install-procs.tcl,v diff -u -r1.4 -r1.5 --- openacs-4/packages/workflow/tcl/install-procs.tcl 12 Feb 2003 14:23:15 -0000 1.4 +++ openacs-4/packages/workflow/tcl/install-procs.tcl 5 Mar 2003 17:18:10 -0000 1.5 @@ -28,19 +28,21 @@ register_implementations + register_notification_types } } ad_proc -private workflow::install::package_uninstall {} { Workflow package uninstall proc } { - db_transaction { - delete_service_contracts + unregister_notification_types unregister_implementations + delete_service_contracts + } } @@ -67,6 +69,7 @@ workflow::install::create_activity_log_format_title_service_contract + workflow::install::create_get_notification_info_service_contract } } @@ -85,6 +88,7 @@ acs_sc::contract::delete -name [workflow::service_contract::activity_log_format_title] + acs_sc::contract::delete -name [workflow::service_contract::notification_info] } } @@ -222,7 +226,7 @@ ad_proc -private workflow::install::create_activity_log_format_title_service_contract {} { set format_title_spec { - description "Create the title format for activity log" + description "Output additional details for the title of an activity log entry" operations { GetObjectType { description "Get the object type for which this implementation is valid." @@ -239,7 +243,11 @@ GetTitle { description "Get the title name of this implementation." input { - entry_id:integer + case_id:integer + object_id:integer + action_id:integer + entry_id:integer + data_arraylist:string,multiple } output { title:string @@ -253,6 +261,41 @@ -spec [concat [list name [workflow::service_contract::activity_log_format_title]] $format_title_spec] } +ad_proc -private workflow::install::create_get_notification_info_service_contract {} { + + set notification_info_spec { + description "Get information for notifications" + operations { + GetObjectType { + description "Get the object type for which this implementation is valid." + output { + object_type:string + } + iscachable_p "t" + } + GetPrettyName { + description "Get the pretty name of this implementation. Will be localized, so it may contain #...#." + output { object_type:string } + iscachable_p "t" + } + GetNotificationInfo { + description "Get the notification information as a 4-element list containing url, one-line summary, details about the object in the form of an array-list with label/value, and finally an optional tag for the notification subject, in the order mentioned here." + input { + case_id:integer + object_id:integer + } + output { + info:string,multiple + } + iscachable_p "f" + } + } + } + + acs_sc::contract::new_from_spec \ + -spec [concat [list name [workflow::service_contract::notification_info]] $notification_info_spec] +} + ##### # # Register implementations @@ -275,7 +318,6 @@ workflow::install::register_notification_impl - workflow::install::register_notification_types } } @@ -436,3 +478,15 @@ db_dml enable_all_delivery_methods {} } } + +ad_proc -public workflow::install::unregister_notification_types {} { + Unregister workflow notification types +} { + db_transaction { + notification::type::delete -short_name "workflow_assignee" + notification::type::delete -short_name "workflow_my_cases" + notification::type::delete -short_name "workflow_case" + notification::type::delete -short_name "workflow" + } +} + Index: openacs-4/packages/workflow/tcl/install-procs.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/install-procs.xql,v diff -u -r1.1 -r1.2 --- openacs-4/packages/workflow/tcl/install-procs.xql 12 Feb 2003 14:23:15 -0000 1.1 +++ openacs-4/packages/workflow/tcl/install-procs.xql 5 Mar 2003 17:18:10 -0000 1.2 @@ -6,7 +6,7 @@ insert into notification_types_intervals (type_id, interval_id) select :type_id, interval_id - from notification_intervals; + from notification_intervals @@ -15,7 +15,7 @@ insert into notification_types_del_methods (type_id, delivery_method_id) select :type_id, delivery_method_id - from notification_delivery_methods; + from notification_delivery_methods Index: openacs-4/packages/workflow/tcl/role-procs-oracle.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/Attic/role-procs-oracle.xql,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/workflow/tcl/role-procs-oracle.xql 5 Mar 2003 17:18:10 -0000 1.1 @@ -0,0 +1,13 @@ + + + oracle8.1.6 + + + + select nvl(max(sort_order),0) + 1 + from workflow_role_callbacks + where role_id = :role_id + + + + Index: openacs-4/packages/workflow/tcl/role-procs-postgresql.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/Attic/role-procs-postgresql.xql,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/workflow/tcl/role-procs-postgresql.xql 5 Mar 2003 17:18:10 -0000 1.1 @@ -0,0 +1,13 @@ + + + postgresql7.2 + + + + select coalesce(max(sort_order),0) + 1 + from workflow_role_callbacks + where role_id = :role_id + + + + Index: openacs-4/packages/workflow/tcl/role-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/role-procs.tcl,v diff -u -r1.6 -r1.7 --- openacs-4/packages/workflow/tcl/role-procs.tcl 12 Feb 2003 14:23:15 -0000 1.6 +++ openacs-4/packages/workflow/tcl/role-procs.tcl 5 Mar 2003 17:18:10 -0000 1.7 @@ -55,6 +55,7 @@ db_dml do_insert {} } + return $role_id } @@ -272,7 +273,7 @@ array unset row callbacks_array array unset row callback_ids array unset row callback_impl_names - + # Get rid of empty strings foreach name [array names row] { if { [empty_string_p $row($name)] } { @@ -337,6 +338,9 @@ db_dml insert_callback {} } + set workflow_id [workflow::role::get_workflow_id -role_id $role_id] + workflow::role::flush_cache -workflow_id $workflow_id + return $acs_sc_impl_id } @@ -349,7 +353,7 @@ @author Peter Marklund } { # TODO: Flush request cache - # ... + # no request cache to flush yet # Flush the thread global cache util_memoize_flush [list workflow::role::get_all_info_not_cached -workflow_id $workflow_id] @@ -399,7 +403,6 @@ [list workflow::role::get_workflow_id_not_cached -role_id $role_id] \ $workflow_id } - unset row # Get the callbacks of all roles of the workflow foreach role_id $role_ids { Index: openacs-4/packages/workflow/tcl/role-procs.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/role-procs.xql,v diff -u -r1.6 -r1.7 --- openacs-4/packages/workflow/tcl/role-procs.xql 12 Feb 2003 14:23:15 -0000 1.6 +++ openacs-4/packages/workflow/tcl/role-procs.xql 5 Mar 2003 17:18:10 -0000 1.7 @@ -71,14 +71,6 @@ - - - select coalesce(max(sort_order),0) + 1 - from workflow_role_callbacks - where role_id = :role_id - - - insert into workflow_role_callbacks (role_id, acs_sc_impl_id, sort_order) Index: openacs-4/packages/workflow/tcl/state-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/state-procs.tcl,v diff -u -r1.3 -r1.4 --- openacs-4/packages/workflow/tcl/state-procs.tcl 12 Feb 2003 14:23:15 -0000 1.3 +++ openacs-4/packages/workflow/tcl/state-procs.tcl 5 Mar 2003 17:18:10 -0000 1.4 @@ -53,7 +53,6 @@ db_dml do_insert {} } - # State info for the workflow changed, flush whole state cache workflow::state::flush_cache -workflow_id $workflow_id return $state_id @@ -71,7 +70,10 @@ # Select the info into the upvar'ed Tcl Array upvar $array row - db_1row state_info {} -column_array row + set workflow_id [workflow::state::fsm::get_workflow_id -state_id $state_id] + array set state_data [workflow::state::fsm::get_all_info -workflow_id $workflow_id] + + array set row $state_data($state_id) } ad_proc -public workflow::state::fsm::get_element { @@ -198,7 +200,7 @@ array unset row state_id array unset row workflow_id array unset row sort_order - + # Get rid of empty strings foreach name [array names row] { if { [empty_string_p $row($name)] } { @@ -249,7 +251,7 @@ # ... # Flush the thread global cache - util_memoize_flush [list workflow::state::get_all_info_not_cached -workflow_id $workflow_id] + util_memoize_flush [list workflow::state::fsm::get_all_info_not_cached -workflow_id $workflow_id] } ad_proc -private workflow::state::fsm::get_all_info { @@ -284,7 +286,6 @@ set state_ids [list] db_foreach select_states {} -column_array state_row { # Cache the state_id -> workflow_id lookup - util_memoize_seed \ [list workflow::state::fsm::get_workflow_id_not_cached -state_id $state_row(state_id)] \ $workflow_id Index: openacs-4/packages/workflow/tcl/state-procs.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/state-procs.xql,v diff -u -r1.3 -r1.4 --- openacs-4/packages/workflow/tcl/state-procs.xql 12 Feb 2003 14:23:15 -0000 1.3 +++ openacs-4/packages/workflow/tcl/state-procs.xql 5 Mar 2003 17:18:10 -0000 1.4 @@ -27,19 +27,6 @@ - - - select state_id, - workflow_id, - sort_order, - short_name, - pretty_name, - hide_fields - from workflow_fsm_states - where state_id = :state_id - - - select state_id Index: openacs-4/packages/workflow/tcl/workflow-procs-oracle.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/workflow-procs-oracle.xql,v diff -u -r1.1 -r1.2 --- openacs-4/packages/workflow/tcl/workflow-procs-oracle.xql 12 Feb 2003 14:23:15 -0000 1.1 +++ openacs-4/packages/workflow/tcl/workflow-procs-oracle.xql 5 Mar 2003 17:18:10 -0000 1.2 @@ -1,26 +1,38 @@ - oracle8.1.7 + oracle8.1.6 - workflow.new ( - :short_name, - :pretty_name, - :package_key, - :object_id, - :object_type, - :creation_user, - :creation_ip, - :context_id + begin + :1 := workflow.new ( + short_name => :short_name, + pretty_name => :pretty_name, + package_key => :package_key, + object_id => :object_id, + object_type => :object_type, + creation_user => :creation_user, + creation_ip => :creation_ip, + context_id => :context_id ); + end; - acs_object.delete(:workflow_id) from dual; + begin + :1 := workflow.delete(:workflow_id); + end; + + + + select nvl(max(sort_order),0) + 1 + from workflow_callbacks + where workflow_id = :workflow_id + + Index: openacs-4/packages/workflow/tcl/workflow-procs-postgresql.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/workflow-procs-postgresql.xql,v diff -u -r1.4 -r1.5 --- openacs-4/packages/workflow/tcl/workflow-procs-postgresql.xql 21 Jan 2003 18:06:00 -0000 1.4 +++ openacs-4/packages/workflow/tcl/workflow-procs-postgresql.xql 5 Mar 2003 17:18:10 -0000 1.5 @@ -19,8 +19,16 @@ - select acs_object__delete(:workflow_id); + select workflow__delete(:workflow_id); + + + + select coalesce(max(sort_order),0) + 1 + from workflow_callbacks + where workflow_id = :workflow_id + + - + Index: openacs-4/packages/workflow/tcl/workflow-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/workflow-procs.tcl,v diff -u -r1.6 -r1.7 --- openacs-4/packages/workflow/tcl/workflow-procs.tcl 12 Feb 2003 14:23:15 -0000 1.6 +++ openacs-4/packages/workflow/tcl/workflow-procs.tcl 5 Mar 2003 17:18:10 -0000 1.7 @@ -67,10 +67,11 @@ } # Insert the workflow - set workflow_id [db_string do_insert {}] + set workflow_id [db_exec_plsql do_insert {}] # Callbacks foreach callback_name $callbacks { + ns_log Notice "callback_name = $callback_name" workflow::callback_insert \ -workflow_id $workflow_id \ -name $callback_name @@ -90,6 +91,15 @@ return $workflow_id } +ad_proc -public workflow::exists_p { + {-workflow_id:required} +} { + Return 1 if the workflow with given id exists and 0 otherwise. + This proc is currently not cached. +} { + return [db_string do_select {} +} + ad_proc -public workflow::delete { {-workflow_id:required} } { @@ -101,32 +111,31 @@ } { workflow::flush_cache -workflow_id $workflow_id - return [db_string do_delete {}] + return [db_exec_plsql do_delete {}] } ad_proc -public workflow::get_id { {-package_key {}} {-object_id {}} {-short_name:required} } { - Get workflow_id by short_name and object_id. + Get workflow_id by short_name and object_id. Provide either package_key + or object_id. @param object_id The ID of the object the workflow's for (typically a package instance) + @param package_key The key of the package workflow belongs to. @param short_name the short name of the workflow you want + @return The id of the workflow or the empty string if no workflow was found. + @author Lars Pind (lars@collaboraid.biz) } { - set workflow_id [util_memoize [list workflow::get_id_not_cached \ -package_key $package_key \ -object_id $object_id \ -short_name $short_name] [workflow::cache_timeout]] - if { ![empty_string_p $workflow_id] } { - return $workflow_id - } else { - error "No workflow found with object_id $object_id and short_name $short_name" - } + return $workflow_id } ad_proc -public workflow::get { @@ -369,7 +378,18 @@ } } +ad_proc -public workflow::get_notification_links { + {-workflow_id:required} + {-case_id} + {-return_url} +} { + Return a links to sign up for notifications. + @return A multirow with columns url, label, title +} { + +} + ##### # # workflow::fsm namespace @@ -467,6 +487,13 @@ set spec [list] + # Get rid of empty strings + foreach name [array names row] { + if { [empty_string_p $row($name)] } { + array unset row $name + } + } + foreach name [lsort [array names row]] { lappend spec $name $row($name) } @@ -606,6 +633,10 @@ return "[workflow::package_key].ActivityLog_FormatTitle" } +ad_proc -public workflow::service_contract::notification_info {} { + return "[workflow::package_key].NotificationInfo" +} + ad_proc -public workflow::service_contract::get_impl_id { {-name:required} } { Index: openacs-4/packages/workflow/tcl/workflow-procs.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/workflow-procs.xql,v diff -u -r1.5 -r1.6 --- openacs-4/packages/workflow/tcl/workflow-procs.xql 12 Feb 2003 14:23:15 -0000 1.5 +++ openacs-4/packages/workflow/tcl/workflow-procs.xql 5 Mar 2003 17:18:10 -0000 1.6 @@ -10,6 +10,12 @@ + + + select count(*) from workflows where workflow_id = :workflow_id + + + select w.workflow_id, @@ -65,15 +71,7 @@ where workflow_id = :workflow_id - - - - select coalesce(max(sort_order),0) + 1 - from workflow_callbacks - where workflow_id = :workflow_id - - - + insert into workflow_callbacks (workflow_id, acs_sc_impl_id, sort_order) Index: openacs-4/packages/workflow/tcl/test/test-workflow.sql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/test/test-workflow.sql,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/workflow/tcl/test/test-workflow.sql 5 Mar 2003 17:18:38 -0000 1.1 @@ -0,0 +1,3 @@ +select workflow__new('test_workflow', 'Test Workflow', '2706', 'acs_object', '3606', '122.122.122.122', '2706'); +insert into workflow_actions select nextval('workflow_actions_seq'), '4212', '1', 'test_action', 'Test Action', NULL, NULL; +insert into workflow_fsm_actions select action_id, NULL from workflow_actions where short_name = 'test_action'; Index: openacs-4/packages/workflow/tcl/test/workflow-test-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/test/workflow-test-procs.tcl,v diff -u -r1.5 -r1.6 --- openacs-4/packages/workflow/tcl/test/workflow-test-procs.tcl 12 Feb 2003 14:24:02 -0000 1.5 +++ openacs-4/packages/workflow/tcl/test/workflow-test-procs.tcl 5 Mar 2003 17:18:38 -0000 1.6 @@ -106,6 +106,36 @@ [empty_string_p $user_roles] } + +ad_proc workflow::test::get_message_key_spec {} { + Get a workflow array style spec containing message keys. +} { + set spec { + pretty_name "#acs-subsite.About_You#" + package_key "acs-automated-testing" + object_type "acs_object" + roles { + short_name { + pretty_name "#acs-subsite.Bad_Password#" + } + } + states { + foobar { + pretty_name "#acs-subsite.Basic_Information#" + } + } + actions { + foobar { + pretty_name "#acs-subsite.Confirm#" + pretty_past_tense "#acs-subsite.Customize_Questions#" + initial_action_p t + } + } + } + + return [list test_message_keys $spec] +} + ad_proc workflow::test::workflow_get_array_style_spec {} { Get the array-style spec for a workflow for the Bug Tracker Bug use case. @@ -114,7 +144,6 @@ pretty_name "Bug Test" package_key "acs-automated-testing" object_type "acs_object" - callbacks { bug-tracker.FormatLogTitle } roles { submitter { pretty_name "Submitter" @@ -124,10 +153,6 @@ } assignee { pretty_name "Assignee" - callbacks { - bug-tracker.ComponentMaintainer - bug-tracker.ProjectMaintainer - } } } states { @@ -152,40 +177,40 @@ pretty_name "Comment" pretty_past_tense "Commented" allowed_roles { submitter assignee } - privileges { read } + privileges read always_enabled_p t } edit { pretty_name "Edit" pretty_past_tense "Edited" allowed_roles { submitter assignee } - privileges { write } + privileges write always_enabled_p t } resolve { pretty_name "Resolve" pretty_past_tense "Resolved" assigned_role assignee - enabled_states { open resolved } + enabled_states { resolved } + assigned_states { open } new_state "resolved" - privileges { write } - callbacks { bug-tracker.CaptureResolutionCode } + privileges write } close { pretty_name "Close" pretty_past_tense "Closed" assigned_role submitter - enabled_states { resolved } + assigned_states resolved new_state "closed" - privileges { write } + privileges write } reopen { pretty_name "Reopen" pretty_past_tense "Closed" - allowed_roles { submitter } + allowed_roles submitter enabled_states { resolved closed } new_state "open" - privileges { write } + privileges write } } } @@ -277,8 +302,7 @@ -pretty_name "Bug Test" \ -package_key "acs-automated-testing" \ -object_id [workflow::test::workflow_object_id] \ - -object_type "acs_object" \ - -callbacks { bug-tracker.FormatLogTitle }] + -object_type "acs_object" ] ##### # @@ -289,15 +313,11 @@ workflow::role::new -workflow_id $workflow_id \ -short_name "submitter" \ -pretty_name "Submitter" \ - -callbacks { workflow.Role_DefaultAssignees_CreationUser } + -callbacks workflow.Role_DefaultAssignees_CreationUser workflow::role::new -workflow_id $workflow_id \ -short_name "assignee" \ -pretty_name "Assignee" \ - -callbacks { - bug-tracker.ComponentMaintainer - bug-tracker.ProjectMaintainer - } ##### # @@ -337,7 +357,7 @@ -pretty_name "Comment" \ -pretty_past_tense "Commented" \ -allowed_roles { submitter assignee } \ - -privileges { read } \ + -privileges read \ -always_enabled_p t workflow::action::fsm::new \ @@ -346,7 +366,7 @@ -pretty_name "Edit" \ -pretty_past_tense "Edited" \ -allowed_roles { submitter assignee } \ - -privileges { write } \ + -privileges write \ -always_enabled_p t workflow::action::fsm::new \ @@ -355,20 +375,20 @@ -pretty_name "Resolve" \ -pretty_past_tense "Resolved" \ -assigned_role assignee \ - -enabled_states { open resolved } \ + -enabled_states resolved \ + -assigned_states open \ -new_state "resolved" \ - -privileges { write } \ - -callbacks { bug-tracker.CaptureResolutionCode } + -privileges write workflow::action::fsm::new \ -workflow_id $workflow_id \ -short_name "close" \ -pretty_name "Close" \ -pretty_past_tense "Closed" \ -assigned_role submitter \ - -enabled_states { resolved } \ + -assigned_states resolved \ -new_state "closed" \ - -privileges { write } + -privileges write workflow::action::fsm::new \ -workflow_id $workflow_id \ @@ -378,7 +398,7 @@ -allowed_roles submitter \ -enabled_states { resolved closed } \ -new_state "open" \ - -privileges { write } + -privileges write return $workflow_id } @@ -411,23 +431,43 @@ return $case_id } +ad_proc workflow::test::run_with_teardown { + test_chunk + teardown_chunk +} { + Execute code in test chunk and guarantee that code in + teardown_chunk will be executed even if error if thrown. + + @author Peter Marklund +} { + set error_p [catch $test_chunk errMsg] + + global errorInfo + set setup_error_stack $errorInfo + + # Teardown + eval $teardown_chunk + + if { $error_p } { + aa_false "error during setup: $errMsg - $setup_error_stack" $error_p + } +} + ad_proc workflow::test::run_bug_tracker_test { {-create_proc "workflow_setup"} } { + # Make sure to run the teardown proc even if there is an error set test_chunk { # Setup - # Make sure to run the teardown proc even if there is an error - # Cannot get this to work as it seems the catch will return true - # if any catch did so in the executed code. - # set error_p [catch workflow::test::workflow_setup error] + set workflow_id [$create_proc] set generated_spec [workflow::fsm::generate_spec -workflow_id $workflow_id] - ns_log Notice "LARS: Generated spec: $generated_spec" - ns_log Notice "LARS: Hard-coded spec: [workflow_get_array_style_spec]" + ns_log Notice "LARS: Generated spec 1: $generated_spec" + ns_log Notice "LARS: Hard-coded spec 1: [workflow_get_array_style_spec]" - aa_true "Checking that generated spec is identical to the spec that we created from (except for ordering)" \ + aa_true "Checking that generated spec 1 is identical to the spec that we created from (except for ordering)" \ [array_lists_equal_p $generated_spec [workflow_get_array_style_spec]] @@ -601,3 +641,32 @@ } } +aa_register_case workflow_spec_with_message_keys { + Test creating a workflow from a spec with message catalog + keys in it and then generating a spec from that workflow + and making sure that the spec is preserved (message keys are not + localized) + + @author Peter Marklund +} { + set test_chunk { + + set workflow_id [workflow::fsm::new_from_spec \ + -spec [workflow::test::get_message_key_spec]] + + set generated_spec [workflow::fsm::generate_spec -workflow_id $workflow_id] + + ns_log Notice "LARS: Generated spec 2: $generated_spec" + ns_log Notice "LARS: Hard-coded spec 2: [workflow::test::get_message_key_spec]" + + aa_true "Checking that generated spec 2 is identical to the spec that we created from (except for ordering)" \ + [array_lists_equal_p $generated_spec [workflow::test::get_message_key_spec]] + } + + set teardown_chunk { + set workflow_id [workflow::get_id -package_key acs-automated-testing -short_name test_message_keys] + workflow::delete -workflow_id $workflow_id + } + + workflow::test::run_with_teardown $test_chunk $teardown_chunk +} Index: openacs-4/packages/workflow/www/doc/developer-guide.html =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/www/doc/developer-guide.html,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/workflow/www/doc/developer-guide.html 5 Mar 2003 17:19:09 -0000 1.1 @@ -0,0 +1,62 @@ + +Package Developer's Guide to Workflow + + + +

Package Developer's Guide to Workflow

+ +

+ By Lars Pind +

+ +

Introduction

+ +

Workflow is used to coordinate the actions of multiple people +working together to accomplish something.

+ +

For example, when a new bug is submitted, someone's assigned to +fix it, and whoever submitted it is assigned to verify the fix and +close the bug. Once the bug's fixed, the submitter will get notified, +and the bug will wait in the 'resolved' state until the submitter has +verified and then closed the bug.

In order to make use of workflow in your own application, here are +the things you need to consider:

+ +
    + +
  1. Define your default process. The idea typically is to allow + your end users to modify the process to suit their needs, but you'll + want eto provide a process which they can use as a starting point. +
  2. + +
  3. Identify, declare, and implement the callbacks that your + application will need.
  4. + +
  5. Write the code to set up the initial process, and to clone that + process for each package instance.
  6. + +
  7. Integrate workflow support into your application's API.
  8. + +
  9. Integrate workflow support into your application's user + interface.
  10. + +
  11. Integrate workflow into your application's queries
  12. + + + +

    Let's look at each of these in order.

    + +

    Defining Your Process

    + +

    Your process + + +


    +
    lars@pinds.com
    + + + + Index: openacs-4/packages/workflow/www/doc/index.html =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/www/doc/index.html,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/workflow/www/doc/index.html 5 Mar 2003 17:19:09 -0000 1.1 @@ -0,0 +1,49 @@ + +Workflow Documentation + + + +

    Workflow Documentation

    + +

    + By Lars Pind +

    + +
    + +

    + The workflow package provides a service to keep track of a process + involving multiple people around some object. Workflow keeps track + of the process you wish to follow, of where you currently are in the + process (the current state), and who's supposed to do what. +

    + +

    + Here's what we have to offer you today: +

    + +
    +
    + Package Developer's Guide to Workflow +
    +
    + This is for developers developing applications that should take advantage of + the workflow service. +
    +
    + Functional Specification +
    +
    + This is the document we wrote before implementing workflow specifying + how we intended to implement the package then. +
    +
    + +
    +
    lars@pinds.com
    + + + + Index: openacs-4/packages/workflow/www/doc/specification.html =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/www/doc/specification.html,v diff -u --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/workflow/www/doc/specification.html 5 Mar 2003 17:19:09 -0000 1.1 @@ -0,0 +1,1329 @@ + +Workflow Specification + + +

    Workflow Specification

    + +

    + By Lars Pind +

    + +

    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. +

    + +

    Goals

    + +

    + The goal is to implement a workflow package that: +

    + +
      +
    • + Is ideally suited for at least 3 use-cases: Bug-tracker, CMS-style + publication process, and simple approval. +
    • +
    • + Gives people a usable UI. +
    • +
    • + Can be used entirely through a clean Tcl API +
    • +
    • + Doesn't require people to learn Petri Nets +
    • +
    • + Is much easier for developers to use in their applications +
    • +
    + +

    + Gripes with the current acs-workflow: +

    + +
      +
    • + Engine is in PL/SQL, not in Tcl, which makes it hard to write + callbacks. +
    • +
    • + Petri net is just too complicated for people to learn how to use, + and there are too many ways for them to mess up. The primary + benefit is parallel routing, which I've never actually come across + any applications that seriously needed. +
    • +
    • + The UI sucks, and it's really hard to use workflow without using + the interface. +
    • +
    • + Graphviz is actually not terribly great at displaying workflow + nets, as it tries to fit everything into a circle, whereas most of + the time with workflow what you really want is to have it appear + as a nice sequence of events with some loops back here and there. +
    • +
    • + It's too restrictive and inflexible. It's hard to change your mind + and go back, or to manually bash the case into a certain state. +
    • +

      +

    • + Also, it was never finished. Unfortunately, finishing it would be + a tremendous amount of work for limited benefit. +
    • +
    • + Data model has some big issues: Workflows are object + types. Workflows aren't tied to packages, and the context idea + isn't working very well. It's based on petri nets. +
    • +
    + +

    Finite State Machine

    + +

    + Take bug-tracker as an example. The bug-tracker workflow and user + interface can be defined as: +

    + +
    +
    Roles
    +
    +
      +
    • Submitter
    • +
    • Assignee
    • +
    +
    +
    States
    +
    +
      +
    • Open
    • +
    • Resolved
    • +
    • Closed
    • +
    +
    +
    Actions
    +
    +
      +
    • + Resolve: Enabled in states open and resolved; changes state to + resolved. +
    • +
    • + Close: Enabled in state resolved; changes state to closed. +
    • +
    • + Reopen: Enabled in states resolved, closed; changes state to open. +
    • +
    • + Edit: Enabled in all states; no changes to state. +
    • +
    • + Comment: Enabled in all states; no changes to state. +
    • +
    • + Reassign: Enabled in states open, resolved; no changes to 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 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. +

    + +

    + 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. +

    + +

    Workflows

    + +

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

    + +

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

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

    + 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. +

    + +

    + 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. +

    + +

    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. +

    + +

    + 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'. +

    + +

    + 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. +

    + +

    + Then, as the workflow case unfolds, people are given roles--you + will be the submitter, you will be the assignee. Roles can get + 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. +

    + +
      +
    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. +
    3. + The Submitter role in bug-tracker, you want to assign to whoever + opened the bug, namely the object creation user. +
    4. +
    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. +
    + +

    + 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". +

    + +

    + The workflow package will supply a few standard implementations: +

    + +
      +
    • + Creation user: Assign to the user who created the given object. +
    • +
    • + Static assignee: Use the static assignment from the workflow definition. +
    • +
    + +

    + 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 +

    + +
      +
    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. +
    3. + 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. +
    4. +
    + +

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

    + +
      +
    • + Current assigness: Returns the list of parties who are currently + assigned to this role in this workflow (for example, all the + current assignees in this bug-tracker instance). +
    • +
    + +

    + For the search query: +

    + +
      +
    • + Registered users: No limitation, search among all registered + users. Simply returns a query name for "cc_users". +
    • +
    • + Nearest subsite members: Limit to members of the nearest subsite + above the current package. +
    • +
    • + Static allowed assignment: The users defined as the allowed + 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 you + specify these three things for each action: +

    + +
      +
    • + Assigned role(s): People who are mapped to this role will + be assigned to this action, e.g., the submitter is assigned + to the Close action once the bug is resolved. When you're assigned + to something, you're expected to go and do something about it. +
    • +
    • + Allowed role(s): People who are mapped to this role will + have the permission to perform this action, e.g., the + submitter is allowed to Reopen the bug once it's resolved, but not + assigned to it. She's only assigned to "Close". +
    • +
    • + Privileges: People who have these privileges on the object + pointed to by workflow_case.object_id (e.g. the bug object for + bug-tracker) will also have permission to do perform this + action. Same as above, but allows for using permissions to grant + 'feedback', 'write', and 'admin', for example. +
    • +
    + +

    + 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. +

    + +

    + 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. +

    + + +

    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 case for + one object. That is, you can only have one workflow in process for + one object. +

    + +

    + The case holds information about the current state, the current + assignments, and an activity log over everything that happens on the + case. +

    + + + +

    Data Model

    + +
    +//--------------------//
    +//  Workflow level    //
    +//--------------------//
    +
    +create table workflows (
    +  workflow_id             integer ... primary key, references acs_objects
    +  short_name              varchar ...
    +  pretty_name             varchar ...
    +  object_id               integer ... references acs_objects
    +  -- object_id points to either a package type, 
    +  -- package instance, or single workflow case
    +  object_type             varchar ... references acs_object_types
    +  -- which object type (or its subtypes) is this workflow designed for
    +  unique (object_id, short_name)
    +);
    +
    +create table workflow_callbacks (
    +  workflow_id             integer ... references workflows
    +  acs_sc_impl_id          integer ... references acs_sc_impls
    +  sort_order              integer ...
    +    
    +  constraint ...
    +  primary key (workflow_id, acs_sc_impl_id)
    +);
    +
    +create table workflow_roles (
    +  role_id                 integer ... primary key
    +  workflow_id             integer ... references workflows
    +  short_name              varchar ...
    +  pretty_name             varchar ...
    +  
    +);
    +
    +create table workflow_role_default_parties (
    +  role_id                 integer ... references workflow_roles
    +  party_id                integer ... references parties
    +
    +  constraint ... 
    +  primary key (role_id, party_id)
    +);
    +
    +create table workflow_role_allowed_parties (
    +  role_id                 integer ... references workflow_roles
    +  party_id                integer ... references parties
    +
    +  constraint ... 
    +  primary key (role_id, party_id)
    +);
    +
    +create table workflow_role_callbacks (
    +  role_id                 integer ... references workflow_roles
    +  acs_sc_impl_id          integer ... references acs_sc_impls
    +  -- this can be an implementation of any of the three assignment
    +  -- service contracts: DefaultAssignee, AssigneePickList, or 
    +  -- AssigneeSubQuery
    +  sort_order              integer ...
    +  
    +  constraint ...
    +  primary key (role_id, acs_sc_impl_id)
    +);
    +
    +create table workflow_actions (
    +  action_id               integer ... primary key
    +  workflow_id             integer ... references workflows
    +  sort_order              integer ...
    +  short_name              varchar ...
    +  pretty_name             varchar ...
    +  pretty_past_tense       varchar ...
    +  assigned_role           integer ... references workflow_roles
    +);
    +
    +create table workflow_action_allowed_roles (
    +  action_id               integer ... references workflow_actions
    +  role_id                 integer ... references workflow_roles
    +);
    +
    +create table workflow_action_privileges (
    +  action_id               integer ... references workflow_actions
    +  privilege               varchar ... references acs_privileges
    +);
    +
    +create table workflow_action_callbacks (
    +  action_id               integer ... references workflow_actions
    +  acs_sc_impl_id          integer ... references acs_sc_impls
    +  sort_order              integer ...
    +    
    +  constraint ...
    +  primary key (action_id, acs_sc_impl_id)
    +);
    +
    +//  Finite State Machine model //
    +
    +create table workflow_fsm_states (
    +  state_id                integer ... primary key
    +  workflow_id             integer ... references workflows
    +  sort_order              integer ...
    +  short_name              varchar ...
    +  pretty_name             varchar ...
    +);
    +
    +create table workflow_fsm_actions (
    +  action_id               integer ... primary key ... references workflow_actions
    +  new_state               integer ... references workflow_fsm_states (can be null)
    +);
    +
    +create table workflow_fsm_action_enabled_in_states (
    +  action_id               integer ... references workflow_fsm_actions
    +  state_id                integer ... references workflow_fsm_states
    +);
    +
    +create table workflow_fsm (
    +  workflow_id             integer ... primary key, references workflows
    +  initial_state           integer ... references workflow_fsm_states
    +);
    +
    +//--------------------//
    +//  Case level        //
    +//--------------------//
    +
    +create table workflow_cases (
    +  case_id                 integer ... primary key
    +  workflow_id             integer ... references workflows
    +  object_id               integer ... references acs_objects ... unique
    +  -- the object which this case is about, e.g. object_id of the bug
    +);
    +
    +create table workflow_case_log (
    +  entry_id                integer ... primary key
    +  case_id                 integer ... references workflow_cases
    +  action_id               integer ... references workflow_actions
    +  user_id                 integer ... references users
    +  action_date             timestamp not null default now(),
    +  comment                 text ...
    +  comment_format          varchar ...
    +);
    +
    +create table workflow_case_log_data (
    +  entry_id                integer ... references workflow_case_log
    +  key                     varchar
    +  value                   varchar
    +
    +  constraint ...
    +  primary key (entry_id, key)
    +);
    +
    +create table workflow_case_role_assigned_parties (
    +  case_id                 integer ... references workflow_cases
    +  role_id                 integer ... references workflow_roles
    +  party_id                integer ... references parties
    +
    +  constraint ...
    +  primary key (case_id, role_id, party_id)
    +);
    +
    +
    +//  Finite State Machine model //
    +
    +create table workflow_case_fsm (
    +  case_id                 integer ... references workflow_cases
    +  current_state           integer ... references workflow_fsm_states
    +);
    +
    +
    +
    + +

    Service Contracts

    + +
    +workflow.Role_DefaultAssignees:
    +  GetObjectType -> string
    +  GetPrettyName -> string
    +  GetAssignees (case_id, object_id, role_id) -> { list of party_id }
    +
    + +
    +workflow.Role_AssigneePickList
    +  GetObjectType -> string
    +  GetPrettyName -> string
    +  GetPickList (case_id, object_id, role_id) -> { list of party_id }
    +
    + +
    +workflow.Role_AssigneeSubQuery
    +  GetObjectType -> string
    +  GetPrettyName -> string
    +  GetSubQueryName (case_id, object_id, role_id) -> { subquery_name { bind variable list } }
    +
    + +
    +workflow.Action_SideEffect
    +  GetObjectType -> string
    +  GetPrettyName -> string
    +  DoSideEffect (case_id, object_id, action_id, entry_id) -> (none)
    +
    + +
    +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 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 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: +

    + +
    +select u.user_id as party_id, 
    +       u.first_names || ' ' || u.last_names as name,
    +       u.email,
    +       u.screen_name
    +from   cc_users u
    +where  (some condition)
    +
    + +

    + This would then typically be used like this: +

    + +
    +select distinct 
    +       q.party_id, 
    +       q.name || ' (' || u.email || ')' as name_and_email
    +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. +

    + +

    + Workflow will supply these service contract implementations by + default: +

    + +
    +
    + workflow.Role_DefaultAssignee +
    +
    +
      +
    • + Creation user: Returns the creation_user of the given object. +
    • +
    • + Status assignee: Returns the contents of the + workflow_role_default_parties table. +
    • +
    +
    + +
    + workflow.Role_AssigneePickList +
    +
    +
      +
    • + Current assignees: Returns the list of parties who are + currently assigned to this role in some case in this workflow. +
    • +
    • + Static allowed assignees: Search through the contents of the + workflow_role_allowed_parties table. +
    • +
    +
    + +
    + workflow.Role_AssigneeSubQuery +
    +
    +
      +
    • + Registered users: Search through all registered users. +
    • +
    • + Static allowed assignees: Search through the contents of the + workflow_role_allowed_parties table. +
    • +
    +
    + +
    + +

    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. +
    3. + Notify me of all activity on ... +
        +
      1. + Any case where I'm assigned to some role. +
      2. +
      3. + A particular case (one bug-tracker bug) +
      4. +
      5. + All cases in the particular workflow (entire bug-tracker project) +
      6. +
      +
    4. +
    + +

    + 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. +

    + +

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

    + +
      +
    1. + We need to be able to pass on the list of already notified users + from one call of notification::new to the next. So + notification::new needs to take a parameter like + -already_notified and to not notify those again, and + likewise, to return the list of users notified by the given + notification. +
    2. +
    3. + 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. +
    4. +
    5. + 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 email/instant subscription automatically. They can then go + back and change their delivery method and frequency later. +
    6. +
    + +

    Workflow API

    + +

    API for Defining Workflows

    + +

    + You can define it using a Tcl interface: +

    + +
    +set workflow_id [workflow::new \
    +    -short_name "bug"
    +    -pretty_name "Bug" \
    +    -object_id [package::object_id "bug-tracker"] \
    +    -object_type "bt_bug" \
    +    -callbacks { bug-tracker.FormatLogTitle } 
    +
    +#####
    +#
    +# Roles
    +#
    +#####
    +
    +workflow::role::add $workflow_id \
    +    -short_name "submitter" \
    +    -pretty_name "Submitter" \
    +    -callbacks { workflow.CreationUser }
    +
    +workflow::role::add $workflow_id \
    +    -short_name "assignee" \
    +    -pretty_name "Assignee" \
    +    -callbacks {
    +                bug-tracker.ComponentMaintainer
    +                bug-tracker.ProjectMaintainer
    +            }
    +
    +#####
    +#
    +# States
    +#
    +#####
    +
    +workflow::fsm::state::add $workflow_id \
    +    -short_name "open" \
    +    -pretty_name "Open" \
    +    
    +workflow::fsm::state::add $workflow_id \
    +    -short_name "resolved" \
    +    -pretty_name "Resolved"
    +    
    +workflow::fsm::state::add $workflow_id \
    +    -short_name "closed" \
    +    -pretty_name "Closed"
    +
    +#####
    +#
    +# Actions
    +#
    +#####
    +
    +workflow::fsm::action::add $workflow_id \
    +    -short_name "comment" \
    +    -pretty_name "Comment" \
    +    -pretty_past_tense "Commented" \
    +    -allowed_roles { submitter assignee } \
    +    -privileges { feedback }
    +
    +workflow::fsm::action::add $workflow_id \
    +    -short_name "edit" \
    +    -pretty_name "Edit" \
    +    -pretty_past_tense "Edited" \
    +    -allowed_roles { submitter assignee } \
    +    -privileges { write }
    +
    +workflow::fsm::action::add $workflow_id \
    +    -short_name "resolve" \
    +    -pretty_name "Resolve" \
    +    -pretty_past_tense "Resolved" \
    +    -assigned_roles { assignee } \
    +    -enabled_states { open resolved } \
    +    -new_state "resolved" \
    +    -privileges { write } \
    +    -callbacks { bug-tracker.CaptureResolutionCode }
    +
    +workflow::fsm::action::add $workflow_id \
    +    -short_name "close" \
    +    -pretty_name "Close" \
    +    -pretty_past_tense "Closed" \
    +    -assigned_roles { submitter } \
    +    -enabled_states { resolved } \
    +    -new_state "closed" \
    +    -privileges { write }
    +
    +workflow::fsm::action::add $workflow_id \
    +    -short_name "reopen" \
    +    -pretty_name "Reopen" \
    +    -pretty_past_tense "Closed" \
    +    -allowed_roles { submitter } \
    +    -enabled_states { resolved closed } \
    +    -new_state "open" \
    +    -privileges { write }
    +
    + +

    + Alternatively, we could have an ad_form/ad_page_contract style spec as well: +

    + +
    +set workflow {
    +     roles {
    +         submitter {
    +             pretty_name "Submitter"
    +             callbacks { 
    +                 workflow.CreationUser 
    +             }
    +         }
    +         assignee {
    +             pretty_name "Assignee"
    +             callbacks {
    +                 bug-tracker.ComponentMaintainer
    +                 bug-tracker.ProjectMaintainer
    +             }
    +         }
    +     }
    +     states {
    +         open {
    +             pretty_name "Open"
    +         }
    +         resolved {
    +             pretty_name "Resolved"
    +         }
    +         closed {
    +             pretty_name "Closed"
    +         }
    +     }
    +     actions {
    +         comment {
    +             pretty_name "Comment"
    +             pretty_past_tense "Commented"
    +             allowed_roles { submitter assignee }
    +             privileges { feedback }
    +         }
    +         edit {
    +             pretty_name "Edit"
    +             pretty_past_tense "Edited"
    +             allowed_roles { submitter assignee }
    +             privileges { write }
    +         }
    +         resolve {
    +             pretty_name "Resolve"
    +             pretty_past_tense "Resolved"
    +             assigned_roles { assignee }
    +             enabled_states { open resolved }
    +             new_state "resolved"
    +             privileges { write }
    +             callbacks { bug-tracker.CaptureResolutionCode }
    +         }
    +         close {
    +             pretty_name "Close"
    +             pretty_past_tense "Closed"
    +             assigned_roles { submitter }
    +             enabled_states { resolved }
    +             new_state "closed"
    +             privileges { write }
    +         }
    +         reopen {
    +             pretty_name "Reopen"
    +             pretty_past_tense "Closed"
    +             allowed_roles { submitter }
    +             enabled_states { resolved closed }
    +             new_state "open"
    +             privileges { write }
    +         }
    +     }
    + }
    +
    +set workflow_id [workflow::new \
    +    -short_name "bug" \
    +    -pretty_name "Bug" \
    +    -object_id [package::object_id "bug-tracker"] \
    +    -object_type "bt_bug" \
    +    -callbacks { bug-tracker.FormatLogTitle } \
    +    -workflow $workflow]
    +
    + +

    API for Starting a Case

    + +
    +set bug_id [bug_tracker::bug::new ...]
    +
    +workflow::case::new \
    +    -workflow_id [workflow::get_id -object_id [ad_conn package_id] -short_name "bug"] \
    +    -object_id $bug_id
    +
    + +

    API for the Form Page

    + +

    + The intended user interface for a workflow-based application is + similar to the bug-tracker. The form is shown in display-only mode, + with buttons corresponding to actions along the bottom + (e.g. Comment, Edit, Resolve, Close). +

    + +
    +
    case::get_case_id(object_id, short_name) -> case_id
    +
    +

    + Find the case_id from object_id and workflow short_name. +

    +
    + +
    case::get_user_roles(case_id, user_id) -> { list of roles }
    +
    +

    + Find out which roles the current user has wrt the current object. +

    +
    + +
    case::get_enabled_actions(case_id, user_id) -> { list of { label name } }
    +
    +

    + The actions currently enabled in this state. +

    +
    + +
    case::get_user_actions(case_id, user_id) -> { list of { 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 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) -> (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, 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: +

    + +
    +ad_page_contract { ... } {
    +    bug_id:integer,notnull  
    +}
    +
    +# Setup return_url, user_id, etc.
    +...
    +
    +# Current action, blank for display mode
    +set action [form get_action bug]
    +
    +# Check permissions
    +workflow::case::require_permission -object_id $bug_id -action $action
    +
    +# Create the form
    +form create bug \
    +    -mode display \
    +    -actions [workflow::case::get_actions -object_id $object_id -action $action] \
    +    -cancel_url $return_url
    +
    +element create ...
    +
    +# Valid submission: Update
    +if { [form is_valid bug] } {
    +    bug_tracker::bug::edit \
    +        -bug_id $bug_id \
    +        ...
    +
    +    ad_returnredirect $return_url
    +    ad_script_abort
    +}
    +
    +# Non-valid submission: Either request or error form
    +if { ![form is_valid bug] } {
    +    bug_tracker::bug::get -bug_id $bug_id -array bug
    +
    +    set bug(status) [workflow::action::new_state -object_id $object_id -action $action]
    +
    +    # Hide elements that should be hidden
    +    foreach element [workflow::state::get_hidden_fields -object_id $object_id] {
    +        element set_properties bug $element -widget hidden
    +    }
    +    
    +    # Set element values
    +    ...
    +    # - if [form is_request] then set all
    +    # - otherwise only set elements in display-mode  
    +
    +    # Page title, context bar, filters, etc.
    +    ...
    +}
    +
    + +

    Future Extensions

    + +
      +
    • + Implement metadata spec and integrate with that so you can pick + which fields in your form to view/edit/hide depending on state and + action. +
    • +
    + +

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

    + +
      +
    • + User interface components that can generate a user interface like + bug-tracker's, i.e. buttons below the form showing the actions + that you can take, the resolution entries, the sub-status codes, + etc. +
    • +
    • + Pluggable models, for example, finite-state machines, petri-nets, + dependency graphs. A service-contract-based interface allows you + to plug in a new model. +
    • +
    • + Integration with a task-list application to maintain the user's + one task list (synchronization with Palm, etc.). +
    • +
    • + Integration with calendar, so deadlines show up there. +
    • +
    + +

    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 generic + workflow framework/engine will interface with the plugin model: +

    + +
      +
    • + All workflow models have some definition of 'state'. For finite + state machines, it's simply the name of the state, you're + currently in: The structure here is a value from an enumeration. A + petri net has as its state a list of tokens, each of which is + currently in a particular place. A dependency graph model has as + its state the list of tasks that have been completed. The workflow + engine must provide an API for the pluggable model to access, + manipulate, and store its state, but need not know anything about + the internals of the state or how it's manipulated. +
    • +
    • + All workflow models has some elements that go into its workflow + specification: FSMs have states and transitions; a transition is + an arc from one state to another. Petri nets have places and + transitions, and it has arcs that point from a place to a + transition, or a transition to a state. Dependency graphcs has + tasks and dependencies, where a depency goes from one task to + another task. +
    • +
    • + All workflow models has some concept of actions (tasks, + transitions). An action has some precondition for when it's + enabled, i.e., for when a user can or should perform this + action. This is a function of the state. And actions also cause a + well-defined change to the state, i.e., we move to a different + state, tokens are consumed from some places and produced on + others, etc. This is a function of the state, and also produces a + new state. +
    • +
    + +

    + These are the interaction points between a generic workflow engine, + and its specific model implementations. +

    + +

    Appendix B. Fix or Rewrite

    + +

    + Should we discard workflow and rewrite, or should we try to + incrementally improve what's there? +

    + +

    + In general, you should be weary of rewriting if: +

    + +
      +
    • + You have many users of your software who'll want to upgrade, + because they'll be annoyed by small changes to how things work. +
    • +
    • + You have a different set of people implementing it the second time + than you had the first time. +
    • +
    + +

    + Neither of these are the case here. We don't have any significant + users of workflow, and we have access to the same people (person) + who did the original implementation to implement it again. +

    + +

    + Besides, the planned changes are so big that there would be no code + left untouched. +

    + +
      +
    • + Switch to FSMs instead of Petri Nets, which obliviates the engine + and most of the admin UI +
    • +
    • + Discard Graphviz for admin UI +
    • +
    • + Current UI not using form builder/ad_form +
    • +
    • + Current data model not using acs-kernel properly, e.g., a new + workflow is an object type. +
    • +
    + +

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

    + + + +