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.10 -r1.11 --- openacs-4/packages/workflow/sql/postgresql/workflow-procedural-create.sql 9 Jan 2004 15:47:53 -0000 1.10 +++ openacs-4/packages/workflow/sql/postgresql/workflow-procedural-create.sql 23 Jan 2004 11:02:27 -0000 1.11 @@ -153,12 +153,10 @@ where action_id = p_action_id; -- use case object as context_id - select top.object_id + select object_id into v_case_object_id - from workflow_cases c, - workflow_cases top - where top.case_id = c.top_case_id - and c.case_id = p_case_id; + from workflow_cases + where case_id = p_case_id; -- build the unique name if p_item_id is not null then 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.24 -r1.25 --- openacs-4/packages/workflow/sql/postgresql/workflow-tables-create.sql 12 Jan 2004 10:56:59 -0000 1.24 +++ openacs-4/packages/workflow/sql/postgresql/workflow-tables-create.sql 23 Jan 2004 11:02:27 -0000 1.25 @@ -147,41 +147,45 @@ ); create table workflow_actions ( - action_id integer - constraint wf_acns_pk - primary key, - workflow_id integer - constraint wf_acns_workflow_id_nn - not null - constraint wf_acns_workflow_id_fk - references workflows(workflow_id) - on delete cascade, - sort_order integer - constraint wf_acns_sort_order_nn - not null, - short_name varchar(100) - constraint wf_acns_short_name_nn - not null, - pretty_name varchar(200) - constraint wf_acns_pretty_name_nn - not null, - pretty_past_tense varchar(200), - description text, - description_mime_type varchar(200), - edit_fields varchar(4000), - assigned_role integer - constraint wf_acns_assigned_role_fk - references workflow_roles(role_id) - on delete set null, - always_enabled_p bool default 'f', + action_id integer + constraint wf_acns_pk + primary key, + workflow_id integer + constraint wf_acns_workflow_id_nn + not null + constraint wf_acns_workflow_id_fk + references workflows(workflow_id) + on delete cascade, + sort_order integer + constraint wf_acns_sort_order_nn + not null, + short_name varchar(100) + constraint wf_acns_short_name_nn + not null, + pretty_name varchar(200) + constraint wf_acns_pretty_name_nn + not null, + pretty_past_tense varchar(200), + description text, + description_mime_type varchar(200), + edit_fields varchar(4000), + assigned_role integer + constraint wf_acns_assigned_role_fk + references workflow_roles(role_id) + on delete set null, + always_enabled_p bool default 'f', -- When the action to automatically fire. -- A value of 0 means immediately, null means never. -- Other values mean x amount of time after having become enabled - timeout interval, - child_workflow_id integer - constraint wf_acns_child_workflow_fk - references workflows(workflow_id) - on delete cascade, + timeout interval, + parent_action_id integer + constraint wf_acns_parent_action_fk + references workflow_actions(action_id) + on delete cascade, + trigger_type varchar(50) + constraint wf_acns_trigger_type_ck + check (trigger_type in ('user','auto','init','time','message','parallel','workflow','dynamic')) + default 'user', constraint wf_actions_short_name_un unique (workflow_id, short_name), constraint wf_actions_pretty_name_un @@ -248,80 +252,47 @@ primary key (action_id, acs_sc_impl_id) ); --- For the initial action, which fires when a new case is started -create table workflow_initial_action ( - workflow_id integer - constraint wf_roles_workflow_id_nn - not null - constraint wf_initial_acn_pk - primary key - constraint wf_initial_acn_wf_fk - references workflows(workflow_id) - on delete cascade, - action_id integer - constraint wf_initial_acn_act_fk - references workflow_actions(action_id) - on delete cascade -); - -create table workflow_action_child_role_map( - action_id integer - constraint wf_act_child_rl_map_child_fk - references workflow_actions(action_id), - child_role_id integer - constraint wf_act_child_rl_map_chld_rl_fk - references workflow_roles(role_id), - parent_role_id integer - constraint wf_act_child_rl_map_prnt_rl_fk - references workflow_roles(role_id), - mapping_type char(40) - constraint wf_act_child_rl_map_type_ck - check (mapping_type in - ('per_role','per_user')) - default 'per_role', - constraint wf_act_chld_rl_map_pk - primary key (action_id, child_role_id) -); - -comment on column workflow_action_child_role_map.mapping_type is ' - If per user, we create a child workflow per user who is a member of any of the parties assigned to the parent_role. - If per role, we create just one child workflow, with the exact same parties that are in the parent_role. - If more than one child_role has a mapping_type other than per_role, the cartesian product of these roles will be created. -'; - --------------------------------- -- Workflow level, Finite State Machine Model --------------------------------- +create sequence workflow_fsm_states_seq; + create table workflow_fsm_states ( - state_id integer - constraint wf_fsm_states_pk - primary key, - workflow_id integer - constraint wf_fsm_states_workflow_id_nn - not null - constraint wf_fsm_states_workflow_id_fk - references workflows(workflow_id) - on delete cascade, - sort_order integer - constraint wf_fsm_states_sort_order_nn - not null, + state_id integer + constraint wf_fsm_states_pk + primary key, + workflow_id integer + constraint wf_fsm_states_workflow_id_nn + not null + constraint wf_fsm_states_workflow_id_fk + references workflows(workflow_id) + on delete cascade, + parent_action_id integer + constraint wf_fsm_states_parent_action_fk + references workflow_actions(action_id) + on delete cascade, + sort_order integer + constraint wf_fsm_states_sort_order_nn + not null, -- The state with the lowest sort order is the initial state - short_name varchar(100) - constraint wf_fsm_states_short_name_nn - not null, - pretty_name varchar(200) - constraint wf_fsm_states_pretty_name_nn - not null, - hide_fields varchar(4000), + short_name varchar(100) + constraint wf_fsm_states_short_name_nn + not null, + pretty_name varchar(200) + constraint wf_fsm_states_pretty_name_nn + not null, + hide_fields varchar(4000), constraint wf_fsm_states_short_name_un unique (workflow_id, short_name), constraint wf_fsm_states_pretty_name_un unique (workflow_id, pretty_name) ); -create sequence workflow_fsm_states_seq; +create index wf_fsm_states_workflow_idx on workflow_fsm_states(workflow_id); +create index wf_fsm_states_prnt_action_idx on workflow_fsm_states(parent_action_id); + create table workflow_fsm_actions ( action_id integer constraint wf_fsm_acns_aid_fk @@ -342,7 +313,7 @@ constraint wf_fsm_acn_enb_in_st_acn_id_nn not null constraint wf_fsm_acn_enb_in_st_acn_id_fk - references workflow_fsm_actions(action_id) + references workflow_actions(action_id) on delete cascade, state_id integer constraint wf_fsm_acn_enb_in_st_st_id_nn @@ -360,8 +331,9 @@ primary key (action_id, state_id) ); +create index wf_fsm_act_en_in_st_action_idx on workflow_fsm_action_en_in_st(action_id); +create index wf_fsm_act_en_in_st_state_idx on workflow_fsm_action_en_in_st(state_id); - -------------------------------------------------------- -- Workflow level, context-dependent (assignments, etc.) -------------------------------------------------------- @@ -425,16 +397,10 @@ object_id integer constraint wf_cases_object_id_fk references acs_objects(object_id) - on delete cascade, - -- the object which this case is about, e.g. the acs-object for a bug-tracker bug - top_case_id integer - constraint wf_cases_top_case_id_fk - references workflow_cases(case_id) on delete cascade ); create index workflow_cases_workflow_id on workflow_cases (workflow_id); -create index workflow_cases_top_case_id on workflow_cases (top_case_id); create table workflow_case_role_party_map ( case_id integer @@ -462,50 +428,50 @@ create sequence workflow_case_enbl_act_seq; create table workflow_case_enabled_actions( - enabled_action_id integer - constraint wf_case_enbl_act_case_id_pk - primary key, - case_id integer - constraint wf_case_enbl_act_case_id_nn - not null - constraint wf_case_enbl_act_case_id_fk - references workflow_cases(case_id) - on delete cascade, - action_id integer - constraint wf_case_enbl_act_action_id_nn - not null - constraint wf_case_enbl_act_action_id_fk - references workflow_actions(action_id) - on delete cascade, - enabled_date timestamptz - default current_timestamp, - executed_date timestamptz, - enabled_state char(40) - constraint wf_case_enbl_act_state_ck - check (enabled_state in ('enabled','completed','canceled','refused')), - -- the timestamp when this action will fire - execution_time timestamptz + enabled_action_id integer + constraint wf_case_enbl_act_case_id_pk + primary key, + case_id integer + constraint wf_case_enbl_act_case_id_nn + not null + constraint wf_case_enbl_act_case_id_fk + references workflow_cases(case_id) + on delete cascade, + action_id integer + constraint wf_case_enbl_act_action_id_nn + not null + constraint wf_case_enbl_act_action_id_fk + references workflow_actions(action_id) + on delete cascade, + parent_enabled_action_id integer + constraint wf_case_enbl_act_parent_id_fk + references workflow_case_enabled_actions(enabled_action_id) + on delete cascade, + assigned_p boolean default 'f', + completed_p boolean default 'f', + -- TOOD: trigger_type, assigned_role, use_action_assignees_p ... + execution_time timestamptz ); create index wf_case_enbl_act_case_idx on workflow_case_enabled_actions(case_id); create index wf_case_enbl_act_action_idx on workflow_case_enabled_actions(action_id); -create index wf_case_enbl_act_state_idx on workflow_case_enabled_actions(enabled_state); +create index wf_case_enbl_act_parent_idx on workflow_case_enabled_actions(parent_enabled_action_id); -create table workflow_case_parent_action( - case_id integer - constraint wf_case_child_cases_case_fk - references workflow_cases - constraint wf_case_child_cases_case_pk - primary key, - parent_enabled_action_id - integer - constraint wf_case_child_cases_en_act_fk - references workflow_case_enabled_actions - constraint wf_case_child_cases_en_act_nn - not null +create table workflow_case_action_assignees( + enabled_action_id integer + constraint wf_case_actn_asgn_enbld_actn_fk + references workflow_case_enabled_actions + on delete cascade, + party_id integer + constraint wf_case_actn_asgn_party_id_fk + references parties(party_id) + on delete cascade, + constraint wf_case_action_assignees_pk + primary key (enabled_action_id, party_id) ); -create index wf_cs_child_cs_en_act_idx on workflow_case_parent_action(parent_enabled_action_id); +create index wf_case_actn_asgn_en_act_idx on workflow_case_action_assignees(enabled_action_id); +create index wf_case_actn_asgn_party_idx on workflow_case_action_assignees(party_id); --------------------------------- -- Deputies @@ -514,76 +480,55 @@ -- When a user is away, for example on vacation, he -- can hand over his workflow roles to some other user - a deputy create table workflow_deputies ( + -- user_id is the user that has a deputy, on whose behalf the deputy will operate user_id integer constraint workflow_deputies_pk primary key constraint workflow_deputies_uid_fk references users(user_id), + -- deputy_user_id is the user taking over the other user's tasks deputy_user_id integer constraint workflow_deputies_duid_fk references users(user_id), - start_date date + start_date timestamptz constraint workflow_deputies_sdate_nn not null, - end_date date + end_date timestamptz constraint workflow_deputies_edate_nn not null, message varchar(4000) ); --- role-to-user-map with deputies. Does not select users who --- have deputies, should we do that? -create view workflow_case_role_user_map as -select distinct q.case_id, - q.role_id, - q.user_id, - q.on_behalf_of_user_id -from ( - select rpm.case_id, - rpm.role_id, - pmm.member_id as user_id, - pmm.member_id as on_behalf_of_user_id - from workflow_case_role_party_map rpm, - party_approved_member_map pmm, - users u - where rpm.party_id = pmm.party_id - and pmm.member_id = u.user_id - and not exists (select 1 - from workflow_deputies - where user_id = pmm.member_id - and now() between start_date and end_date) - union - select rpm.case_id, - rpm.role_id, - dep.deputy_user_id as user_id, - pmm.member_id as on_behalf_of_user_id - from workflow_case_role_party_map rpm, - party_approved_member_map pmm, - users u, - workflow_deputies dep - where rpm.party_id = pmm.party_id - and pmm.member_id = u.user_id - and dep.user_id = pmm.member_id - and now() between dep.start_date and dep.end_date -) q; +create index workflow_deputies_deputy_idx on workflow_deputies(deputy_user_id); +create index workflow_deputies_start_date_idx on workflow_deputies(start_date); +create index workflow_deputies_end_date_idx on workflow_deputies(end_date); + --------------------------------- -- Case level, Finite State Machine Model --------------------------------- create table workflow_case_fsm ( - case_id integer - constraint wf_case_fsm_case_id_nn - not null - constraint wf_case_fsm_case_id_fk - references workflow_cases(case_id) - on delete cascade, - current_state integer - constraint wf_case_fsm_st_id_fk - references workflow_fsm_states(state_id) - on delete cascade + case_id integer + constraint wf_case_fsm_case_id_nn + not null + constraint wf_case_fsm_case_id_fk + references workflow_cases(case_id) + on delete cascade, + parent_enabled_action_id integer + constraint wf_case_fsm_action_id_fk + references workflow_case_enabled_actions(enabled_action_id) + on delete cascade, + current_state integer + constraint wf_case_fsm_st_id_fk + references workflow_fsm_states(state_id) + on delete cascade, + constraint wf_case_fsm_case_parent_un + unique (case_id, parent_enabled_action_id) ); +create index wf_case_fsm_prnt_enbl_actn_idx on workflow_case_fsm(parent_enabled_action_id); +create index wf_case_fsm_state_idx on workflow_case_fsm(current_state); --------------------------------- -- Case level, Activity Log @@ -636,19 +581,87 @@ -- Useful views ----------------- -create view workflow_case_assigned_actions as +-- Answers the question: Who is this user acting on behalf of? Which user is allowed to act on behalf of me? +-- A mapping between users and their deputies +create or replace view workflow_user_deputy_map as + select coalesce(dep.deputy_user_id, u.user_id) as user_id, + u.user_id as on_behalf_of_user_id + from users u left outer join + workflow_deputies dep on (dep.user_id = u.user_id and current_timestamp between start_date and end_date); + +-- Answers the question: What are the enabled and assigned actions and which role are they assigned to? +-- Useful for showing the task list for a particular user or role. +-- Note that dynamic actions can very well be assigned even though they don't have an assigned_role; +-- the assignees will be in workflow_case_action_assignees. +create or replace view workflow_case_assigned_actions as select c.workflow_id, - c.case_id, + wcea.case_id, c.object_id, - a.action_id, - a.assigned_role as role_id - from workflow_cases c, - workflow_case_fsm cfsm, - workflow_actions a, - workflow_fsm_action_en_in_st aeis - where cfsm.case_id = c.case_id - and a.always_enabled_p = 'f' - and aeis.state_id = cfsm.current_state - and aeis.assigned_p = 't' - and a.action_id = aeis.action_id - and a.assigned_role is not null; + wcea.action_id, + wa.assigned_role as role_id, + wcea.enabled_action_id + from workflow_case_enabled_actions wcea, + workflow_actions wa, + workflow_cases c + where wcea.completed_p = 'f' + and wcea.assigned_p = 't' + and wa.action_id = wcea.action_id + and c.case_id = wcea.case_id; + +-- This view specifically answers the question: What are the actions assigned to this user? + +-- Answers the question: Which parties are currently assigned to which actions? +-- Does not take deputies into account. +-- Pimarily needed for building the workflow_case_assigned_user_actions view. +-- TODO: See if we can find a way to improve this without the union? +create or replace view workflow_case_assigned_party_actions as + select wcaa.enabled_action_id, + wcaa.action_id, + wcaa.case_id, + wcaasgn.party_id + from workflow_case_assigned_actions wcaa, + workflow_case_action_assignees wcaasgn + where wcaasgn.enabled_action_id = wcaa.enabled_action_id + union + select wcaa.enabled_action_id, + wcaa.action_id, + wcaa.case_id, + wcrpm.party_id + from workflow_case_assigned_actions wcaa, + workflow_case_role_party_map wcrpm + where wcrpm.role_id = wcaa.role_id + and wcrpm.case_id = wcaa.case_id + and not exists (select 1 + from workflow_case_action_assignees + where enabled_action_id = wcaa.enabled_action_id); +-- TODO: Above 'not exists' can be removed, if we store the assigned_role_id with the +-- workflow_case_enabled_actions table, +-- and set it to null when assignment is dynamic like here + + +-- Answers the question: which actions is this user assigned to? +-- Does take deputies into account +create or replace view workflow_case_assigned_user_actions as + select wcapa.enabled_action_id, + wcapa.action_id, + wcapa.case_id, + wudm.user_id, + wudm.on_behalf_of_user_id + from workflow_case_assigned_party_actions wcapa, + party_approved_member_map pamm, + workflow_user_deputy_map wudm + where pamm.party_id = wcapa.party_id + and wudm.on_behalf_of_user_id = pamm.member_id; + +-- Answers the question: which roles is this user playing? +-- Does take deputies into account +create or replace view workflow_case_role_user_map as + select wcrpm.case_id, + wcrpm.role_id, + wudm.user_id, + wudm.on_behalf_of_user_id + from workflow_case_role_party_map wcrpm, + party_approved_member_map pamm, + workflow_user_deputy_map wudm + where pamm.party_id = wcrpm.party_id + and wudm.on_behalf_of_user_id = pamm.member_id; Index: openacs-4/packages/workflow/sql/postgresql/upgrade/upgrade-1.2-2.0d1.sql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/sql/postgresql/upgrade/upgrade-1.2-2.0d1.sql,v diff -u -r1.7 -r1.8 --- openacs-4/packages/workflow/sql/postgresql/upgrade/upgrade-1.2-2.0d1.sql 12 Jan 2004 10:57:00 -0000 1.7 +++ openacs-4/packages/workflow/sql/postgresql/upgrade/upgrade-1.2-2.0d1.sql 23 Jan 2004 11:02:27 -0000 1.8 @@ -4,43 +4,10 @@ -- @cvs-id $Id$ -- +---------------------------------------------------------------------- +-- Fixing various problems and omissions with the old data model +---------------------------------------------------------------------- -alter table workflow_actions add timeout interval; - - -create sequence workflow_case_enbl_act_seq; - -create table workflow_case_enabled_actions( - enabled_action_id integer - constraint wf_case_enbl_act_case_id_pk - primary key, - case_id integer - constraint wf_case_enbl_act_case_id_nn - not null - constraint wf_case_enbl_act_case_id_fk - references workflow_cases(case_id) - on delete cascade, - action_id integer - constraint wf_case_enbl_act_action_id_nn - not null - constraint wf_case_enbl_act_action_id_fk - references workflow_actions(action_id) - on delete cascade, - enabled_date timestamptz - default current_timestamp, - executed_date timestamptz, - enabled_state char(40) - constraint wf_case_enbl_act_state_ck - check (enabled_state in ('enabled','completed','canceled','refused')), - -- the timestamp when this action will fire - execution_time timestamptz -); - -create index wf_case_enbl_act_case_idx on workflow_case_enabled_actions(case_id); -create index wf_case_enbl_act_action_idx on workflow_case_enabled_actions(action_id); -create index wf_case_enbl_act_state_idx on workflow_case_enabled_actions(enabled_state); - - -- Missing unique constraints on names -- TODO: Test these alter table workflow_roles add constraint wf_roles_short_name_un unique (workflow_id, short_name); @@ -64,152 +31,230 @@ constraint wf_fsm_acns_new_st_fk foreign key (new_state) references workflow_fsm_states(state_id) on delete set null; +-- Adding unique constraint on workflow fsm enabled in actions +-- This could cause upgrades to fail, if there are in fact duplicates, so let's pray that there aren't +alter table workflow_fsm_action_en_in_st add constraint workflow_fsm_action_en_in_st_pk primary key (action_id, state_id); --- Adding recursive actions +-- Workflow deputies should use timestamp, not date +alter table workflow_deputies rename column start_date to start_date_old; +alter table workflow_deputies rename column end_date to end_date_old; +alter table workflow_deputies add start_date timestamptz; +alter table workflow_deputies add end_date timestamptz; +update workflow_deputies set start_date = start_date_old, end_date = end_date_old; +alter table workflow_deputies alter column start_date set not null; +alter table workflow_deputies alter column end_date set not null; +alter table workflow_deputies drop column start_date_old; +alter table workflow_deputies drop column end_date_old; + +-- adding foreign key/cascade and start/end date indices +create index workflow_deputies_deputy_idx on workflow_deputies(deputy_user_id); +create index workflow_deputies_start_date_idx on workflow_deputies(start_date); +create index workflow_deputies_end_date_idx on workflow_deputies(end_date); + +-- TODO: This isn't strictly required, but might be useful, anyhow +-- object_id can now be null, and doesn't have to be unique +-- (since we're going to have plenty of rows with null object_id) +alter table workflow_cases drop constraint wf_cases_object_id_un; +alter table workflow_cases alter object_id drop not null; + +-- Adding foreign key index on workflow_fsm_states +create index wf_fsm_states_workflow_idx on workflow_fsm_states(workflow_id); + +-- Changing referential integrity constraint on workflow_fsm_action_en_in_st +alter table workflow_fsm_action_en_in_st drop constraint wf_fsm_acn_enb_in_st_acn_id_fk; +alter table workflow_fsm_action_en_in_st add foreign key (action_id) references workflow_actions(action_id) on delete cascade; + +-- Missing cascading delete indices +create index wf_fsm_act_en_in_st_action_idx on workflow_fsm_action_en_in_st(action_id); +create index wf_fsm_act_en_in_st_state_idx on workflow_fsm_action_en_in_st(state_id); + +---------------------------------------------------------------------- +-- Adding hierarchy and parallelism +---------------------------------------------------------------------- + +-- Adding hierarchical actions alter table workflow_actions add - child_workflow_id integer - constraint wf_acns_child_workflow_fk - references workflows(workflow_id) - on delete cascade; + parent_action_id integer + constraint wf_acns_parent_action_fk + references workflow_actions(action_id) + on delete cascade; -create table workflow_action_child_role_map( +-- Adding explicit trigger_type, for use with hierarchy and parallelism; replacing workflow_initial_action table +alter table workflow_actions add + trigger_type varchar(50) + constraint wf_acns_trigger_type_ck + check (trigger_type in ('user','auto','init','message','parallel','workflow','dynamic')); +alter table workflow_actions alter column trigger_type set default 'user'; +update workflow_actions set trigger_type = 'user'; +update workflow_actions +set trigger_type = 'init' +where action_id in (select action_id + from workflow_initial_action); +drop table workflow_initial_action; + + +-- Adding timeout for timed actions +alter table workflow_actions add timeout interval; + +-- Add parent_action_id to states table + +alter table workflow_fsm_states add + parent_action_id integer + constraint wf_fsm_states_parent_action_fk + references workflow_actions(action_id) + on delete cascade; +create index wf_fsm_states_prnt_action_idx on workflow_fsm_states(parent_action_id); + + +-- Adding enabled actions table to hold dynamic/parallel actions +create sequence workflow_case_enbl_act_seq; +create table workflow_case_enabled_actions( + enabled_action_id integer + constraint wf_case_enbl_act_case_id_pk + primary key, + case_id integer + constraint wf_case_enbl_act_case_id_nn + not null + constraint wf_case_enbl_act_case_id_fk + references workflow_cases(case_id) + on delete cascade, action_id integer - constraint wf_act_child_rl_map_child_fk - references workflow_actions(action_id), - child_role_id integer - constraint wf_act_child_rl_map_chld_rl_fk - references workflow_roles(role_id), - parent_role_id integer - constraint wf_act_child_rl_map_prnt_rl_fk - references workflow_roles(role_id), - mapping_type char(40) - constraint wf_act_child_rl_map_type_ck - check (mapping_type in - ('per_role','per_user')) - default 'per_role', - constraint wf_act_chld_rl_map_pk - primary key (action_id, child_role_id) + constraint wf_case_enbl_act_action_id_nn + not null + constraint wf_case_enbl_act_action_id_fk + references workflow_actions(action_id) + on delete cascade, + parent_enabled_action_id integer + constraint wf_case_enbl_act_parent_id_fk + references workflow_case_enabled_actions(enabled_action_id) + on delete cascade, + assigned_p boolean default 'f', + completed_p boolean default 'f', + -- TOOD: trigger_type, assigned_role, use_action_assignees_p ... + execution_time timestamptz ); -comment on column workflow_action_child_role_map.mapping_type is ' - If per user, we create a child workflow per user who is a member of any of the parties assigned to the parent_role. - If per role, we create just one child workflow, with the exact same parties that are in the parent_role. - If more than one child_role has a mapping_type other than per_role, the cartesian product of these roles will be created. -'; +create index wf_case_enbl_act_case_idx on workflow_case_enabled_actions(case_id); +create index wf_case_enbl_act_action_idx on workflow_case_enabled_actions(action_id); -create table workflow_case_parent_action( - case_id integer - constraint wf_case_child_cases_case_fk - references workflow_cases - constraint wf_case_child_cases_case_pk - primary key, - parent_enabled_action_id - integer - constraint wf_case_child_cases_en_act_fk - references workflow_case_enabled_actions - constraint wf_case_child_cases_en_act_nn - not null +-- Adding enabled action assignees table for dynamic actions +create table workflow_case_action_assignees( + enabled_action_id integer + constraint wf_case_actn_asgn_enbld_actn_fk + references workflow_case_enabled_actions + on delete cascade, + party_id integer + constraint wf_case_actn_asgn_party_id_fk + references parties(party_id) + on delete cascade, + constraint wf_case_action_assignees_pk + primary key (enabled_action_id, party_id) ); -create index wf_cs_child_cs_en_act_idx on workflow_case_parent_action(parent_enabled_action_id); -alter table workflow_cases add - top_case_id integer - constraint wf_cases_top_case_id_fk - references workflow_cases(case_id) +create index wf_case_actn_asgn_en_act_idx on workflow_case_action_assignees(enabled_action_id); +create index wf_case_actn_asgn_party_idx on workflow_case_action_assignees(party_id); + +-- A case now has multiple states, but only one per parent_action_id +alter table workflow_case_fsm add + parent_enabled_action_id integer + constraint wf_case_fsm_action_id_fk + references workflow_case_enabled_actions(enabled_action_id) on delete cascade; -update workflow_cases set top_case_id = case_id; +alter table workflow_case_fsm add + constraint wf_case_fsm_case_parent_un + unique (case_id, parent_enabled_action_id); --- object_id can now be null, and doesn't have to be unique --- (since we're going to have plenty of rows with null object_id) -alter table workflow_cases drop constraint wf_cases_object_id_un; -alter table workflow_cases alter object_id drop not null; +create index wf_case_fsm_prnt_enbl_actn_idx on workflow_case_fsm(parent_enabled_action_id); +create index workflow_case_fsm_state_idx on workflow_case_fsm(current_state); --- Find case's object_id from the top_case_id -create or replace function workflow_case_log_entry__new ( - integer, -- entry_id - varchar, -- content_type - integer, -- case_id - integer, -- action_id - varchar, -- comment - varchar, -- comment_mime_type - integer, -- creation_user - varchar -- creation_ip -) returns integer as ' -declare - p_item_id alias for $1; - p_content_type alias for $2; - p_case_id alias for $3; - p_action_id alias for $4; - p_comment alias for $5; - p_comment_mime_type alias for $6; - p_creation_user alias for $7; - p_creation_ip alias for $8; - - v_name varchar; - v_action_short_name varchar; - v_action_pretty_past_tense varchar; - v_case_object_id integer; - v_item_id integer; - v_revision_id integer; -begin - select short_name, pretty_past_tense - into v_action_short_name, v_action_pretty_past_tense - from workflow_actions - where action_id = p_action_id; +-- New and changed views - -- use case object as context_id - select top.object_id - into v_case_object_id - from workflow_cases c, - workflow_cases top - where top.case_id = c.top_case_id - and c.case_id = p_case_id; +TODO: see which views have changed columns - -- build the unique name - if p_item_id is not null then - v_item_id := p_item_id; - else - select nextval - into v_item_id - from acs_object_id_seq; - end if; - v_name := v_action_short_name || '' '' || v_item_id; +-- Answers the question: Who is this user acting on behalf of? Which user is allowed to act on behalf of me? +-- A mapping between users and their deputies +create or replace view workflow_user_deputy_map as + select coalesce(dep.deputy_user_id, u.user_id) as user_id, + u.user_id as on_behalf_of_user_id + from users u left outer join + workflow_deputies dep on (dep.user_id = u.user_id and current_timestamp between start_date and end_date); - v_item_id := content_item__new ( - v_item_id, -- item_id - v_name, -- name - v_case_object_id, -- parent_id - v_action_pretty_past_tense, -- title - now(), -- creation_date - p_creation_user, -- creation_user - v_case_object_id, -- context_id - p_creation_ip, -- creation_ip - ''t'', -- is_live - p_comment_mime_type, -- mime_type - p_comment, -- text - ''text'', -- storage_type - ''t'', -- security_inherit_p - ''CR_FILES'', -- storage_area_key - ''content_item'', -- item_subtype - p_content_type -- content_type - ); +-- Answers the question: What are the enabled and assigned actions and which role are they assigned to? +-- Useful for showing the task list for a particular user or role. +-- Note that dynamic actions can very well be assigned even though they don't have an assigned_role; +-- the assignees will be in workflow_case_action_assignees. +drop view workflow_case_assigned_actions; +create or replace view workflow_case_assigned_actions as + select c.workflow_id, + wcea.case_id, + c.object_id, + wcea.action_id, + wa.assigned_role as role_id, + wcea.enabled_action_id + from workflow_case_enabled_actions wcea, + workflow_actions wa, + workflow_cases c + where wcea.completed_p = 'f' + and wcea.assigned_p = 't' + and wa.action_id = wcea.action_id + and c.case_id = wcea.case_id; - -- insert the row into the single-column entry revision table - select content_item__get_live_revision (v_item_id) - into v_revision_id; - insert into workflow_case_log_rev (entry_rev_id) - values (v_revision_id); +-- This view specifically answers the question: What are the actions assigned to this user? - -- insert into workflow-case-log - insert into workflow_case_log (entry_id, case_id, action_id) - values (v_item_id, p_case_id, p_action_id); +-- Answers the question: Which parties are currently assigned to which actions? +-- Does not take deputies into account. +-- Pimarily needed for building the workflow_case_assigned_user_actions view. +-- TODO: See if we can find a way to improve this without the union? +create or replace view workflow_case_assigned_party_actions as + select wcaa.enabled_action_id, + wcaa.action_id, + wcaa.case_id, + wcaasgn.party_id + from workflow_case_assigned_actions wcaa, + workflow_case_action_assignees wcaasgn + where wcaasgn.enabled_action_id = wcaa.enabled_action_id + union + select wcaa.enabled_action_id, + wcaa.action_id, + wcaa.case_id, + wcrpm.party_id + from workflow_case_assigned_actions wcaa, + workflow_case_role_party_map wcrpm + where wcrpm.role_id = wcaa.role_id + and wcrpm.case_id = wcaa.case_id + and not exists (select 1 + from workflow_case_action_assignees + where enabled_action_id = wcaa.enabled_action_id); +-- TODO: Above 'not exists' can be removed, if we store the assigned_role_id with the +-- workflow_case_enabled_actions table, +-- and set it to null when assignment is dynamic like here - -- return id of newly created item - return v_item_id; -end;' language 'plpgsql'; --- Adding unique constraint on workflow fsm enabled in actions --- This could cause upgrades to fail, if there are in fact duplicates, so let's pray that there aren't -alter table workflow_fsm_action_en_in_st add constraint workflow_fsm_action_en_in_st_pk primary key (action_id, state_id); +-- Answers the question: which actions is this user assigned to? +-- Does take deputies into account +create or replace view workflow_case_assigned_user_actions as + select wcapa.enabled_action_id, + wcapa.action_id, + wcapa.case_id, + wudm.user_id, + wudm.on_behalf_of_user_id + from workflow_case_assigned_party_actions wcapa, + party_approved_member_map pamm, + workflow_user_deputy_map wudm + where pamm.party_id = wcapa.party_id + and wudm.on_behalf_of_user_id = pamm.member_id; + +-- Answers the question: which roles is this user playing? +-- Does take deputies into account +create or replace view workflow_case_role_user_map as + select wcrpm.case_id, + wcrpm.role_id, + wudm.user_id, + wudm.on_behalf_of_user_id + from workflow_case_role_party_map wcrpm, + party_approved_member_map pamm, + workflow_user_deputy_map wudm + where pamm.party_id = wcrpm.party_id + and wudm.on_behalf_of_user_id = pamm.member_id; 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 -r1.7 -r1.8 --- openacs-4/packages/workflow/tcl/action-procs-oracle.xql 9 Jan 2004 15:47:53 -0000 1.7 +++ openacs-4/packages/workflow/tcl/action-procs-oracle.xql 23 Jan 2004 11:02:27 -0000 1.8 @@ -33,20 +33,16 @@ a.pretty_name, a.pretty_past_tense, a.edit_fields, + a.trigger_type, + a.parent_action_id, + (select short_name from workflow_actions where action_id = a.parent_action_id) as parent_action, a.assigned_role as assigned_role_id, (select short_name from workflow_roles where role_id = a.assigned_role) as assigned_role, 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, a.description, a.description_mime_type, - a.child_workflow_id, - (select short_name from workflows where workflow_id = a.child_workflow_id) as child_workflow, a.timeout_seconds from workflow_actions a, workflow_fsm_actions fa 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 -r1.9 -r1.10 --- openacs-4/packages/workflow/tcl/action-procs-postgresql.xql 9 Jan 2004 15:47:53 -0000 1.9 +++ openacs-4/packages/workflow/tcl/action-procs-postgresql.xql 23 Jan 2004 11:02:27 -0000 1.10 @@ -23,20 +23,16 @@ a.pretty_name, a.pretty_past_tense, a.edit_fields, + a.trigger_type, + a.parent_action_id, + (select short_name from workflow_actions where action_id = a.parent_action_id) as parent_action, a.assigned_role as assigned_role_id, (select short_name from workflow_roles where role_id = a.assigned_role) as assigned_role, 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, a.description, a.description_mime_type, - a.child_workflow_id, - (select short_name from workflows where workflow_id = a.child_workflow_id) as child_workflow, extract(seconds from a.timeout) as timeout_seconds from workflow_actions a left outer join workflow_fsm_actions fa on (a.action_id = fa.action_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.27 -r1.28 --- openacs-4/packages/workflow/tcl/action-procs.tcl 13 Jan 2004 12:36:47 -0000 1.27 +++ openacs-4/packages/workflow/tcl/action-procs.tcl 23 Jan 2004 11:02:27 -0000 1.28 @@ -32,7 +32,9 @@ {-privileges {}} {-callbacks {}} {-always_enabled_p f} - {-initial_action_p f} + -initial_action_p + {-trigger_type user} + {-parent_action {}} {-description {}} {-description_mime_type {}} {-timeout_seconds {}} @@ -75,7 +77,11 @@ @param callbacks List of names of service contract implementations of callbacks for the action in impl_owner_name.impl_name format. - @param initial_action_p Use this switch to indicate that this is the initial + @param trigger_type user, auto, message, time, init, workflow, parallel, dynamic. + + @param parent_action Short_name of the action's parent action. + + @param initial_action_p Deprecated. Use this switch to indicate that this is the initial action that will fire whenever a case of the workflow is created. The initial action is used to determine the initial state of the worklow as well as any @@ -104,9 +110,11 @@ initial_action_p sort_order short_name pretty_name pretty_past_tense edit_fields allowed_roles assigned_role privileges callbacks always_enabled_p description description_mime_type - timeout_seconds + timeout_seconds trigger_type parent_action } { - set row($col) [set $col] + if { [info exists $col] } { + set row($col) [set $col] + } } set action_id [workflow::action::edit \ @@ -124,28 +132,36 @@ {-workflow_id {}} {-array {}} {-internal:boolean} + {-no_complain:boolean} } { Edit an action. Attributes of the array: - short_name - pretty_name - pretty_past_tense - edit_fields - description - description_mime_type - sort_order - always_enabled_p - assigned_role - timeout_seconds - privileges - allowed_roles - initial_action_p - callbacks - child_workflow - child_workflow_id - child_role_map + + + Deprecated but still supported: + + @param operation insert, update, delete @@ -161,6 +177,8 @@ for a particular workflow model. Will cause this proc to not flush the cache or call workflow::definition_changed_handler, which the caller must then do. + @param no_complain Silently ignore extra attributes that we don't know how to handle. + @return action_id @author Lars Pind (lars@collaboraid.biz) @@ -217,43 +235,52 @@ # Parse column values switch $operation { insert - update { - # Special-case: array entry child_workflow (short_name) and child_workflow_id (state_id) -- - # DB column is child_workflow_id (workflow_id) - if { [info exists row(child_workflow)] } { - if { [info exists row(child_worklfow_id)] } { - error "You cannot supply both child_workflow (takes short_name) and child_workflow_id (workflow_id)" + # Special-case: array entry parent_action (takes short_name) and parent_action_id (takes action_id) -- + # DB column is parent_action_id (takes action_id_id) + if { [info exists row(parent_action)] } { + if { [info exists row(parent_action_id)] } { + error "You cannot supply both parent_action (takes short_name) and parent_action_id (takes action_id)" } - if { ![empty_string_p $row(child_workflow)] } { - workflow::get -workflow_id $workflow_id -array this_workflow - # TODO: Think about what's appropriate - object_id/package_key ... - - set object_id $this_workflow(object_id) - if { [empty_string_p $object_id] } { - set package_key $this_workflow(package_key) - } else { - set package_key {} - } - set row(child_workflow_id) [workflow::get_id \ - -object_id $object_id \ - -package_key $package_key \ - -short_name $row(child_workflow)] + if { ![empty_string_p $row(parent_action)] } { + set row(parent_action_id) [workflow::action::get_id \ + -workflow_id $workflow_id \ + -short_name $row(parent_action)] } else { - set row(child_workflow_id) [db_null] + set row(parent_action_id) [db_null] } - unset row(child_workflow) - unset missing_elm(child_workflow) + unset row(parent_action) + unset missing_elm(parent_action) } + # Record if this is an initial action (deprecated) + if { [info exists row(initial_action_p)] } { + if { [info exists row(trigger_type)] && ![string equal $row(trigger_type) "user"] } { + error "You can't specify both initial_action_p (which is deprecated) and trigger_type (which has replaced it) at the same time. Stick to trigger_type." + } + if { [template::util::is_true $row(initial_action_p)] } { + set row(trigger_type) "init" + } + unset row(initial_action_p) + unset missing_elm(initial_action_p) + } + set update_clauses [list] set insert_names [list] set insert_values [list] # Handle columns in the workflow_actions table foreach attr { - short_name pretty_name pretty_past_tense edit_fields description description_mime_type sort_order + short_name + pretty_name + pretty_past_tense + edit_fields + description + description_mime_type + sort_order always_enabled_p assigned_role timeout_seconds - child_workflow_id + trigger_type + parent_action_id } { if { [info exists row($attr)] } { set varname attr_$attr @@ -359,6 +386,7 @@ } } + # Auxilliary rows switch $operation { insert - update { # Record which roles are allowed to take action @@ -385,18 +413,6 @@ unset missing_elm(privileges) } - # Record if this is an initial action - if { [info exists row(initial_action_p)] } { - if { [template::util::is_true $row(initial_action_p)] } { - db_dml delete_initial_action { - delete from workflow_initial_action - where workflow_id = :workflow_id - } - db_dml insert_initial_action {} - } - unset missing_elm(initial_action_p) - } - # Callbacks if { [info exists row(callbacks)] } { db_dml delete_callbacks { @@ -411,45 +427,8 @@ unset missing_elm(callbacks) } - # Child role map - if { [info exists row(child_role_map)] } { - db_dml delete_callbacks { - delete from workflow_action_child_role_map - where action_id = :action_id - } - - if { [llength $row(child_role_map)] > 0 } { - if { ![exists_and_not_null row(child_workflow_id)] } { - error "You cannot set child_role_map without also setting child_workflow_id." - } - - foreach { child_role_short_name spec } $row(child_role_map) { - # Allow simple list of short_name -> short_name - if { [llength $spec] == 1 } { - set parent_role_short_name $spec - set mapping_type "per_role" - } else { - foreach { parent_role_short_name mapping_type } $spec {} - } - - set child_role_id [workflow::role::get_id \ - -workflow_id $row(child_workflow_id) \ - -short_name $child_role_short_name] - set parent_role_id [workflow::role::get_id \ - -workflow_id $workflow_id \ - -short_name $parent_role_short_name] - - db_dml insert_child_role_map { - insert into workflow_action_child_role_map (action_id, child_role_id, parent_role_id, mapping_type) - values (:action_id, :child_role_id, :parent_role_id, :mapping_type) - } - } - } - unset missing_elm(child_role_map) - } - # Check that there are no unknown attributes - if { [llength [array names missing_elm]] > 0 } { + if { [llength [array names missing_elm]] > 0 && !$no_complain_p } { error "Trying to set illegal action attributes: [join [array names missing_elm] ", "]" } } @@ -524,7 +503,7 @@ } } - error "workflow::action::get_id role with short_name $short_name not found for workflow $workflow_id" + error "workflow::action::get_id: Action with short_name $short_name not found for workflow $workflow_id" } ad_proc -public workflow::action::get_workflow_id { @@ -561,8 +540,8 @@ @return The array will contain the following entries: workflow_id, sort_order, short_name, pretty_name, pretty_past_tense, assigned_role (short_name), assigned_role_id, - always_enabled_p, initial_action_p, description, - description_mime_type, child_workflow_id column values for an action. + always_enabled_p, trigger_type, parent_action, parent_action_id, description, + description_mime_type values for an action. @see workflow::action::get_all_info @see workflow::action::get_all_info_not_cached @@ -778,8 +757,10 @@ {-new_state {}} {-new_state_id {}} {-callbacks {}} + -initial_action_p {-always_enabled_p f} - {-initial_action_p f} + {-trigger_type user} + {-parent_action {}} {-description {}} {-description_mime_type {}} {-timeout_seconds {}} @@ -802,9 +783,11 @@ initial_action_p sort_order short_name pretty_name pretty_past_tense edit_fields allowed_roles assigned_role privileges callbacks always_enabled_p description description_mime_type - timeout_seconds + timeout_seconds trigger_type parent_action } { - set row($col) [set $col] + if { [info exists $col] } { + set row($col) [set $col] + } } foreach elm { new_state new_state_id @@ -834,7 +817,7 @@ } { Edit an action. - Attributes: new_state_id + Attributes: new_state_id, enabled_states, enabled_state_ids, enabled_actions, enabled_action_ids @param operation insert, update, delete @@ -991,6 +974,18 @@ -workflow_id $workflow_id \ -array row] + # Verify insert/update + switch $operation { + insert - update { + set row_exists_p [db_string row_exists_p { select count(*) from workflow_fsm_actions where action_id = :action_id }] + if { $row_exists_p } { + set operation "update" + } else { + set operation "insert" + } + } + } + # FSM action row switch $operation { insert { @@ -1076,7 +1071,7 @@ {-array:required} } { Return information about an action with a given id, including - FSM-related info such as 'enabled_states', and 'new_state'. + FSM-related info: enabled_states, enabled_state_ids, assigned_states, assigned_state_ids, new_state, new_state_id. @author Peter Marklund @author Lars Pind (lars@collaboraid.biz) @@ -1210,32 +1205,22 @@ enabled_states {} assigned_states {} new_state {} - initial_action_p f + trigger_type user + parent_action {} callbacks {} } # Get the info from the spec foreach { key value } $spec { set action($key) [string trim $value] } + set action(short_name) $short_name # Create the action - set action_id [workflow::action::fsm::new \ + set action_id [workflow::action::fsm::edit \ + -operation "insert" \ -workflow_id $workflow_id \ - -short_name $short_name \ - -pretty_name $action(pretty_name) \ - -pretty_past_tense $action(pretty_past_tense) \ - -edit_fields $action(edit_fields) \ - -allowed_roles $action(allowed_roles) \ - -assigned_role $action(assigned_role) \ - -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) - ] + -array action] } ad_proc -private workflow::action::fsm::parse_actions_spec { @@ -1298,14 +1283,17 @@ array unset row allowed_role_ids array unset row enabled_state_ids array unset row assigned_state_ids - array unset row child_workflow_id + array unset row parent_action_id if { ![exists_and_not_null row(description)] } { array unset row description_mime_type } # Get rid of a few defaults - array set defaults { initial_action_p f always_enabled_p f } + array set defaults { + trigger_type user + always_enabled_p f + } set spec [list] foreach name [lsort [array names row]] { @@ -1427,6 +1415,7 @@ [list workflow::action::get_workflow_id_not_cached -action_id $action_row(action_id)] \ $workflow_id + set action_row(trigger_type) [string trim $action_row(trigger_type)] set action_data($action_row(action_id)) [array get action_row] lappend action_ids $action_row(action_id) } @@ -1483,29 +1472,6 @@ } } - # Build array of child_role_map for all actions - array set child_role_map [list] - db_foreach select_child_role_map { - select m.action_id, - m.child_role_id, - m.parent_role_id, - m.mapping_type, - (select short_name from workflow_roles where role_id = m.child_role_id) as child_role_short_name, - (select short_name from workflow_roles where role_id = m.parent_role_id) as parent_role_short_name - from workflow_action_child_role_map m, - workflow_actions a - where m.action_id = a.action_id - and a.workflow_id = :workflow_id - } { - set mapping_type [string trim $mapping_type] - # Short-cut when mapping_type is per_role - if { [string equal $mapping_type "per_role"] } { - lappend child_role_map($action_id) $child_role_short_name $parent_role_short_name - } else { - lappend child_role_map($action_id) $child_role_short_name [list $parent_role_short_name $mapping_type] - } - } - # For each action_id, add to the array of that action the contents of the # sub arrays (callbacks, allowed_roles, allowed_role_ids, privileges) foreach action_id $action_ids { @@ -1535,12 +1501,6 @@ set one_action(callback_ids) [list] } - if { [info exists child_role_map($action_id)] } { - set one_action(child_role_map) $child_role_map($action_id) - } else { - set one_action(child_role_map) [list] - } - set action_data($action_id) [array get one_action] # Have to unset the array as otherwise array set will append to previous values 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.7 -r1.8 --- openacs-4/packages/workflow/tcl/case-procs-oracle.xql 9 Jan 2004 15:47:53 -0000 1.7 +++ openacs-4/packages/workflow/tcl/case-procs-oracle.xql 23 Jan 2004 11:02:27 -0000 1.8 @@ -23,16 +23,52 @@ + + + + TODO: Oracle + 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 + and cfsm.parent_action_id is null + + + - select case_id, - action_id + select enabled_action_id from workflow_case_enabled_actions where execution_time <= sysdate - and enabled_state = 'enabled' + and completed_p = 'f' + + + + TODO: PORT to Oracle + + select cfsm.parent_action_id, + a.short_name as parent_action, + cfsm.current_state as current_state_id, + s.short_name as current_state + from workflow_case_fsm cfsm left outer join + workflow_actions a on (a.action_id = cfsm.parent_action_id), + workflow_states s, + where cfsm.case_id = :case_id + and s.state_id = cfsm.current_state + + + select m.party_id, @@ -107,10 +143,7 @@ workflow_actions a where c.case_id = :case_id and a.workflow_id = c.workflow_id - and not exists (select 1 - from workflow_initial_action wia - where wia.workflow_id = c.workflow_id - and wia.action_id = a.action_id) + and a.trigger_type != 'init' and (a.always_enabled_p = 't' or exists (select 1 from workflow_case_fsm cfsm, @@ -150,11 +183,34 @@ insert into workflow_case_enabled_actions - (enabled_action_id, case_id, action_id, enabled_state, execution_time) - select :enabled_action_id, :case_id, a.action_id, 'enabled', sysdate + a.timeout_seconds/(24*60*60) + (enabled_action_id, + case_id, + action_id, + parent_enabled_action_id, + assigned_p, + execution_time) + select :enabled_action_id, + :case_id, + :action_id, + :parent_enabled_action_id, + :db_assigned_p, + sysdate + a.timeout_seconds/(24*60*60) from workflow_actions a where a.action_id = :action_id + + + TODO PORT + + select 1 + from workflow_case_enabled_actions ean + where ean.action_id = :action_id + and ean.case_id = :case_id + and completed_p = 'f' + limit 1 + + + 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.6 -r1.7 --- openacs-4/packages/workflow/tcl/case-procs-postgresql.xql 9 Jan 2004 15:47:53 -0000 1.6 +++ openacs-4/packages/workflow/tcl/case-procs-postgresql.xql 23 Jan 2004 11:02:27 -0000 1.7 @@ -5,28 +5,48 @@ select c.case_id, - c.top_case_id, c.workflow_id, - top.object_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, - parent.parent_enabled_action_id, - (select case_id - from workflow_case_enabled_actions - where enabled_action_id = parent.parent_enabled_action_id) as parent_case_id + s.hide_fields as state_hide_fields from workflow_cases c, - workflow_cases top, workflow_case_fsm cfsm left outer join - workflow_fsm_states s on (s.state_id = cfsm.current_state) left outer join - workflow_case_parent_action parent using (case_id) + workflow_fsm_states s on (s.state_id = cfsm.current_state) where c.case_id = :case_id and cfsm.case_id = c.case_id - and top.case_id = c.top_case_id + and cfsm.parent_enabled_action_id = :parent_enabled_action_id + + + 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 + and cfsm.parent_enabled_action_id is null + + + + + + select cfsm.parent_enabled_action_id, + cfsm.current_state as current_state_id + from workflow_case_fsm cfsm + where cfsm.case_id = :case_id + + + select m.party_id, @@ -69,35 +89,12 @@ - - - select a.action_id - from workflow_cases c, - workflow_actions a - where c.case_id = :case_id - and a.workflow_id = c.workflow_id - and not exists (select 1 - from workflow_initial_action wia - where wia.workflow_id = c.workflow_id - and wia.action_id = a.action_id) - and (a.always_enabled_p = 't' - or exists (select 1 - from workflow_case_fsm cfsm, - workflow_fsm_action_en_in_st waeis - where cfsm.case_id = c.case_id - and waeis.state_id = cfsm.current_state - and waeis.action_id = a.action_id)) - order by a.sort_order - - - - select case_id, - action_id + select enabled_action_id from workflow_case_enabled_actions where execution_time <= current_timestamp - and enabled_state = 'enabled' + and completed_p = 'f' @@ -131,25 +128,35 @@ - - - update workflow_case_enabled_actions - set enabled_state = 'completed', - executed_date = current_timestamp - where case_id = :case_id - and action_id = :action_id - and enabled_state = 'enabled' - - - insert into workflow_case_enabled_actions - (enabled_action_id, case_id, action_id, enabled_state, execution_time) - select :enabled_action_id, :case_id, a.action_id, 'enabled', current_timestamp + a.timeout + (enabled_action_id, + case_id, + action_id, + parent_enabled_action_id, + assigned_p, + execution_time) + select :enabled_action_id, + :case_id, + :action_id, + :parent_enabled_action_id, + :db_assigned_p, + current_timestamp + a.timeout from workflow_actions a where a.action_id = :action_id + + + select 1 + from workflow_case_enabled_actions ean + where ean.action_id = :action_id + and ean.case_id = :case_id + and completed_p = 'f' + limit 1 + + + 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.24 -r1.25 --- openacs-4/packages/workflow/tcl/case-procs.tcl 22 Jan 2004 09:53:20 -0000 1.24 +++ openacs-4/packages/workflow/tcl/case-procs.tcl 23 Jan 2004 11:02:27 -0000 1.25 @@ -22,8 +22,6 @@ ad_proc -private workflow::case::insert { {-workflow_id:required} {-object_id:required} - {-parent_enabled_action_id {}} - {-top_case_id {}} } { Internal procedure that creates a new workflow case in the database. Should not be used by applications. Use workflow::case::new instead. @@ -32,12 +30,6 @@ @param workflow_id The ID of the workflow. - @param parent_enabled_action_id - The ID of an enabled_action in the parent, if this is a child case - - @param top_case_id If this is a child case, this must be the ID of the case at the top - of the tree. - @return The case_id of the case. Returns the empty string if no case could be found. @see workflow::case::new @@ -47,21 +39,9 @@ db_transaction { set case_id [db_nextval "workflow_cases_seq"] - if { [empty_string_p $top_case_id] } { - if { ![empty_string_p $parent_enabled_action_id] } { - error "You cannot create a child case without specifying the top_case_id." - } - set top_case_id $case_id - } - # Create the case db_dml insert_case {} - # Create the mapping - if { [exists_and_not_null parent_enabled_action_id] } { - db_dml insert_case_parent_map {} - } - # Initialize the FSM state to NULL db_dml insert_case_fsm {} } @@ -73,8 +53,6 @@ {-no_notification:boolean} -workflow_id:required {-object_id {}} - {-parent_enabled_action_id {}} - {-top_case_id {}} {-comment {}} {-comment_mime_type {}} -user_id @@ -91,12 +69,6 @@ @param assignment Array-list of role_short_name and list of party_ids to assign to roles before starting. - @param parent_enabled_action_id - The ID of an enabled_action in the parent, if this is a child case - - @param top_case_id If this is a child case, this must be the ID of the case at the top - of the tree. - @return The case_id of the case. @author Lars Pind (lars@collaboraid.biz) @@ -107,20 +79,17 @@ db_transaction { - # If there is no initial-action, we create one now - # TODO: Should we do this here, or throw an error like we used to? - # Initial action set initial_action_id [workflow::get_element -workflow_id $workflow_id -element initial_action_id] - if { [empty_string_p $initial_action_id] } { - # TODO: This is the old error message, if we want to throw it still, maybe make it optional - # error "The workflow must have an initial action." + # If there is no initial-action, we create one now + # TODO: Should we do this here, or throw an error like we used to? + # If we change this, we should throw an error instead set action_row(pretty_name) "Start" set action_row(pretty_past_tense) "Started" - set action_row(initial_action_p) "t" + set action_row(trigger_type) "init" set states [workflow::fsm::get_states -workflow_id $workflow_id] @@ -134,19 +103,19 @@ -workflow_id $workflow_id] } else { # NOTE: FSM-specific check here - set new_state [workflow::action::fsm::get_element -action_id $initial_action_id -element new_state] + workflow::action::fsm::get -action_id $initial_action_id -array initial_action + set new_state $initial_action(new_state) + if { [empty_string_p $new_state] } { - error "Initial action must change state." + error "Initial action with short_name \"$initial_action(short_name)\" does not have any new_state. In order to be an initial state, it must have new_state set." } } # Insert the case set case_id [insert \ -workflow_id $workflow_id \ - -object_id $object_id \ - -parent_enabled_action_id $parent_enabled_action_id \ - -top_case_id $top_case_id] + -object_id $object_id] # Assign roles if { [exists_and_not_null assignment] } { @@ -195,14 +164,17 @@ {-array:required} {-action_id {}} } { - Get information about a case + Get information about a case. Implemented by workflow::case::fsm::get, because we do not yet + support multiple workflow engines. @param case_id The case ID @param array The name of an array in which information will be returned. @param action_id If specified, will return the case information as if the given action had already been executed. This is useful for presenting forms for actions that do not take place until the user hits OK. @author Lars Pind (lars@collaboraid.biz) + + @see workflow::case::fsm::get } { # Select the info into the upvar'ed Tcl Array upvar $array row @@ -290,30 +262,84 @@ return [db_list select_user_roles {}] } -ad_proc -public workflow::case::get_enabled_actions { +ad_proc -public -deprecated workflow::case::get_enabled_actions { {-case_id:required} } { - Get the currently enabled actions, based on the state of the case + Get the currently enabled user actions, based on the state of the case @param case_id The ID of the case. - @return A list of id's of the actions which are currently + + @return A list of action_id's of the actions which are currently enabled @author Lars Pind (lars@collaboraid.biz) + + @see workflow::case::get_enabled_action_ids } { 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 +ad_proc -private -deprecated workflow::case::get_enabled_actions_not_cached { case_id } { + Used internally by the workflow API only. Goes to the database to get the enabled actions for the case. } { return [db_list select_enabled_actions {}] } -ad_proc -public workflow::case::get_available_actions { +ad_proc -public workflow::case::get_enabled_action_ids { {-case_id:required} + {-trigger_type {user}} +} { + Get the currently enabled_action_id's of enabled user actions in the case. + + Note, that this is different from get_enabled_actions, which only returns + the action_id, which will not work for dynamic actions. + + @param case_id The ID of the case. + + @param trigger_type You can limit to e.g. user actions here. Defaults to user actions. + Specify the empty string if you want all actions. + + @return A list of currently available enabled_action_id's. + + + @author Lars Pind (lars@collaboraid.biz) +} { + return [util_memoize [list workflow::case::get_enabled_action_ids_not_cached $case_id $trigger_type] \ + [workflow::case::cache_timeout]] +} + +ad_proc -public workflow::case::get_enabled_action_ids_not_cached { + case_id + {trigger_type {}} +} { + Used internally by the workflow API only. Goes to the database to + get the enabled actions for the case. +} { + if { [empty_string_p $trigger_type] } { + return [db_list select_enabled_actions { + select ena.enabled_action_id + from workflow_case_enabled_actions ena + where ena.case_id = :case_id + and ena.completed_p = 'f' + }] + } else { + return [db_list select_enabled_actions { + select ena.enabled_action_id + from workflow_case_enabled_actions ena, + workflow_actions a + where ena.case_id = :case_id + and a.action_id = ena.action_id + and ena.completed_p = 'f' + and a.trigger_type = 'user' + order by a.sort_order + }] + } +} + +ad_proc -public -deprecated workflow::case::get_available_actions { + {-case_id:required} -user_id } { Get the actions which are enabled and which the current user have permission to execute. @@ -322,22 +348,53 @@ @return A list of ID's of the available actions. @author Lars Pind (lars@collaboraid.biz) + + @see workflow::case::get_available_enabled_action_ids } { if { ![exists_and_not_null user_id] } { set user_id [ad_conn user_id] } set action_list [list] - foreach action_id [get_enabled_actions -case_id $case_id] { - if { [workflow::case::action::permission_p -case_id $case_id -action_id $action_id -user_id $user_id] } { - lappend action_list $action_id + foreach enabled_action_id [workflow::case::get_enabled_action_ids -case_id $case_id] { + if { [workflow::case::action::permission_p -enabled_action_id $enabled_action_id -user_id $user_id] } { + lappend action_list [workflow::case::enabled_action_get_element \ + -enabled_action_id $enabled_action_id \ + -element action_id] } } return $action_list } +ad_proc -public workflow::case::get_available_enabled_action_ids { + {-case_id:required} + -user_id +} { + Get the enabled_action_id's of the actions available to the given user. + + @param case_id The ID of the case. + + @return A list of ID's of the available actions. + + @author Lars Pind (lars@collaboraid.biz) +} { + if { ![exists_and_not_null user_id] } { + set user_id [ad_conn user_id] + } + + set action_list [list] + + foreach enabled_action_id [get_enabled_action_ids -case_id $case_id] { + if { [workflow::case::action::permission_p -enabled_action_id $enabled_action_id -user_id $user_id] } { + lappend action_list $enabled_action_id + } + } + + return $action_list +} + ad_proc -private workflow::case::assign_roles { {-case_id:required} {-all:boolean} @@ -360,7 +417,7 @@ where c.case_id = :case_id and r.workflow_id = c.workflow_id and not exists (select 1 - from workflow_case_role_user_map m + from workflow_case_role_party_map m where m.role_id = r.role_id and m.case_id = :case_id) }] @@ -752,6 +809,7 @@ workflow::case::fsm::get_info_not_cached workflow::case::get_user_roles_not_cached workflow::case::get_enabled_actions_not_cached + workflow::case::get_enabled_action_ids_not_cached } { util_memoize_flush_regexp "^$proc_name [ad_decode $case_id "" {\.*} $case_id]" } @@ -762,149 +820,16 @@ workflow::case::role::flush_cache -case_id $case_id } -ad_proc -private workflow::case::state_changed_handler { - {-case_id:required} - {-user_id {}} -} { - Scans for newly enabled actions, as well as actions which were - enabled but are now no longer enabled. Does not flush the cache. - Should only be called indirectly through the workflow API. - @author Lars Pind (lars@collaboraid.biz) -} { - db_transaction { - # DB query to get actually enabled actions - set enabled_action_ids [db_list select_enabled_actions {}] - - # This array, keyed by action_id, will store the enabled_action_id for previously enabled actions - array set action_enabled [list] - db_foreach select_previously_enabled_actions {} { - set action_enabled($action_id) $enabled_action_id - } - - # Loop over currently enabled actions and find out which ones are new - array set newly_enabled_action [list] - foreach action_id $enabled_action_ids { - if { [info exists action_enabled($action_id)] } { - # Action was already enabled. Unset the array entry, so what remains will be - # previously but no longer enabled actions - unset action_enabled($action_id) - } else { - # Newly enabled action - set newly_enabled_action($action_id) 1 - } - } - - # First, unenable the previously but no longer enabled actions - foreach action_id [array names action_enabled] { - workflow::case::action::unenable \ - -enabled_action_id $action_enabled($action_id) - } - - # Second, enable the newly enabled actions - foreach action_id $enabled_action_ids { - if { [info exists newly_enabled_action($action_id)] } { - workflow::case::action::enable \ - -case_id $case_id \ - -action_id $action_id \ - -user_id $user_id - } - } - - # Flush cache, now that things have changed - workflow::case::flush_cache -case_id $case_id - - # Make sure roles are assigned, if possible - workflow::case::assign_roles -all -case_id $case_id - - # Call parent child-state-changed handler - workflow::case::get -case_id $case_id -array case - if { $case(top_case_id) != $case_id } { - workflow::case::child_state_changed_handler \ - -parent_case_id $case(parent_case_id) \ - -parent_enabled_action_id $case(parent_enabled_action_id) \ - -child_case_id $case_id \ - -user_id $user_id - } - } -} - -ad_proc -private workflow::case::child_state_changed_handler { - {-parent_case_id:required} - {-parent_enabled_action_id:required} - {-child_case_id:required} - {-user_id {}} -} { - Check if all child cases of this enabled action are complete, and if so - cause this action to execute -} { - db_transaction { - - #---------------------------------------------------------------------- - # 1. Check if this child-case is inactive - #---------------------------------------------------------------------- - if { [workflow::case::active_p -case_id $child_case_id] } { - # Child still running - return - } - - #---------------------------------------------------------------------- - # 2. Check if all child-cases of this enabled_action_id are done - #---------------------------------------------------------------------- - set child_case_ids [workflow::case::get_child_cases -enabled_action_id $parent_enabled_action_id] - foreach one_child $child_case_ids { - - # No reason to check this child again - if { $one_child != $child_case_id } { - if { [workflow::case::active_p -case_id $one_child] } { - # Other child still running - return - } - } - } - - #---------------------------------------------------------------------- - # 3. If all are complete, execute the action - #---------------------------------------------------------------------- - - # TODO: API to get action_id from enabled_action_id - set action_id [db_string select_action_id { - select action_id - from workflow_case_enabled_actions - where enabled_action_id = :parent_enabled_action_id - }] - - workflow::case::action::execute \ - -no_notification \ - -no_perm_check \ - -case_id $parent_case_id \ - -action_id $action_id \ - -user_id $user_id - } -} - - -ad_proc -private workflow::case::get_child_cases { - -enabled_action_id:required -} { -} { - return [db_list child_cases { - select case_id - from workflow_case_parent_action - where parent_enabled_action_id = :enabled_action_id - }] -} - ad_proc -public workflow::case::timed_actions_sweeper {} { Sweep for timed actions ready to fire. } { - db_multirow -local actions select_timed_out_actions {} + set enabled_action_ids [db_list select_timed_out_actions {}] - template::multirow -local foreach actions { + foreach enabled_action_id $enabled_action_ids { workflow::case::action::execute \ -no_perm_check \ - -case_id $case_id \ - -action_id $action_id + -enabled_action_id $enabled_action_id } } @@ -925,11 +850,16 @@ select enabled_action_id, case_id, action_id, - enabled_date, - executed_date, - enabled_state, - execution_time - from workflow_case_enabled_actions + assigned_p, + completed_p, + parent_enabled_action_id, + to_char(execution_time, 'YYYY-MM-DD HH24:MI:SS') as execution_time_ansi, + coalesce((select a2.trigger_type + from workflow_case_enabled_actions e2, + workflow_actions a2 + where e2.enabled_action_id = e.parent_enabled_action_id + and a2.action_id = e2.action_id), 'workflow') as parent_trigger_type + from workflow_case_enabled_actions e where enabled_action_id = :enabled_action_id } -column_array row } @@ -1295,16 +1225,16 @@ db_transaction { foreach name [array names assignees] { - + set role_id [workflow::role::get_id \ - -workflow_id $workflow_id \ - -short_name $name] + -workflow_id $workflow_id \ + -short_name $name] - assignee_insert \ - -replace=$replace_p \ - -case_id $case_id \ - -role_id $role_id \ - -party_ids $assignees($name) + workflow::case::role::assignee_insert \ + -replace=$replace_p \ + -case_id $case_id \ + -role_id $role_id \ + -party_ids $assignees($name) } } } @@ -1328,29 +1258,51 @@ @author Lars Pind (lars@collaboraid.biz) } { - #return [db_string select_current_state {}] return [workflow::case::fsm::get_element -case_id $case_id -element state_id] } ad_proc -public workflow::case::fsm::get { {-case_id:required} {-array:required} + {-parent_enabled_action_id {}} {-action_id {}} + {-enabled_action_id {}} } { - Get information about an FSM case set as values in your array. + Get information about an FSM case set as values in your array. + case_id state_short_name pretty_state state_hide_fields state_id parent_enabled_action_id parent_case_id + entry_id top_case_id workflow_id object_id @param case_id The ID of the case + @param array The name of an array in which information will be returned. - @param action_id If specified, will return the case information as if the given action had already been executed. - This is useful for presenting forms for actions that do not take place until the user hits OK. + @param parent_enabled_action_id + If specified, will return the sub-case information for the given action. + + @param action_id Deprecated. Same effect as enabled_action_id, but will not work for dynamic workflows. + + @param enabled_action_id + If specified, will return the case information as if the given action had already + been executed. This is useful for presenting forms for actions that do not take place + until the user hits OK. + @author Lars Pind (lars@collaboraid.biz) } { # Select the info into the upvar'ed Tcl Array upvar $array row - if { [empty_string_p $action_id] } { - array set row [util_memoize [list workflow::case::fsm::get_info_not_cached $case_id] \ + if { ![empty_string_p $action_id] } { + if { ![empty_string_p $enabled_action_id] } { + error "You cannot specify both action_id and enabled_action_id. enabled_action_id is preferred." + } + set enabled_action_id [workflow::case::action::get_enabled_action_id \ + -case_id $case_id \ + -action_id $action_id \ + -any_parent] + } + + if { [empty_string_p $enabled_action_id] } { + array set row [util_memoize [list workflow::case::fsm::get_info_not_cached $case_id $parent_enabled_action_id] \ [workflow::case::cache_timeout]] set row(entry_id) {} } else { @@ -1363,6 +1315,7 @@ ad_proc -public workflow::case::fsm::get_element { {-case_id:required} {-element:required} + {-parent_enabled_action_id {}} {-action_id {}} } { Return a single element from the information about a case. @@ -1376,40 +1329,96 @@ @author Lars Pind (lars@collaboraid.biz) } { - get -case_id $case_id -action_id $action_id -array row + get -case_id $case_id -parent_enabled_action_id $parent_enabled_action_id -action_id $action_id -array row return $row($element) } -ad_proc -private workflow::case::fsm::get_info_not_cached { case_id } { +ad_proc -private workflow::case::fsm::get_info_not_cached { case_id { parent_enabled_action_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 + if { [empty_string_p $parent_enabled_action_id] } { + db_1row select_case_info_null_parent_id {} -column_array row + } else { + db_1row select_case_info {} -column_array row + } return [array get row] } +ad_proc -private workflow::case::fsm::get_state_info { + -case_id:required + {-parent_enabled_action_id {}} + {-all:boolean} + } { + Gets all state info from the database, include sub-action state. + @return a list of (action_id, current_state) tuples. + The top-level state is the one that has action_id empty. +} { + # TODO: Cache and flush + return [workflow::case::fsm::get_state_info_not_cached $case_id $parent_enabled_action_id $all_p] +} +ad_proc -private workflow::case::fsm::get_state_info_not_cached { + case_id + parent_enabled_action_id + all_p +} { + Gets all state info from the database, include sub-action state. + + @return a list of (action_id, current_state) tuples. + The top-level state is the one that has action_id empty. + + @see workflow::case::fsm::get_state_info +} { + if { $all_p } { + return [db_list_of_lists select_state_info {}] + } else { + if { [empty_string_p $parent_enabled_action_id] } { + return [db_string null_parent { + select current_state + from workflow_case_fsm + where case_id = :case_id + and parent_enabled_action_id is null + }] + } else { + return [db_string null_parent { + select current_state + from workflow_case_fsm + where case_id = :case_id + and parent_enabled_action_id = :parent_enabled_action_id + }] + } + } +} + + ##### # # workflow::case::action # ##### ad_proc -public workflow::case::action::permission_p { - {-case_id:required} - {-action_id:required} + {-enabled_action_id {}} + {-case_id {}} + {-action_id {}} {-user_id} } { Does the user have permission to perform this action. Doesn't check whether the action is enabled. - @param case_id The ID of the case. - @param action_id The ID of the action - @param user_id The user. + @param enabled_action_id The enabled action you want to test for permission on. + + @param case_id Deprecated. The ID of the case. + + @param action_id Deprecated. The ID of the action + + @param user_id The user. + @return true or false. @author Lars Pind (lars@collaboraid.biz) @@ -1418,39 +1427,53 @@ set user_id [ad_conn user_id] } + if { ![empty_string_p $enabled_action_id] } { + workflow::case::enabled_action_get -enabled_action_id $enabled_action_id -array enabled_action + set case_id $enabled_action(case_id) + set action_id $enabled_action(action_id) + } else { + set enabled_action_id [workflow::case::action::get_enabled_action_id \ + -any_parent \ + -case_id $case_id \ + -action_id $action_id] + } + set object_id [workflow::case::get_element -case_id $case_id -element object_id] set user_role_ids [workflow::case::get_user_roles -case_id $case_id -user_id $user_id] set permission_p 0 + set assigned_p [db_string assigned_p { + select 1 + from workflow_case_assigned_user_actions + where enabled_action_id = :enabled_action_id + and user_id = :user_id + } -default 0] + + if { $assigned_p } { + return 1 + } + foreach role_id $user_role_ids { - # Is this an assigned role for this action? - set assigned_role_id [workflow::action::get_assigned_role -action_id $action_id] - if { $assigned_role_id == $role_id } { - set permission_p 1 - break - } # Is this an allowed role for this action? set allowed_roles [workflow::action::get_allowed_roles -action_id $action_id] if { [lsearch $allowed_roles $role_id] != -1 } { - set permission_p 1 - break + return 1 } } if { !$permission_p } { set privileges [concat "admin" [workflow::action::get_privileges -action_id $action_id]] foreach privilege $privileges { if { [permission::permission_p -object_id $object_id -privilege $privilege -party_id $user_id] } { - set permission_p 1 - break + return 1 } } } - return $permission_p + return 0 } ad_proc -public workflow::case::action::enabled_p { @@ -1459,8 +1482,10 @@ } { Is this action currently enabled. - @param case_id The ID of the case. - @param action_id The ID of the action + @param case_id The ID of the case. + + @param action_id The ID of the action + @return true or false. @author Lars Pind (lars@collaboraid.biz) @@ -1469,251 +1494,103 @@ } ad_proc -public workflow::case::action::available_p { - {-case_id:required} - {-action_id:required} - {-user_id} + {-enabled_action_id {}} + {-case_id {}} + {-action_id {}} + {-user_id {}} } { Is this action currently enabled and does the user have permission to perform it? - @param case_id The ID of the case. - @param action_id The ID of the action - @param user_id The user. + @param enabled_action_id The enabled action you want to test for permission on. + + @param case_id Deprecated. The ID of the case. + + @param action_id Deprecated. The ID of the action + + @param user_id The user. + @return true or false. @author Lars Pind (lars@collaboraid.biz) } { # Always permit the no-op - if { [empty_string_p $action_id] } { + if { [empty_string_p $action_id] && [empty_string_p $enabled_action_id] } { return 1 } - if { ![exists_and_not_null user_id] } { - set user_id [ad_conn user_id] + if { ![empty_string_p $enabled_action_id] } { + workflow::case::enabled_action_get -enabled_action_id $enabled_action_id -array enabled_action + set case_id $enabled_action(case_id) + set action_id $enabled_action(action_id) + } else { + set enabled_action_id [workflow::case::action::get_enabled_action_id \ + -any_parent \ + -case_id $case_id \ + -action_id $action_id] } - - if { - [enabled_p -case_id $case_id -action_id $action_id] - && - [permission_p -case_id $case_id -action_id $action_id -user_id $user_id] - } { + + if { [workflow::case::action::enabled_p -case_id $case_id -action_id $action_id] && + [workflow::case::action::permission_p -enabled_action_id $enabled_action_id -user_id $user_id] } { return 1 } else { return 0 } } -ad_proc -public workflow::case::action::execute { - {-no_notification:boolean} - {-no_perm_check:boolean} + +ad_proc -private workflow::case::action::get_enabled_action_id { {-case_id:required} {-action_id:required} - {-comment ""} - {-comment_mime_type "text/plain"} - {-user_id} - {-initial:boolean} - {-entry_id {}} + {-parent_enabled_action_id {}} + {-all:boolean} + {-any_parent:boolean} } { - Execute the action + Get the enabled_action_id from case_id and action_id. Doesn't find completed enabled actions. + Provided for backwards compatibility. Doesn't work properly for dynamic actions. - @param case_id The ID of the case. + @param all If specified, will return all if more than one is found. Otherwise returns just the first. - @param action_id The ID of the action - - @param comment Comment for the case activity log - - @param comment_mime_type MIME Type of the comment, according to - OpenACS standard text formatting - - @param user_id The user who's executing the action - - @param initial Use this switch to signal that this is the initial action. This causes - permissions/enabled checks to be bypasssed, and causes all roles to get assigned. - - @param entry_id Optional item_id for double-click protection. If you call workflow::case::fsm::get - with a non-empty action_id, it will generate a new entry_id for you, which you can pass in here. - - @param no_perm_check Set this switch if you do not want any permissions chcecking, e.g. for automatic actions. - - @return entry_id of the new log entry (will be a cr_item). - - @author Lars Pind (lars@collaboraid.biz) + @return enabled_action_id. Returns blank if no enabled action exists. } { - if { ![exists_and_not_null user_id] } { - set user_id [ad_conn user_id] - } - - if { !$initial_p } { - if { ![enabled_p -case_id $case_id -action_id $action_id] } { - error "This action is not enabled at this time." + if { $any_parent_p } { + set result [db_list select_enabled_action_id { + select enabled_action_id + from workflow_case_enabled_actions + where case_id = :case_id + and action_id = :action_id + and completed_p = 'f' + }] + } else { + if { [empty_string_p $parent_enabled_action_id] } { + set result [db_list select_enabled_action_id { + select enabled_action_id + from workflow_case_enabled_actions + where case_id = :case_id + and action_id = :action_id + and completed_p = 'f' + and parent_enabled_action_id = :parent_enabled_action_id + }] + } else { + set result [db_list select_enabled_action_id { + select enabled_action_id + from workflow_case_enabled_actions + where case_id = :case_id + and action_id = :action_id + and completed_p = 'f' + and parent_enabled_action_id is null + }] } - - if { !$no_perm_check_p } { - if { ![permission_p -case_id $case_id -action_id $action_id -user_id $user_id] } { - error "This user is not allowed to perform this action at this time." - } - } } - if { [empty_string_p $comment] } { - set comment { } + if { $all_p } { + return $result + } else { + return [lindex $result 0] } - - # We can't have empty comment_mime_type, default to text/plain - if { [empty_string_p $comment_mime_type] } { - set comment_mime_type "text/plain" - } - - db_transaction { - - # Update the case workflow state - workflow::case::action::fsm::execute_state_change \ - -case_id $case_id \ - -action_id $action_id - - # Update workflow_case_enabled_transactions - db_dml set_completed {} - - # Double-click protection - if { ![empty_string_p $entry_id] } { - if { [db_string log_entry_exists_p {}] } { - return $entry_id - } - } - - # Insert activity log entry - set extra_vars [ns_set create] - oacs_util::vars_to_ns_set \ - -ns_set $extra_vars \ - -var_list { entry_id case_id action_id comment comment_mime_type } - - # ns_set put $extra_vars parent_id $object_id - - set entry_id [package_instantiate_object \ - -creation_user $user_id \ - -extra_vars $extra_vars \ - -package_name "workflow_case_log_entry" \ - "workflow_case_log_entry"] - - # Fire side-effects - do_side_effects \ - -case_id $case_id \ - -action_id $action_id \ - -entry_id $entry_id - - # Notifications - if { !$no_notification_p } { - workflow::case::action::notify \ - -case_id $case_id \ - -action_id $action_id \ - -entry_id $entry_id \ - -comment $comment \ - -comment_mime_type $comment_mime_type - } - - # Scan for enabled actions - workflow::case::state_changed_handler \ - -case_id $case_id \ - -user_id $user_id - } - - workflow::case::flush_cache -case_id $case_id - - return $entry_id } -ad_proc -private workflow::case::action::unenable { - {-enabled_action_id:required} -} { - Update the workflow_case_enabled_actions table to say that the - previously enabled actions are no longer enabled. - Does not flush the cache. - Should only be called indirectly through the workflow API. - @author Lars Pind (lars@collaboraid.biz) -} { - db_transaction { - db_dml set_canceled { - update workflow_case_enabled_actions - set enabled_state = 'canceled' - where enabled_action_id = :enabled_action_id - } - } -} -ad_proc -private workflow::case::action::enable { - {-case_id:required} - {-action_id:required} - {-user_id {}} -} { - Update the workflow_case_enabled_actions table to say that the - action is now enabled. Will automatically fire an automatic action. - Does not flush the cache. - Should only be called indirectly through the workflow API. - - @author Lars Pind (lars@collaboraid.biz) -} { - workflow::action::get -action_id $action_id -array action - - db_transaction { - set enabled_action_id [db_nextval workflow_case_enbl_act_seq] - - db_dml insert_enabled {} - - # Spawn child workflows - if { ![empty_string_p $action(child_workflow_id)] } { - - # NOTE: Role is mapped at spawn time. - # Changes made after that are not synchronized from parent to child or vice versa. - - # Assignment will be a list of { role_short_name { party_id party_id ... } ... } - set assignment [list] - foreach { child_role_short_name spec } $action(child_role_map) { - lappend assignment $child_role_short_name - - # Allow simple list of short_name -> short_name - if { [llength $spec] == 1 } { - set parent_role_short_name $spec - set mapping_type "per_role" - } else { - foreach { parent_role_short_name mapping_type } $spec {} - } - - if { [string equal $mapping_type "per_user"] } { - # TODO: Handle per_user role mapping - error "child_role_map mapping_type of per_user not implemented." - } else { - set parent_role_id [workflow::role::get_id \ - -workflow_id $action(workflow_id) \ - -short_name $parent_role_short_name] - set top_case_id [workflow::case::get_element \ - -case_id $case_id \ - -element top_case_id] - lappend assignment [workflow::case::role::get_assignees \ - -case_id $case_id \ - -role_id $parent_role_id] - } - } - - workflow::case::new \ - -no_notification \ - -workflow_id $action(child_workflow_id) \ - -parent_enabled_action_id $enabled_action_id \ - -user_id $user_id \ - -top_case_id $top_case_id \ - -assignment $assignment - } - - # Automatic actions execute immediately - if { $action(timeout_seconds) == 0 } { - workflow::case::action::execute \ - -no_perm_check \ - -case_id $case_id \ - -action_id $action_id \ - -user_id $user_id - } - } -} - ad_proc -public workflow::case::action::do_side_effects { {-case_id:required} {-action_id:required} @@ -1854,6 +1731,8 @@ # List of user_id's for people who are in the assigned_role to any enabled actions # This takes deputies into account + +#XXXXX Verify this ... probably wrong set assignee_list [db_list enabled_action_assignees {}] # List of users who play some role in this case @@ -1933,50 +1812,622 @@ +####################################################################### +# +# WORKFLOW ENGINE PROCS +# +####################################################################### + +# Below are all the procs that drive the workflow engine, +# the logic to change state and determine which actions +# are availble given the current state. + ##### # -# workflow::case::action::fsm +# Causing changes to state # ##### -ad_proc -public workflow::case::action::fsm::new_state { +ad_proc -public workflow::case::action::execute { + {-no_notification:boolean} + {-no_perm_check:boolean} + {-enabled_action_id {}} + {-case_id {}} + {-action_id {}} + {-parent_enabled_action_id {}} + {-comment ""} + {-comment_mime_type "text/plain"} + {-user_id} + {-initial:boolean} + {-entry_id {}} +} { + Execute the action. Either provide (case_id, action_id, parent_enabled_action_id), or simply enabled_action_id. + + @param enabled_action_id The ID of the enabled action to execute. Alternatively, you can specify the case_id/action_id pair. + + @param case_id The ID of the case. + + @param action_id The ID of the action + + @param comment Comment for the case activity log + + @param comment_mime_type MIME Type of the comment, according to + OpenACS standard text formatting + + @param user_id The user who's executing the action + + @param initial Use this switch to signal that this is the initial action. This causes + permissions/enabled checks to be bypasssed, and causes all roles to get assigned. + + @param entry_id Optional item_id for double-click protection. If you call workflow::case::fsm::get + with a non-empty action_id, it will generate a new entry_id for you, which you can pass in here. + + @param no_perm_check Set this switch if you do not want any permissions chcecking, e.g. for automatic actions. + + @return entry_id of the new log entry (will be a cr_item). + + @author Lars Pind (lars@collaboraid.biz) +} { + if { ![exists_and_not_null user_id] } { + set user_id [ad_conn user_id] + } + + if { [empty_string_p $case_id] || [empty_string_p $action_id] } { + if { [empty_string_p $enabled_action_id] } { + error "You must supply either case_id and action_id, or enabled_action_id" + } + } + if { [empty_string_p $enabled_action_id] } { + if { $initial_p } { + set enabled_action_id {} + } else { + # This will not work with dynamic actions + # This is provided for backwards-compatibility, so we hope there's no dynamicism + # TODO: Figure out a better solution to this problem + set enabled_action_id [workflow::case::action::get_enabled_action_id \ + -any_parent \ + -case_id $case_id \ + -action_id $action_id] + if { [empty_string_p $enabled_action_id] } { + error "This action is not enabled at this time." + } + } + } + if { ![empty_string_p $enabled_action_id] } { + workflow::case::enabled_action_get -enabled_action_id $enabled_action_id -array enabled_action + set case_id $enabled_action(case_id) + set action_id $enabled_action(action_id) + set parent_enabled_action_id $enabled_action(parent_enabled_action_id) + set parent_trigger_type $enabled_action(parent_trigger_type) + } else { + set parent_trigger_type "workflow" + } + + if { !$initial_p && !$no_perm_check_p } { + if { ![workflow::case::action::permission_p -enabled_action_id $enabled_action_id -user_id $user_id] } { + error "This user ($user_id) is not allowed to perform this action ($action_id) at this time." + } + } + + if { [empty_string_p $comment] } { + # single-space comment + set comment { } + } + + # We can't have empty comment_mime_type, default to text/plain + if { [empty_string_p $comment_mime_type] } { + set comment_mime_type "text/plain" + } + + db_transaction { + + # Double-click protection + if { ![empty_string_p $entry_id] } { + if { [db_string log_entry_exists_p {}] } { + return $entry_id + } + } + + # Update the case workflow state + workflow::case::action::fsm::execute_state_change \ + -initial=$initial_p \ + -enabled_action_id $enabled_action_id \ + -case_id $case_id \ + -action_id $action_id \ + -parent_enabled_action_id $parent_enabled_action_id + + # Mark the action completed + if { ![empty_string_p $enabled_action_id] } { + workflow::case::action::complete \ + -enabled_action_id $enabled_action_id \ + -user_id $user_id + } + + # Insert activity log entry + set extra_vars [ns_set create] + oacs_util::vars_to_ns_set \ + -ns_set $extra_vars \ + -var_list { entry_id case_id action_id comment comment_mime_type } + + set entry_id [package_instantiate_object \ + -creation_user $user_id \ + -extra_vars $extra_vars \ + -package_name "workflow_case_log_entry" \ + "workflow_case_log_entry"] + + # Fire side-effects + workflow::case::action::do_side_effects \ + -case_id $case_id \ + -action_id $action_id \ + -entry_id $entry_id + + # Notifications + if { !$no_notification_p } { + workflow::case::action::notify \ + -case_id $case_id \ + -action_id $action_id \ + -entry_id $entry_id \ + -comment $comment \ + -comment_mime_type $comment_mime_type + } + + # Scan for enabled actions + if { [string equal $parent_trigger_type "workflow"] } { + workflow::case::state_changed_handler \ + -case_id $case_id \ + -parent_enabled_action_id $parent_enabled_action_id \ + -user_id $user_id + } + + # If there's a parent, alert the parent + if { ![empty_string_p $parent_enabled_action_id] } { + workflow::case::child_state_changed_handler \ + -parent_enabled_action_id $parent_enabled_action_id \ + -user_id $user_id + } + } + + workflow::case::flush_cache -case_id $case_id + + return $entry_id +} + + + + + + +##### +# +# Handling changes to state +# +#### + +ad_proc -private workflow::case::state_changed_handler { {-case_id:required} + {-parent_enabled_action_id {}} + {-user_id {}} +} { + Scans for newly enabled actions, as well as actions which were + enabled but are now no longer enabled. Does not flush the cache. + Should only be called indirectly through the workflow API. + + @author Lars Pind (lars@collaboraid.biz) +} { + db_transaction { + + #---------------------------------------------------------------------- + # 1. Find the actually enabled actions, based on the current state(s) of the case + #---------------------------------------------------------------------- + + workflow::case::get_actual_state \ + -case_id $case_id \ + -parent_enabled_action_id $parent_enabled_action_id \ + -array assigned_p + + # assigned_p($action_id): 1 = assigned, 0 = enabled, nonexistent = not available ... + + #---------------------------------------------------------------------- + # 2. Output data structure + #---------------------------------------------------------------------- + + # Array with a key entry per action to enable + array set enable_action_ids [array get assigned_p] + + # List of enabled_action_id's of actions that are no longer enabled + set unenable_enabled_action_ids [list] + + #---------------------------------------------------------------------- + # 2. Get the rows in workflow_case_enabled_actions + #---------------------------------------------------------------------- + if { [empty_string_p $parent_enabled_action_id] } { + set db_rows [db_list_of_lists select_previously_enabled_actions_null_parent {}] + } else { + set db_rows [db_list_of_lists select_previously_enabled_actions {}] + } + + foreach elm $db_rows { + foreach { action_id enabled_action_id } $elm {} + + if { [info exists assigned_p($action_id)] } { + # This action is enabled, and should be enabled => ignore + unset enable_action_ids($action_id) + } else { + # This action is enabled, and shouldn't be, kill it + lappend unenable_enabled_action_ids $enabled_action_id + } + } + + #---------------------------------------------------------------------- + # 3. Unenable the no-longer-enabled actions + #---------------------------------------------------------------------- + foreach enabled_action_id $unenable_enabled_action_ids { + workflow::case::action::unenable \ + -enabled_action_id $enabled_action_id + } + + #---------------------------------------------------------------------- + # 4. Enabled the newly enabled actions + #---------------------------------------------------------------------- + + foreach action_id [array names enable_action_ids] { + workflow::case::action::enable \ + -case_id $case_id \ + -action_id $action_id \ + -parent_enabled_action_id $parent_enabled_action_id \ + -user_id $user_id \ + -assigned=[exists_and_equal assigned_p($action_id) 1] + } + + #---------------------------------------------------------------------- + # 6. Flush cache, assign roles + #---------------------------------------------------------------------- + workflow::case::flush_cache -case_id $case_id + workflow::case::assign_roles -all -case_id $case_id + } +} + +ad_proc -private workflow::case::child_state_changed_handler { + -parent_enabled_action_id:required + {-user_id {}} +} { + Check if all child actions of this action are complete, and if so + cause this action to execute +} { + db_transaction { + + set num_incomplete [db_string select_num_incomplete { + select count(*) + from workflow_case_enabled_actions + where parent_enabled_action_id = :parent_enabled_action_id + and completed_p = 'f' + }] + + if { $num_incomplete > 0 } { + # Still incomplete actions, do nothing + return + } + + #---------------------------------------------------------------------- + # All child actions are complete, execute the action + #---------------------------------------------------------------------- + + workflow::case::action::execute \ + -no_notification \ + -no_perm_check \ + -enabled_action_id $parent_enabled_action_id \ + -user_id $user_id + } +} + + +##### +# +# Enable/Unenable/Complete individual actions +# +##### + +ad_proc -private workflow::case::action::unenable { + {-enabled_action_id:required} +} { + Update the workflow_case_enabled_actions table to say that the + previously enabled actions are no longer enabled. + Does not flush the cache. + Should only be called indirectly through the workflow API. + + @author Lars Pind (lars@collaboraid.biz) +} { + set action_id [workflow::case::enabled_action_get_element -enabled_action_id $enabled_action_id -element action_id] + + db_dml delete_enabled_action { + delete + from workflow_case_enabled_actions + where enabled_action_id = :enabled_action_id + } +} + +ad_proc -private workflow::case::action::enable { + {-case_id:required} {-action_id:required} + {-parent_enabled_action_id {}} + {-user_id {}} + {-assigned:boolean} + {-assignees {}} } { - Get the ID of the new state which the workflow will be in after a certain action. + Update the workflow_case_enabled_actions table to say that the + action is now enabled. Will automatically fire an automatic action. + Does not flush the cache. + Should only be called indirectly through the workflow API. - @param case_id The ID of the case. - @param action_id The ID of the action - @return The state_id of the new state which the workflow will be in after this action + @author Lars Pind (lars@collaboraid.biz) +} { + workflow::action::get -action_id $action_id -array action + set workflow_id $action(workflow_id) + db_transaction { + set enabled_action_id [db_nextval "workflow_case_enbl_act_seq"] + + if { ![string equal $action(trigger_type) "user"] } { + # Action can only be assigned if it has trigger_type user + # But its children can be assigned, so we keep the original assigned_p variable + set db_assigned_p f + } else { + set db_assigned_p [db_boolean $assigned_p] + } + + # Insert the enabled action row + db_dml insert_enabled {} + + # Insert assignees + if { [exists_and_not_null assignees] } { + foreach party_id $assignees { + db_dml insert_assignee { + insert into workflow_case_action_assignees (enabled_action_id, party_id) + values (:enabled_action_id, :party_id) + } + } + } + + + switch $action(trigger_type) { + "workflow" { + # Find and execute child init action + set child_init_id [db_string child_init { + select action_id + from workflow_actions + where parent_action_id = :action_id + and trigger_type = 'init' + }] + + workflow::case::action::execute \ + -no_notification \ + -initial \ + -case_id $case_id \ + -action_id $child_init_id \ + -parent_enabled_action_id $enabled_action_id \ + -user_id $user_id + } + "parallel" { + # Find and enable child actions + # TODO: Move this to action::get + set child_actions [db_list child_actions { + select action_id + from workflow_actions + where parent_action_id = :action_id + }] + foreach child_action_id $child_actions { + workflow::case::action::enable \ + -case_id $case_id \ + -action_id $child_action_id \ + -parent_enabled_action_id $enabled_action_id \ + -user_id $user_id \ + -assigned=$assigned_p + } + } + "dynamic" { + # HACK: just pick each user from the assigned role ... + # TODO: Move this to action::get + set child_actions [db_list child_actions { + select action_id + from workflow_actions + where parent_action_id = :action_id + }] + + foreach child_action_id $child_actions { + + set child_role_id [workflow::action::get_element \ + -action_id $child_action_id \ + -element assigned_role_id] + + set parties [workflow::case::role::get_assignees \ + -case_id $case_id \ + -role_id $child_role_id] + + + foreach elm $parties { + array unset party + array set party $elm + + workflow::case::action::enable \ + -case_id $case_id \ + -action_id $child_action_id \ + -parent_enabled_action_id $enabled_action_id \ + -user_id $user_id \ + -assigned=$assigned_p \ + -assignees $party(party_id) + } + } + } + "auto" { + workflow::case::action::execute \ + -no_perm_check \ + -enabled_action_id $enabled_action_id \ + -user_id $user_id + } + } + } +} + +ad_proc -private workflow::case::action::complete { + {-enabled_action_id:required} + {-user_id {}} +} { + Mark an action complete. + @author Lars Pind (lars@collaboraid.biz) } { - set new_state_id [workflow::action::fsm::get_new_state -action_id $action_id] - if { [empty_string_p $new_state_id] } { - set new_state_id [workflow::case::fsm::get_current_state -case_id $case_id] + db_transaction { + workflow::case::enabled_action_get -enabled_action_id $enabled_action_id -array enabled_action + workflow::action::get -action_id $enabled_action(action_id) -array action + + if { [lsearch -exact { parallel dynamic } $enabled_action(parent_trigger_type)] != -1 } { + db_dml completed_p { + update workflow_case_enabled_actions + set completed_p = 't' + where enabled_action_id = :enabled_action_id + } + + # Delete children + db_dml delete_enabled_actions { + delete + from workflow_case_enabled_actions + where parent_enabled_action_id = :enabled_action_id + } + } else { + # Delete the workflow_case_enabled_actions row + # Will cascade delete the corresponding state information + set case_id $enabled_action(case_id) + db_dml delete_enabled_actions { + delete + from workflow_case_enabled_actions + where enabled_action_id = :enabled_action_id + } + } } - return $new_state_id } -ad_proc -public workflow::case::action::fsm::execute_state_change { + + + + +##### +# +# Helper +# +##### + +ad_proc -private workflow::case::get_actual_state { {-case_id:required} - {-action_id:required} + {-parent_enabled_action_id {}} + {-array:required} } { + Flushes cache, gets actual state of case, and finds which actions + should be enabled/assigned based on that actual state. This can + then be used to manage the contents of + workflow_case_enabled_actions table. +} { + # TODO B: Make polymorphic -- this should go into a ::fsm:: namespace + upvar 1 $array assigned_p + + workflow::case::flush_cache -case_id $case_id + + set state_id [workflow::case::fsm::get_state_info \ + -case_id $case_id \ + -parent_enabled_action_id $parent_enabled_action_id] + + workflow::state::fsm::get -state_id $state_id -array state + + foreach action_id $state(enabled_action_ids) { + set assigned_p($action_id) 0 + } + + foreach action_id $state(assigned_action_ids) { + set assigned_p($action_id) 1 + } +} + +ad_proc -private workflow::case::action::fsm::execute_state_change { + {-initial:boolean} + {-case_id {}} + {-action_id {}} + {-enabled_action_id {}} + {-parent_enabled_action_id {}} +} { Modify the state of the case as required when executing the given action. - @param case_id The ID of the case. - @param action_id The ID of the action + @param case_id The ID of the case. + @param action_id The ID of the action + + @param enabled_action_id The ID of the action + + @param initial Set this if this is an initial action. + + @param parent_enabled_action_id + Specify this, if this is an initial action. + @author Lars Pind (lars@collaboraid.biz) } { - # We wrap this in a transaction, which will be the same transaction as the parent one inside - # workflow::case::action::execute db_transaction { - set new_state_id [workflow::action::fsm::get_new_state -action_id $action_id] + + if { [empty_string_p $case_id] || [empty_string_p $action_id] } { + if { [empty_string_p $enabled_action_id] } { + error "You must supply either case_id and action_id, or enabled_action_id" + } + } + + if { [empty_string_p $enabled_action_id] } { + if { $initial_p } { + set enabled_action_p {} + # We rely on parent_enabled_action_id being set by the caller here + } else { + # This will not work with dynamic actions, but is necessary for inital actions + set enabled_action_id [workflow::case::action::get_enabled_action_id \ + -case_id $case_id \ + -action_id $action_id \ + -parent_enabled_action_id $parent_enabled_action_id] + } + } + + if { ![empty_string_p $enabled_action_id] } { + workflow::case::enabled_action_get -enabled_action_id $enabled_action_id -array enabled_action + # Even if these are provided, we overide them with the DB call + set case_id $enabled_action(case_id) + set action_id $enabled_action(action_id) + set parent_enabled_action_id $enabled_action(parent_enabled_action_id) + } + + # Find the new state from the action + workflow::action::get -action_id $action_id -array action + set new_state_id $action(new_state_id) + + # Actually change the state, if any state change if { ![empty_string_p $new_state_id] } { - db_dml update_fsm_state {} + # Delete any existing state with this parent_enabled_action_id + + if { [empty_string_p $parent_enabled_action_id] } { + db_dml delete_fsm_state { + delete + from workflow_case_fsm + where case_id = :case_id + and parent_enabled_action_id is null + } + } else { + db_dml delete_fsm_state { + delete + from workflow_case_fsm + where case_id = :case_id + and parent_enabled_action_id = :parent_enabled_action_id + } + } + + # Insert the new one + db_dml insert_fsm_state { + insert into workflow_case_fsm (case_id, parent_enabled_action_id, current_state) + values (:case_id, :parent_enabled_action_id, :new_state_id) + } } } } 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.11 -r1.12 --- openacs-4/packages/workflow/tcl/case-procs.xql 22 Jan 2004 09:53:20 -0000 1.11 +++ openacs-4/packages/workflow/tcl/case-procs.xql 23 Jan 2004 11:02:27 -0000 1.12 @@ -14,29 +14,19 @@ insert into workflow_cases ( - case_id, workflow_id, object_id, top_case_id + case_id, workflow_id, object_id ) values ( - :case_id, :workflow_id, :object_id, :top_case_id + :case_id, :workflow_id, :object_id ) - - - insert into workflow_case_parent_action ( - case_id, parent_enabled_action_id - ) values ( - :case_id, :parent_enabled_action_id - ) - - - insert into workflow_case_fsm ( - case_id, current_state + case_id, parent_enabled_action_id, current_state ) values ( - :case_id, null + :case_id, null, null ) @@ -68,7 +58,8 @@ workflow_actions a where ena.case_id = :case_id and a.action_id = ena.action_id - and enabled_state = 'enabled' + and ena.completed_p = 'f' + and a.trigger_type = 'user' order by a.sort_order @@ -112,17 +103,24 @@ - select a.action_id, + select ena.action_id, ena.enabled_action_id - from workflow_case_enabled_actions ena, - workflow_actions a + from workflow_case_enabled_actions ena where ena.case_id = :case_id - and a.action_id = ena.action_id - and enabled_state = 'enabled' - order by a.sort_order + and parent_enabled_action_id = :parent_enabled_action_id + + + select ena.action_id, + ena.enabled_action_id + from workflow_case_enabled_actions ena + where ena.case_id = :case_id + and parent_enabled_action_id is null + + + select impl.impl_name @@ -180,6 +178,7 @@ select current_state from workflow_case_fsm c where c.case_id = :case_id + and c.parent_enabled_action_id is null @@ -195,32 +194,17 @@ from workflow_cases c, workflow_case_fsm cfsm, workflow_fsm_states s, - workflow_fsm_actions a + workflow_fsm_actions afsm, + workflow_case_enabled_actions ena where c.case_id = :case_id and cfsm.case_id = c.case_id - and a.action_id = :action_id - and ((a.new_state is null and s.state_id = cfsm.current_state) or (s.state_id = a.new_state)) + and cfsm.parent_enabled_action_id = :parent_enabled_action_id + and ena.enabled_action_id = :enabled_action_id + and afsm.action_id = ena.action_id + and ((afsm.new_state is null and s.state_id = cfsm.current_state) or (s.state_id = afsm.new_state)) - - - select 1 - from workflow_case_enabled_actions ean - where ean.action_id = :action_id - and ean.case_id = :case_id - and enabled_state = 'enabled' - - - - - - update workflow_case_fsm - set current_state = :new_state_id - where case_id = :case_id - - - select count(*) 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.17 -r1.18 --- openacs-4/packages/workflow/tcl/role-procs.tcl 22 Jan 2004 09:53:20 -0000 1.17 +++ openacs-4/packages/workflow/tcl/role-procs.tcl 23 Jan 2004 11:02:27 -0000 1.18 @@ -58,6 +58,7 @@ {-workflow_id {}} {-array {}} {-internal:boolean} + {-no_complain:boolean} } { Edit a workflow role. @@ -78,6 +79,12 @@ @param array For insert/update: Name of an array in the caller's namespace with attributes to insert/update. + @param internal Set this flag if you're calling this proc from within the corresponding proc + for a particular workflow model. Will cause this proc to not flush the cache + or call workflow::definition_changed_handler, which the caller must then do. + + @param no_complain Silently ignore extra attributes that we don't know how to handle. + @return role_id @see workflow::role::new @@ -246,7 +253,7 @@ } # Check that there are no unknown attributes - if { [llength [array names missing_elm]] > 0 } { + if { [llength [array names missing_elm]] > 0 && !$no_complain } { error "Trying to set illegal role attributes: [join [array names missing_elm] ", "]" } } 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.11 -r1.12 --- openacs-4/packages/workflow/tcl/state-procs.tcl 13 Jan 2004 12:36:47 -0000 1.11 +++ openacs-4/packages/workflow/tcl/state-procs.tcl 23 Jan 2004 11:02:27 -0000 1.12 @@ -23,6 +23,7 @@ {-pretty_name:required} {-hide_fields {}} {-sort_order {}} + {-parent_action {}} } { Creates a new state for a certain FSM (Finite State Machine) workflow. @@ -38,6 +39,9 @@ @param sort_order The number which this state should be in the sort ordering sequence. Leave blank to add state at the end. If you provide a sort_order number which already exists, existing states are pushed down one number. + + @param parent_action + Which action with trigger_type 'workflow' does this state belong to. @param internal Set this flag if you're calling this proc from within the corresponding proc for a particular workflow model. Will cause this proc to not flush the cache @@ -49,7 +53,7 @@ } { # Wrapper for workflow::state::fsm::edit - foreach elm { short_name pretty_name sort_order } { + foreach elm { short_name pretty_name sort_order parent_action } { set row($elm) [set $elm] } @@ -67,16 +71,21 @@ {-workflow_id {}} {-array {}} {-internal:boolean} + {-no_complain:boolean} } { Edit a workflow state. Attributes of the array are: - short_name - pretty_name - sort_order - hide_fields + + @param operation insert, update, delete @param state_id For update/delete: The state to update or delete. @@ -87,6 +96,12 @@ @param array For insert/update: Name of an array in the caller's namespace with attributes to insert/update. + @param internal Set this flag if you're calling this proc from within the corresponding proc + for a particular workflow model. Will cause this proc to not flush the cache + or call workflow::definition_changed_handler, which the caller must then do. + + @param no_complain Silently ignore extra attributes that we don't know how to handle. + @return state_id @see workflow::state::new @@ -144,13 +159,30 @@ # Parse column values switch $operation { insert - update { + # Special-case: array entry parent_action (takes short_name) and parent_action_id (takes action_id) -- + # DB column is parent_action_id (takes action_id_id) + if { [info exists row(parent_action)] } { + if { [info exists row(parent_action_id)] } { + error "You cannot supply both parent_action (takes short_name) and parent_action_id (takes action_id)" + } + if { ![empty_string_p $row(parent_action)] } { + set row(parent_action_id) [workflow::action::get_id \ + -workflow_id $workflow_id \ + -short_name $row(parent_action)] + } else { + set row(parent_action_id) [db_null] + } + unset row(parent_action) + unset missing_elm(parent_action) + } + set update_clauses [list] set insert_names [list] set insert_values [list] # Handle columns in the workflow_fsm_states table foreach attr { - short_name pretty_name hide_fields sort_order + short_name pretty_name hide_fields sort_order parent_action_id } { if { [info exists row($attr)] } { set varname attr_$attr @@ -188,6 +220,47 @@ } } } + + # Auxilliary helper attributes (enabled_actions -> enabled_action_ids, assigned_actions -> assigned_action_ids) + + # Enabled actions + if { [info exists row(enabled_actions)] } { + if { [info exists row(enabled_action_ids)] } { + error "You cannot supply both enabled_actions and enabled_actions_ids" + } + set row(enabled_action_ids) [list] + foreach action_short_name $row(enabled_actions) { + lappend row(enabled_action_ids) [workflow::action::get_id \ + -workflow_id $workflow_id \ + -short_name $action_short_name] + } + unset row(enabled_actions) + } + + # Assigend actions + if { [info exists row(assigned_actions)] } { + if { [info exists row(assigned_action_ids)] } { + error "You cannot supply both assigned_actions and assigned_action_ids" + } + set row(assigned_action_ids) [list] + foreach action_short_name $row(assigned_actions) { + lappend row(assigned_action_ids) [workflow::action::get_id \ + -workflow_id $workflow_id \ + -short_name $action_short_name] + } + unset row(assigned_actions) + } + + # Handle auxillary rows + array set aux [list] + foreach attr { + enabled_action_ids assigned_action_ids + } { + if { [info exists row($attr)] } { + set aux($attr) $row($attr) + unset row($attr) + } + } } } @@ -238,10 +311,32 @@ } } + # Auxilliary rows switch $operation { insert - update { + + # Record in which actions the action is enabled but not assigned + if { [info exists aux(enabled_action_ids)] } { + set assigned_p "f" + db_dml delete_enabled_actions {} + foreach enabled_action_id $aux(enabled_action_ids) { + db_dml insert_enabled_action {} + } + unset aux(enabled_action_ids) + } + + # Record where the action is both enabled and assigned + if { [info exists aux(assigned_action_ids)] } { + set assigned_p "t" + db_dml delete_enabled_actions {} + foreach enabled_action_id $aux(assigned_action_ids) { + db_dml insert_enabled_action {} + } + unset aux(assigned_action_ids) + } + # Check that there are no unknown attributes - if { [llength [array names missing_elm]] > 0 } { + if { [llength [array names missing_elm]] > 0 && !$no_complain_p } { error "Trying to set illegal state attributes: [join [array names missing_elm] ", "]" } } @@ -453,14 +548,14 @@ foreach { key value } $spec { set state($key) [string trim $value] } + set state(short_name) $short_name # Create the state - set state_id [workflow::state::fsm::new \ - -workflow_id $workflow_id \ - -short_name $short_name \ - -pretty_name $state(pretty_name) \ - -hide_fields $state(hide_fields) \ - ] + set state_id [workflow::state::fsm::edit \ + -operation "insert" \ + -workflow_id $workflow_id \ + -array state] + } ad_proc -private workflow::state::fsm::parse_states_spec { @@ -515,6 +610,11 @@ array unset row state_id array unset row workflow_id array unset row sort_order + array unset row parent_action_id + array unset row enabled_actions + array unset row enabled_action_ids + array unset row assigned_actions + array unset row assigned_action_ids set spec {} foreach name [lsort [array names row]] { @@ -590,8 +690,20 @@ @author Peter Marklund } { - array set state_data {} + # state_data will be an array keyed by state_id + # state_data(123) = array-list with: hide_fields, pretty_name, short_name, state_id, sort_order, workflow_id, + # enabled_actions, enabled_action_ids, assigned_actions, assigned_action_ids + # In addition: + # state_data(state_ids) = [list of state_ids in sort order] + array set state_data [list] + # state_array_$state_id is an internal datastructure. It's the array for each state_id entry + # but as a separate array making it easier to lappend to individual entries + + #---------------------------------------------------------------------- + # Get core state information from DB + #---------------------------------------------------------------------- + # Use a list to be able to retrieve states in sort order set state_ids [list] db_foreach select_states {} -column_array state_row { @@ -600,10 +712,107 @@ [list workflow::state::fsm::get_workflow_id_not_cached -state_id $state_row(state_id)] \ $workflow_id - set state_data($state_row(state_id)) [array get state_row] - lappend state_ids $state_row(state_id) + set state_id $state_row(state_id) + + array set state_array_$state_id [array get state_row] + + lappend state_ids $state_id } set state_data(state_ids) $state_ids - return [array get state_data] -} + array set action_short_name [list] + + #---------------------------------------------------------------------- + # Build state-action map + #---------------------------------------------------------------------- + + # Will be stored like this: + # assigned_p_${state_id}($action_id) = 1 if assigned, 0 if enabled, non-existent if neither + + # In addition, we have a supporting structure of action information + # action_info(${action_id},short_name) + # action_info(${action_id},trigger_type) + # action_info(${action_id},always_enabled_p) + # action_info(${action_id},parent_action_id) + # action_info(${action_id},child_action_ids) + + # 1. Get action data: trigger_type, always_enabled, hierarchy + db_foreach always_enabled_actions { + select action_id, + short_name, + trigger_type, + always_enabled_p, + parent_action_id + from workflow_actions + where workflow_id = :workflow_id + } { + set action_info(${action_id},short_name) $short_name + set action_info(${action_id},trigger_type) [string trim $trigger_type] + set action_info(${action_id},parent_action_id) $parent_action_id + if { [template::util::is_true $always_enabled_p] && [lsearch { user auto message } $trigger_type] != -1 } { + set action_info(${action_id},always_enabled_p) 1 + } else { + set action_info(${action_id},always_enabled_p) 0 + } + + # Store as a child of parent NOTE: Not needed any longer + if { ![empty_string_p $parent_action_id] } { + lappend action_info(${parent_action_id},child_action_ids) $action_id + } + + # Mark enabled in all states that have the same parent as the action + if { $action_info(${action_id},always_enabled_p) } { + foreach state_id $state_ids { + if { [string equal $parent_action_id [set state_array_${state_id}(parent_action_id)]] } { + set assigned_p_${state_id}($action_id) 0 + } + } + } + } + + # 2. Get action-state map + db_foreach always_enabled_actions { + select e.action_id, + e.state_id, + e.assigned_p + from workflow_actions a, + workflow_fsm_action_en_in_st e + where a.workflow_id = :workflow_id + and a.action_id = e.action_id + } { + set assigned_p_${state_id}($action_id) [template::util::is_true $assigned_p] + } + + # 3. Put stuff back into the output array + foreach state_id $state_ids { + set state_array_${state_id}(enabled_action_ids) [list] + set state_array_${state_id}(enabled_actions) [list] + set state_array_${state_id}(assigned_action_ids) [list] + set state_array_${state_id}(assigned_actions) [list] + + if { [info exists assigned_p_${state_id}] } { + foreach action_id [array names assigned_p_${state_id}] { + # Enabled + lappend state_array_${state_id}(enabled_action_ids) $action_id + lappend state_array_${state_id}(enabled_actions) $action_info(${action_id},short_name) + + # Assigned + if { [set assigned_p_${state_id}($action_id)] } { + lappend state_array_${state_id}(assigned_action_ids) $action_id + lappend state_array_${state_id}(assigned_actions) $action_info(${action_id},short_name) + } + } + } + } + + #---------------------------------------------------------------------- + # Final output + #---------------------------------------------------------------------- + + # Move over to normal array + foreach state_id $state_ids { + set state_data($state_id) [array get state_array_$state_id] + } + + return [array get state_data]} + 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.5 -r1.6 --- openacs-4/packages/workflow/tcl/state-procs.xql 9 Dec 2003 15:46:43 -0000 1.5 +++ openacs-4/packages/workflow/tcl/state-procs.xql 23 Jan 2004 11:02:27 -0000 1.6 @@ -38,15 +38,17 @@ - select workflow_id, - state_id, - sort_order, - short_name, - pretty_name, - hide_fields - from workflow_fsm_states - where workflow_id = :workflow_id - order by sort_order + select s.workflow_id, + s.state_id, + s.sort_order, + s.short_name, + s.pretty_name, + s.hide_fields, + s.parent_action_id, + (select short_name from workflow_actions where action_id = s.parent_action_id) as parent_action + from workflow_fsm_states s + where s.workflow_id = :workflow_id + order by s.sort_order @@ -58,4 +60,20 @@ + + + delete from workflow_fsm_action_en_in_st + where state_id = :state_id + and assigned_p = :assigned_p + + + + + + insert into workflow_fsm_action_en_in_st + (action_id, state_id, assigned_p) + values (:enabled_action_id, :state_id, :assigned_p) + + + 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.5 -r1.6 --- openacs-4/packages/workflow/tcl/workflow-procs-oracle.xql 16 Dec 2003 18:18:49 -0000 1.5 +++ openacs-4/packages/workflow/tcl/workflow-procs-oracle.xql 23 Jan 2004 11:02:27 -0000 1.6 @@ -14,6 +14,9 @@ w.description_mime_type, a.short_name as initial_action, a.action_id as initial_action_id + + TODO: Changed, see PG version + from workflows w, workflow_initial_action wia, workflow_actions a 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.7 -r1.8 --- openacs-4/packages/workflow/tcl/workflow-procs-postgresql.xql 16 Dec 2003 18:18:49 -0000 1.7 +++ openacs-4/packages/workflow/tcl/workflow-procs-postgresql.xql 23 Jan 2004 11:02:27 -0000 1.8 @@ -15,10 +15,9 @@ a.short_name as initial_action, a.action_id as initial_action_id from workflows w left outer join - workflow_initial_action wia - on (w.workflow_id = wia.workflow_id) left outer join - workflow_actions a - on (a.action_id = wia.action_id) + workflow_actions a on (a.workflow_id = w.workflow_id + and a.parent_action_id is null + and a.trigger_type = 'init') where w.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.20 -r1.21 --- openacs-4/packages/workflow/tcl/workflow-procs.tcl 13 Jan 2004 12:36:47 -0000 1.20 +++ openacs-4/packages/workflow/tcl/workflow-procs.tcl 23 Jan 2004 11:02:27 -0000 1.21 @@ -71,29 +71,38 @@ {-workflow_id {}} {-array {}} {-internal:boolean} + {-no_complain:boolean} } { Edit a workflow. Attributes of the array are: - short_name - pretty_name - object_id - package_key - object_type - description - description_mime_type - callbacks - context_id - creation_user - creation_ip + @param operation insert, update, delete @param workflow_id For update/delete: The workflow to update or delete. @param array For insert/update: Name of an array in the caller's namespace with attributes to insert/update. + @param internal Set this flag if you're calling this proc from within the corresponding proc + for a particular workflow model. Will cause this proc to not flush the cache + or call workflow::definition_changed_handler, which the caller must then do. + + @param no_complain Silently ignore extra attributes that we don't know how to handle. + @return workflow_id @see workflow::new @@ -312,7 +321,7 @@ } # Check that there are no unknown attributes - if { [llength [array names missing_elm]] > 0 } { + if { [llength [array names missing_elm]] > 0 && !$no_complain_p } { error "Trying to set illegal workflow attributes: [join [array names missing_elm] ", "]" } } @@ -387,7 +396,7 @@ @param workflow_id ID of workflow @param array name of array in which the info will be returned @return An array list with keys workflow_id, short_name, - pretty_name, object_id, package_key, object_type, initial_action, + pretty_name, object_id, package_key, object_type, and callbacks. } { @@ -781,13 +790,23 @@ # Pull out the extra types, roles/actions/states, so we don't try to create the workflow with them array set aux [list] + array set counter [list] + array set remain [list] foreach { key namespace } $handlers { if { [info exists workflow($key)] } { set aux($key) $workflow($key) + if { [info exists count($key)] } { + incr remain($key) + } else { + set remain($key) 1 + } + set counter($key) 0 unset workflow($key) } } + array set sub_id [list] + db_transaction { # Create the workflow set workflow_id [${workflow_handler}::edit \ @@ -799,6 +818,8 @@ foreach { type namespace } $handlers { # type is 'roles', 'actions', 'states', etc. if { [info exists aux($type)] } { + incr remain($type) -1 + incr counter($type) foreach { subshort_name subspec } $aux($type) { # subshort_name is the short_name of a single role/action/state array unset row @@ -810,12 +831,22 @@ set row($key) [string trim $row($key)] } - ${namespace}::edit \ - -internal \ - -operation "insert" \ - -workflow_id $workflow_id \ - -array row + set cmd [list ${namespace}::edit \ + -internal \ + -workflow_id $workflow_id \ + -array row] + if { $counter($type) == 1 } { + lappend cmd -operation insert + } else { + lappend cmd -[string range $type 0 end-1]_id $sub_id(${type},${subshort_name}) + } + if { $remain($type) == 0 } { + lappend cmd -no_complain + } + + set sub_id(${type},${subshort_name}) [eval $cmd] + # Flush the cache after all creates workflow::flush_cache -workflow_id $workflow_id } @@ -1055,6 +1086,7 @@ -workflow_handler "workflow::fsm" \ -handlers { roles workflow::role + actions workflow::action states workflow::state::fsm actions workflow::action::fsm }] @@ -1100,8 +1132,8 @@ {-workflow_handler "workflow"} {-handlers { roles workflow::role - states workflow::state::fsm actions workflow::action::fsm + states workflow::state::fsm }} {-deep:boolean} } { 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.15 -r1.16 --- openacs-4/packages/workflow/tcl/test/workflow-test-procs.tcl 9 Jan 2004 15:47:53 -0000 1.15 +++ openacs-4/packages/workflow/tcl/test/workflow-test-procs.tcl 23 Jan 2004 11:02:28 -0000 1.16 @@ -37,7 +37,6 @@ } { return [db_string some_object_id {select min(object_id) from acs_objects where object_type = 'apm_parameter'}] - } ad_proc workflow::test::workflow_id {} { @@ -74,36 +73,70 @@ ad_proc workflow::test::assert_case_state { {-workflow_id:required} {-case_id:required} + {-user_id {}} {-expect_current_state:required} {-expect_enabled_actions:required} - {-expect_user_actions:required} + -expect_user_actions } { Make assertions about what the current state should be and what actions are enabled etc. } { + set actual_states [list] + foreach elm [workflow::case::fsm::get_state_info -all -case_id $case_id] { + foreach { parent_action_id state_id } $elm {} + lappend actual_states [workflow::state::fsm::get_element -state_id $state_id -element short_name] + } - set user_roles \ - [workflow::case::get_user_roles -case_id $case_id \ - -user_id [workflow::test::admin_owner_id]] + if { ![aa_true "Current states should be: $expect_current_state" \ + [util_sets_equal_p $expect_current_state $actual_states]] } { + aa_log "States are: $actual_states" + } + + set enabled_actions [workflow::test::action_short_names \ [workflow::case::get_enabled_actions -case_id $case_id]] + + if { ![aa_true "Enabled actions should be: $expect_enabled_actions" \ + [util_sets_equal_p $enabled_actions $expect_enabled_actions]] } { + aa_log "Enabled actions are: $enabled_actions" + } - workflow::state::fsm::get \ - -state_id [workflow::case::fsm::get_current_state -case_id $case_id] \ - -array state_info + if { [info exists expect_user_actions] } { + if { [empty_string_p $user_id] } { + set user_id [workflow::test::admin_owner_id] + } + set user_actions [workflow::test::action_short_names \ + [workflow::case::get_available_actions \ + -case_id $case_id \ + -user_id $user_id]] + + if { ![aa_true "Available user actions for user $user_id should be: $expect_user_actions" \ + [util_sets_equal_p $user_actions $expect_user_actions]] } { + aa_log "Available user actions are: $user_actions" + } + } +} +ad_proc workflow::test::assert_user_actions { + {-workflow_id:required} + {-case_id:required} + {-user_id {}} + {-expect_user_actions {}} +} { + Make assertions about user actions. +} { + if { [empty_string_p $user_id] } { + set user_id [workflow::test::admin_owner_id] + } set user_actions [workflow::test::action_short_names \ - [workflow::case::get_available_actions -case_id $case_id \ - -user_id [workflow::test::admin_owner_id]]] - - aa_true "current state should be $expect_current_state" \ - [string equal $state_info(short_name) $expect_current_state] - aa_true "checking enabled actions ($enabled_actions) in $expect_current_state state, expecting ($expect_enabled_actions)" \ - [util_sets_equal_p $enabled_actions $expect_enabled_actions] - aa_true "checking user actions ($user_actions) in $expect_current_state state, expecting ($expect_user_actions)" \ - [util_sets_equal_p $user_actions $expect_user_actions] - aa_true "user not assigned to any roles yet" \ - [empty_string_p $user_roles] + [workflow::case::get_available_actions \ + -case_id $case_id \ + -user_id $user_id]] + + if { ![aa_true "Available user actions for user $user_id should be: $expect_user_actions" \ + [util_sets_equal_p $user_actions $expect_user_actions]] } { + aa_log "Available user actions are: $user_actions" + } } @@ -128,7 +161,7 @@ foobar { pretty_name "#acs-subsite.Confirm#" pretty_past_tense "#acs-subsite.Confirm#" - initial_action_p t + trigger_type init } } } @@ -171,7 +204,7 @@ pretty_name "Open" pretty_past_tense "Opened" new_state "open" - initial_action_p t + trigger_type init } comment { pretty_name "Comment" @@ -340,7 +373,7 @@ ##### workflow::action::fsm::new \ - -initial_action_p t \ + -trigger_type init \ -workflow_id $workflow_id \ -short_name [workflow::test::initial_action_short_name] \ -pretty_name "Open" \ @@ -618,20 +651,20 @@ # Close the bug workflow::case::action::execute \ - -case_id $case_id \ - -action_id [workflow::action::get_id -workflow_id $workflow_id \ - -short_name "close"] \ - -comment "Closing Bug" \ - -comment_mime_type "text/plain" \ - -user_id [workflow::test::admin_owner_id] - + -case_id $case_id \ + -action_id [workflow::action::get_id -workflow_id $workflow_id \ + -short_name "close"] \ + -comment "Closing Bug" \ + -comment_mime_type "text/plain" \ + -user_id [workflow::test::admin_owner_id] + set expect_enabled_actions [list comment edit reopen] workflow::test::assert_case_state \ - -workflow_id $workflow_id \ - -case_id $case_id \ - -expect_current_state closed \ - -expect_enabled_actions $expect_enabled_actions \ - -expect_user_actions $expect_enabled_actions + -workflow_id $workflow_id \ + -case_id $case_id \ + -expect_current_state closed \ + -expect_enabled_actions $expect_enabled_actions \ + -expect_user_actions $expect_enabled_actions } @@ -751,7 +784,7 @@ -pretty_name "Closed" workflow::action::fsm::new \ - -initial_action_p t \ + -trigger_type init \ -workflow_id $workflow_id \ -short_name [ad_generate_random_string] \ -pretty_name "Open" \ @@ -763,7 +796,7 @@ -pretty_name "Auto" \ -enabled_states "open" \ -new_state "closed" \ - -timeout_seconds 0] + -trigger_type auto] # Start a case @@ -781,6 +814,7 @@ # Change the action to be timed set update_cols(timeout_seconds) 1 + set update_cols(trigger_type) "time" workflow::action::fsm::edit \ -action_id $auto_action_id \ -array update_cols @@ -865,245 +899,695 @@ } -aa_register_case recursive_workflow { - Testing a recursive workflow +aa_register_case hierarchical_workflow { + Testing a hierarchical workflow } { aa_run_with_teardown -rollback -test_code { #---------------------------------------------------------------------- - # Create inner workflow + # Create hierarchical workflow #---------------------------------------------------------------------- - # [open] -> (open) -> [ask] -> (asked) -> [give] -> (done) - set inner_workflow_id [workflow::fsm::new_from_spec -package_key "acs-automated-testing" -spec { - recursive_workflow_inner { - pretty_name "Recursive Workflow Inner Workflow" + # action_id | trigger |ask_cl|ask_lwr|lac-ask|lac-give|cal-ask|cal-give + # ---------------------------+----------+------+-------+-------+--------+-------+--------- + # open | init | | | | | | + # lawyer_asks_client | workflow | X | | | | | + # lawyer_asks_client_init | init | | | | | | + # lawyer_asks_client_ask | user | | | X | | | + # lawyer_asks_client_give | user | | | | X | | + # client_asks_lawyer | workflow | | X | | | | + # client_asks_lawyer_init | init | | | | | | + # client_asks_lawyer_ask | user | | | | | X | + # client_asks_lawyer_give | user | | | | | | X + + set workflow_id [workflow::fsm::new_from_spec -package_key "acs-automated-testing" -spec { + hierarchical_workflow { + pretty_name "Hierarchical Workflow" states { - open { - pretty_name Open + asking_client { + pretty_name "Asking Client" + enabled_actions { lawyer_asks_client } } - asked { - pretty_name Asked + asking_lawyer { + pretty_name "Asking Lawyer" + enabled_actions { client_asks_lawyer } } - done { - pretty_name Done + done { + pretty_name "Done" } + lawyer_asks_client_asking { + pretty_name "AC-Asking" + parent_action "lawyer_asks_client" + enabled_actions { lawyer_asks_client_ask } + } + lawyer_asks_client_giving { + pretty_name "AC-Giving" + parent_action "lawyer_asks_client" + enabled_actions { lawyer_asks_client_give } + } + lawyer_asks_client_done { + pretty_name "AC-Done" + parent_action "lawyer_asks_client" + } + client_asks_lawyer_asking { + pretty_name "AL-Asking" + parent_action "client_asks_lawyer" + enabled_actions { client_asks_lawyer_ask } + } + client_asks_lawyer_giving { + pretty_name "AL-Giving" + parent_action "client_asks_lawyer" + enabled_actions { client_asks_lawyer_give } + } + client_asks_lawyer_done { + pretty_name "AL-Done" + parent_action "client_asks_lawyer" + } } roles { - asker { - pretty_name "Asker" + lawyer { + pretty_name "Lawyer" } - giver { - pretty_name "Giver" + client { + pretty_name "Client" } } actions { open { pretty_name "Open" pretty_past_tense "Opened" - new_state "open" - initial_action_p t + new_state "asking_client" + trigger_type init } - ask { - pretty_name "Ask" - pretty_past_tense "Asked" - new_state "asked" - enabled_states { open } + lawyer_asks_client { + pretty_name "Lawyer asks client" + pretty_past_tense "Lawyer asked client" + new_state "asking_lawyer" + trigger_type workflow } - give { - pretty_name "Give" - pretty_past_tense "Given" + lawyer_asks_client_init { + pretty_name "Lawyer asks client-Init" + pretty_past_tense "Lawyer asked client-Init" + trigger_type init + parent_action "lawyer_asks_client" + new_state "lawyer_asks_client_asking" + } + lawyer_asks_client_ask { + pretty_name "Ask client" + pretty_past_tense "Asked client" + parent_action "lawyer_asks_client" + new_state "lawyer_asks_client_giving" + assigned_role "lawyer" + } + lawyer_asks_client_give { + pretty_name "Respond to lawyer" + pretty_past_tense "Responded to lawyer" + parent_action "lawyer_asks_client" + new_state "lawyer_asks_client_done" + assigned_role "client" + } + client_asks_lawyer { + pretty_name "Client asks lawyer" + pretty_past_tense "Client asked lawyer" + enabled_states { asking_lawyer } new_state "done" - enabled_states { asked } + trigger_type workflow } + client_asks_lawyer_init { + pretty_name "Client asks lawyer-Init" + pretty_past_tense "Client asked lawyer-Init" + trigger_type init + parent_action "client_asks_lawyer" + new_state "client_asks_lawyer_asking" + } + client_asks_lawyer_ask { + pretty_name "Ask lawyer" + pretty_past_tense "Asked lawyer" + parent_action "client_asks_lawyer" + enabled_states { client_asks_lawyer_asking } + new_state "client_asks_lawyer_giving" + assigned_role "client" + } + client_asks_lawyer_give { + pretty_name "Respond to client" + pretty_past_tense "Responded to client" + parent_action "client_asks_lawyer" + enabled_states { client_asks_lawyer_giving } + new_state "client_asks_lawyer_done" + assigned_role "lawyer" + } } } }] - aa_log "Inner workflow created" + #---------------------------------------------------------------------- + # Test the state-action map + #---------------------------------------------------------------------- + array set state_action_map { + asking_client { lawyer_asks_client } + asking_lawyer { client_asks_lawyer } + done {} + lawyer_asks_client_asking { lawyer_asks_client_ask } + lawyer_asks_client_giving { lawyer_asks_client_give } + lawyer_asks_client_done {} + client_asks_lawyer_asking { client_asks_lawyer_ask } + client_asks_lawyer_giving { client_asks_lawyer_give } + client_asks_lawyer_done {} + } + foreach state [array names state_action_map] { + set state_id [workflow::state::fsm::get_id -workflow_id $workflow_id -short_name $state] + set enabled_actions [workflow::state::fsm::get_element -state_id $state_id -element enabled_actions] + #aa_true "Enabled actions in state $state are $enabled_actions, should be $state_action_map($state)" \ + [util_sets_equal_p $state_action_map($state) $enabled_actions] + } + + #---------------------------------------------------------------------- - # Create outer workflow + # Start a case of the workflow #---------------------------------------------------------------------- - # [open] -> (open) -> [do] -> (done) - set outer_workflow_id [workflow::fsm::new_from_spec -package_key "acs-automated-testing" -spec { - recursive_workflow_outer { - pretty_name "Recursive Workflow Outer Workflow" + aa_log "Starting case." + + set case_id [workflow::case::new \ + -workflow_id $workflow_id \ + -object_id [workflow::test::workflow_object_id] \ + -user_id [workflow::test::admin_owner_id]] + + #---------------------------------------------------------------------- + # 'lawyer_asks_client_ask' should now be available + #---------------------------------------------------------------------- + + workflow::test::assert_case_state \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -expect_current_state { "asking_client" "lawyer_asks_client_asking" } \ + -expect_enabled_actions "lawyer_asks_client_ask" + + #---------------------------------------------------------------------- + # Execute 'lawyer_asks_client_ask' + #---------------------------------------------------------------------- + + aa_log "Executing: lawyer_asks_client_ask" + workflow::case::action::execute \ + -case_id $case_id \ + -action_id [workflow::action::get_id \ + -workflow_id $workflow_id \ + -short_name "lawyer_asks_client_ask"] \ + -comment "Lawyer asks" \ + -comment_mime_type "text/plain" \ + -user_id [workflow::test::admin_owner_id] + + #---------------------------------------------------------------------- + # Enabled action: 'lawyer_asks_client_give' + #---------------------------------------------------------------------- + + workflow::test::assert_case_state \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -expect_current_state { "asking_client" "lawyer_asks_client_giving" } \ + -expect_enabled_actions "lawyer_asks_client_give" + + #---------------------------------------------------------------------- + # Execute 'lawyer_asks_client_give' + #---------------------------------------------------------------------- + + aa_log "Executing: lawyer_asks_client_give" + workflow::case::action::execute \ + -case_id $case_id \ + -action_id [workflow::action::get_id \ + -workflow_id $workflow_id \ + -short_name "lawyer_asks_client_give"] \ + -comment "Client responds" \ + -comment_mime_type "text/plain" \ + -user_id [workflow::test::admin_owner_id] + + + #---------------------------------------------------------------------- + # 'client_asks_lawyer_ask' should now be available + #---------------------------------------------------------------------- + + workflow::test::assert_case_state \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -expect_current_state { "asking_lawyer" "client_asks_lawyer_asking" } \ + -expect_enabled_actions "client_asks_lawyer_ask" + + + #---------------------------------------------------------------------- + # Execute 'client_asks_lawyer_ask' + #---------------------------------------------------------------------- + + aa_log "Executing: client_asks_lawyer_ask" + workflow::case::action::execute \ + -case_id $case_id \ + -action_id [workflow::action::get_id \ + -workflow_id $workflow_id \ + -short_name "client_asks_lawyer_ask"] \ + -comment "Client asks" \ + -comment_mime_type "text/plain" \ + -user_id [workflow::test::admin_owner_id] + + #---------------------------------------------------------------------- + # Enabled action: 'client_asks_lawyer_give' + #---------------------------------------------------------------------- + + workflow::test::assert_case_state \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -expect_current_state { "asking_lawyer" "client_asks_lawyer_giving" } \ + -expect_enabled_actions "client_asks_lawyer_give" + + #---------------------------------------------------------------------- + # Execute 'client_asks_lawyer_give' + #---------------------------------------------------------------------- + + aa_log "Executing: client_asks_lawyer_give" + workflow::case::action::execute \ + -case_id $case_id \ + -action_id [workflow::action::get_id \ + -workflow_id $workflow_id \ + -short_name "client_asks_lawyer_give"] \ + -comment "Lawyer responds" \ + -comment_mime_type "text/plain" \ + -user_id [workflow::test::admin_owner_id] + + #---------------------------------------------------------------------- + # 'done', Nothing enabled + #---------------------------------------------------------------------- + + workflow::test::assert_case_state \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -expect_current_state { "done" } \ + -expect_enabled_actions [list] + } + +} + + +aa_register_case parallel_simple_workflow { + Testing a simple parallel workflow +} { + aa_run_with_teardown -rollback -test_code { + + #---------------------------------------------------------------------- + # Create parallel workflow + #---------------------------------------------------------------------- + # action_id | trigger | s1 | s2 | done | + # ---------------------------+----------+------+------+-------+ + # inti | init | | | | + # task_one | parallel | X | | | + # para_a | user | | | | + # para_b | user | | | | + # task_two | user | | X | | + + set workflow_id [workflow::fsm::new_from_spec -package_key "acs-automated-testing" -spec { + parallel_simple { + pretty_name "Simple Parallel" states { - open { - pretty_name Open + s1 { + pretty_name "S1" + enabled_actions { task_one } } - done { - pretty_name Done + s2 { + pretty_name "S2" + enabled_actions { task_two } } + done { + pretty_name "Done" + } } roles { - lawyer { - pretty_name "Lawyer" + role1 { + pretty_name "Role1" } - client { - pretty_name "Client" + role2 { + pretty_name "Role2" } } actions { - open { + init { pretty_name "Open" - pretty_past_tense "Opened" - new_state "open" - initial_action_p t + new_state "s1" + trigger_type init } - do { - pretty_name "Do" - pretty_past_tense "Done" + task_one { + pretty_name "Task 1 (parallel)" + new_state "s2" + trigger_type parallel + } + para_a { + pretty_name "Para A (user)" + parent_action "task_one" + assigned_role role1 + } + para_b { + pretty_name "Para B (user)" + parent_action "task_one" + assigned_role role2 + } + task_two { + pretty_name "Task 2 (user)" new_state "done" - child_workflow recursive_workflow_inner - child_role_map { - asker lawyer - giver client - } - enabled_states { open } + assigned_role role1 } } } }] + + #---------------------------------------------------------------------- + # Test the state-action map + #---------------------------------------------------------------------- - aa_log "Outer workflow created" + array set state_action_map { + s1 { task_one } + s2 { task_two } + done {} + } + foreach state [array names state_action_map] { + set state_id [workflow::state::fsm::get_id -workflow_id $workflow_id -short_name $state] + set enabled_actions [workflow::state::fsm::get_element -state_id $state_id -element enabled_actions] + aa_true "Enabled actions in state $state are $enabled_actions, should be $state_action_map($state)" \ + [util_sets_equal_p $state_action_map($state) $enabled_actions] + } #---------------------------------------------------------------------- - # Test that it found the right inner workflow ID from the short_name + # Start a case of the workflow #---------------------------------------------------------------------- - set action_with_child_id [workflow::action::get_id -workflow_id $outer_workflow_id -short_name do] - workflow::action::get -action_id $action_with_child_id -array action_with_child - - aa_equals "Child_workflow has the right ID" $action_with_child(child_workflow_id) $inner_workflow_id + aa_log "Starting case." + set case_id [workflow::case::new \ + -workflow_id $workflow_id \ + -object_id [workflow::test::workflow_object_id] \ + -user_id [workflow::test::admin_owner_id]] + + workflow::test::assert_case_state \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -expect_current_state { "s1" } \ + -expect_enabled_actions { "para_a" "para_b" } + #---------------------------------------------------------------------- - # Start a case of the outer workflow + # Execute 'para_b' #---------------------------------------------------------------------- - - set outer_case_id [workflow::case::new \ - -workflow_id $outer_workflow_id \ - -object_id [workflow::test::workflow_object_id] \ - -user_id [workflow::test::admin_owner_id]] - aa_log "Outer case started" + workflow::case::action::execute \ + -case_id $case_id \ + -action_id [workflow::action::get_id \ + -workflow_id $workflow_id \ + -short_name "para_b"] \ + -user_id [workflow::test::admin_owner_id] + workflow::test::assert_case_state \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -expect_current_state { "s1" } \ + -expect_enabled_actions "para_a" + #---------------------------------------------------------------------- - # Do should be enabled immediately + # Execute 'para_a' #---------------------------------------------------------------------- - set enabled_actions [workflow::case::get_enabled_actions -case_id $outer_case_id] + aa_log "Executing: para_a" + workflow::case::action::execute \ + -case_id $case_id \ + -action_id [workflow::action::get_id \ + -workflow_id $workflow_id \ + -short_name "para_a"] \ + -user_id [workflow::test::admin_owner_id] - if { [aa_equals "One enabled action" [llength $enabled_actions] 1] } { - - set enabled_action_short_name [workflow::action::get_element \ - -action_id [lindex $enabled_actions 0] \ - -element short_name] - aa_equals "And that is 'do'" $enabled_action_short_name "do" - } else { - aa_log "Parent case: [db_list_of_lists foo { select * from workflow_cases where case_id = :outer_case_id }]" - aa_log "Enabled actions outer: [db_list_of_lists foo { select * from workflow_case_enabled_actions where case_id = :outer_case_id }]" - aa_log "Child cases: [db_list_of_lists foo { select * from workflow_cases where top_case_id = :outer_case_id and case_id != top_case_id }]" - } + workflow::test::assert_case_state \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -expect_current_state { "s2" } \ + -expect_enabled_actions "task_two" + #---------------------------------------------------------------------- - # Which means inner case should be created + # Execute 'task_two' #---------------------------------------------------------------------- - set inner_case_ids [db_list foo { select case_id from workflow_cases where top_case_id = :outer_case_id and case_id != top_case_id }] - aa_equals "One child case" [llength $inner_case_ids] 1 + aa_log "Executing: task_two" + workflow::case::action::execute \ + -case_id $case_id \ + -action_id [workflow::action::get_id \ + -workflow_id $workflow_id \ + -short_name "task_two"] \ + -user_id [workflow::test::admin_owner_id] - set inner_case_id [lindex $inner_case_ids 0] + #---------------------------------------------------------------------- + # 'done', Nothing enabled + #---------------------------------------------------------------------- - aa_log "Child cases: $inner_case_ids" + workflow::test::assert_case_state \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -expect_current_state { "done" } \ + -expect_enabled_actions [list] + } + +} + +aa_register_case dynamic_simple_workflow { + Testing a simple parallel workflow +} { + aa_run_with_teardown -rollback -test_code { + #---------------------------------------------------------------------- - # Which means 'ask' should be available + # Create dynamic workflow #---------------------------------------------------------------------- + # action_id | trigger | s1 | s2 | done | + # ---------------------------+----------+------+------+-------+ + # inti | init | | | | + # task_one | dynamic | X | | | + # dyn | user | | | | + # task_two | user | | X | | + + set workflow_id [workflow::fsm::new_from_spec -package_key "acs-automated-testing" -spec { + dynamic_simple { + pretty_name "Simple Dynamic Test Workflow" + states { + s1 { + pretty_name "S1" + assigned_actions { task_one } + } + s2 { + pretty_name "S2" + assigned_actions { task_two } + } + done { + pretty_name "Done" + } + } + roles { + role1 { + pretty_name "Role1" + } + role2 { + pretty_name "Role2" + } + } + actions { + init { + pretty_name "Open" + new_state "s1" + trigger_type init + } + task_one { + pretty_name "Task 1 (dynamic)" + new_state "s2" + trigger_type dynamic + } + dyn { + pretty_name "Dynamic (user)" + parent_action "task_one" + assigned_role role1 + } + task_two { + pretty_name "Task 2 (user)" + new_state "done" + assigned_role role2 + } + } + } + }] - set inner_enabled_actions [workflow::case::get_enabled_actions -case_id $inner_case_id] + #---------------------------------------------------------------------- + # Test the state-action map + #---------------------------------------------------------------------- - if { [aa_equals "One enabled action" [llength $inner_enabled_actions] 1] } { - - set inner_enabled_action_id [lindex $inner_enabled_actions 0] - - set inner_enabled_action_short_name [workflow::action::get_element \ - -action_id $inner_enabled_action_id \ - -element short_name] - aa_equals "And that is 'ask'" $inner_enabled_action_short_name "ask" + array set state_action_map { + s1 { task_one } + s2 { task_two } + done {} } + foreach state [array names state_action_map] { + set state_id [workflow::state::fsm::get_id -workflow_id $workflow_id -short_name $state] + set enabled_actions [workflow::state::fsm::get_element -state_id $state_id -element enabled_actions] + aa_true "Enabled actions in state $state are $enabled_actions, should be $state_action_map($state)" \ + [util_sets_equal_p $state_action_map($state) $enabled_actions] + } + #---------------------------------------------------------------------- + # Create the required users + #---------------------------------------------------------------------- + + array set r1u1_array [auth::create_user -username [ad_generate_random_string] -email "[ad_generate_random_string]@test.test" \ + -first_names [ad_generate_random_string] -last_name [ad_generate_random_string]] + set r1u1 $r1u1_array(user_id) + + array set r1u2_array [auth::create_user -username [ad_generate_random_string] -email "[ad_generate_random_string]@test.test" \ + -first_names [ad_generate_random_string] -last_name [ad_generate_random_string]] + set r1u2 $r1u2_array(user_id) + + array set r2u1_array [auth::create_user -username [ad_generate_random_string] -email "[ad_generate_random_string]@test.test" \ + -first_names [ad_generate_random_string] -last_name [ad_generate_random_string]] + set r2u1 $r2u1_array(user_id) + + #---------------------------------------------------------------------- + # Start a case of the workflow + #---------------------------------------------------------------------- + + aa_log "Starting case." + + set case_id [workflow::case::new \ + -workflow_id $workflow_id \ + -object_id [workflow::test::workflow_object_id] \ + -user_id [workflow::test::admin_owner_id] \ + -assignment [list role1 [list $r1u1 $r1u2] role2 [list $r2u1]]] + workflow::test::assert_case_state \ - -workflow_id $inner_workflow_id \ - -case_id $inner_case_id \ - -expect_current_state "open" \ - -expect_enabled_actions "ask" \ - -expect_user_actions "ask" + -workflow_id $workflow_id \ + -case_id $case_id \ + -expect_current_state { s1 } \ + -expect_enabled_actions { dyn dyn } + workflow::test::assert_user_actions \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -user_id $r1u1 \ + -expect_user_actions { dyn } + workflow::test::assert_user_actions \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -user_id $r1u2 \ + -expect_user_actions { dyn } + + workflow::test::assert_user_actions \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -user_id $r2u1 \ + -expect_user_actions { } + #---------------------------------------------------------------------- - # Execute 'ask' + # Execute 'sub' as r1u2 #---------------------------------------------------------------------- + set enabled_action_id [workflow::case::get_available_enabled_action_ids \ + -case_id $case_id \ + -user_id $r1u2] + workflow::case::action::execute \ - -case_id $inner_case_id \ - -action_id [workflow::action::get_id \ - -workflow_id $inner_workflow_id \ - -short_name "ask"] \ - -comment "Asking" \ - -comment_mime_type "text/plain" \ - -user_id [workflow::test::admin_owner_id] + -case_id $case_id \ + -enabled_action_id $enabled_action_id \ + -user_id $r1u2 #---------------------------------------------------------------------- - # Enabled action: 'give' + # Available should now be 'sub' as r1u1 #---------------------------------------------------------------------- - + workflow::test::assert_case_state \ - -workflow_id $inner_workflow_id \ - -case_id $inner_case_id \ - -expect_current_state "asked" \ - -expect_enabled_actions "give" \ - -expect_user_actions "give" + -workflow_id $workflow_id \ + -case_id $case_id \ + -expect_current_state { s1 } \ + -expect_enabled_actions { dyn } + workflow::test::assert_user_actions \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -user_id $r1u1 \ + -expect_user_actions { dyn } + + workflow::test::assert_user_actions \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -user_id $r1u2 \ + -expect_user_actions { } + + workflow::test::assert_user_actions \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -user_id $r2u1 \ + -expect_user_actions { } + #---------------------------------------------------------------------- - # Execute 'give' + # Execute 'sub' as r1u1 #---------------------------------------------------------------------- + set enabled_action_id [workflow::case::get_available_enabled_action_ids \ + -case_id $case_id \ + -user_id $r1u1] + workflow::case::action::execute \ - -case_id $inner_case_id \ - -action_id [workflow::action::get_id \ - -workflow_id $inner_workflow_id \ - -short_name "give"] \ - -comment "Giving" \ - -comment_mime_type "text/plain" \ - -user_id [workflow::test::admin_owner_id] + -case_id $case_id \ + -enabled_action_id $enabled_action_id \ + -user_id $r1u1 #---------------------------------------------------------------------- - # No enabled actions in inner workflow + # Available should now be 'task_two' as r2u1 #---------------------------------------------------------------------- - + workflow::test::assert_case_state \ - -workflow_id $inner_workflow_id \ - -case_id $inner_case_id \ - -expect_current_state "done" \ - -expect_enabled_actions {} \ - -expect_user_actions {} + -workflow_id $workflow_id \ + -case_id $case_id \ + -expect_current_state { s2 } \ + -expect_enabled_actions { task_two } + workflow::test::assert_user_actions \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -user_id $r1u1 \ + -expect_user_actions { } + + workflow::test::assert_user_actions \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -user_id $r1u2 \ + -expect_user_actions { } + + workflow::test::assert_user_actions \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -user_id $r2u1 \ + -expect_user_actions { task_two } + #---------------------------------------------------------------------- - # No enabled actions in outer workflow + # Execute 'task_two' #---------------------------------------------------------------------- + + set enabled_action_id [workflow::case::get_available_enabled_action_ids \ + -case_id $case_id \ + -user_id $r2u1] - workflow::test::assert_case_state \ - -workflow_id $outer_workflow_id \ - -case_id $outer_case_id \ - -expect_current_state "done" \ - -expect_enabled_actions {} \ - -expect_user_actions {} + workflow::case::action::execute \ + -case_id $case_id \ + -enabled_action_id $enabled_action_id \ + -user_id $r2u1 + #---------------------------------------------------------------------- + # 'done', Nothing enabled + #---------------------------------------------------------------------- - + workflow::test::assert_case_state \ + -workflow_id $workflow_id \ + -case_id $case_id \ + -expect_current_state { done } \ + -expect_enabled_actions { } \ + -expect_user_actions { } } } Index: openacs-4/packages/workflow/www/doc/fall-2003-extensions.html =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/www/doc/fall-2003-extensions.html,v diff -u -r1.3 -r1.4 --- openacs-4/packages/workflow/www/doc/fall-2003-extensions.html 9 Jan 2004 15:47:53 -0000 1.3 +++ openacs-4/packages/workflow/www/doc/fall-2003-extensions.html 23 Jan 2004 11:02:28 -0000 1.4 @@ -32,8 +32,25 @@ -

Hierarchical Workflows

+ +

Hierarchical Workflows

+

Requirements

@@ -54,6 +71,32 @@ +

Questions we need answered by the design

+ +
    +
  1. + Which actions are enabled? +
  2. +
  3. + How is the state changed after this action has executed? +
  4. +
  5. + Which roles are assigned/allowed to perform an action? +
  6. +
  7. + Which roles do a user play? +
  8. +
  9. + What is the activity history on this case? +
  10. +
  11. + What is the name of this action? +
  12. +
  13. + How do we clone a workflow? +
  14. +
+

Design