Index: openacs-4/packages/workflow/tcl/case-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/workflow/tcl/case-procs.tcl,v diff -u -r1.4 -r1.5 --- openacs-4/packages/workflow/tcl/case-procs.tcl 3 Feb 2003 12:23:01 -0000 1.4 +++ openacs-4/packages/workflow/tcl/case-procs.tcl 12 Feb 2003 14:23:14 -0000 1.5 @@ -51,14 +51,14 @@ {-workflow_id:required} {-object_id:required} {-comment:required} - {-comment_format:required} + {-comment_mime_type:required} {-user_id} } { Start a new case for this workflow and object. @param object_id The object_id which the case is about @param workflow_short_name The short_name of the workflow. - @param comment_format html, plain or pre + @param comment_mime_type html, plain or pre @return The case_id of the case. Returns the empty string if no case could be found. @author Lars Pind (lars@collaboraid.biz) @@ -69,24 +69,17 @@ db_transaction { - ns_log Notice "LARS - case::new1: object_id = $object_id, [db_string foobar { select count(*) from acs_objects where object_id = :object_id }]" - # Insert the case set case_id [insert -workflow_id $workflow_id -object_id $object_id] - ns_log Notice "LARS - case::new2: object_id = $object_id, [db_string foobar { select count(*) from acs_objects where object_id = :object_id }]" - # Execute the initial action workflow::case::action::execute \ -case_id $case_id \ - -action_id [workflow::get_initial_action -workflow_id $workflow_id] \ + -action_id [workflow::get_element -workflow_id $workflow_id -element initial_action_id] \ -comment $comment \ - -comment_format $comment_format \ + -comment_mime_type $comment_mime_type \ -user_id $user_id \ - -no_check - - ns_log Notice "LARS - case::new3: object_id = $object_id, [db_string foobar { select count(*) from acs_objects where object_id = :object_id }]" - + -initial } return $case_id @@ -214,6 +207,7 @@ ad_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 @@ -225,10 +219,15 @@ } { set role_id_list [list] - foreach action_id [get_enabled_actions -case_id $case_id] { - set role_id [workflow::action::get_assigned_role -action_id $action_id] - if { ![empty_string_p $role_id] && [lsearch $role_id_list $role_id] == -1 } { - lappend role_id_list $role_id + if { $all_p } { + set workflow_id [workflow::case::get_element -case_id $case_id -element workflow_id] + set role_id_list [workflow::get_roles -workflow_id $workflow_id] + } else { + foreach action_id [get_enabled_actions -case_id $case_id] { + set role_id [workflow::action::get_assigned_role -action_id $action_id] + if { ![empty_string_p $role_id] && [lsearch $role_id_list $role_id] == -1 } { + lappend role_id_list $role_id + } } } @@ -247,6 +246,7 @@ + ##### # # workflow::case::role namespace @@ -265,15 +265,15 @@ @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] - - ns_log Notice "LARS - case::role::set_default_assignees: object_id = $object_id, [db_string foobar { select count(*) from acs_objects where object_id = :object_id }]" - - set impl_names [db_list select_callbacks {}] - + foreach impl_name $impl_names { # Call the service contract implementation set party_id_list [acs_sc::invoke \ @@ -283,20 +283,228 @@ -call_args [list $case_id $object_id $role_id]] if { [llength $party_id_list] != 0 } { - foreach party_id $party_id_list { - assignee_insert -case_id $case_id -role_id $role_id -party_id $party_id - } + assignee_insert -case_id $case_id -role_id $role_id -party_ids $party_id_list + # We stop when the first callback returned something break } } } } +ad_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 +} + +ad_proc -public workflow::case::role::get_seach_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 { ![empty_string_p $subquery] } { + # Return after the first non-empty list + break + } + } + set query " + select distinct acs_object__name(p.party_id) || ' (' || p.email || ')' as label, p.party_id + from [ad_decode $subquery "" "cc_users" $subquery] p + where upper(coalesce(acs_object__name(p.party_id) || ' ', '') || p.email) like upper('%'||:value||'%') + order by label + " + return $query +} + +ad_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_seach_query -case_id $case_id -role_id $role_id] + set picklist [workflow::case::role::get_picklist -case_id $case_id -role_id $role_id] + + return [list "${element}:search(search)" [list label $role(pretty_name)] [list mode display] \ + [list search_query $query] [list options $picklist] optional] +} + +ad_proc -public workflow::case::role::add_assignee_widgets { + {-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 roles [list] + foreach role_id [workflow::get_roles -workflow_id $workflow_id] { + ad_form -extend -name $form_name -form [list [get_assignee_widget -case_id $case_id -role_id $role_id -prefix $prefix]] + } +} + +ad_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] + + # LARS TODO: + # Set role assignee values + foreach role_id [workflow::get_roles -workflow_id $workflow_id] { + workflow::role::get -role_id $role_id -array role + 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 bug:$element] } { + # Set normal value + if { [uplevel template::form is_request bug] || [string equal [uplevel [list element get_property bug $element mode]] "display"] } { + uplevel [list element set_value bug $element $cur_assignee(party_id)] + } + + # Set display value + if { [empty_string_p $cur_assignee(party_id)] } { + set display_value "None" + } else { + set display_value [acs_community_member_link \ + -user_id $cur_assignee(party_id) \ + -label $cur_assignee(name)] + + append display_value " ($cur_assignee(email))" + } + + uplevel [list element set_properties bug $element -display_value $display_value] + } + } +} + +ad_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. + + @author Lars Pind (lars@collaboraid.biz) +} { + set result {} + db_foreach select_assignees {} -column_array row { + lappend result [array get row] + } + return $result +} + ad_proc -public workflow::case::role::assignee_insert { {-case_id:required} {-role_id:required} - {-party_id:required} + {-party_ids:required} + {-replace:boolean} } { Insert a new assignee for this role @@ -306,36 +514,205 @@ @author Lars Pind (lars@collaboraid.biz) } { - if { [catch { - db_dml insert_assignee {} - } errMsg] } { - set already_assigned_p [db_string already_assigned_p {}] - if { !$already_assigned_p } { - global errorInfo errorCode - error $errMsg $errorInfo $errorCode + db_transaction { + if { $replace_p } { + db_dml delete_assignees {} } + + foreach party_id $party_ids { + if { [catch { + db_dml insert_assignee {} + } errMsg] } { + set already_assigned_p [db_string already_assigned_p {}] + if { !$already_assigned_p } { + global errorInfo errorCode + error $errMsg $errorInfo $errorCode + } + } + } } } +ad_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] + + assignee_insert \ + -replace=$replace_p \ + -case_id $case_id \ + -role_id $role_id \ + -party_ids $assignees($name) + } + } +} + ad_proc -public workflow::case::get_activity_html { -case_id:required } { Get the activity log for a case as an HTML chunk } { - #LARS TODO: Template this + # LARS TODO: Template this + set workflow_id [workflow::case::get_element -case_id $case_id -element workflow_id] + set contract_name [workflow::service_contract::activity_log_format_title] + + # Get the name of any title Tcl callback proc + set impl_names [workflow::get_callbacks \ + -workflow_id $workflow_id \ + -contract_name $contract_name] + + # If there are more than one FormatLogTitle callback, we only use the first. + set impl_name [lindex $impl_names 0] + set log_html {} db_foreach select_log {} { - append log_html "$action_date_pretty $action_pretty_past_tense by $user_first_names $user_last_name -
[ad_html_text_convert -from $comment_format -to "text/html" -- $comment]
" + if { ![empty_string_p $impl_name] } { + set log_title [acs_sc::invoke \ + -contract $contract_name \ + -operation "GetTitle" \ + -impl $impl_name \ + -call_args [list $entry_id]] + set log_title [ad_decode $log_title "" "" "($log_title)"] + } + + append log_html "$creation_date_pretty $action_pretty_past_tense $log_title by $user_first_names $user_last_name +
[ad_html_text_convert -from $comment_mime_type -to "text/html" -- $comment]
" } return $log_html } +ad_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 { ![exists_and_not_null case_id] } { + return {} + } + return [workflow::case::get_element -case_id $case_id -element object_id] + } + workflow { + if { ![exists_and_not_null workflow_id] } { + return {} + } + return [workflow::get_element -workflow_id $workflow_id -element object_id] + } + default { + return [apm_package_id_from_key [workflow::package_key]] + } + } +} +ad_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 { [empty_string_p $object_id] } { + return {} + } + + if { ![exists_and_not_null return_url] } { + set return_url [util_get_current_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 +} + +ad_proc workflow::case::get_notification_requests_multirow { + {-multirow_name:required} + {-label ""} + {-workflow_id ""} + {-case_id ""} + {-return_url ""} +} { + +} { + 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 { ![empty_string_p $url] } { + set title "Subscribe to $pretty($type)" + if { ![empty_string_p $label] } { + set row_label $label + } else { + set row_label $title + } + template::multirow append $multirow_name $url $row_label $title + } + } +} + + + ##### # # workflow::case::fsm @@ -377,7 +754,7 @@ set row(entry_id) {} } else { db_1row select_case_info_after_action {} -column_array row - set row(entry_id) [db_nextval "workflow_case_log_seq"] + set row(entry_id) [db_nextval "acs_object_id_seq"] } } @@ -433,7 +810,7 @@ } if { !$permission_p } { - set privileges [workflow::action::get_privileges -action_id $action_id] + 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] } { set permission_p 1 @@ -498,113 +875,212 @@ {-case_id:required} {-action_id:required} {-comment:required} - {-comment_format:required} + {-comment_mime_type:required} {-user_id} - {-no_check:boolean} + {-initial:boolean} {-entry_id {}} } { Execute the action - @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_format Format of the comment (plain, text or html), according to - OpenACS standard text formatting (HM!) - @param user_id User_id - @param no_check Use this switch to bypass a check of whether the action is - enabled and the user is allowed to perform it. This - switch should normally not be used. - @param entry_id Optional entry_id for double-click protection. - This can be gotten from workflow::case::fsm::get. - @return entry_id of the new log entry. + @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. + @return entry_id of the new log entry (will be a cr_item). + @author Lars Pind (lars@collaboraid.biz) } { if { ![exists_and_not_null user_id] } { set user_id [ad_conn user_id] } - if { !$no_check_p } { + if { !$initial_p } { if { ![available_p -case_id $case_id -action_id $action_id -user_id $user_id] } { error "This user is not allowed to perform this action at this time." } } - set new_state [workflow::action::fsm::get_new_state -action_id $action_id] + set new_state_id [workflow::action::fsm::get_new_state -action_id $action_id] db_transaction { # Update the workflow state - if { ![empty_string_p $new_state] } { + if { ![empty_string_p $new_state_id] } { db_dml update_fsm_state {} } - # Maybe get entry_id, if one wasn't supplied - if { [empty_string_p $entry_id] } { - set entry_id [db_nextval "workflow_case_log_seq"] + # Double-click protection + if { ![empty_string_p $entry_id] } { + if { [db_string log_entry_exists_p {}] } { + return $entry_id + } } - - # Check if the row already exists - set exists_p [db_string log_entry_exists_p {}] - if { $exists_p } { - return $entry_id - } - - # We can't have empty comment_format - if { [empty_string_p $comment_format] } { + # We can't have empty comment_mime_type + if { [empty_string_p $comment_mime_type] } { # We need a default value here - set comment_format "text/plain" + set comment_mime_type "text/plain" } # Insert activity log entry - db_dml insert_log_entry {} + set extra_vars [ns_set create] + oacs_util::vars_to_ns_set \ + -ns_set $extra_vars \ + -var_list { entry_id case_id action_id comment comment_mime_type } + set entry_id [package_instantiate_object \ + -creation_user $user_id \ + -extra_vars $extra_vars \ + -package_name "workflow_case_log_entry" \ + "workflow_case_log_entry"] + # Assign new enabled roles, if currently unassigned - workflow::case::assign_roles -case_id $case_id + workflow::case::assign_roles -all=$initial_p -case_id $case_id - # Fire side-effects, both action ones and workflow ones - # ... TODO ... + # Fire side-effects + do_side_effects \ + -case_id $case_id \ + -action_id $action_id \ + -entry_id $entry_id # Notifications - # ... TODO ... + notify \ + -case_id $case_id \ + -action_id $action_id \ + -entry_id $entry_id \ + -comment $comment \ + -comment_mime_type $comment_mime_type + } + + return $entry_id +} +ad_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] - # LARS: TODO - # Taken from bug-tracker - if 0 { - # Setup any assignee for alerts on the bug - if { [info exists row(assignee)] && ![empty_string_p $row(assignee)] } { - bug_tracker::add_instant_alert \ - -bug_id $bug_id \ - -user_id $row(assignee) - } - } + # 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]] - # LARS: TODO - # Taken from bug-tracker - if 0 { - set resolution {} - if { [exists_and_not_null row(resolution)] } { - set resolution $row(resolution) - } - - # Send out notifications - bug_tracker::bug_notify \ - -bug_id $bug_id \ - -action $action \ - -comment $description \ - -comment_format $desc_format \ - -resolution $resolution + 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] + } +} + +ad_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. +} { + # LARS TODO: + # Not implemented yet + return + + # Get workflow_id + workflow::case::get \ + -case_id $case_id \ + -array case + + workflow::get \ + -workflow_id $workflow_id \ + -array workflow + + # LARS TODO: + # we probably need a callback to format the message... + set subject "New notification" + set body "Here's the body" + + # LARS TODO: + # List of user_id's for people who are assigned to some task + # Don't forget to map parties to users + set assignee_list [list] + + # List of users who play some role in this case + set case_player_list [list] + + # LARS TODO: + # We want the subject/body to be customized depending on the type of notification + + foreach type { + workflow_assignee workflow_my_cases workflow_case workflow + } { + set subject($type) $subject + set body($type) $body + set force_p($type) 0 + set intersection($type) {} } + + set force_p(workflow_assignee) 1 + set intersection(workflow_assignee) $assignee_list + set intersection(workflow_my_cases) $case_player_list + - return $entry_id + set notified_list [list] + + foreach type { + workflow_assignee workflow_my_cases workflow_case workflow + } { + set object_id [get_notification_object \ + -type $type \ + -workflow_id $workflow_id \ + -case_id $case_id] + + if { ![empty_string_p $object_id] } { + set notified_list [notification::new \ + -type_id [notification::type::get_type_id -short_name $type] \ + -object_id $object_id \ + -response_id $case(object_id) \ + -notif_subject $subject($type) \ + -notif_text $body($type) \ + -already_notified $notified_list \ + -intersection $intersection($type) \ + -force=$force_p($type)] + } + } } + ##### # # workflow::case::action::fsm @@ -615,17 +1091,17 @@ {-case_id:required} {-action_id:required} } { - Get the new state which the workflow will be in after a certain action. + Get the ID of the new state which the workflow will be in after a certain action. @param case_id The ID of the case. @param action_id The ID of the action @return The state_id of the new state which the workflow will be in after this action @author Lars Pind (lars@collaboraid.biz) } { - set new_state [workflow::action::fsm::get_new_state -action_id $action_id] - if { [empty_string_p $new_state] } { - set new_state [workflow::case::fsm::get_current_state -case_id $case_id] + set new_state_id [workflow::action::fsm::get_new_state -action_id $action_id] + if { [empty_string_p $new_state_id] } { + set new_state_id [workflow::case::fsm::get_current_state -case_id $case_id] } - return $new_state + return $new_state_id }