in last 10 minutes
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.- Documentation on Workflow click on download next to the most recent version of the documentation.
- Developers guide for workflow you will have to click on this link instead of the link in the index.html document. The link doesn't go to the most recent version of the document, which will deprive you of the rest of the document!
- You also really need to look at the source code for both Workflow and bug-tracker.
- A powerpoint presentation on adding 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
project-manager-table-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
-- 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:- 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.
- Identify, declare, and implement the callbacks that your application will need.
- Write the code to set up the initial process, and to clone that process for each package instance.
- Integrate workflow support into your application's API.
- Integrate workflow support into your application's user interface.
- 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:- roles - who are the actors in your application
- states - what are the valid states for this object
- actions - what are valid actions on this object
Roles for projects
- creater
- participant
- manager
- client_representative
States for projects
- open
- closed
Actions for projects
- create
- close
- reopen
- edit
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-contract
s 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.