case-procs.tcl
Does not contain a contract.
- Location:
- /packages/workflow/tcl/case-procs.tcl
Related Files
- packages/workflow/tcl/case-procs.xql
- packages/workflow/tcl/case-procs.tcl
- packages/workflow/tcl/case-procs-postgresql.xql
- packages/workflow/tcl/case-procs-oracle.xql
[ hide source ] | [ make this the default ]
File Contents
ad_library { Procedures in the case namespace. @creation-date 13 January 2003 @author Lars Pind (lars@collaboraid.biz) @author Peter Marklund (peter@collaboraid.biz) @cvs-id $Id: case-procs.tcl,v 1.59 2024/10/13 09:03:15 gustafn Exp $ } namespace eval workflow::case {} namespace eval workflow::case::fsm {} namespace eval workflow::case::action {} namespace eval workflow::case::role {} namespace eval workflow::case::action::fsm {} ##### # # workflow::case # ##### d_proc -private workflow::case::insert { {-workflow_id:required} {-case_id {}} {-object_id:required} } { Internal procedure that creates a new workflow case in the database. Should not be used by applications. Use workflow::case::new instead. @param object_id The object_id which the case is about @param workflow_id The ID of the workflow. @return The case_id of the case. Returns the empty string if no case could be found. @see workflow::case::new @author Lars Pind (lars@collaboraid.biz) } { db_transaction { if { (![info exists case_id] || $case_id eq "") } { set case_id [db_nextval "workflow_cases_seq"] } # Create the case db_dml insert_case {} # Initialize the FSM state to NULL db_dml insert_case_fsm {} } return $case_id } d_proc -public workflow::case::new { {-no_notification:boolean} -workflow_id:required {-case_id {}} {-object_id {}} {-comment {}} {-comment_mime_type {}} -user_id -assignment -package_id {-initial_action_id ""} } { Start a new case for this workflow and object. @param object_id The object_id which the case is about @param workflow_id The ID of the workflow for the case. @param comment_mime_type text/html, text/plain, text/pre, text/enhanced. @param assignment Array-list of role_short_name and list of party_ids to assign to roles before starting. @return The case_id of the case. @author Lars Pind (lars@collaboraid.biz) } { if { (![info exists user_id] || $user_id eq "") } { set user_id [ad_conn user_id] } if { (![info exists package_id] || $package_id eq "") } { set package_id [ad_conn package_id] } db_transaction { # Initial action if {0} { # get initial action not from cache array set row [workflow::get_not_cached -workflow_id $workflow_id] set initial_action_id $row(initial_action_id) array unset row } if {$initial_action_id eq ""} { set initial_action_id [workflow::get_element -workflow_id $workflow_id -element initial_action_id] } if { $initial_action_id eq "" } { # 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(trigger_type) "init" set states [workflow::fsm::get_states -workflow_id $workflow_id] if { [llength $states] == 0 } { error "workflow $workflow_id doesn't have any states" } # We use the first state as the initial state set action_row(new_state_id) [lindex $states 0] # Add the new initial action set initial_action_id [workflow::action::fsm::edit \ -operation "insert" \ -array action_row \ -workflow_id $workflow_id] workflow::flush_cache -workflow_id $workflow_id } else { # NOTE: FSM-specific check here workflow::action::fsm::get -action_id $initial_action_id -array initial_action set new_state $initial_action(new_state) if { $new_state eq "" } { 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 \ -case_id $case_id \ -object_id $object_id] # Assign roles if { ([info exists assignment] && $assignment ne "") } { array set assignment_array $assignment workflow::case::role::assign -case_id $case_id -array assignment_array } # Execute the initial action workflow::case::action::execute \ -no_notification=$no_notification_p \ -case_id $case_id \ -action_id $initial_action_id \ -comment $comment \ -comment_mime_type $comment_mime_type \ -user_id $user_id \ -package_id $package_id \ -initial } return $case_id } d_proc -public workflow::case::get_id { {-object_id:required} {-workflow_short_name:required} } { Gets the case_id from the object_id which the case is about, along with the short_name of the workflow. @param object_id The object_id which the case is about @param workflow_short_name The short_name of the workflow. @return The case_id of the case. Returns the empty string if no case could be found. @author Lars Pind (lars@collaboraid.biz) } { set found_p [db_0or1row select_case_id {}] if { $found_p } { return $case_id } else { error "No matching workflow case found for object_id $object_id and workflow_short_name $workflow_short_name" } } d_proc -public workflow::case::get { {-case_id:required} {-array:required} {-action_id {}} } { 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 workflow::case::fsm::get -case_id $case_id -array row -action_id $action_id # TODO: Should we redesign the API so that it's polymorphic, wrt. to workflow type (FSM/Petri Net) # That way, you'd call workflow::case::get and get a state_pretty pseudocolumn, which would be # the pretty-name of the state in an FSM, but it would be some kind of human-readable summary of # the active tokens in a petri net. } d_proc -public workflow::case::active_p { -case_id:required } { Returns true if the case is active, otherwise false. } { # Implementation note: The case is active if there are any enabled actions, otherwise not db_transaction { set enabled_actions [workflow::case::get_enabled_actions -case_id $case_id] } return [expr {[llength $enabled_actions] > 0}] } d_proc -public workflow::case::get_element { {-case_id:required} {-element:required} {-action_id {}} } { Return a single element from the information about a case. @param case_id The ID of the case @param element The element you want @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. @return The element you asked for @author Lars Pind (lars@collaboraid.biz) } { get -case_id $case_id -action_id $action_id -array row return $row($element) } d_proc -public workflow::case::delete { {-case_id:required} } { Delete a workflow case. @param case_id The case_id you wish to delete @author Simon Carstensen (simon@collaboraid.biz) } { db_exec_plsql delete_case {} } d_proc -public workflow::case::get_user_roles { {-case_id:required} -user_id } { Get the roles which this user is assigned to. Takes deputies into account, so that if the user is a deputy for someone else, he or she will have the roles of the user for whom he/she is a deputy. @param case_id The ID of the case. @param user_id The user_id of the user for which you want to know the roles. Defaults to ad_conn user_id. @return A list of role_id's of the roles which the user is assigned to in this case. @author Lars Pind (lars@collaboraid.biz) } { if { (![info exists user_id] || $user_id eq "") } { set user_id [ad_conn user_id] } return [util_memoize [list workflow::case::get_user_roles_not_cached $case_id $user_id] \ [workflow::case::cache_timeout]] } ad_proc -private workflow::case::get_user_roles_not_cached { case_id user_id } { Used internally by the workflow Tcl API only. Goes to the database to retrieve roles that user is assigned to. @author Peter Marklund } { return [db_list select_user_roles {}] } d_proc -public -deprecated workflow::case::get_enabled_actions { {-case_id:required} } { 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 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 -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 {}] } d_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]] } d_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 { $trigger_type eq "" } { 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 }] } } d_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. @param case_id The ID of the case. @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 { (![info exists user_id] || $user_id eq "") } { set user_id [ad_conn user_id] } set action_list [list] 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 } d_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 { (![info exists user_id] || $user_id eq "") } { 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 } d_proc -private workflow::case::assign_roles { {-case_id:required} {-all:boolean} } { Find out which roles are assigned to currently enabled actions. If any of these currently have zero assignees, run the default assignment process. @param case_id The ID of the case. @param all Set this to assign all roles for this case. This parameter is deprecated, and always assumed. @author Lars Pind (lars@collaboraid.biz) } { set role_ids [db_list select_unassigned_roles { select r.role_id from workflow_roles r, workflow_cases c where c.case_id = :case_id and r.workflow_id = c.workflow_id and not exists (select 1 from workflow_case_role_party_map m where m.role_id = r.role_id and m.case_id = :case_id) }] foreach role_id $role_ids { workflow::case::role::set_default_assignees \ -case_id $case_id \ -role_id $role_id } workflow::case::role::flush_cache -case_id $case_id } d_proc -private workflow::case::get_activity_html { {-case_id:required} {-action_id ""} {-max_n_actions ""} {-style "activity-entry"} } { Get the activity log for a case as an HTML chunk. If action_id is non-empty, it means that we're in the progress of executing that action, and the corresponding line for the current action will be appended. @param case_id The case for which you want the activity log. @param action_id optional action which is currently being executed. @param max_n_actions Limit history to the max_n_actions number of most recent actions @return Activity log as HTML @author Lars Pind (lars@collaboraid.biz) } { set default_file_stub [file join [acs_package_root_dir "workflow"] lib activity-entry] set file_stub [template::util::url_to_file $style $default_file_stub] if { ![file exists "${file_stub}.adp"] } { ns_log Warning "workflow::case::get_activity_html: Cannot find log entry template file $file_stub, reverting to default template." # We always have a template named 'activity-entry' set file_stub $default_file_stub } # ensure that the style template has been compiled and is up-to-date set stub_call [template::adp_init adp $file_stub] set activity_entry_list [get_activity_log_info_not_cached -case_id $case_id] set start_index 0 if { $max_n_actions ne "" && [llength $activity_entry_list] > $max_n_actions} { # Only return the last max_n_actions actions set start_index [expr {[llength $activity_entry_list] - $max_n_actions}] } set log_html {} foreach entry_arraylist [lrange $activity_entry_list $start_index end] { foreach { var value } $entry_arraylist { set $var $value } set comment_html [ad_html_text_convert -from $comment_mime_type -to "text/html" -- $comment] if { [ad_conn isconnected] == 1 } { set community_member_url [acs_community_member_url -user_id $creation_user] } else { set community_member_url [export_vars -base [parameter::get -package_id [ad_acs_kernel_id] \ -parameter CommunityMemberURL \ -default "/shared/community-member"] \ {user_id $ass(party_id)}] } # The output of this procedure will be placed in __adp_output in this stack frame. $stub_call append log_html $__adp_output } if { $action_id ne "" } { set action_pretty_past_tense [lang::util::localize \ [workflow::action::get_element \ -action_id $action_id \ -element pretty_past_tense]] # sets first_names, last_name, email acs_user::get -user_id [ad_conn untrusted_user_id] -array user set creation_date_pretty [clock format [clock seconds] -format "%b %e, %Y"] set comment_html {} set user_first_names $user(first_names) set user_last_name $user(last_name) if { [ad_conn isconnected] == 1 } { set community_member_url [acs_community_member_url -user_id [ad_conn untrusted_user_id]] } else { set community_member_url [export_vars -base [parameter::get -package_id [ad_acs_kernel_id] \ -parameter CommunityMemberURL \ -default "/shared/community-member"] \ {user_id $ass(party_id)}] } # The output of this procedure will be placed in __adp_output in this stack frame. $stub_call append log_html $__adp_output } return $log_html } d_proc -private workflow::case::get_activity_text { {-case_id:required} } { Get the activity log for a case as a text chunk @author Lars Pind } { set log_text {} foreach entry_arraylist [get_activity_log_info -case_id $case_id] { foreach { var value } $entry_arraylist { set $var $value } set entry_text "$creation_date_pretty $action_pretty_past_tense [ad_decode $log_title "" "" "$log_title "]by $user_first_names $user_last_name ($user_email)" if { $comment ne "" } { append entry_text ":\n\n [join [split [ad_html_text_convert -from $comment_mime_type -to "text/plain" -maxlen 66 -- $comment] "\n"] "\n "]" } lappend log_text $entry_text } return [join $log_text "\n\n"] } d_proc -private workflow::case::get_activity_log_info { {-case_id:required} } { Get the data for the case activity log. @return a list of array-lists with the following entries: comment comment_mime_type creation_date_pretty action_pretty_past_tense log_title user_first_names user_last_name user_email creation_user data_arraylist @author Lars Pind } { global __cache__workflow__case__get_activity_log_info if { ![info exists __cache__workflow__case__get_activity_log_info] } { set __cache__workflow__case__get_activity_log_info [get_activity_log_info_not_cached -case_id $case_id] } return $__cache__workflow__case__get_activity_log_info } d_proc -private workflow::case::get_activity_log_info_not_cached { {-case_id:required} } { Get the data for the case activity log. This version is cached for a single thread. @return a list of array-lists with the following entries: comment comment_mime_type creation_date_pretty action_pretty_past_tense log_title user_first_names user_last_name user_email creation_user data_arraylist @author Lars Pind } { set workflow_id [workflow::case::get_element -case_id $case_id -element workflow_id] set object_id [workflow::case::get_element -case_id $case_id -element object_id] set contract_name [workflow::service_contract::activity_log_format_title] # Get the name of any title Tcl callback proc set impl_names [workflow::get_callbacks \ -workflow_id $workflow_id \ -contract_name $contract_name] # First, we build up a multirow so we have all the data in memory, which lets us peek ahead at the contents db_multirow -extend {comment} -local entries select_log {} { set comment $comment_string set action_pretty_past_tense [lang::util::localize $action_pretty_past_tense] } set rowcount [template::multirow -local size entries] set counter 1 set last_entry_id {} set data_arraylist [list] # Then iterate over the multirow to build up the activity log HTML # We need to peek ahead, because this is an outer join to get the rows in workflow_case_log_data set entries [list] template::multirow -local foreach entries { if { $key ne "" } { lappend data_arraylist $key $value } if { $counter == $rowcount || $last_entry_id ne [set "entries:[expr {$counter + 1}](entry_id)"] } { set log_title_elements [list] foreach impl_name $impl_names { set result [acs_sc::invoke \ -contract $contract_name \ -operation "GetTitle" \ -impl $impl_name \ -call_args [list $case_id $object_id $action_id $entry_id $data_arraylist]] if { $result ne "" } { lappend log_title_elements $result } } set log_title [ad_decode [llength $log_title_elements] 0 "" "([join $log_title_elements ", "])"] set row [list] foreach var { comment comment_mime_type creation_date_pretty action_pretty_past_tense log_title user_first_names user_last_name user_email creation_user data_arraylist } { lappend row $var [set $var] } lappend entries $row set data_arraylist [list] } set last_entry_id $entry_id incr counter } return $entries } d_proc workflow::case::get_notification_object { {-type:required} {-workflow_id ""} {-case_id ""} } { Get the relevant object for this notification type. @param type Type is one of 'workflow_assignee', 'workflow_my_cases', 'workflow_case' (requires case_id), and 'workflow' (requires workflow_id). } { switch $type { workflow_case { if { (![info exists case_id] || $case_id eq "") } { return {} } return [workflow::case::get_element -case_id $case_id -element object_id] } default { if { (![info exists workflow_id] || $workflow_id eq "") } { return {} } return [workflow::get_element -workflow_id $workflow_id -element object_id] } } } d_proc workflow::case::get_notification_request_url { {-type:required} {-workflow_id ""} {-case_id ""} {-return_url ""} {-pretty_name ""} } { Get the URL to subscribe to notifications @param type Type is one of 'workflow_assignee', 'workflow_my_cases', 'workflow_case' (requires case_id), and 'workflow' (requires workflow_id). } { if { [ad_conn user_id] == 0 } { return {} } set object_id [get_notification_object \ -type $type \ -workflow_id $workflow_id \ -case_id $case_id] if { $object_id eq "" } { return {} } if { (![info exists return_url] || $return_url eq "") } { set return_url [ad_return_url] } set url [notification::display::subscribe_url \ -type $type \ -object_id $object_id \ -url $return_url \ -user_id [ad_conn user_id] \ -pretty_name $pretty_name] return $url } d_proc workflow::case::get_notification_requests_multirow { {-multirow_name:required} {-label ""} {-workflow_id ""} {-case_id ""} {-return_url ""} } { Returns a multirow with columns url, label, title, of the possible workflow notification types. Use this to present the user with a list of subscription links. } { array set pretty { workflow_assignee {my actions} workflow_my_cases {my cases} workflow_case {this case} workflow {cases in this workflow} } template::multirow create $multirow_name url label title foreach type { workflow_assignee workflow_my_cases workflow_case workflow } { set url [get_notification_request_url \ -type $type \ -workflow_id $workflow_id \ -case_id $case_id \ -return_url $return_url] if { $url ne "" } { set title "Subscribe to $pretty($type)" if { $label ne "" } { set row_label $label } else { set row_label $title } template::multirow append $multirow_name $url $row_label $title } } } d_proc workflow::case::add_log_data { {-entry_id:required} {-key:required} {-value:required} } { Adds extra data information to a log entry, which can later be retrieved using workflow::case::get_log_data_by_key. Data are stored as simple key/value pairs. @param entry_id The ID of the log entry to which you want to attach data. @param key The data key. @param value The data value @see workflow::case::get_log_data_by_key @see workflow::case::get_log_data @author Lars Pind (lars@collaboraid.biz) } { db_dml insert_log_data {} } d_proc workflow::case::get_log_data_by_key { {-entry_id:required} {-key:required} } { Retrieve extra data for a workflow log entry, previously stored using workflow::case::add_log_data. @param entry_id The ID of the log entry to which the data you want are attached. @param key The key of the data you're looking for. @return The value, or the empty string if no such key exists for this entry. @see workflow::case::add_log_data @see workflow::case::get_log_data @author Lars Pind (lars@collaboraid.biz) } { db_string select_log_data {} -default {} } d_proc workflow::case::get_log_data { {-entry_id:required} } { Retrieve extra data for a workflow log entry, previously stored using workflow::case::add_log_data. @param entry_id The ID of the log entry to which the data you want are attached. @return A tcl list of key/value pairs in array-list format, i.e. { key1 value1 key2 value2 ... }. @see workflow::case::add_log_data @see workflow::case::get_log_data_by_key @author Lars Pind (lars@collaboraid.biz) } { db_string select_log_data {} -default {} } ad_proc -private workflow::case::cache_timeout {} { Number of seconds before we timeout the case level workflow cache. @author Peter Marklund } { # 60 * 60 seconds is 1 hour return 3600 } d_proc -private workflow::case::flush_cache0 { {-case_id ""} } { Flush all cached data for a given case or for all cases if none is specified. @param case_id The id of the workflow case to flush. If not provided the cache will be flushed for all workflow cases. @author Peter Marklund } { foreach proc_name { 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]" } util_memoize_flush_regexp [list workflow::case::get_activity_log_info_not_cached -case_id $case_id] # Flush role info (assignees etc) workflow::case::role::flush_cache0 -case_id $case_id } d_proc -private workflow::case::flush_cache { {-case_id ""} } { Flush all cached data for a given case or for all cases if none is specified. @param case_id The id of the workflow case to flush. If not provided the cache will be flushed for all workflow cases. @author Peter Marklund } { foreach proc_name { workflow::case::fsm::get_info_not_cached workflow::case::get_user_roles_not_cached workflow::case::get_enabled_action_ids_not_cached } { if {$case_id eq ""} { util_memoize_flush_pattern "$proc_name *" } else { util_memoize_flush_pattern "$proc_name $case_id *" } } if {$case_id eq ""} { util_memoize_flush_pattern "workflow::case::get_activity_log_info_not_cached -case_id *" util_memoize_flush_pattern "workflow::case::get_enabled_actions_not_cached *" } else { util_memoize_flush "workflow::case::get_activity_log_info_not_cached -case_id $case_id" util_memoize_flush "workflow::case::get_enabled_actions_not_cached $case_id" } # Flush role info (assignees etc) workflow::case::role::flush_cache -case_id $case_id } ad_proc -public workflow::case::timed_actions_sweeper {} { Sweep for timed actions ready to fire. } { set enabled_action_ids [db_list select_timed_out_actions {}] foreach enabled_action_id $enabled_action_ids { workflow::case::action::execute \ -no_perm_check \ -enabled_action_id $enabled_action_id } } d_proc -public workflow::case::enabled_action_get { {-enabled_action_id:required} {-array:required} } { Get information about an enabled action @param array The name of an array in which information will be returned. @author Lars Pind (lars@collaboraid.biz) } { # Select the info into the upvar'ed Tcl Array upvar $array row db_1row select_enabled_action {} -column_array row } d_proc -public workflow::case::enabled_action_get_element { {-enabled_action_id:required} {-element:required} } { Return a single element from the information about an enabled action @param element The element you want @return The element you asked for @author Lars Pind (lars@collaboraid.biz) } { enabled_action_get -enabled_action_id $enabled_action_id -array row return $row($element) } ##### # # workflow::case::role namespace # ##### d_proc -public workflow::case::role::set_default_assignees { {-case_id:required} {-role_id:required} } { Find the default assignee for this role. @param case_id the ID of the case. @param role_id the ID of the role to assign. @author Lars Pind (lars@collaboraid.biz) } { set contract_name [workflow::service_contract::role_default_assignees] db_transaction { set impl_names [workflow::role::get_callbacks \ -role_id $role_id \ -contract_name $contract_name] set object_id [workflow::case::get_element -case_id $case_id -element object_id] foreach impl_name $impl_names { # Call the service contract implementation set party_id_list [acs_sc::invoke \ -contract $contract_name \ -operation "GetAssignees" \ -impl $impl_name \ -call_args [list $case_id $object_id $role_id]] if { [llength $party_id_list] != 0 } { assignee_insert -case_id $case_id -role_id $role_id -party_ids $party_id_list # We stop when the first callback returned something break } } } } d_proc -public workflow::case::role::get_picklist { {-case_id:required} {-role_id:required} } { Get the picklist for this role. @param case_id the ID of the case. @param role_id the ID of the role. @author Lars Pind (lars@collaboraid.biz) } { set contract_name [workflow::service_contract::role_assignee_pick_list] set party_id_list [list] db_transaction { set impl_names [workflow::role::get_callbacks \ -role_id $role_id \ -contract_name $contract_name] set object_id [workflow::case::get_element -case_id $case_id -element object_id] foreach impl_name $impl_names { # Call the service contract implementation set party_id_list [acs_sc::invoke \ -contract $contract_name \ -operation "GetPickList" \ -impl $impl_name \ -call_args [list $case_id $object_id $role_id]] if { [llength $party_id_list] != 0 } { # Return after the first non-empty list break } } } if { [ad_conn isconnected] && [ad_conn user_id] != 0 } { lappend party_id_list [ad_conn user_id] } if { [llength $party_id_list] > 0 } { set options [db_list_of_lists select_options {}] } else { set options {} } set options [concat { { "Unassigned" "" } } $options] lappend options { "Search..." ":search:"} return $options } d_proc -public workflow::case::role::get_search_query { {-case_id:required} {-role_id:required} } { Get the search query for this role. @param case_id the ID of the case. @param role_id the ID of the role. @author Lars Pind (lars@collaboraid.biz) } { set contract_name [workflow::service_contract::role_assignee_subquery] set impl_names [workflow::role::get_callbacks \ -role_id $role_id \ -contract_name $contract_name] set object_id [workflow::case::get_element -case_id $case_id -element object_id] set subquery {} foreach impl_name $impl_names { # Call the service contract implementation set subquery [acs_sc::invoke \ -contract $contract_name \ -operation "GetSubquery" \ -impl $impl_name \ -call_args [list $case_id $object_id $role_id]] if { $subquery ne "" } { # Return after the first non-empty list break } } return [db_map select_search_results] } d_proc -public workflow::case::role::get_assignee_widget { {-case_id:required} {-role_id:required} {-prefix "role_"} } { Get the assignee widget for use with ad_form for this role. @param case_id the ID of the case. @param role_id the ID of the role. @author Lars Pind (lars@collaboraid.biz) } { set workflow_id [workflow::case::get_element -case_id $case_id -element workflow_id] workflow::role::get -role_id $role_id -array role set element "${prefix}$role(short_name)" set query [workflow::case::role::get_search_query -case_id $case_id -role_id $role_id] set picklist [workflow::case::role::get_picklist -case_id $case_id -role_id $role_id] return [list "${element}:search(search),optional" [list label $role(pretty_name)] [list mode display] \ [list search_query $query] [list options $picklist]] } d_proc -public workflow::case::role::add_assignee_widgets { {-case_id:required} {-form_name:required} {-prefix "role_"} {-role_ids {}} } { Get the assignee widget for use with ad_form for this role. @param case_id the ID of the case. @param role_id the ID of the role. @param role_ids Only add assignee widgets for the roles supplied. If no roles are specified then all roles are used. @author Lars Pind (lars@collaboraid.biz) } { set workflow_id [workflow::case::get_element -case_id $case_id -element workflow_id] if { $role_ids eq "" } { set role_ids [workflow::get_roles -workflow_id $workflow_id] } foreach role_id $role_ids { ad_form -extend -name $form_name -form [list [get_assignee_widget -case_id $case_id -role_id $role_id -prefix $prefix]] } } d_proc -public workflow::case::role::set_assignee_values { {-case_id:required} {-form_name:required} {-prefix "role_"} } { Get the assignee widget for use with ad_form for this role. @param case_id the ID of the case. @param role_id the ID of the role. @author Lars Pind (lars@collaboraid.biz) } { set workflow_id [workflow::case::get_element -case_id $case_id -element workflow_id] # Set role assignee values foreach role_id [workflow::get_roles -workflow_id $workflow_id] { workflow::role::get -role_id $role_id -array role set element "${prefix}$role(short_name)" # HACK: Only care about the first assignee set assignees [workflow::case::role::get_assignees -case_id $case_id -role_id $role_id] if { [llength $assignees] == 0 } { array set cur_assignee { party_id {} name {} email {} } } else { array set cur_assignee [lindex $assignees 0] } if { [uplevel info exists $form_name:$element] } { # Set normal value if { [uplevel template::form is_request $form_name] || [uplevel [list element get_property $form_name $element mode]] eq "display" } { uplevel [list element set_value $form_name $element $cur_assignee(party_id)] } # Set display value if { $cur_assignee(party_id) eq "" } { set display_value "<i>None</i>" } else { set display_value [acs_community_member_link \ -user_id $cur_assignee(party_id) \ -label $cur_assignee(name)] if { [ad_conn user_id] != 0 } { append display_value " (<a href=\"mailto:$cur_assignee(email)\">$cur_assignee(email)</a>)" } else { append display_value " ([string replace $cur_assignee(email) \ [expr {[string first "@" $cur_assignee(email)]+3}] end "..."])" } } uplevel [list element set_properties $form_name $element -display_value $display_value] } } } d_proc -public workflow::case::role::get_assignees { {-case_id:required} {-role_id:required} } { Get the current assignees for a role in a case as a list of [array get]'s of party_id, email, name. @param case_id the ID of the case. @param role_id the ID of the role. @return a list of [array get]'s of party_id, email, name. @author Lars Pind (lars@collaboraid.biz) } { return [util_memoize [list workflow::case::role::get_assignees_not_cached $case_id $role_id] \ [workflow::case::cache_timeout]] } ad_proc -private workflow::case::role::get_assignees_not_cached { case_id role_id } { Proc used only internally by the workflow API. Retrieves role assignees directly from the database. @author Peter Marklund } { set result {} db_foreach select_assignees {} -column_array row { lappend result [array get row] } return $result } d_proc -private workflow::case::role::flush_cache0 { {-case_id ""} } { Flush all role related info for a certain case or for all cases if none is specified. } { util_memoize_flush_regexp "^workflow::case::role::get_assignees_not_cached [ad_decode $case_id "" {\.*} $case_id]" } d_proc -private workflow::case::role::flush_cache { {-case_id ""} } { Flush all role related info for a certain case or for all cases if none is specified. } { if {$case_id eq ""} { util_memoize_flush_pattern "workflow::case::role::get_assignees_not_cached *" } else { util_memoize_flush_pattern "workflow::case::role::get_assignees_not_cached $case_id *" } } d_proc -public workflow::case::role::assignee_insert { {-case_id:required} {-role_id:required} {-party_ids:required} {-replace:boolean} } { Insert a new assignee for this role @param case_id the ID of the case. @param role_id the ID of the role to assign. @param party_id the ID of party to assign to this role @author Lars Pind (lars@collaboraid.biz) } { db_transaction { if { $replace_p } { workflow::case::role::assignees_remove -case_id $case_id -role_id $role_id } foreach party_id $party_ids { if { [catch { db_dml insert_assignee {} callback workflow::case::role::after_assign \ -case_id $case_id \ -party_id $party_id } errMsg] } { set already_assigned_p [db_string already_assigned_p {}] if { !$already_assigned_p } { global errorInfo errorCode error $errMsg $errorInfo $errorCode } } } } workflow::case::role::flush_cache -case_id $case_id } d_proc -public workflow::case::role::assignee_remove { {-case_id:required} {-role_id:required} {-party_id:required} } { Remove an assignee from this role @param case_id the ID of the case. @param role_id the ID of the role to remove the assignee from. @param party_id the ID of party to remove from the role @author Peter Marklund } { db_dml delete_assignee {} callback workflow::case::role::after_unassign \ -case_id $case_id \ -party_id $party_id workflow::case::role::flush_cache -case_id $case_id } d_proc -public workflow::case::role::assignees_remove { {-case_id:required} {-role_id:required} } { Remove all assignees in this role @param case_id the ID of the case. @param role_id the ID of the role to remove the assignees from. @author Ryan Gallimore } { set assignees [workflow::case::role::get_assignees -case_id $case_id -role_id $role_id] foreach assignee $assignees { array set elm $assignee if { ([info exists elm(party_id)] && $elm(party_id) ne "") } { callback workflow::case::role::after_unassign \ -case_id $case_id \ -party_id $elm(party_id) } array unset elm } db_dml delete_assignees {} workflow::case::role::flush_cache -case_id $case_id } d_proc -public workflow::case::role::assign { {-case_id:required} {-array:required} {-replace:boolean} } { Assign roles from an array with entries like this: array(short_name) = [list of party_ids]. @param case_id The ID of the case. @param array Name of array with assignment info @param replace Should the new assignees replace existing assignees? @author Lars Pind (lars@collaboraid.biz) } { upvar $array assignees set workflow_id [workflow::case::get_element -case_id $case_id -element workflow_id] db_transaction { foreach name [array names assignees] { set role_id [workflow::role::get_id \ -workflow_id $workflow_id \ -short_name $name] workflow::case::role::assignee_insert \ -replace=$replace_p \ -case_id $case_id \ -role_id $role_id \ -party_ids $assignees($name) } } } ##### # # workflow::case::fsm # ##### d_proc -public workflow::case::fsm::get_current_state { {-case_id:required} } { Gets the current state_id of this case. @param case_id The case_id. @return The state_id of the state which this case is in @author Lars Pind (lars@collaboraid.biz) } { return [workflow::case::fsm::get_element -case_id $case_id -element state_id] } d_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. 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 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 { $action_id ne "" } { if { $enabled_action_id ne "" } { 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 { $enabled_action_id eq "" } { 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 { # TODO: cache this query as well db_1row select_case_info_after_action {} -column_array row set row(entry_id) [db_nextval "acs_object_id_seq"] } } d_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. @param case_id The ID of the case @param element The element you want @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. @return The element you asked for @author Lars Pind (lars@collaboraid.biz) } { 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 { parent_enabled_action_id "" } } { Used internally by the workflow id to get FSM case info from the database. @author Peter Marklund } { if { $parent_enabled_action_id eq "" } { db_1row select_case_info_null_parent_id {} -column_array row } else { db_1row select_case_info {} -column_array row } return [array get row] } d_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] } d_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 { $parent_enabled_action_id eq "" } { 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 # ##### d_proc -public workflow::case::action::permission_p { {-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 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) } { if { (![info exists user_id] || $user_id eq "") } { set user_id [ad_conn user_id] } if { $enabled_action_id ne "" } { ns_log notice "#### workflow::case::enabled_action_get -enabled_action_id $enabled_action_id -array enabled_action" 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 wf_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 allowed role for this action? set allowed_roles [workflow::action::get_allowed_roles -action_id $action_id] if {$role_id in $allowed_roles} { 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] } { return 1 } } } return 0 } d_proc -public workflow::case::action::enabled_p { {-case_id:required} {-action_id:required} } { Is this action currently enabled. @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) } { return [db_string select_enabled_p {} -default 0] } d_proc -public workflow::case::action::available_p { {-enabled_action_id {}} {-case_id {}} {-action_id {}} {-user_id {}} } { Is this action currently enabled and does the user have permission to perform it? @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 { $action_id eq "" && $enabled_action_id eq "" } { return 1 } if { $enabled_action_id ne "" } { 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 { [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 } } d_proc -private workflow::case::action::get_enabled_action_id { {-case_id:required} {-action_id:required} {-parent_enabled_action_id {}} {-all:boolean} {-any_parent:boolean} } { 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 all If specified, will return all if more than one is found. Otherwise returns just the first. @return enabled_action_id. Returns blank if no enabled action exists. } { 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 { $parent_enabled_action_id eq "" } { 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 { $all_p } { return $result } else { return [lindex $result 0] } } d_proc -public workflow::case::action::do_side_effects { {-case_id:required} {-action_id:required} {-entry_id:required} } { Fire the side-effects for this action } { set contract_name [workflow::service_contract::action_side_effect] # Get info for the callbacks set workflow_id [workflow::case::get_element \ -case_id $case_id \ -element workflow_id] # Get the callbacks, workflow and action set impl_names [workflow::get_callbacks \ -workflow_id $workflow_id \ -contract_name $contract_name] set impl_names [concat $impl_names [workflow::action::get_callbacks \ -action_id $action_id \ -contract_name $contract_name]] if { [llength $impl_names] == 0 } { return } set object_id [workflow::case::get_element \ -case_id $case_id \ -element object_id] # Invoke them foreach impl_name $impl_names { acs_sc::invoke \ -contract $contract_name \ -operation "DoSideEffect" \ -impl $impl_name \ -call_args [list $case_id $object_id $action_id $entry_id] } } d_proc -public workflow::case::action::notify { {-case_id:required} {-action_id:required} {-entry_id:required} {-comment:required} {-comment_mime_type:required} } { Send out notifications to relevant people. } { # Get workflow_id workflow::case::get \ -case_id $case_id \ -array case workflow::get \ -workflow_id $case(workflow_id) \ -array workflow set hr [string repeat "=" 70] # TODO: Get activity log for top-case array set latest_action [lindex [workflow::case::get_activity_log_info_not_cached -case_id $case_id] end] # Variables used by I18N messages: set action_past_tense "$latest_action(action_pretty_past_tense)[ad_decode $latest_action(log_title) "" "" " $latest_action(log_title)"]" set user_name "$latest_action(user_first_names) $latest_action(user_last_name)" set user_email $latest_action(user_email) set latest_action_chunk [_ workflow.notification_email_latest_action_chunk] if { $latest_action(comment) ne "" } { append latest_action_chunk ":\n\n [join [split [ad_html_text_convert -from $latest_action(comment_mime_type) -to "text/plain" -maxlen 66 -- $latest_action(comment)] "\n"] "\n "]" } # Callback to get notification info # TODO: Should this be the parent/top-workflow that does this? set contract_name [workflow::service_contract::notification_info] set impl_names [workflow::get_callbacks \ -workflow_id $case(workflow_id) \ -contract_name $contract_name] # We only use the first callback set impl_name [lindex $impl_names 0] if { $impl_name ne "" } { set notification_info [acs_sc::invoke \ -contract $contract_name \ -operation "GetNotificationInfo" \ -impl $impl_name \ -call_args [list $case_id $case(object_id)]] } # Make sure the notification info list has at least 4 elements, so we can do below lindex's safely lappend notification_info {} {} {} {} set object_url [lindex $notification_info 0] set object_one_line [lindex $notification_info 1] set object_details_list [lindex $notification_info 2] set object_notification_tag [lindex $notification_info 3] if { $object_one_line eq "" } { # Default: Case #$case_id: acs_object__name(case.object_id) set object_id $case(object_id) db_1row select_object_name {} -column_array case_object set object_one_line "[_ workflow.Case] #$case_id: $case_object(name)" } # Roles and their current assignees foreach role_id [workflow::get_roles -workflow_id $case(workflow_id)] { set label [lang::util::localize [workflow::role::get_element -role_id $role_id -element pretty_name]] foreach assignee_arraylist [workflow::case::role::get_assignees -case_id $case_id -role_id $role_id] { array set assignee $assignee_arraylist lappend object_details_list $label "$assignee(name) ($assignee(email))" set label {} } } # Find the length of the longest label set max_label_len 0 foreach { label value } $object_details_list { if { [string length $label] > $max_label_len } { set max_label_len [string length $label] } } # Output notification info set object_details_lines [list] foreach { label value } $object_details_list { if { $label ne "" } { lappend object_details_lines "$label[string repeat " " [expr {$max_label_len - [string length $label]}]] : $value" } else { lappend object_details_lines "[string repeat " " $max_label_len] $value" } } set object_details_chunk [join $object_details_lines "\n"] set activity_log_chunk [workflow::case::get_activity_text -case_id $case_id] set the_subject "[ad_decode $object_notification_tag "" "" "\[$object_notification_tag\] "]$object_one_line: $latest_action(action_pretty_past_tense) [ad_decode $latest_action(log_title) "" "" "$latest_action(log_title) "]by $latest_action(user_first_names) $latest_action(user_last_name)" # List of user_id's for people who are in the assigned_role to any enabled actions # This takes deputies into account #XXXXX Verify this ... probably wrong set assigned_role_id [workflow::action::get_assigned_role -action_id $action_id] set assignee_list [list] foreach assignee_array [workflow::case::role::get_assignees \ -case_id $case_id \ -role_id $assigned_role_id] { array set ass $assignee_array lappend assignee_list $ass(party_id) } # List of users who play some role in this case # This takes deputies into account set case_player_list [db_list case_players {}] # Get pretty_name and pretty_plural for the case's object type set object_id $case(object_id) db_1row select_object_type_info {} -column_array object_type # Get name of the workflow's object set object_id $workflow(object_id) db_1row select_object_name {} -column_array workflow_object set next_action_chunk(workflow_assignee) [_ workflow.lt_You_are_assigned_to_t] set next_action_chunk(workflow_my_cases) [_ workflow.lt_You_are_a_participant] set next_action_chunk(workflow_case) [_ workflow.lt_You_have_a_watch_on_t] set next_action_chunk(workflow) [_ workflow.lt_You_have_requested_to] # Initialize stuff that depends on the notification type foreach type { workflow_assignee workflow_my_cases workflow_case workflow } { set subject($type) $the_subject set body($type) "$hr $object_one_line $hr $latest_action_chunk $hr $next_action_chunk($type)[ad_decode $object_url "" "" "\n\n[_ workflow.lt_Please_click_here_to_]\n\n$object_url"] $hr[ad_decode $object_details_chunk "" "" "\n$object_details_chunk\n$hr"] $activity_log_chunk $hr " set force_p($type) 0 set subset($type) {} } set force_p(workflow_assignee) 1 set subset(workflow_assignee) $assignee_list set subset(workflow_my_cases) $case_player_list set notified_list [list] foreach type { workflow_assignee workflow_my_cases workflow_case workflow } { set object_id [workflow::case::get_notification_object \ -type $type \ -workflow_id $case(workflow_id) \ -case_id $case_id] if { $object_id ne "" && ($type eq "workflow" || $subset($type) ne "" || $type eq "workflow_case")} { set notified_list [concat $notified_list [notification::new \ -type_id [notification::type::get_type_id -short_name $type] \ -object_id $object_id \ -action_id $entry_id \ -response_id $case(object_id) \ -notif_subject $subject($type) \ -notif_text $body($type) \ -already_notified $notified_list \ -subset $subset($type) \ -return_notified]] } } } ####################################################################### # # WORKFLOW ENGINE PROCS # ####################################################################### # Below are all the procs that drive the workflow engine, # the logic to change state and determine which actions # are available given the current state. ##### # # Causing changes to state # ##### d_proc -public workflow::case::action::execute { {-no_notification:boolean} {-no_perm_check:boolean} {-no_logging:boolean} {-enabled_action_id {}} {-case_id {}} {-action_id {}} {-parent_enabled_action_id {}} {-comment ""} {-comment_mime_type "text/plain"} {-user_id} {-initial:boolean} {-entry_id {}} {-package_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. @param no_perm_check Set this switch if you do not want to have any workflow_case loggings. @param package_id The package_id the case object belongs to. This is optional but is useful if the case objects are not CR items. @return entry_id of the new log entry (will be a cr_item). @author Lars Pind (lars@collaboraid.biz) } { if { (![info exists user_id] || $user_id eq "") } { if { ![ad_conn isconnected] } { set user_id 0 } else { set user_id [ad_conn user_id] } } if { (![info exists package_id] || $package_id eq "") } { if { ![ad_conn isconnected] } { set package_id {} } else { set package_id [ad_conn package_id] } } if { $case_id eq "" || $action_id eq "" } { if { $enabled_action_id eq "" } { error "You must supply either case_id and action_id, or enabled_action_id" } } if { $enabled_action_id eq "" } { 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 { $enabled_action_id eq "" } { error "This action is not enabled at this time." } } } if { $enabled_action_id ne "" } { 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 { $comment eq "" } { # single-space comment set comment { } } # We can't have empty comment_mime_type, default to text/plain if { $comment_mime_type eq "" } { set comment_mime_type "text/plain" } #ns_log notice "case::execute start = [set start [clock clicks -milliseconds]]" db_transaction { # Double-click protection if { $entry_id ne "" } { 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 #ns_log notice "case::execute two = [expr {[set two [clock clicks -milliseconds]] - $start}]" # Mark the action completed if { $enabled_action_id ne "" } { workflow::case::action::complete \ -enabled_action_id $enabled_action_id \ -user_id $user_id } #ns_log notice "case::execute three = [expr {[set three [clock clicks -milliseconds]] - $two}]" # Insert activity log entry if {!$no_logging_p} { set extra_vars [ns_set create \ entry_id $entry_id \ case_id $case_id \ action_id $action_id\ comment $comment \ comment_mime_type $comment_mime_type \ package_id $package_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 workflow::case::action::do_side_effects \ -case_id $case_id \ -action_id $action_id \ -entry_id $entry_id #ns_log notice "case::execute five = [expr {[set five [clock clicks -milliseconds]] - $three}]" # Scan for enabled actions if {$parent_trigger_type eq "workflow"} { workflow::case::state_changed_handler \ -case_id $case_id \ -parent_enabled_action_id $parent_enabled_action_id \ -user_id $user_id } #ns_log notice "case::execute six = [expr {[set six [clock clicks -milliseconds]] - $five}]" # 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 } #ns_log notice "case::execute seven = [expr {[set seven [clock clicks -milliseconds]] - $six}]" # If there's a parent, alert the parent if { $parent_enabled_action_id ne "" } { workflow::case::child_state_changed_handler \ -parent_enabled_action_id $parent_enabled_action_id \ -user_id $user_id } } #ns_log notice "case::execute eight = [expr {[set eight [clock clicks -milliseconds]] - $seven}]" workflow::case::flush_cache -case_id $case_id #ns_log notice "case::execute nine = [expr {[set nine [clock clicks -milliseconds]] - $eight}]" #ns_log notice "case::execute end = [expr {[set end [clock clicks -milliseconds]] - $start}]" return $entry_id } ##### # # Handling changes to state # #### d_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 { $parent_enabled_action_id eq "" } { 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=[expr {[info exists assigned_p($action_id)] && $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 } } d_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 # ##### d_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 } } d_proc -private workflow::case::action::enable { {-case_id:required} {-action_id:required} {-parent_enabled_action_id {}} {-user_id {}} {-assigned:boolean} {-assignees {}} } { 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 set workflow_id $action(workflow_id) db_transaction { set enabled_action_id [db_nextval "workflow_case_enbl_act_seq"] if { $action(trigger_type) ne "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 { ([info exists assignees] && $assignees ne "") } { 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' } -default {}] if { $child_init_id eq "" } { error "Child workflow for action $action(pretty_name) doesn't have an action with trigger_type = 'init', or it has more than one." } workflow::action::fsm::get -action_id $child_init_id -array initial_action if { $initial_action(new_state) eq "" } { 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." } 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 foreach child_action_id $action(child_action_ids) { 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" { # Find and enable all child actions, once for each party assigned to the role foreach child_action_id $action(child_action_ids) { 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 } } } } d_proc -private workflow::case::action::complete { {-enabled_action_id:required} {-user_id {}} } { Mark an action complete. @author Lars Pind (lars@collaboraid.biz) } { 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 {$enabled_action(parent_trigger_type) in { parallel dynamic }} { 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 } } } } ##### # # Helper # ##### d_proc -private workflow::case::get_actual_state { {-case_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 } } d_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 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) } { db_transaction { if { $case_id eq "" || $action_id eq "" } { if { $enabled_action_id eq "" } { error "You must supply either case_id and action_id, or enabled_action_id" } } if { $enabled_action_id eq "" } { 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 initial 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 { $enabled_action_id ne "" } { workflow::case::enabled_action_get -enabled_action_id $enabled_action_id -array enabled_action # Even if these are provided, we override 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 { $new_state_id ne "" } { # Delete any existing state with this parent_enabled_action_id if { $parent_enabled_action_id eq "" } { 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) } } } }