state-procs.tcl
Does not contain a contract.
- Location:
- /packages/workflow/tcl/state-procs.tcl
Related Files
[ hide source ] | [ make this the default ]
File Contents
ad_library { Procedures in the workflow::fsm::state namespace and in its child namespaces. @creation-date 8 January 2003 @author Lars Pind (lars@collaboraid.biz) @author Peter Marklund (peter@collaboraid.biz) @cvs-id $Id: state-procs.tcl,v 1.25 2022/09/28 09:12:55 gustafn Exp $ } namespace eval workflow::state::fsm {} ##### # # workflow::state::fsm namespace # ##### d_proc -public workflow::state::fsm::new { {-workflow_id:required} {-internal:boolean} {-short_name {}} {-pretty_name:required} {-hide_fields {}} {-sort_order {}} {-parent_action {}} } { Creates a new state for a certain FSM (Finite State Machine) workflow. @param workflow_id The id of the FSM workflow to add the state to @param short_name If you leave blank, the short_name will be generated from pretty_name. @param pretty_name @param hide_fields A space-separated list of the names of form fields which should be hidden when in this state, because they're irrelevant in a certain state. @param sort_order The number which this state should be in the sort ordering sequence. Leave blank to add state at the end. If you provide a sort_order number which already exists, existing states are pushed down one number. @param parent_action Which action with trigger_type 'workflow' does this state belong to. @param internal Set this flag if you're calling this proc from within the corresponding proc for a particular workflow model. Will cause this proc to not flush the cache or call workflow::definition_changed_handler, which the caller must then do. @return ID of new state. @author Peter Marklund } { # Wrapper for workflow::state::fsm::edit foreach elm { short_name pretty_name sort_order parent_action } { set row($elm) [set $elm] } set state_id [workflow::state::fsm::edit \ -operation "insert" \ -workflow_id $workflow_id \ -array row] return $state_id } d_proc -public workflow::state::fsm::edit { {-operation "update"} {-state_id {}} {-workflow_id {}} {-array {}} {-internal:boolean} {-no_complain:boolean} {-handlers {}} } { Edit a workflow state. Attributes of the array are: <ul> <li>short_name <li>pretty_name <li>sort_order <li>hide_fields <li>parent_action </ul> @param operation insert, update, delete @param state_id For update/delete: The state to update or delete. For insert: Optionally specify a pre-generated state_id for the state. @param workflow_id For update/delete: Optionally specify the workflow_id. If not specified, we will execute a query to find it. For insert: The workflow_id of the new state. @param array For insert/update: Name of an array in the caller's namespace with attributes to insert/update. @param internal Set this flag if you're calling this proc from within the corresponding proc for a particular workflow model. Will cause this proc to not flush the cache or call workflow::definition_changed_handler, which the caller must then do. @param no_complain Silently ignore extra attributes that we don't know how to handle. @return state_id @see workflow::state::new @author Peter Marklund @author Lars Pind (lars@collaboraid.biz) } { switch $operation { update - delete { if { $state_id eq "" } { error "You must specify the state_id of the state to $operation." } } insert {} default { error "Illegal operation '$operation'" } } switch $operation { insert - update { upvar 1 $array row if { ![array exists row] } { error "Array $array does not exist or is not an array" } foreach name [array names row] { set missing_elm($name) 1 } } } switch $operation { insert { if { $workflow_id eq "" } { error "You must supply workflow_id" } # Default sort_order if { (![info exists row(sort_order)] || $row(sort_order) eq "") } { set row(sort_order) [workflow::default_sort_order \ -workflow_id $workflow_id \ -table_name "workflow_fsm_states"] } # Default short_name on insert if { ![info exists row(short_name)] } { set row(short_name) {} } } update { if { $workflow_id eq "" } { set workflow_id [workflow::state::fsm::get_element \ -state_id $state_id \ -element workflow_id] } } } # Parse column values switch $operation { insert - update { # Special-case: array entry parent_action (takes short_name) and parent_action_id (takes action_id) -- # DB column is parent_action_id (takes action_id_id) if { [info exists row(parent_action)] } { if { [info exists row(parent_action_id)] } { error "You cannot supply both parent_action (takes short_name) and parent_action_id (takes action_id)" } if { $row(parent_action) ne "" } { set row(parent_action_id) [workflow::action::get_id \ -workflow_id $workflow_id \ -short_name $row(parent_action)] } unset row(parent_action) unset missing_elm(parent_action) } set update_clauses [list] set insert_names [list] set insert_values [list] # Handle columns in the workflow_fsm_states table foreach attr { short_name pretty_name hide_fields sort_order parent_action_id } { if { [info exists row($attr)] } { set varname attr_$attr # Convert the Tcl value to something we can use in the query switch $attr { short_name { if { (![info exists row(pretty_name)] || $row(pretty_name) eq "") } { if { $row(short_name) eq "" } { error "You cannot edit with an empty short_name without also setting pretty_name" } else { set row(pretty_name) {} } } set $varname [workflow::state::fsm::generate_short_name \ -workflow_id $workflow_id \ -pretty_name $row(pretty_name) \ -short_name $row(short_name) \ -state_id $state_id] } default { set $varname $row($attr) } } # Add the column to the insert/update statement switch $attr { default { lappend update_clauses "$attr = :$varname" lappend insert_names $attr lappend insert_values :$varname } } if { [info exists missing_elm($attr)] } { unset missing_elm($attr) } } } # Auxiliary helper attributes (enabled_actions -> enabled_action_ids, assigned_actions -> assigned_action_ids) # Enabled actions if { [info exists row(enabled_actions)] } { if { [info exists row(enabled_action_ids)] } { error "You cannot supply both enabled_actions and enabled_actions_ids" } set row(enabled_action_ids) [list] foreach action_short_name $row(enabled_actions) { lappend row(enabled_action_ids) [workflow::action::get_id \ -workflow_id $workflow_id \ -short_name $action_short_name] } unset row(enabled_actions) } # Assigend actions if { [info exists row(assigned_actions)] } { if { [info exists row(assigned_action_ids)] } { error "You cannot supply both assigned_actions and assigned_action_ids" } set row(assigned_action_ids) [list] foreach action_short_name $row(assigned_actions) { lappend row(assigned_action_ids) [workflow::action::get_id \ -workflow_id $workflow_id \ -short_name $action_short_name] } unset row(assigned_actions) } # Handle auxiliary rows array set aux [list] foreach attr { enabled_action_ids assigned_action_ids } { if { [info exists row($attr)] } { set aux($attr) $row($attr) unset row($attr) } } } } db_transaction { # Sort_order switch $operation { insert - update { if { [info exists row(sort_order)] } { workflow::state::fsm::update_sort_order \ -workflow_id $workflow_id \ -sort_order $row(sort_order) } } } # Do the insert/update/delete switch $operation { insert { if { $state_id eq "" } { set state_id [db_nextval "workflow_fsm_states_seq"] } lappend insert_names state_id lappend insert_values :state_id lappend insert_names workflow_id lappend insert_values :workflow_id db_dml insert_state " insert into workflow_fsm_states ([join $insert_names ", "]) values ([join $insert_values ", "]) " } update { if { [llength $update_clauses] > 0 } { db_dml update_state " update workflow_fsm_states set [join $update_clauses ", "] where state_id = :state_id " } } delete { db_dml delete_state { delete from workflow_fsm_states where state_id = :state_id } } } # Auxiliary rows switch $operation { insert - update { # Record in which actions the action is enabled but not assigned if { [info exists aux(enabled_action_ids)] } { set assigned_p "f" db_dml delete_enabled_actions {} foreach enabled_action_id $aux(enabled_action_ids) { db_dml insert_enabled_action {} } unset aux(enabled_action_ids) } # Record where the action is both enabled and assigned if { [info exists aux(assigned_action_ids)] } { set assigned_p "t" db_dml delete_enabled_actions {} foreach enabled_action_id $aux(assigned_action_ids) { db_dml insert_enabled_action {} } unset aux(assigned_action_ids) } # Check that there are no unknown attributes if { [array size missing_elm] > 0 && !$no_complain_p } { error "Trying to set illegal state attributes: [join [array names missing_elm] ", "]" } } } if { !$internal_p } { workflow::definition_changed_handler -workflow_id $workflow_id } } return $state_id } d_proc -private workflow::state::fsm::update_sort_order { {-workflow_id:required} {-sort_order:required} } { Increase the sort_order of other states, if the new sort_order is already taken. } { set sort_order_taken_p [db_string select_sort_order_p {}] if { $sort_order_taken_p } { db_dml update_sort_order {} } } d_proc -public workflow::state::fsm::get_existing_short_names { {-workflow_id:required} {-ignore_state_id {}} } { Returns a list of existing state short_names in this workflow. Useful when you're trying to ensure a short_name is unique, or construct a new short_name that is guaranteed to be unique. @param ignore_state_id If specified, the short_name for the given state will not be included in the result set. } { set result [list] foreach state_id [workflow::fsm::get_states -all -workflow_id $workflow_id] { if { $ignore_state_id eq "" || $ignore_state_id ne $state_id } { lappend result [workflow::state::fsm::get_element -state_id $state_id -element short_name] } } return $result } d_proc -public workflow::state::fsm::generate_short_name { {-workflow_id:required} {-pretty_name:required} {-short_name {}} {-state_id {}} } { Generate a unique short_name from pretty_name. @param state_id If you pass in this, we will allow that state's short_name to be reused. } { set existing_short_names [workflow::state::fsm::get_existing_short_names \ -workflow_id $workflow_id \ -ignore_state_id $state_id] if { $short_name eq "" } { if { $pretty_name eq "" } { error "Cannot have empty pretty_name when short_name is empty" } set short_name [util_text_to_url \ -replacement "_" \ -existing_urls $existing_short_names \ -text $pretty_name] } else { # Make lowercase, remove illegal characters set short_name [string tolower $short_name] regsub -all {[- ]} $short_name {_} short_name regsub -all {[^a-zA-Z_0-9]} $short_name {} short_name if {$short_name in $existing_short_names} { error "State with short_name '$short_name' already exists in this workflow." } } return $short_name } d_proc -public workflow::state::fsm::get { {-state_id:required} {-array:required} } { Return workflow_id, sort_order, short_name, and pretty_name for a certain FSM workflow state. @author Peter Marklund } { # Select the info into the upvar'ed Tcl Array upvar $array row set workflow_id [workflow::state::fsm::get_workflow_id -state_id $state_id] array set state_data [workflow::state::fsm::get_all_info -workflow_id $workflow_id] array set row $state_data($state_id) } d_proc -public workflow::state::fsm::get_element { {-state_id {}} {-one_id {}} {-element:required} } { Return a single element from the information about a state. @param state_id The ID of the workflow @param one_id Same as state_id, just used for consistency across roles/actions/states. @return The element you asked for @author Lars Pind (lars@collaboraid.biz) } { if { $state_id eq "" } { if { $one_id eq "" } { error "You must supply either state_id or one_id" } set state_id $one_id } else { if { $one_id ne "" } { error "You can only supply either state_id or one_id" } } get -state_id $state_id -array row return $row($element) } d_proc -public workflow::state::fsm::get_id { {-workflow_id:required} {-short_name:required} } { Return the id of the state with given short name @param workflow_id The id of the workflow the state belongs to. @param short_name The name of the state to return the id for. @author Peter Marklund } { return [db_string select_id {}] } d_proc -public workflow::state::fsm::get_workflow_id { {-state_id:required} } { Lookup the workflow that the given state belongs to. @return The id of the workflow the state belongs to. @author Peter Marklund } { return [util_memoize \ [list workflow::state::fsm::get_workflow_id_not_cached -state_id $state_id]] } d_proc -public workflow::state::fsm::pretty_name_unique_p { -workflow_id:required -pretty_name:required {-parent_action_id {}} {-state_id {}} } { Check if suggested pretty_name is unique. @return 1 if unique, 0 if not unique. } { set exists_p [db_string name_exists { select count(*) from workflow_fsm_states where workflow_id = :workflow_id and pretty_name = :pretty_name and (:parent_action_id is null or parent_action_id = :parent_action_id) and (:state_id is null or state_id != :state_id) }] return [expr {!$exists_p}] } ##### # Private procs ##### d_proc -private workflow::state::fsm::get_ids { {-all:boolean} {-workflow_id:required} {-parent_action_id {}} } { Get the state_id's of all the states in the workflow. @param workflow_id The ID of the workflow @return list of state_id's. @author Lars Pind (lars@collaboraid.biz) } { # Use cached data array set state_data [workflow::state::fsm::get_all_info -workflow_id $workflow_id] if { $all_p } { return $state_data(state_ids) } set state_ids [list] foreach state_id $state_data(state_ids) { if { [workflow::state::fsm::get_element \ -state_id $state_id \ -element parent_action_id] == $parent_action_id } { lappend state_ids $state_id } } return $state_ids } d_proc -private workflow::state::fsm::get_workflow_id_not_cached { {-state_id:required} } { This proc is used internally by the workflow API only. Use the proc workflow::state::fsm::get_workflow_id instead. @author Peter Marklund } { return [db_string select_workflow_id {}] } d_proc -private workflow::state::fsm::parse_spec { {-workflow_id:required} {-short_name:required} {-spec:required} {-parent_action_id {}} } { Parse the spec for an individual state definition. @param workflow_id The id of the workflow to delete. @param short_name The short_name of the state @param spec The state spec @author Lars Pind (lars@collaboraid.biz) } { # Initialize array with default values array set state { hide_fields {} } # Get the info from the spec foreach { key value } $spec { set state($key) [string trim $value] } set state(short_name) $short_name set state(parent_action_id) $parent_action_id # Create the state set state_id [workflow::state::fsm::edit \ -operation "insert" \ -workflow_id $workflow_id \ -array state] } d_proc -private workflow::state::fsm::generate_spec { {-state_id {}} {-one_id {}} {-handlers {}} } { Generate the spec for an individual state definition. @param state_id The id of the state to generate spec for. @param one_id Same as state_id, just used for consistency across roles/actions/states. @return spec The states spec @author Lars Pind (lars@collaboraid.biz) } { if { $state_id eq "" } { if { $one_id eq "" } { error "You must supply either state_id or one_id" } set state_id $one_id } else { if { $one_id ne "" } { error "You can only supply either state_id or one_id" } } get -state_id $state_id -array row # Get rid of elements that shouldn't go into the spec array unset row short_name array unset row state_id array unset row workflow_id array unset row sort_order array unset row parent_action array unset row parent_action_id array unset row enabled_actions array unset row enabled_action_ids array unset row assigned_actions array unset row assigned_action_ids set spec {} foreach name [lsort [array names row]] { if { $row($name) ne "" } { lappend spec $name $row($name) } } return $spec } d_proc -private workflow::state::fsm::generate_states_spec { {-workflow_id:required} } { Generate the spec for the block containing the definition of all states for the workflow. @param workflow_id The id of the workflow to get the states spec for @return The states spec @author Lars Pind (lars@collaboraid.biz) } { # states(short_name) { ... state-spec ... } set states_list [list] foreach state_id [workflow::fsm::get_states -workflow_id $workflow_id] { lappend states_list [get_element -state_id $state_id -element short_name] [generate_spec -state_id $state_id] } return $states_list } d_proc -private workflow::state::flush_cache { {-workflow_id:required} } { Flush all caches related to state information for the given workflow. Used internally by the workflow API only. @author Peter Marklund } { # TODO: Flush request cache # ... # Flush the thread global cache util_memoize_flush [list workflow::state::fsm::get_all_info_not_cached -workflow_id $workflow_id] } d_proc -private workflow::state::fsm::get_all_info { {-workflow_id:required} } { This proc is for internal use in the workflow API only. Returns all information related to states for a certain workflow instance. Uses util_memoize to cache values. @see workflow::state::fsm::get_all_info_not_cached @author Peter Marklund } { return [util_memoize [list workflow::state::fsm::get_all_info_not_cached \ -workflow_id $workflow_id] [workflow::cache_timeout]] } d_proc -private workflow::state::fsm::get_all_info_not_cached { {-workflow_id:required} } { This proc is for internal use in the workflow API only and should not be invoked directly from application code. Returns all information related to states for a certain workflow instance. Goes to the database on every invocation and should be used together with util_memoize. @author Peter Marklund } { # state_data will be an array keyed by state_id # state_data(123) = array-list with: hide_fields, pretty_name, short_name, state_id, sort_order, workflow_id, # enabled_actions, enabled_action_ids, assigned_actions, assigned_action_ids # In addition: # state_data(state_ids) = [list of state_ids in sort order] array set state_data [list] # state_array_$state_id is an internal datastructure. It's the array for each state_id entry # but as a separate array making it easier to lappend to individual entries #---------------------------------------------------------------------- # Get core state information from DB #---------------------------------------------------------------------- # Use a list to be able to retrieve states in sort order set state_ids [list] db_foreach select_states {} -column_array state_row { # Cache the state_id -> workflow_id lookup util_memoize_seed \ [list workflow::state::fsm::get_workflow_id_not_cached -state_id $state_row(state_id)] \ $workflow_id set state_id $state_row(state_id) array set state_array_$state_id [array get state_row] lappend state_ids $state_id } set state_data(state_ids) $state_ids array set action_short_name [list] #---------------------------------------------------------------------- # Build state-action map #---------------------------------------------------------------------- # Will be stored like this: # assigned_p_${state_id}($action_id) = 1 if assigned, 0 if enabled, non-existent if neither # In addition, we have a supporting structure of action information # action_info(${action_id},short_name) # action_info(${action_id},trigger_type) # action_info(${action_id},always_enabled_p) # action_info(${action_id},parent_action_id) # action_info(${action_id},child_action_ids) # 1. Get action data: trigger_type, always_enabled, hierarchy db_foreach always_enabled_actions { select action_id, short_name, trigger_type, always_enabled_p, parent_action_id from workflow_actions where workflow_id = :workflow_id } { set action_info(${action_id},short_name) $short_name set action_info(${action_id},trigger_type) [string trim $trigger_type] set action_info(${action_id},parent_action_id) $parent_action_id if { [string is true -strict $always_enabled_p] && [lsearch { user auto message } $trigger_type] != -1 } { set action_info(${action_id},always_enabled_p) 1 } else { set action_info(${action_id},always_enabled_p) 0 } # Store as a child of parent NOTE: Not needed any longer if { $parent_action_id ne "" } { lappend action_info(${parent_action_id},child_action_ids) $action_id } # Mark enabled in all states that have the same parent as the action if { $action_info(${action_id},always_enabled_p) } { foreach state_id $state_ids { if {$parent_action_id eq [set state_array_${state_id}(parent_action_id)]} { set assigned_p_${state_id}($action_id) 0 } } } } # 2. Get action-state map db_foreach always_enabled_actions { select e.action_id, e.state_id, e.assigned_p from workflow_actions a, workflow_fsm_action_en_in_st e where a.workflow_id = :workflow_id and a.action_id = e.action_id } { set assigned_p_${state_id}($action_id) [string is true -strict $assigned_p] } # 3. Put stuff back into the output array foreach state_id $state_ids { set state_array_${state_id}(enabled_action_ids) [list] set state_array_${state_id}(enabled_actions) [list] set state_array_${state_id}(assigned_action_ids) [list] set state_array_${state_id}(assigned_actions) [list] if { [info exists assigned_p_${state_id}] } { foreach action_id [array names assigned_p_${state_id}] { # Enabled lappend state_array_${state_id}(enabled_action_ids) $action_id lappend state_array_${state_id}(enabled_actions) $action_info(${action_id},short_name) # Assigned if { [set assigned_p_${state_id}($action_id)] } { lappend state_array_${state_id}(assigned_action_ids) $action_id lappend state_array_${state_id}(assigned_actions) $action_info(${action_id},short_name) } } } } #---------------------------------------------------------------------- # Final output #---------------------------------------------------------------------- # Move over to normal array foreach state_id $state_ids { set state_data($state_id) [array get state_array_$state_id] } return [array get state_data]} # Local variables: # mode: tcl # tcl-indent-level: 4 # indent-tabs-mode: nil # End: