Adding Workflow to a Project

Adding Workflow to an OpenACS package

I'm trying to add Workflow to the project-manager I'm writing (in collaboration with other OpenACS programmers), so I thought I might as well document what needs to be done, so others can learn from my hard knocks.

Why use workflow, and for that matter, what is workflow?

Lars, the author of the workflow package, defines workflow like this: "[Workflow] provides a service to keep track of a process involving multiple people around some object. Workflow keeps track of the process you wish to follow, of where you currently are in the process (the current state), and who's supposed to do what.

I must confess that I've been awfully tempted not to use workflow. There seems to be quite a learning curve, and it's much easier to duplicate the functionality of workflow doing it your own way. However, several programmers with much more experience than I insist that it is really the way to do things. So if you have a choice, they would recommend using workflow. I'll reserve judgement until I figure out enough to see if it was worth the effort or not.

Documentation

Lars, the uber-programmer who created Workflow, fortunately wrote a lot of documentation for Workflow.

project-manager: before workflow

Here is what the application looked like before adding in workflow. I know there are problems with this code, and I don't recommend using it as a basis for your own coding. However, if will give you an idea of what changed. This code is pretty basic. It allows you to add and edit projects, and there is a page for viewing them as well.

project-manager-create.sql

--
-- packages/project-manager/sql/postgresql/project-manager-create.sql
--
-- @author jade@bread.com
-- @creation-date 2003-05-15
-- @cvs-id $Id: project-manager-create.sql,v 1.1 2003/05/15 21:52:31 oacs Exp $
--
--

\i project-manager-table-create.sql
\i project-manager-functions-create.sql

project-manager-table-create.sql

-- TODO:
-- 
-- which items in this data model need to use the content repository?
-- need to add in workflow (for status among other things)
-- need to take into account acs-rels
-- add categories to projects

--
-- packages/project-manager/sql/postgresql/project-manager-table-create.sql
--
-- @author jader@bread.com and everyone else involved in this thread: http://ope
nacs.org/forums/message-view?message_id=90742
-- @creation-date 2003-05-15
--

create table pm_project (
        project_id              integer
                                constraint project_manager_id_fk
                                references acs_objects(object_id) 
                                constraint pm_project_id_pk
                                primary key,
        project_name            varchar(255) 
                                constraint pm_project_name_nn
                                not null,
        -- a user-specified
        project_code            varchar(255),
        -- for subprojects
        parent_project_id       integer
                                constraint pm_project_parent_project_id_fk
                                references pm_project,
        goal                    varchar(4000),
        description             varchar(4000),
        -- is the deadline computed from the end date, or from 
        -- today?
        -- e = end, t = today
        deadline_scheduling     char(1) default 't'
                                constraint pm_project_dline_scheduling_ck
                                check (deadline_scheduling in ('t','e')),
        planned_start_date      timestamptz,
        planned_end_date        timestamptz,
        actual_start_date       timestamptz,
        actual_end_date         timestamptz,
        ongoing_p               char(1) default 'f' 
                                constraint pm_project_ongoing_p_ck
                                check (ongoing_p in ('t','f'))
);


create function inline_0 ()
returns integer as '
begin
    PERFORM acs_object_type__create_type (
        ''pm_project'',                         -- object_type
        ''Project'',                            -- pretty_name
        ''Projects'',                           -- pretty_plural
        ''acs_object'',                         -- supertype
        ''pm_project'',                         -- table_name
        ''project_id'',                         -- id_column
        null,                                   -- package_name
        ''f'',                                  -- abstract_p
        null,                                   -- type_extension_table
        ''pm_project__name''                    -- name_method
        );
    return 0;
end;' language 'plpgsql';
select inline_0 ();
drop function inline_0 ();

project-manager-functions-create.sql

--
-- packages/project-manager/sql/postgresql/project-manager-functions-create.sql
--
-- @author jade@bread.com
-- @creation-date 2003-05-15
-- @cvs-id $Id: project-manager-functions-create.sql,v 1.1 2003/05/15 21:52:31 o
acs Exp $
--
--

select define_function_args('pm_project__new','project_id,project_name,project_c
ode,parent_project_id,goal,description,deadline_scheduling,planned_start_date,pl
anned_end_date,actual_start_date,actual_end_date,ongoing_p,creation_date,creatio
n_user,creation_ip,context_id'); 

create or replace function pm_project__new (integer,varchar,varchar,integer,varc
har,varchar,char(1),timestamptz,timestamptz,timestamptz,timestamptz,char(1),time
stamptz,integer,varchar,integer)
returns integer as '
declare
  p_project_id                                  alias for $1;
  p_project_name                                alias for $2;
  p_project_code                                alias for $3;
  p_parent_project_id                           alias for $4;
  p_goal                                        alias for $5;
  p_description                                 alias for $6;
  p_deadline_scheduling                         alias for $7;
  p_planned_start_date                          alias for $8;
  p_planned_end_date                            alias for $9;
  p_actual_start_date                           alias for $10;
  p_actual_end_date                             alias for $11;
  p_ongoing_p                                   alias for $12;
  p_creation_date                               alias for $13;
  p_creation_user                               alias for $14;
  p_creation_ip                                 alias for $15;
  p_context_id                                  alias for $16;
  v_pm_project_id                               int;
begin
        v_pm_project_id := acs_object__new (
                p_project_id,
                ''pm_project'',
                p_creation_date,
                p_creation_user,
                p_creation_ip,
                p_context_id
        );
        insert into pm_project
          (project_id,project_name,project_code,parent_project_id,goal,descripti
on,deadline_scheduling,planned_start_date,planned_end_date,actual_start_date,act
ual_end_date,ongoing_p) 
        values
          (v_pm_project_id, p_project_name, p_project_code, p_parent_project_id,
 p_goal, p_description, p_deadline_scheduling, p_planned_start_date, p_planned_e
nd_date, p_actual_start_date, p_actual_end_date, p_ongoing_p);
        PERFORM acs_permission__grant_permission(
          v_pm_project_id,
          p_creation_user,
          ''admin''
    );
        return v_pm_project_id;
end;' language 'plpgsql';
/* The __delete function deletes a record and all related overhead. */
  

select define_function_args('pm_project___delete','project_id');
create or replace function pm_project__delete (integer)
returns integer as '
declare
  p_pm_project_id                               alias for $1;
begin
    delete from acs_permissions
                   where object_id = p_pm_project_id;
        delete from pm_project
                   where project_id = p_pm_project_id;
        raise NOTICE ''Deleting pm_project...'';
        PERFORM acs_object__delete(p_pm_project_id);
        return 0;
end;' language 'plpgsql';
/* When we created the acs object type above, we specified a
'name_method'.  This is the name of a function that will return the
name of the object.  This is a convention ensuring that all objects
can be identified.  Now we have to build that function.  In this case,
we'll return a field called title as the name. */

select define_function_args('pm_project___name','project_id');

create or replace function pm_project__name (integer)
returns varchar as '
declare
    p_pm_project_id      alias for $1;
    v_pm_project_name    pm_project.project_name%TYPE;
begin
        select project_name into v_pm_project_name
                from pm_project
                where project_id = p_pm_project_id;
    return v_pm_project_name;
end;
' language 'plpgsql';

project-manager-drop

-- packages/project-manager/sql/project-manager-drop.sql
-- drop script
--
-- @author jade@bread.com
-- @creation-date 2003-05-15
-- @cvs-id $Id: project-manager-drop.sql,v 1.1 2003/05/15 21:52:31 oacs Exp $
--

--drop package, which drops all functions created with define_function_args

select drop_package('pm_project');

--drop permissions
delete from acs_permissions where object_id in (select project_id from pm_projec
t);

--drop objects
create function inline_0 ()
returns integer as '
declare
        object_rec              record;
begin
        for object_rec in select object_id from acs_objects where object_type=''
pm_project''
        loop
                perform acs_object__delete( object_rec.object_id );
        end loop;
        return 0;
end;' language 'plpgsql';

select inline_0();
drop function inline_0();

--drop table
drop table pm_project;

--drop type
select acs_object_type__drop_type(
           'pm_project',
           't'
    );

index.tcl

ad_page_contract {
    Main view page for projects.

    @author jader@bread.com
    @creation-date 2003-05-15
    @cvs-id $Id: index.tcl,v 1.3 2003/05/16 22:23:58 oacs Exp $
    @param orderby indicates when the user clicks on a column to order by that \
column
    @return table_html preformatting html table constructed by querying the sam\
plenotes table

} {
    {orderby:optional {title}}
} -properties {
    table_html
}
# define the columns in the table
set table_def   {
    {edit "" {} {<td><a href="add-edit?project_id=$project_id">Edit</a></td>}}
    {view "" {} {<td><a href="one?project_id=$project_id">View</a></td>}}
    {project_name "Name"}
    {project_code "Project code"}
    {parent_project_id "Parent project"}
    {goal "Goal"}
    {description "Description"}
    {planned_start_date "Start date (planned)"}
    {planned_end_date "End date (planned)"}
    {ongoing_p "Ongoing?"}
}

# construct an html table from the samplenotes database table
set table_html [ad_table -Torderby $orderby project_query {} $table_def]

index.adp

<master>
<property name="title">Projects</property>
@table_html@
<p><a href="add-edit">Add a project</a></p>

index.xql


<?xml version="1.0"?>
<queryset>
  <fullquery name="project_query">
    <querytext>
        SELECT
        project_id,
        project_name,
        project_code,
        parent_project_id,
        goal,
        description,
        deadline_scheduling,
        to_char(planned_start_date,'YYYY MM DD') as planned_start_date,
        to_char(planned_end_date,'YYYY MM DD') as planned_end_date,
        ongoing_p
        FROM
        pm_project
    [ad_order_by_from_sort_spec $orderby $table_def]
    </querytext>
  </fullquery>
</queryset>

add-edit.tcl

ad_page_contract {
    Simple add/edit form for projects
} {
    project_id:integer,optional
    {planned_start_date ""}
    {planned_end_date ""}
} -properties {
    context_bar:onevalue
    title:onevalue
}


set user_id [ad_maybe_redirect_for_registration]

set package_id [ad_conn package_id]

if {[exists_and_not_null project_id]} {
    set title "Edit a project"
    set context_bar [ad_context_bar "Edit Project"]
} else {
    set title "Add a project"
    set context_bar [ad_context_bar "New Project"]
}


ad_form -name add_edit -form {
    project_id:key

    {project_name:text
        {label "Project name"}
    }

    {project_code:text
        {label "Project code"}
    }

    {parent_project_id:text(hidden)
	{value ""}}

    {goal:text(textarea)
	{label "Project goal"}
	{optional}
	{html { rows 5 cols 40 wrap soft}}}

    {description:text(textarea)
	{label "Description"}
	{optional}
	{html { rows 5 cols 40 wrap soft}}}

    {deadline_scheduling:text(select)
	{label "Scheduling"}
	{options {{"From today" "t"} {"From deadline" "e"}} {value $deadline_scheduling}} }

    {planned_start_date:date {value {[util::date acquire clock [clock scan $planned_start_date]]}} optional
	{label "Planned start date"}
	{format "MONTH DD YYYY"}
	{help}
    }

    {planned_end_date:date {value {[util::date acquire clock [clock scan $planned_end_date]]}} optional
	{label "Planned end date"}
	{format "MONTH DD YYYY"}
	{help}
    }

    {ongoing_p:text(select)
	{label "Project is ongoing?"}
	{options {{"Yes" "t"} {"No" "f"}} {value $ongoing_p}} }

} -select_query_name project_query -on_submit {

    set user_id [ad_conn user_id]
    set peeraddr [ad_conn peeraddr]

} -new_data {
    db_exec_plsql do_insert { *SQL* }

    ad_returnredirect "."
    ad_script_abort

} -edit_data {

    db_dml do_update { *SQL* }

} -after_submit {

    ad_returnredirect "index"
    ad_script_abort
}

add-edit.adp

<master>
<property name="context_bar">@context_bar@</property>
<property name="title">@title@</property>

<center>
<formtemplate id="add_edit" style="plain-ibr"></formtemplate>
</center>

add-edit.xql

<?xml version="1.0"?>
<queryset>
  <fullquery name="do_insert">
    <querytext>
        select pm_project__new(
	:project_id, 
	:project_name,
	:project_code,
	:parent_project_id,
	:goal,
	:description,
	:deadline_scheduling,
	to_timestamp(:planned_start_date,'YYYY MM DD HH24 MI SS'),
	to_timestamp(:planned_end_date,'YYYY MM DD HH24 MI SS'),
	null,
	null,
	:ongoing_p,
	now(),
	:user_id,
	:peeraddr,
	:package_id);
    </querytext>
  </fullquery>

  <fullquery name="do_update">
    <querytext>
	UPDATE
	pm_project
	SET 
		project_name		= :project_name,
		project_code		= :project_code,
		parent_project_id	= :parent_project_id,
		goal			= :goal,
		description		= :description,
		deadline_scheduling	= :deadline_scheduling,
		planned_start_date	= to_timestamp(:planned_start_date,'YYYY MM DD HH24 MI SS'),
		planned_end_date	= to_timestamp(:planned_end_date,'YYYY MM DD HH24 MI SS'),
		ongoing_p		= :ongoing_p
	WHERE
		project_id = :project_id
    </querytext>
  </fullquery>

  <fullquery name="project_query">
    <querytext>
      select
	project_id,
        project_name,
        project_code,
	parent_project_id,
	goal,
	description,
	deadline_scheduling,
	to_char(planned_start_date,'YYYY MM DD') as planned_start_date,
	to_char(planned_end_date,'YYYY MM DD') as planned_end_date,
	ongoing_p
	FROM
	pm_project
       where project_id = :project_id
    </querytext>
  </fullquery>
</queryset>

Adding in workflow

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

Step 1: Define your default process

What does it mean to define your default process? In my understanding, this is to define three things: So let's do that for the project-manager:
Roles for projects
States for projects
Actions for projects

project-procs.tcl

After this, we're supposed to define our workflow. The definition file goes in /tcl/project-procs.tcl. You'll need to copy a lot of the files from bug-tracker.
namespace eval project_manager::project {}

ad_proc -private project_manager::project::workflow_create {} {
    Create the 'project' workflow for project-manager
} {
    set spec {
        project {
            pretty_name "Project"
            package_key "project-manager"
            object_type "pm_project"
            callbacks { 
		# leave these blank for now
                # bug-tracker.FormatLogTitle 
                # bug-tracker.BugNotificationInfo
            }
            roles {
                participant {
                    pretty_name "Participant"
                    callbacks { 
			# leave these blank for now
                        # workflow.Role_DefaultAssignees_CreationUser
                    }
                }
                manager {
                    pretty_name "Manager"
                    callbacks { 
			# leave these blank for now
                        # workflow.Role_DefaultAssignees_CreationUser
                    }
                }
                client_representative {
                    pretty_name "Client representative"
                    callbacks {
                        # bug-tracker.ComponentMaintainer
                        # bug-tracker.ProjectMaintainer
                        # workflow.Role_PickList_CurrentAssignees
                        # workflow.Role_AssigneeSubquery_RegisteredUsers
                    }
                }
            }
            states {
                open {
                    pretty_name "Open"
                    # hide_fields { resolution fixed_in_version }
                }
                closed {
                    pretty_name "Closed"
                }
            }
            actions {
                create {
                    pretty_name "Create"
                    pretty_past_tense "Created"
                    new_state "open"
                    initial_action_p t
                }
                edit {
                    pretty_name "Edit"
                    pretty_past_tense "Edited"
                    privileges { write }
                    always_enabled_p t
                    edit_fields { 
			# not sure what to do here
                        #component_id 
                        #summary 
                        #found_in_version
                        #role_assignee
                        #fix_for_version
                        #resolution 
                        #fixed_in_version 
                    }
                }
                close {
                    pretty_name "Close"
                    pretty_past_tense "Closed"
                    # what is this doing?
		    assigned_role "submitter"
                    assigned_states { resolved }
                    new_state "closed"
                    privileges { write }
                }
                reopen {
                    pretty_name "Reopen"
                    pretty_past_tense "Reopened"
                    enabled_states { resolved closed }
                    new_state "open"
                    privileges { write }
                }
            }
        }
    }

    set workflow_id [workflow::fsm::new_from_spec -spec $spec]
    
    return $workflow_id
}
Acceptance test
At this point, we probably should check to make sure that what we've done is correct. I have no clue as to how to do that.

Step two: callbacks service contracts

We now need to work to identify, declare, and implement the callbacks that our application will need. Why would we want to do this? These callbacks are functions that can do things like determine default assignees based on certain data in your application, get information about your application's object for use when sending out notifications, or perform "side-effects", such as actually changing the publication state of a content item when you execute the [Publish] action.

So, how do you go about adding in the callbacks? Well, first of all, you better make sure you understand how acs-service-contracts work.

See my page on acs-service-contracts for a background on this. Don't skip this unless you already are comfortable with acs-service-contracts.

We add in an /tcl/install-procs.tcl file. I simply copied it over from bug-tracker, which is probably the best example to use for Workflow code.

The point I'm currently confused on is how to choose what service contracts to implement. Help?



If the namespace portion looks confusing, then see my namespace page.