state-procs.tcl

Procedures in the workflow::fsm::state namespace and in its child namespaces.

Location:
packages/workflow/tcl/state-procs.tcl
Created:
8 January 2003
Authors:
Lars Pind <lars@collaboraid.biz>
Peter Marklund <peter@collaboraid.biz>
CVS Identification:
$Id: state-procs.tcl,v 1.25 2022/09/28 09:12:55 gustafn Exp $

Procedures in this file

Detailed information

workflow::state::flush_cache (private)

 workflow::state::flush_cache -workflow_id workflow_id

Flush all caches related to state information for the given workflow. Used internally by the workflow API only.

Switches:
-workflow_id (required)
Author:
Peter Marklund

Partial Call Graph (max 5 caller/called nodes):
%3 workflow::flush_cache workflow::flush_cache (private) workflow::state::flush_cache workflow::state::flush_cache workflow::flush_cache->workflow::state::flush_cache util_memoize_flush util_memoize_flush (public) workflow::state::flush_cache->util_memoize_flush

Testcases:
No testcase defined.

workflow::state::fsm::edit (public)

 workflow::state::fsm::edit [ -operation operation ] \
    [ -state_id state_id ] [ -workflow_id workflow_id ] \
    [ -array array ] [ -internal ] [ -no_complain ] \
    [ -handlers handlers ]

Edit a workflow state. Attributes of the array are:

  • short_name
  • pretty_name
  • sort_order
  • hide_fields
  • parent_action

Switches:
-operation (optional, defaults to "update")
insert, update, delete
-state_id (optional)
For update/delete: The state to update or delete. For insert: Optionally specify a pre-generated state_id for the state.
-workflow_id (optional)
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.
-array (optional)
For insert/update: Name of an array in the caller's namespace with attributes to insert/update.
-internal (optional, boolean)
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.
-no_complain (optional, boolean)
Silently ignore extra attributes that we don't know how to handle.
-handlers (optional)
Returns:
state_id
Authors:
Peter Marklund
Lars Pind <lars@collaboraid.biz>
See Also:
  • workflow::state::new

Partial Call Graph (max 5 caller/called nodes):
%3 packages/workflow/www/admin/delete-confirm.tcl packages/workflow/ www/admin/delete-confirm.tcl workflow::state::fsm::edit workflow::state::fsm::edit packages/workflow/www/admin/delete-confirm.tcl->workflow::state::fsm::edit packages/workflow/www/admin/state-ae.tcl packages/workflow/ www/admin/state-ae.tcl packages/workflow/www/admin/state-ae.tcl->workflow::state::fsm::edit workflow::state::fsm::new workflow::state::fsm::new (public) workflow::state::fsm::new->workflow::state::fsm::edit workflow::state::fsm::parse_spec workflow::state::fsm::parse_spec (private) workflow::state::fsm::parse_spec->workflow::state::fsm::edit db_dml db_dml (public) workflow::state::fsm::edit->db_dml db_nextval db_nextval (public) workflow::state::fsm::edit->db_nextval db_transaction db_transaction (public) workflow::state::fsm::edit->db_transaction workflow::action::get_id workflow::action::get_id (public) workflow::state::fsm::edit->workflow::action::get_id workflow::default_sort_order workflow::default_sort_order (private) workflow::state::fsm::edit->workflow::default_sort_order

Testcases:
No testcase defined.

workflow::state::fsm::generate_short_name (public)

 workflow::state::fsm::generate_short_name -workflow_id workflow_id \
    -pretty_name pretty_name [ -short_name short_name ] \
    [ -state_id state_id ]

Generate a unique short_name from pretty_name.

Switches:
-workflow_id (required)
-pretty_name (required)
-short_name (optional)
-state_id (optional)
If you pass in this, we will allow that state's short_name to be reused.

Partial Call Graph (max 5 caller/called nodes):
%3 workflow::state::fsm::edit workflow::state::fsm::edit (public) workflow::state::fsm::generate_short_name workflow::state::fsm::generate_short_name workflow::state::fsm::edit->workflow::state::fsm::generate_short_name _ _ (public) workflow::state::fsm::generate_short_name->_ util_text_to_url util_text_to_url (public) workflow::state::fsm::generate_short_name->util_text_to_url workflow::state::fsm::get_existing_short_names workflow::state::fsm::get_existing_short_names (public) workflow::state::fsm::generate_short_name->workflow::state::fsm::get_existing_short_names

Testcases:
No testcase defined.

workflow::state::fsm::generate_spec (private)

 workflow::state::fsm::generate_spec [ -state_id state_id ] \
    [ -one_id one_id ] [ -handlers handlers ]

Generate the spec for an individual state definition.

Switches:
-state_id (optional)
The id of the state to generate spec for.
-one_id (optional)
Same as state_id, just used for consistency across roles/actions/states.
-handlers (optional)
Returns:
spec The states spec
Author:
Lars Pind <lars@collaboraid.biz>

Partial Call Graph (max 5 caller/called nodes):
%3 workflow::state::fsm::generate_states_spec workflow::state::fsm::generate_states_spec (private) workflow::state::fsm::generate_spec workflow::state::fsm::generate_spec workflow::state::fsm::generate_states_spec->workflow::state::fsm::generate_spec workflow::state::fsm::get workflow::state::fsm::get (public) workflow::state::fsm::generate_spec->workflow::state::fsm::get

Testcases:
No testcase defined.

workflow::state::fsm::generate_states_spec (private)

 workflow::state::fsm::generate_states_spec -workflow_id workflow_id

Generate the spec for the block containing the definition of all states for the workflow.

Switches:
-workflow_id (required)
The id of the workflow to get the states spec for
Returns:
The states spec
Author:
Lars Pind <lars@collaboraid.biz>

Partial Call Graph (max 5 caller/called nodes):
%3 workflow::fsm::get_states workflow::fsm::get_states (public) workflow::state::fsm::generate_spec workflow::state::fsm::generate_spec (private) workflow::state::fsm::get_element workflow::state::fsm::get_element (public) workflow::state::fsm::generate_states_spec workflow::state::fsm::generate_states_spec workflow::state::fsm::generate_states_spec->workflow::fsm::get_states workflow::state::fsm::generate_states_spec->workflow::state::fsm::generate_spec workflow::state::fsm::generate_states_spec->workflow::state::fsm::get_element

Testcases:
No testcase defined.

workflow::state::fsm::get (public)

 workflow::state::fsm::get -state_id state_id -array array

Return workflow_id, sort_order, short_name, and pretty_name for a certain FSM workflow state.

Switches:
-state_id (required)
-array (required)
Author:
Peter Marklund

Partial Call Graph (max 5 caller/called nodes):
%3 bug_tracker::status_get_options bug_tracker::status_get_options (public) workflow::state::fsm::get workflow::state::fsm::get bug_tracker::status_get_options->workflow::state::fsm::get bug_tracker::status_pretty bug_tracker::status_pretty (public) bug_tracker::status_pretty->workflow::state::fsm::get packages/workflow/www/admin/state-ae.tcl packages/workflow/ www/admin/state-ae.tcl packages/workflow/www/admin/state-ae.tcl->workflow::state::fsm::get packages/workflow/www/admin/workflow-edit.tcl packages/workflow/ www/admin/workflow-edit.tcl packages/workflow/www/admin/workflow-edit.tcl->workflow::state::fsm::get workflow::action::get_all_info_not_cached workflow::action::get_all_info_not_cached (private) workflow::action::get_all_info_not_cached->workflow::state::fsm::get workflow::state::fsm::get_all_info workflow::state::fsm::get_all_info (private) workflow::state::fsm::get->workflow::state::fsm::get_all_info workflow::state::fsm::get_workflow_id workflow::state::fsm::get_workflow_id (public) workflow::state::fsm::get->workflow::state::fsm::get_workflow_id

Testcases:
No testcase defined.

workflow::state::fsm::get_all_info (private)

 workflow::state::fsm::get_all_info -workflow_id workflow_id

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.

Switches:
-workflow_id (required)
Author:
Peter Marklund
See Also:

Partial Call Graph (max 5 caller/called nodes):
%3 workflow::state::fsm::get workflow::state::fsm::get (public) workflow::state::fsm::get_all_info workflow::state::fsm::get_all_info workflow::state::fsm::get->workflow::state::fsm::get_all_info workflow::state::fsm::get_ids workflow::state::fsm::get_ids (private) workflow::state::fsm::get_ids->workflow::state::fsm::get_all_info util_memoize util_memoize (public) workflow::state::fsm::get_all_info->util_memoize workflow::cache_timeout workflow::cache_timeout (private) workflow::state::fsm::get_all_info->workflow::cache_timeout workflow::state::fsm::get_all_info_not_cached workflow::state::fsm::get_all_info_not_cached (private) workflow::state::fsm::get_all_info->workflow::state::fsm::get_all_info_not_cached

Testcases:
No testcase defined.

workflow::state::fsm::get_all_info_not_cached (private)

 workflow::state::fsm::get_all_info_not_cached -workflow_id workflow_id

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.

Switches:
-workflow_id (required)
Author:
Peter Marklund

Partial Call Graph (max 5 caller/called nodes):
%3 workflow::state::fsm::get_all_info workflow::state::fsm::get_all_info (private) workflow::state::fsm::get_all_info_not_cached workflow::state::fsm::get_all_info_not_cached workflow::state::fsm::get_all_info->workflow::state::fsm::get_all_info_not_cached db_foreach db_foreach (public) workflow::state::fsm::get_all_info_not_cached->db_foreach util_memoize_seed util_memoize_seed (public) workflow::state::fsm::get_all_info_not_cached->util_memoize_seed workflow::state::fsm::get_workflow_id_not_cached workflow::state::fsm::get_workflow_id_not_cached (private) workflow::state::fsm::get_all_info_not_cached->workflow::state::fsm::get_workflow_id_not_cached

Testcases:
No testcase defined.

workflow::state::fsm::get_element (public)

 workflow::state::fsm::get_element [ -state_id state_id ] \
    [ -one_id one_id ] -element element

Return a single element from the information about a state.

Switches:
-state_id (optional)
The ID of the workflow
-one_id (optional)
Same as state_id, just used for consistency across roles/actions/states.
-element (required)
Returns:
The element you asked for
Author:
Lars Pind <lars@collaboraid.biz>

Partial Call Graph (max 5 caller/called nodes):
%3 workflow::state::fsm::edit workflow::state::fsm::edit (public) workflow::state::fsm::get_element workflow::state::fsm::get_element workflow::state::fsm::edit->workflow::state::fsm::get_element workflow::state::fsm::generate_states_spec workflow::state::fsm::generate_states_spec (private) workflow::state::fsm::generate_states_spec->workflow::state::fsm::get_element workflow::state::fsm::get_existing_short_names workflow::state::fsm::get_existing_short_names (public) workflow::state::fsm::get_existing_short_names->workflow::state::fsm::get_element workflow::state::fsm::get_ids workflow::state::fsm::get_ids (private) workflow::state::fsm::get_ids->workflow::state::fsm::get_element workflow::test::assert_case_state workflow::test::assert_case_state (public) workflow::test::assert_case_state->workflow::state::fsm::get_element workflow::state::fsm::get workflow::state::fsm::get (public) workflow::state::fsm::get_element->workflow::state::fsm::get

Testcases:
No testcase defined.

workflow::state::fsm::get_existing_short_names (public)

 workflow::state::fsm::get_existing_short_names \
    -workflow_id workflow_id [ -ignore_state_id 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.

Switches:
-workflow_id (required)
-ignore_state_id (optional)
If specified, the short_name for the given state will not be included in the result set.

Partial Call Graph (max 5 caller/called nodes):
%3 workflow::state::fsm::generate_short_name workflow::state::fsm::generate_short_name (public) workflow::state::fsm::get_existing_short_names workflow::state::fsm::get_existing_short_names workflow::state::fsm::generate_short_name->workflow::state::fsm::get_existing_short_names workflow::fsm::get_states workflow::fsm::get_states (public) workflow::state::fsm::get_existing_short_names->workflow::fsm::get_states workflow::state::fsm::get_element workflow::state::fsm::get_element (public) workflow::state::fsm::get_existing_short_names->workflow::state::fsm::get_element

Testcases:
No testcase defined.

workflow::state::fsm::get_id (public)

 workflow::state::fsm::get_id -workflow_id workflow_id \
    -short_name short_name

Return the id of the state with given short name

Switches:
-workflow_id (required)
The id of the workflow the state belongs to.
-short_name (required)
The name of the state to return the id for.
Author:
Peter Marklund

Partial Call Graph (max 5 caller/called nodes):
%3 bug_tracker::status_pretty bug_tracker::status_pretty (public) workflow::state::fsm::get_id workflow::state::fsm::get_id bug_tracker::status_pretty->workflow::state::fsm::get_id workflow::action::fsm::edit workflow::action::fsm::edit (public) workflow::action::fsm::edit->workflow::state::fsm::get_id db_string db_string (public) workflow::state::fsm::get_id->db_string

Testcases:
No testcase defined.

workflow::state::fsm::get_ids (private)

 workflow::state::fsm::get_ids [ -all ] -workflow_id workflow_id \
    [ -parent_action_id parent_action_id ]

Get the state_id's of all the states in the workflow.

Switches:
-all (optional, boolean)
-workflow_id (required)
The ID of the workflow
-parent_action_id (optional)
Returns:
list of state_id's.
Author:
Lars Pind <lars@collaboraid.biz>

Partial Call Graph (max 5 caller/called nodes):
%3 workflow::fsm::get_states workflow::fsm::get_states (public) workflow::state::fsm::get_ids workflow::state::fsm::get_ids workflow::fsm::get_states->workflow::state::fsm::get_ids workflow::state::fsm::get_all_info workflow::state::fsm::get_all_info (private) workflow::state::fsm::get_ids->workflow::state::fsm::get_all_info workflow::state::fsm::get_element workflow::state::fsm::get_element (public) workflow::state::fsm::get_ids->workflow::state::fsm::get_element

Testcases:
No testcase defined.

workflow::state::fsm::get_workflow_id (public)

 workflow::state::fsm::get_workflow_id -state_id state_id

Lookup the workflow that the given state belongs to.

Switches:
-state_id (required)
Returns:
The id of the workflow the state belongs to.
Author:
Peter Marklund

Partial Call Graph (max 5 caller/called nodes):
%3 workflow::state::fsm::get workflow::state::fsm::get (public) workflow::state::fsm::get_workflow_id workflow::state::fsm::get_workflow_id workflow::state::fsm::get->workflow::state::fsm::get_workflow_id util_memoize util_memoize (public) workflow::state::fsm::get_workflow_id->util_memoize workflow::state::fsm::get_workflow_id_not_cached workflow::state::fsm::get_workflow_id_not_cached (private) workflow::state::fsm::get_workflow_id->workflow::state::fsm::get_workflow_id_not_cached

Testcases:
No testcase defined.

workflow::state::fsm::get_workflow_id_not_cached (private)

 workflow::state::fsm::get_workflow_id_not_cached -state_id state_id

This proc is used internally by the workflow API only. Use the proc workflow::state::fsm::get_workflow_id instead.

Switches:
-state_id (required)
Author:
Peter Marklund

Partial Call Graph (max 5 caller/called nodes):
%3 workflow::state::fsm::get_all_info_not_cached workflow::state::fsm::get_all_info_not_cached (private) workflow::state::fsm::get_workflow_id_not_cached workflow::state::fsm::get_workflow_id_not_cached workflow::state::fsm::get_all_info_not_cached->workflow::state::fsm::get_workflow_id_not_cached workflow::state::fsm::get_workflow_id workflow::state::fsm::get_workflow_id (public) workflow::state::fsm::get_workflow_id->workflow::state::fsm::get_workflow_id_not_cached db_string db_string (public) workflow::state::fsm::get_workflow_id_not_cached->db_string

Testcases:
No testcase defined.

workflow::state::fsm::new (public)

 workflow::state::fsm::new -workflow_id workflow_id [ -internal ] \
    [ -short_name short_name ] -pretty_name pretty_name \
    [ -hide_fields hide_fields ] [ -sort_order sort_order ] \
    [ -parent_action parent_action ]

Creates a new state for a certain FSM (Finite State Machine) workflow.

Switches:
-workflow_id (required)
The id of the FSM workflow to add the state to
-internal (optional, boolean)
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.
-short_name (optional)
If you leave blank, the short_name will be generated from pretty_name.
-pretty_name (required)
-hide_fields (optional)
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.
-sort_order (optional)
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.
-parent_action (optional)
Which action with trigger_type 'workflow' does this state belong to.
Returns:
ID of new state.
Author:
Peter Marklund

Partial Call Graph (max 5 caller/called nodes):
%3 workflow::test::workflow_setup workflow::test::workflow_setup (public) workflow::state::fsm::new workflow::state::fsm::new workflow::test::workflow_setup->workflow::state::fsm::new workflow::state::fsm::edit workflow::state::fsm::edit (public) workflow::state::fsm::new->workflow::state::fsm::edit

Testcases:
No testcase defined.

workflow::state::fsm::parse_spec (private)

 workflow::state::fsm::parse_spec -workflow_id workflow_id \
    -short_name short_name -spec spec \
    [ -parent_action_id parent_action_id ]

Parse the spec for an individual state definition.

Switches:
-workflow_id (required)
The id of the workflow to delete.
-short_name (required)
The short_name of the state
-spec (required)
The state spec
-parent_action_id (optional)
Author:
Lars Pind <lars@collaboraid.biz>

Partial Call Graph (max 5 caller/called nodes):
%3 workflow::state::fsm::edit workflow::state::fsm::edit (public) workflow::state::fsm::parse_spec workflow::state::fsm::parse_spec workflow::state::fsm::parse_spec->workflow::state::fsm::edit

Testcases:
No testcase defined.

workflow::state::fsm::pretty_name_unique_p (public)

 workflow::state::fsm::pretty_name_unique_p -workflow_id workflow_id \
    -pretty_name pretty_name [ -parent_action_id parent_action_id ] \
    [ -state_id state_id ]

Check if suggested pretty_name is unique.

Switches:
-workflow_id (required)
-pretty_name (required)
-parent_action_id (optional)
-state_id (optional)
Returns:
1 if unique, 0 if not unique.

Partial Call Graph (max 5 caller/called nodes):
%3 db_string db_string (public) workflow::state::fsm::pretty_name_unique_p workflow::state::fsm::pretty_name_unique_p workflow::state::fsm::pretty_name_unique_p->db_string

Testcases:
No testcase defined.

workflow::state::fsm::update_sort_order (private)

 workflow::state::fsm::update_sort_order -workflow_id workflow_id \
    -sort_order sort_order

Increase the sort_order of other states, if the new sort_order is already taken.

Switches:
-workflow_id (required)
-sort_order (required)

Partial Call Graph (max 5 caller/called nodes):
%3 workflow::state::fsm::edit workflow::state::fsm::edit (public) workflow::state::fsm::update_sort_order workflow::state::fsm::update_sort_order workflow::state::fsm::edit->workflow::state::fsm::update_sort_order db_dml db_dml (public) workflow::state::fsm::update_sort_order->db_dml db_string db_string (public) workflow::state::fsm::update_sort_order->db_string

Testcases:
No testcase defined.
[ hide source ] | [ make this the default ]

Content File Source

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: