workflow.module

  1. workflow
    1. 5
    2. 7
    3. 6.2
    4. 5.2
    5. 6

Support workflows made up of arbitrary states.

Functions & methods

NameDescription
workflow_allowable_transitionsGet allowable transitions for a given workflow state. Typical use:
workflow_comment_insertImplements hook_comment_insert().
workflow_comment_updateImplements hook_comment_update().
workflow_cronImplements hook_cron().
workflow_delete_workflows_by_widGiven a wid, delete the workflow and its stuff.
workflow_delete_workflow_node_by_nidGiven nid, delete associated workflow data.
workflow_delete_workflow_node_by_sidGiven sid, delete associated workflow data.
workflow_delete_workflow_scheduled_transition_by_nidGiven a node, delete transitions for it.
workflow_delete_workflow_states_by_sidGiven a sid, delete the state and all associated data.
workflow_delete_workflow_transitions_by_rolesGiven a sid and target_sid, get the transition. This will be unique.
workflow_delete_workflow_transitions_by_tidGiven a tid, delete the transition.
workflow_delete_workflow_type_map_allDelete all type maps.
workflow_delete_workflow_type_map_by_typeGiven a type, delete the map for that workflow.
workflow_delete_workflow_type_map_by_widGiven a wid, delete the map for that workflow.
workflow_execute_transitionExecute a transition (change state of a node).
workflow_field_choicesGet the states current user can move to for a given node.
workflow_field_extra_fields_alterImplements hook_field_extra_fields_alter().
workflow_form_alterImplements hook_form_alter().
workflow_get_creation_state_by_widReturn the ID of the creation state for this workflow.
workflow_get_workflowsGet all workflows.
workflow_get_workflows_by_widGet a specific workflow, wid is a unique ID.
workflow_get_workflow_node_by_nidGiven a node id, find out what it's current state is. Unique (for now).
workflow_get_workflow_node_by_sidGiven a sid, find out the nodes associated.
workflow_get_workflow_node_history_by_nidGet all recored history for a node id. Since this may return a lot of data, a limit is included to allow for only one result.
workflow_get_workflow_scheduled_transition_by_betweenGiven a timeframe, get all scheduled transistions.
workflow_get_workflow_scheduled_transition_by_nidGiven a node, get all scheduled transitions for it.
workflow_get_workflow_statesGet all states in the system, with options to filter.
workflow_get_workflow_states_by_sidGiven a sid, return a state. Sids are a unique id.
workflow_get_workflow_states_by_wid
workflow_get_workflow_transitions_by_rolesGiven a role string get any transition involved.
workflow_get_workflow_transitions_by_sidGiven a sid, get the transition.
workflow_get_workflow_transitions_by_sid_involvedGiven a sid get any transition involved.
workflow_get_workflow_transitions_by_sid_target_sidGiven a sid and target_sid, get the transition. This will be unique.
workflow_get_workflow_transitions_by_target_sidGiven a target_sid, get the transition.
workflow_get_workflow_transitions_by_tidGiven a tid, get the transition. It is a unique object, only one return.
workflow_get_workflow_type_mapGet all workflow_type_map.
workflow_get_workflow_type_map_by_typeGet workflow_type_map for a type. On no record, FALSE is returned. Currently this is a unique result but requests have been made to allow a node to have multiple workflows. This is trickier than it sounds as a lot of our processing code will have to…
workflow_get_workflow_type_map_by_widGiven a wid, find all node types mapped to it.
workflow_insert_workflow_node_historyGiven data, insert a new history. Always insert.
workflow_insert_workflow_scheduled_transitionInsert a new scheduled transistion. Only one transistion at a time (for now).
workflow_insert_workflow_type_mapGiven information, insert a new workflow_type_map. Returns data by ref. (like node_save).
workflow_menuImplements hook_menu().
workflow_node_current_stateGet the current state of a given node.
workflow_node_deleteImplements hook_node_delete().
workflow_node_formForm builder. Add form widgets for workflow change to $form.
workflow_node_insertImplements hook_node_insert().
workflow_node_loadImplements hook_node_load(). Consider replacing with hook_entity_load().
workflow_node_previous_state
workflow_node_tab_accessMenu access control callback. Determine access to Workflow tab.
workflow_node_updateImplements hook_node_update().
workflow_permissionImplements hook_permission().
workflow_themeImplements hook_theme().
workflow_tokensImplements hook_tokens().
workflow_token_infoImplements hook_token_info().
workflow_transitionValidate target state and either execute a transition immediately or schedule a transition to be executed later by cron.
workflow_transition_allowedSee if a transition is allowed for a given role.
workflow_update_workflowsGiven information, update or insert a new workflow. Returns data by ref. (like node_save).
workflow_update_workflow_nodeGiven data, insert the node association.
workflow_update_workflow_node_history_uidGiven a user id, re-assign history to the new user account. Called by user_delete().
workflow_update_workflow_node_stampGiven nid, update the new stamp. This probably can be refactored. Called by workflow_execute_transition().
workflow_update_workflow_node_uidGiven data, update the new user account. Called by user_delete().
workflow_update_workflow_statesGiven data, update or insert into workflow_states.
workflow_update_workflow_transitionsGiven data, insert or update a workflow_transitions.
workflow_update_workflow_transitions_rolesGiven a tid and new roles, update them. TODO - this should be refactored out, and the update made a full actual update.
workflow_user_deleteImplements hook_user_delete().

Constants

NameDescription
WORKFLOW_CREATION
WORKFLOW_CREATION_DEFAULT_WEIGHT
WORKFLOW_DELETION
View source
<?php
// $Id$
/**
 * @file
 * Support workflows made up of arbitrary states.
 */

/**
 * Implements hook_workflow(). Provided as an example only.
 *
 * @param $op
 *   The current workflow operation: 'transition pre' or 'transition post'.
 * @param $old_state
 *   The state ID of the current state.
 * @param  $new_state
 *   The state ID of the new state.
 * @param $node
 *   The node whose workflow state is changing.
 *
function workflow_workflow($op, $old_state, $new_state, $node) {
  switch ($op) {
    case 'transition pre':
      // The workflow module does nothing during this operation.
      // But your module's Implements the workflow hook could
      // return FALSE here and veto the transition.
      break;
    case 'transition post':
      break;
    case 'transition delete':
      break;
  }
}
//*/

define('WORKFLOW_CREATION', 1);
define('WORKFLOW_CREATION_DEFAULT_WEIGHT', -50);
define('WORKFLOW_DELETION', 0);

// Include CTools integration functions and callbacks.
include('workflow.ctools.inc');

/**
 * Implements hook_permission().
 */
function workflow_permission() {
  return array(
    'schedule workflow transitions' => array(
      'title' => t('Schedule workflow transitions'),
      'description' => t('Schedule workflow transitions.'),
    ),
  );
}

/**
 * Implements hook_menu().
 */
function workflow_menu() {
  $items['node/%node/workflow'] = array(
    'title' => 'Workflow',
    'type' => MENU_LOCAL_TASK,
    'file' => 'workflow.pages.inc',
    'access callback' => 'workflow_node_tab_access',
    'access arguments' => array(1),
    'page callback' => 'workflow_tab_page',
    'page arguments' => array(1),
    'weight' => 2,
  );
  return $items;
}

/**
 * Menu access control callback. Determine access to Workflow tab.
 */
function workflow_node_tab_access($node = NULL) {
  global $user;
  if ($workflow = workflow_get_workflow_type_map_by_type($node->type)) {
    if($workflow = workflow_get_workflows_by_wid($workflow->wid)) {
      $roles = array_keys($user->roles);
      if ($node->uid == $user->uid) {
        $roles = array_merge(array('author'), $roles);
      }
      $allowed_roles = $workflow->tab_roles ? explode(',', $workflow->tab_roles) : array();
      if (user_access('administer nodes') || array_intersect($roles, $allowed_roles)) {
        return TRUE;
      }
      else {
        return FALSE;
      }
    }
  }
  return FALSE;
}

/**
 * Implements hook_theme().
 */
function workflow_theme() {
  return array(
    'workflow_history_table_row' => array(
      'variables' => array(
        'history' => NULL,
        'old_state_name' => NULL,
        'state_name' => NULL
      ),
    ),
    'workflow_history_table' => array(
      'variables' => array(
        'rows' => array(),
        'footer' => NULL,
      ),
    ),
    'workflow_current_state' => array(
      'variables' => array(
        'state_name' => NULL,
      ),
    ),
    'workflow_deleted_state' => array(
      'variables' => array(
        'state_name' => NULL,
      ),
    ),
  );
}

/**
 * Implements hook_cron().
 */
function workflow_cron() {
  $clear_cache = FALSE;
  // If the time now is greater than the time to execute a
  // transition, do it.
  foreach (workflow_get_workflow_scheduled_transition_by_between() as $row) {
    $node = node_load($row->nid);
    // Make sure transition is still valid; i.e., the node is
    // still in the state it was when the transition was scheduled.
    if ($node->workflow == $row->old_sid) {
      // Do transition.
      workflow_execute_transition($node, $row->sid, $row->comment, TRUE);
      watchdog('content', '%type: scheduled transition of %title.',
        array('%type' => t($node->type), '%title' => $node->title), WATCHDOG_NOTICE, l(t('view'), 'node/' . $node->nid));
      $clear_cache = TRUE;
    }
    else {
      // Node is not in the same state it was when the transition
      // was scheduled. Defer to the node's current state and
      // abandon the scheduled transition.
      workflow_delete_workflow_scheduled_transition_by_nid($node->nid);
    }
  }
  if ($clear_cache) {
    // Clear the cache so that if the transition resulted in a node
    // being published, the anonymous user can see it.
    cache_clear_all();
  }
}

/**
 * Implements hook_field_extra_fields_alter().
 */
function workflow_field_extra_fields_alter(&$info) {
  foreach (node_type_get_types() as $bundle) {
    if (in_array('node', variable_get('workflow_' . $bundle->type, array('node')))) {
      $info['node'][$bundle->type]['form']['workflow'] = array(
        'label' => t('Workflow'),
        'description' => t('Workflow module form'),
        'weight' => 10,
      );
    }
  }
  return $info;
}

/**
 * Implements hook_user_delete().
 */
function workflow_user_delete($account) {
  // Update tables for deleted account, move account to user 0 (anon.)
  // ALERT: This may cause previously non-anon posts to suddenly be accessible to anon.
  workflow_update_workflow_node_uid($account->uid, 0);
  workflow_update_workflow_node_history_uid($account->uid, 0);
}

/**
 * Implements hook_tokens().
 */
function workflow_tokens($type, $tokens, array $data = array(), array $options = array()) {
  $values = array();
  switch ($type) {
    case 'node':
    case 'workflow':
      $node = $data['node'];
      if ($workflow = workflow_get_workflow_type_map_by_type($node->type)) {
        // TODO If we ever move Workflow to allow M:M content types to workflows, this will need updating.
        if ($workflow = workflow_get_workflows_by_wid($workflow->wid)) {
          $wid = $workflow->wid;
          $values['workflow-name'] = $workflow->name;
          $last_history = workflow_get_workflow_node_history_by_nid($node->nid, 1);
          if (isset($node->workflow) && !isset($node->workflow_stamp)) {
            // The node is being submitted but the form data has not been saved to the database yet,
            // so we set the token values from the workflow form fields.
            $sid = $node->workflow;
            $old_sid = isset($last_history->sid) ? $last_history->sid : workflow_get_creation_state_by_wid($workflow->wid);
            $sid = workflow_get_workflow_states_by_sid($sid);
            $old_sid = workflow_get_workflow_states_by_sid($old_sid);
            $date = REQUEST_TIME;
            $user_name = $node->uid ? $node->name : variable_get('anonymous', 'Anonymous');
            $uid = $node->uid;
            $mail = $node->uid ? $node->user_mail : '';
            $comment = isset($node->workflow_comment) ? $node->workflow_comment : '';
          }
          elseif (!isset($node->workflow) && empty($last_history->sid)) {
            // If the state is not specified and the node has no workflow history,
            // the node is being inserted and will soon be transitioned to the first valid state.
            // We find this state using the same logic as workflow_nodeapi().
            $choices = workflow_field_choices($node);
            $keys = array_keys($choices);
            $sid = array_shift($keys);
            $old_sid = workflow_get_creation_state_by_wid($workflow->wid);
            $sid = workflow_get_workflow_states_by_sid($sid);
            $old_sid = workflow_get_workflow_states_by_sid($old_sid);
            $date = REQUEST_TIME;
            $user_name = $node->uid ? $node->name : variable_get('anonymous', 'Anonymous');
            $uid = $node->uid;
            $mail = $node->uid ? $node->user_mail : '';
            $comment = isset($node->workflow_comment) ? $node->workflow_comment : '';
          }
          else {
            // Default to the most recent transition data in the workflow history table.
            $account = user_load($last_history->uid);
            $sid = $last_history->sid;
            $old_sid = $last_history->old_sid;
            $sid = workflow_get_workflow_states_by_sid($sid);
            $old_sid = workflow_get_workflow_states_by_sid($old_sid);
            $date = $last_history->stamp;
            $user_name = $account->uid ? $account->name : variable_get('anonymous', 'Anonymous');
            $uid = $account->uid;
            $mail = $account->uid ? $account->mail : '';
            $comment = $last_history->comment;
          }
          $values['workflow-current-state-name']                =  $sid->name;
          $values['workflow-old-state-name']                    =  $old_sid->name;
          $values['workflow-current-state-date-iso']            =  date('Ymdhis', $date);
          $values['workflow-current-state-date-tstamp']         =  $date;
          $values['workflow-current-state-date-formatted']      =  date('M d, Y h:i:s', $date);
          $values['workflow-current-state-updating-user-name']  = check_plain($user_name);
          $values['workflow-current-state-updating-user-uid']   = $uid;
          $values['workflow-current-state-updating-user-mail']  = check_plain($mail);
          $values['workflow-current-state-log-entry']           = filter_xss($comment, array('a', 'em', 'strong'));
        }
      }
    break;
  }
  return $values;
}

/**
 * Implements hook_token_info().
 */
function workflow_token_info() {
  $type = array(
    'name' => t('Workflows'),
    'description' => t('Tokens related to workflows.'),
    'needs-data' => 'node',
  );
  $tokens['workflow-name'] = array(
    'name' => t('Workflow name'),
    'description' => t('Name of workflow appied to this node'),
  );
  $tokens['workflow-current-state-name'] = array(
    'name' => t('Current state name'),
    'description' => t('Current state of content'),
  );
  $tokens['workflow-old-state-name'] = array(
    'name' => t('Old state name'),
    'description' => t('Old state of content'),
  );
  $tokens['workflow-current-state-date-iso'] = array(
    'name' => t('Current state date (ISO)'),
    'description' => t('Date of last state change (ISO)'),
  );
  $tokens['workflow-current-state-date-tstamp'] = array(
    'name' => t('Current state date (timestamp)'),
    'description' => t('Date of last state change (timestamp)'),
  );
  $tokens['workflow-current-state-date-formatted'] = array(
    'name' => t('Current state date (formated - M d, Y h:i:s)'),
    'description' => t('Date of last state change (formated - M d, Y h:i:s)'),
  );
  $tokens['workflow-current-state-updating-user-name'] = array(
    'name' => t('Username of last state changer'),
    'description' => t('Username of last state changer'),
  );
  $tokens['workflow-current-state-updating-user-uid'] = array(
    'name' => t('Uid of last state changer'),
    'description' => t('uid of last state changer'),
  );
  $tokens['workflow-current-state-updating-user-mail'] = array(
    'name' => t('Email of last state changer'),
    'description' => t('email of last state changer'),
  );
  $tokens['workflow-current-state-log-entry'] =array(
    'name' => t('Last workflow comment log'),
    'description' => t('Last workflow comment log'),
    'type' => 'workflow',
  );
  return array(
    'types' => array('node' => $type),
    'tokens' => array('node' => $tokens),
  );
}

/**
 * Node specific functions, remants of nodeapi.
 */

/**
 * Implements hook_node_load(). Consider replacing with hook_entity_load().
 */
function workflow_node_load($nodes, $types) {
  foreach ($nodes as $node) {
    // ALERT: With the upgrade to Drupal 7, values stored on the node as _workflow_x
    // have been standardized to workflow_x, dropping the initial underscore.
    // Module maintainers integrating with workflow should keep that in mind.
    $node->workflow = workflow_node_current_state($node);
    // Add scheduling information.
    // Technically you could have more than one scheduled, but this will only add the soonest one.
    foreach (workflow_get_workflow_scheduled_transition_by_nid($node->nid) as $row) {
      $node->workflow_scheduled_sid = $row->sid;
      $node->workflow_scheduled_timestamp = $row->scheduled;
      $node->workflow_scheduled_comment = $row->comment;
      break;
    }
  }
}

/**
 * Implements hook_node_insert().
 */
function workflow_node_insert($node) {
  // Skip if there are no workflows.
  if ($workflow = workflow_get_workflow_type_map_by_type($node->type)) {
    // If the state is not specified, use first valid state.
    // For example, a new node must move from (creation) to some
    // initial state.
    if (empty($node->workflow)) {
      $choices = workflow_field_choices($node);
      $keys = array_keys($choices);
      $sid = array_shift($keys);
    }
    if (!isset($sid)) {
      $sid = $node->workflow;
    }
    // And make the transition.
    workflow_transition($node, $sid);
  }
}

/**
 * Implements hook_node_update().
 */
function workflow_node_update($node) {
  // Skip if there are no workflows.
  if ($workflow = workflow_get_workflow_type_map_by_type($node->type)) {
    // Get new state from value of workflow form field, stored in $node->workflow.
    if (!isset($sid)) {
      $sid = $node->workflow;
    }
    workflow_transition($node, $sid);
  }
}

/**
 * Implements hook_node_delete().
 */
function workflow_node_delete($node) {
  $node->workflow_stamp = REQUEST_TIME;
  // Delete the association of node to state.
  workflow_delete_workflow_node_by_nid($node->nid);
  if (!empty($node->_worfklow)) {
    global $user;
    $data = array(
      'nid' => $node->nid,
      'old_sid' => $node->workflow,
      'sid' => WORKFLOW_DELETION,
      'uid' => $user->uid,
      'stamp' => $node->workflow_stamp,
      'comment' => t('Node deleted'),
    );
    workflow_insert_workflow_node_history($data);
  }
  // Delete any scheduled transitions for this node.
  workflow_delete_workflow_scheduled_transition_by_nid($node->nid);
}

/**
 * Implements hook_comment_insert().
 */
function workflow_comment_insert($comment) {
  workflow_comment_update($comment);
}

/**
 * Implements hook_comment_update().
 */
function workflow_comment_update($comment) {
  if (isset($comment->workflow)) {
    $node = node_load($comment->nid);
    $sid = $comment->workflow;
    $node->workflow_comment = $comment->workflow_comment;
    if (isset($comment->workflow_scheduled)) {
      $node->workflow_scheduled = $comment->workflow_scheduled;
    }
    if (isset($comment->workflow_scheduled_date)) {
      $node->workflow_scheduled_date = $comment->workflow_scheduled_date;
    }
    if (isset($comment->workflow_scheduled_hour)) {
      $node->workflow_scheduled_hour = $comment->workflow_scheduled_hour;
    }
    workflow_transition($node, $sid);
  }
}

/**
 * Business related functions, the API.
 */
 
/**
 * Validate target state and either execute a transition immediately or schedule
 * a transition to be executed later by cron.
 *
 * @param $node
 * @param $sid
 *   An integer; the target state ID.
 */
function workflow_transition($node, $sid) {
  // Make sure new state is a valid choice.
  if (array_key_exists($sid, workflow_field_choices($node))) {
    $node->workflow_scheduled = isset($node->workflow_scheduled) ? $node->workflow_scheduled : FALSE;
    if (!$node->workflow_scheduled) {
      // It's an immediate change. Do the transition.
      workflow_execute_transition($node, $sid, isset($node->workflow_comment) ? $node->workflow_comment : NULL);
    }
    else {
      // Schedule the the time to change the state.
      $old_sid = workflow_node_current_state($node);
      if ($node->workflow_scheduled_date['day'] < 10) {
        $node->workflow_scheduled_date['day'] = '0' .
        $node->workflow_scheduled_date['day'];
      }
      if ($node->workflow_scheduled_date['month'] < 10) {
        $node->workflow_scheduled_date['month'] = '0' .
        $node->workflow_scheduled_date['month'];
      }
      if (!isset($node->workflow_scheduled_hour)) {
        $node->workflow_scheduled_hour = '00:00';
      }
      $scheduled = $node->workflow_scheduled_date['year'] . $node->workflow_scheduled_date['month'] . $node->workflow_scheduled_date['day'] . ' ' . $node->workflow_scheduled_hour . 'Z';
      if ($scheduled = strtotime($scheduled)) {
        // Adjust for user and site timezone settings.
        global $user;
        if (variable_get('configurable_timezones', 1) && $user->uid && drupal_strlen($user->timezone)) {
          $timezone = $user->timezone;
        }
        else {
          $timezone = variable_get('date_default_timezone', 0);
        }
        $scheduled = $scheduled - $timezone;
        // Clear previous entries and insert.
        $data = array(
          'nid' => $node->nid,
          'old_sid' => $old_sid,
          'sid' => $sid,
          'scheduled' => $scheduled,
          'comment' => $node->workflow_comment,
        );
        workflow_insert_workflow_scheduled_transition($data);
        // Get name of state.
        if ($state = workflow_get_workflow_states_by_sid($sid)) {
          watchdog('workflow', '@node_title scheduled for state change to %state_name on !scheduled_date',
            array('@node_title' => $node->title, '%state_name' => $state->state, '!scheduled_date' => format_date($scheduled)),
            WATCHDOG_NOTICE, l('view', 'node/' . $node->nid . '/workflow'));
          drupal_set_message(t('@node_title is scheduled for state change to %state_name on !scheduled_date',
            array('@node_title' => $node->title, '%state_name' => $state->state, '!scheduled_date' => format_date($scheduled))));
        }
      }
    }
  }
}

/**
 * Form builder. Add form widgets for workflow change to $form.
 *
 * This builder is factored out of workflow_form_alter() because
 * it is also used on the Workflow tab.
 *
 * @param $form
 *   An existing form definition array.
 * @param $name
 *   The name of the workflow.
 * @param $current
 *   The state ID of the current state, used as the default value.
 * @param $choices
 *   An array of possible target states.
 */
function workflow_node_form(&$form, $form_state, $title, $name, $current, $choices, $timestamp = NULL, $comment = NULL) {
  // No sense displaying choices if there is only one choice.
  if (sizeof($choices) == 1) {
    $form['workflow'][$name] = array(
      '#type' => 'hidden',
      '#value' => $current
    );
  }
  else {
    $form['workflow'][$name] = array(
      '#type' => 'radios',
      '#title' => $form['#wf']->options['name_as_title'] ? t('@title', array('@title' => $title)) : '',
      '#options' => $choices,
      '#name' => $name,
      '#parents' => array('workflow'),
      '#default_value' => $current
    );
    // Display scheduling form only if a node is being edited and user has
    // permission. State change cannot be scheduled at node creation because
    // that leaves the node in the (creation) state.
    if (!(arg(0) == 'node' && arg(1) == 'add') && user_access('schedule workflow transitions')) {
      $scheduled = $timestamp ? 1 : 0;
      $timestamp = $scheduled ? $timestamp : REQUEST_TIME;
      $form['workflow']['workflow_scheduled'] = array(
        '#type' => 'radios',
        '#title' => t('Schedule'),
        '#options' => array(
          t('Immediately'),
          t('Schedule for state change at:'),
        ),
        '#default_value' => isset($form_state['values']['workflow_scheduled']) ? $form_state['values']['workflow_scheduled'] : $scheduled,
      );
      $form['workflow']['workflow_scheduled_date'] = array(
        '#type' => 'date',
        '#default_value' => array(
          'day'   => isset($form_state['values']['workflow_scheduled_date']['day']) ? $form_state['values']['workflow_scheduled_date']['day'] : format_date($timestamp, 'custom', 'j'),
          'month' => isset($form_state['values']['workflow_scheduled_date']['month']) ? $form_state['values']['workflow_scheduled_date']['month'] :format_date($timestamp, 'custom', 'n'),
          'year'  => isset($form_state['values']['workflow_scheduled_date']['year']) ? $form_state['values']['workflow_scheduled_date']['year'] : format_date($timestamp, 'custom', 'Y')
        ),
      );
      $hours = format_date($timestamp, 'custom', 'H:i');
      $form['workflow']['workflow_scheduled_hour'] = array(
        '#type' => 'textfield',
        '#description' => t('Please enter a time in 24 hour (eg. HH:MM) format. If no time is included, the default will be midnight on the specified date. The current time is: ') . format_date(REQUEST_TIME),
        '#default_value' => $scheduled ? (isset($form_state['values']['workflow_scheduled_hour']) ? $form_state['values']['workflow_scheduled_hour'] : $hours) : NULL,
      );
    }
    if (isset($form['#tab'])) {
      $determiner = 'comment_log_tab';
    }
    else {
      $determiner = 'comment_log_node';
    }
    $form['workflow']['workflow_comment'] = array(
      '#type' => $form['#wf']->options[$determiner] ? 'textarea': 'hidden',
      '#title' => t('Comment'),
      '#description' => t('A comment to put in the workflow log.'),
      '#default_value' => $comment,
      '#rows' => 2,
    );
  }
}

/**
 * Implements hook_form_alter().
 *
 * @param object &$node
 * @return array
 */
function workflow_form_alter(&$form, &$form_state, $form_id) {
  // Ignore all forms except comment forms and node editing forms.
  if ((isset($form['#node']) && $form_id == 'comment_node_' . $form['#node']->type . '_form')
    || (isset($form['#node']->type) && isset($form['#node']) && $form['#node']->type . '_node_form' == $form_id)) {
    // Skip if there are no workflows.
    if (isset($form['#node'])) {
      $node = $form['#node'];
      // Abort if user does not want to display workflow form on node editing form.
      if (!in_array('node', variable_get('workflow_' . $form['#node']->type, array('node')))) {
        return;
      }
    }
    else {
      $node = node_load($form['nid']['#value']);
      $type = $node->type;
      // Abort if user does not want to display workflow form on node editing form.
      if (!in_array('comment', variable_get('workflow_' . $type, array('node')))) {
        return;
      }
    }
    if ($workflow = workflow_get_workflow_type_map_by_type($node->type)) {
      $choices = workflow_field_choices($node);
      $workflow = workflow_get_workflows_by_wid($workflow->wid);
      $states = workflow_get_workflow_states_by_wid($workflow->wid);
      // If this is a preview, the current state should come from
      // the form values, not the node, as the user may have changed
      // the state.
      $current = isset($form_state['values']['workflow']) ? $form_state['values']['workflow'] : workflow_node_current_state($node);
      $min = 2; // Our current status, and our new status.
      foreach ($states as $state) {
        if ($state->sid == $current) {
          $min = $state->status == t('(creation)') ? 1 : 2;
        }
      }
      // Stop if user has no new target state(s) to choose.
      if (count($choices) < $min) {
        return;
      }
      $form['#wf'] = $workflow;
      $name = check_plain($workflow->name);
      // If the current node state is not one of the choices, pick first choice.
      // We know all states in $choices are states that user has permission to
      // go to because workflow_field_choices() has already checked that.
      if (!isset($choices[$current])) {
        $array = array_keys($choices);
        $current = $array[0];
      }
      if (sizeof($choices) > 1) {
        $form['workflow'] = array(
          '#type' => 'fieldset',
          '#title' => t('@name', array('@name' => $name)),
          '#collapsible' => TRUE,
          '#collapsed' => FALSE,
          '#weight' => 10,
        );
      }
      $timestamp = NULL;
      $comment = '';
      // See if scheduling information is present.
      if (isset($node->workflow_scheduled_timestamp) && isset($node->workflow_scheduled_sid)) {
        // The default value should be the upcoming sid.
        $current = $node->workflow_scheduled_sid;
        $timestamp = $node->workflow_scheduled_timestamp;
        $comment = $node->workflow_scheduled_comment;
      }
      if (isset($form_state['values']['workflow_comment'])) {
        $comment = $form_state['values']['workflow_comment'];
      }
      workflow_node_form($form, $form_state, $name, $name, $current, $choices, $timestamp, $comment);
    }
  }
}

/**
 * Execute a transition (change state of a node).
 *
 * @param $node
 * @param $sid
 *   Target state ID.
 * @param $comment
 *   A comment for the node's workflow history.
 * @param $force
 *   If set to TRUE, workflow permissions will be ignored.
 *
 * @return int
 *   ID of new state.
 */
function workflow_execute_transition($node, $sid, $comment = NULL, $force = FALSE) {
  global $user;
  $old_sid = workflow_node_current_state($node);
  if ($old_sid == $sid) {
    // Stop if not going to a different state.
    // Write comment into history though.
    if ($comment && empty($node->workflow_scheduled_comment)) {
      $node->workflow_stamp = REQUEST_TIME;
      workflow_update_workflow_node_stamp($node->nid, $node->workflow_stamp);
      $result = module_invoke_all('workflow', 'transition pre', $old_sid, $sid, $node);
      $data = array(
        'nid' => $node->nid,
        'old_sid' => $node->workflow,
        'sid' => $sid,
        'uid' => $user->uid,
        'stamp' => $node->workflow_stamp,
        'comment' => $comment,
      );
      workflow_insert_workflow_node_history($data);
      unset($node->workflow_comment);
      $result = module_invoke_all('workflow', 'transition post', $old_sid, $sid, $node);
    }
    return;
  }
  $transition = workflow_get_workflow_transitions_by_sid_target_sid($old_sid, $sid);
  if (!$transition && !$force) {
      watchdog('workflow', 'Attempt to go to nonexistent transition (from %old to %new)', array('%old' => $old_sid, '%new' => $sid, WATCHDOG_ERROR));
      return;
  }
  // Make sure this transition is valid and allowed for the current user.
  // Check allowability of state change if user is not superuser (might be cron).
  if (($user->uid != 1) && !$force) {
    if (!workflow_transition_allowed($transition->tid, array_merge(array_keys($user->roles), array('author')))) {
      watchdog('workflow', 'User %user not allowed to go from state %old to %new',
        array('%user' => $user->name, '%old' => $old_sid, '%new' => $sid, WATCHDOG_NOTICE));
      return;
    }
  }
  // Invoke a callback indicating a transition is about to occur. Modules
  // may veto the transition by returning FALSE.
  $result = module_invoke_all('workflow', 'transition pre', $old_sid, $sid, $node);
  // Stop if a module says so.
  if (in_array(FALSE, $result)) {
    watchdog('workflow', 'Transition vetoed by module.');
    return;
  }
  // If the node does not have an existing $node->workflow property, save the $old_sid there so it can be logged.
  if (!isset($node->workflow)) {
    $node->workflow = $old_sid;
  }
  // Change the state.
  global $user;
  $data = array(
    'nid' => $node->nid,
    'sid' => $sid,
    'uid' => $user->uid,
    'stamp' => REQUEST_TIME,
  );
  // workflow_update_workflow_node places a history comment as well.
  workflow_update_workflow_node($data, $old_sid, $comment);
  $node->workflow = $sid;
  // Register state change with watchdog.
  $type = node_type_get_name($node->type);
  if ($state = workflow_get_workflow_states_by_sid($sid)) {
    watchdog('workflow', 'State of @type %node_title set to %state_name',
      array('@type' => $type, '%node_title' => $node->title, '%state_name' => $state->state), WATCHDOG_NOTICE, l('view', 'node/' . $node->nid));
  }
  // Notify modules that transition has occurred. Action triggers should take place in response to this callback, not the previous one.
  module_invoke_all('workflow', 'transition post', $old_sid, $sid, $node);
  // Clear any references in the scheduled listing.
  workflow_delete_workflow_scheduled_transition_by_nid($node->nid);
  return $sid;
}


/**
 * Get the states current user can move to for a given node.
 *
 * @param object $node
 *   The node to check.
 * @return
 *   Array of transitions.
 */
function workflow_field_choices($node) {
  global $user;
  $choices = array();
  if ($workflow = workflow_get_workflow_type_map_by_type($node->type)) {
    $roles = array_keys($user->roles);
    $current_sid = workflow_node_current_state($node);
    // If user is node author or this is a new page, give the authorship role.
    if (($user->uid == $node->uid && $node->uid > 0) || (arg(0) == 'node' && arg(1) == 'add')) {
      $roles += array('author' => 'author');
    }
    if ($user->uid == 1) {
      // Superuser is special.
      $roles = 'ALL';
    }
    $current_sid == workflow_get_creation_state_by_wid($workflow->wid);
    // workflow_allowable_transitions() does not return the entire transition row. Would like it to, but doesn't.
    // Instead it returns just the allowable data as:
    // [tid] => 1 [state_id] => 1 [state_name] => (creation) [state_weight] => -50
    $transitions = workflow_allowable_transitions($current_sid, 'to', $roles);
    // Include current state if it is not the (creation) state.
    foreach($transitions as $transition) {
      if ($transition->state_name != t('(creation)')) {
        $choices[$transition->state_id] = check_plain(t($transition->state_name));
      }
    }
  }
  return $choices;
}

/**
 * Get the current state of a given node.
 *
 * @param $node
 *   The node to check.
 * @return
 *   The ID of the current state.
 */
function workflow_node_current_state($node) {
  $sid = FALSE;
  $state = FALSE;
  // There is no nid when creating a node.
  if (!empty($node->nid)) {
    $state = workflow_get_workflow_node_by_nid($node->nid);
    if($state){  
      $sid = $state->sid;
    }
  }
  if (!$state && !empty($node->type)) {
    // No current state. Use creation state.
    if( $workflow = workflow_get_workflow_type_map_by_type($node->type)) {
      $sid = workflow_get_creation_state_by_wid($workflow->wid);
    }
  }
  return $sid;
}

function workflow_node_previous_state($node) {
  $sid = FALSE;
  // There is no nid when creating a node.
  if (!empty($node->nid)) {
    $sids = array();
    $sid = -1;
    $last_history = workflow_get_workflow_node_history_by_nid($node->nid, 1);
    $sid = $last_history->sid;
  }
  if (!$sid && !empty($node->type)) {
    // No current state. Use creation state.
    $sid = FALSE;
    if ($workflow = workflow_get_workflow_type_map_by_type($node->type)) {
      $sid = workflow_get_creation_state_by_wid($workflow->wid);
    }
  }
  return $sid;
}

/**
 * See if a transition is allowed for a given role.
 *
 * @param int $tid
 * @param mixed $role
 *   A single role (int or string 'author') or array of roles.
 * @return
 *   TRUE if the role is allowed to do the transition.
 */
function workflow_transition_allowed($tid, $role = NULL) {
  $transition = workflow_get_workflow_transitions_by_tid($tid);
  $allowed = $transition->roles;
  $allowed = explode(',', $allowed);
  if ($role) {
    if (!is_array($role)) {
      $role = array($role);
    }
    return array_intersect($role, $allowed) ==  TRUE;
  }
}

/**
 * Return the ID of the creation state for this workflow.
 *
 * @param $wid
 *   The ID of the workflow.
 */
function workflow_get_creation_state_by_wid($wid) {
  $options = array(':sysid' => WORKFLOW_CREATION);
  $result = workflow_get_workflow_states_by_wid($wid, $options);
  return isset($result[0]->sid) ? $result[0]->sid : 0;
}

/**
 * DB functions. All SQL in workflow.module should be put into its own function and placed here.
 * This encourages good separation of code and reuse of SQL statements. It *also* makes it easy to
 * make schema updates and changes without rummaging through every single inch of code looking for SQL.
 * Sure it's a little type A, granted. But it's useful in the long run.
 */

//TODO: Go through all of these and replace * with specific field names - indexing = effeciency.

/**
 * Functions related to table workflows.
 */

/**
 * Get all workflows.
 */ 
function workflow_get_workflows() {
  $results = db_query('SELECT * FROM {workflows}');
  $workflows = $results->fetchAll();
  foreach ($workflows as $index => $workflow) {
    $workflows[$index]->options = unserialize($workflows[$index]->options);
  }
  return $workflows;
}

/**
 * Get a specific workflow, wid is a unique ID.
 */
function workflow_get_workflows_by_wid($wid) {
  $results = db_query('SELECT * FROM {workflows} WHERE wid = :wid', array(':wid' => $wid));
  if ($workflow = $results->fetchObject()) {
    $workflow->options = unserialize($workflow->options);
    return $workflow;
  }
  return FALSE;
}

/**
 * Given a wid, delete the workflow and its stuff.
 */
function workflow_delete_workflows_by_wid($wid) {
  // Notify any interested modules before we delete, in case there's data needed.
  module_invoke_all('workflow', 'workflow delete', $wid, NULL, NULL);
  // Delete associated state (also deletes any associated transitions).
  foreach (workflow_get_workflow_states_by_wid($wid) as $data) {
    workflow_delete_workflow_states_by_sid($data->sid);
  }
  // Delete type map.
  workflow_delete_workflow_type_map_by_wid($wid);
  // Delete the workflow.
  db_delete('workflows')->condition('wid', $wid)->execute();
  // Workflow deletion affects tabs (local tasks), so force menu rebuild.
  cache_clear_all('*', 'cache_menu', TRUE);
  menu_rebuild();
}

/**
 * Given information, update or insert a new workflow. Returns data by ref. (like node_save).
 */
function workflow_update_workflows(&$data, $create_creation_state = TRUE) {
  $data = (object) $data;
  if (isset($data->tab_roles) && is_array($data->tab_roles)) {
    $data->tab_roles = implode(',', $data->tab_roles);
  }
  if (isset($data->wid) && count(workflow_get_workflows_by_wid($data->wid)) > 0) {
    drupal_write_record('workflows', $data, 'wid');
  }
  else {
    drupal_write_record('workflows', $data);
    if ($create_creation_state) {
      $state_data = array(
        'wid' => $data->wid,
        'state' => t('(creation)'),
        'sysid' => WORKFLOW_CREATION,
        'weight' => WORKFLOW_CREATION_DEFAULT_WEIGHT,
        );
      workflow_update_workflow_states($state_data);
      // TODO consider adding state data to return here as part of workflow data structure.
      // That way we could past sructs and transitions around as a data boject as a whole.
      // Might make clone easier, but it might be a little hefty for our needs?
    }
  }
  // Workflow name change affects tabs (local tasks), so force menu rebuild.
  menu_rebuild();
}

/**
 * Functions related to table workflow_type_map.
 */
 
/**
 * Get all workflow_type_map.
 */
function workflow_get_workflow_type_map() {
  $results = db_query('SELECT * FROM {workflow_type_map}');
  return $results->fetchAll();
}

/**
 * Get workflow_type_map for a type. On no record, FALSE is returned.
 * Currently this is a unique result but requests have been made to allow a node to have multiple
 * workflows. This is trickier than it sounds as a lot of our processing code will have to be
 * tweaked to account for multiple results.
 * ALERT: If a node type is *not* mapped to a workflow it will be listed as wid 0.
 * Hence, we filter out the non-mapped results.
 */
function workflow_get_workflow_type_map_by_type($type) {
  $results = db_query('SELECT * FROM {workflow_type_map} WHERE type = :type AND wid != 0', array(':type' => $type));
  return $results->fetchObject();
}

/**
 * Given a wid, find all node types mapped to it.
 */
function workflow_get_workflow_type_map_by_wid($wid) {
  $results = db_query('SELECT * FROM {workflow_type_map} WHERE wid = :wid', array(':wid' => $wid));
  return $results->fetchAll();
}

/**
 * Delete all type maps.
 */
function workflow_delete_workflow_type_map_all() {
  return db_delete('workflow_type_map')->execute();
}

/**
 * Given a wid, delete the map for that workflow.
 */
function workflow_delete_workflow_type_map_by_wid($wid) {
  return db_delete('workflow_type_map')->condition('wid', $wid)->execute();
}

/**
 * Given a type, delete the map for that workflow.
 */
function workflow_delete_workflow_type_map_by_type($type) {
  return db_delete('workflow_type_map')->condition('type', $type)->execute();
}

/**
 * Given information, insert a new workflow_type_map. Returns data by ref. (like node_save).
 */
function workflow_insert_workflow_type_map(&$data) {
  $data = (object) $data;
  // Be sure we have a clean insert. There should never be more than one map for a type.
  if (isset($data->type)) {
    workflow_delete_workflow_type_map_by_type($data->type);
  }
  drupal_write_record('workflow_type_map', $data);
}

/**
 * Functions related to table workflow_transitions.
 */
 
/**
 * Given a tid, get the transition. It is a unique object, only one return.
 */
function workflow_get_workflow_transitions_by_tid($tid) {
  $results = db_query('SELECT * FROM {workflow_transitions} WHERE tid = :tid', array(':tid' => $tid));
  return $results->fetchObject();
}

/**
 * Given a sid, get the transition.
 */
function workflow_get_workflow_transitions_by_sid($sid) {
  $results = db_query('SELECT * FROM {workflow_transitions} WHERE sid = :sid', array(':sid' => $sid));
  return $results->fetchAll();
}

/**
 * Given a target_sid, get the transition.
 */
function workflow_get_workflow_transitions_by_target_sid($target_sid) {
  $results = db_query('SELECT * FROM {workflow_transitions} WHERE target_sid = :target_sid', array(':target_sid' => $target_sid));
  return $results->fetchAll();
}

/**
 * Given a sid get any transition involved.
 */
function workflow_get_workflow_transitions_by_sid_involved($sid) {
  $results = db_query('SELECT * FROM {workflow_transitions} WHERE sid = :sid OR target_sid = :sid', array(':sid' => $sid));
  return $results->fetchAll();
}

/**
 * Given a role string get any transition involved.
 */
function workflow_get_workflow_transitions_by_roles($roles) {
  $results = db_query('SELECT * FROM {workflow_transitions} WHERE roles LIKE :roles', array(':roles' => $roles));
  return $results->fetchAll();
}

/**
 * Given a sid and target_sid, get the transition. This will be unique.
 */
function workflow_get_workflow_transitions_by_sid_target_sid($sid, $target_sid) {
  $results = db_query('SELECT * FROM {workflow_transitions} WHERE sid = :sid AND target_sid = :target_sid', array(':sid' => $sid, ':target_sid' => $target_sid));
  return $results->fetchObject();
}

/**
 * Given a tid, delete the transition.
 */
function workflow_delete_workflow_transitions_by_tid($tid) {
  // Notify any interested modules before we delete, in case there's data needed.
  module_invoke_all('workflow', 'transition delete', $tid, NULL, NULL);
  return db_delete('workflow_transitions')->condition('tid', $tid)->execute();
}

/**
 * Given a sid and target_sid, get the transition. This will be unique.
 */
function workflow_delete_workflow_transitions_by_roles($roles) {
  // NOTE: This allows us to send notifications out.
  foreach (workflow_get_workflow_transitions_by_roles($roles) as $transistion) {
    workflow_delete_workflow_transitions_by_tid($transistion->tid);
  }
}

/**
 * Given data, insert or update a workflow_transitions.
 */
function workflow_update_workflow_transitions(&$data) {
  $data = (object) $data;
  $transition = workflow_get_workflow_transitions_by_sid_target_sid($data->sid, $data->target_sid);
  if ($transition) {
    $roles = explode(',', $transition->roles);
    foreach (explode(',', $data->roles) as $role) {
      if (array_search($role, $roles) === FALSE) {
        $roles[] = $role;
      }
    }
    $transition->roles = implode(',', $roles);
    drupal_write_record('workflow_transitions', $transition, 'tid');
  }
  else {
    drupal_write_record('workflow_transitions', $data);
  }
  $data = $transition;
}

/**
 * Given a tid and new roles, update them. TODO - this should be refactored out, and the update made a full actual update.
 */
function workflow_update_workflow_transitions_roles($tid, $roles) {
  return db_update('workflow_transitions')->fields(array('roles' => implode(',', $roles),))->condition('tid', $tid, '=')->execute();
}

/**
 * Functions related to table workflow_states.
 */
 
/**
 * Get all states in the system, with options to filter.
 */
function workflow_get_workflow_states($options = array()) {
  $query = 'SELECT * FROM {workflow_states} WHERE 1=1 ';
  $query_array = array();
  if (isset($options['sysid'])) {
    $query_array[':sysid'] = $options['sysid'];
    $query .= 'AND sysid = :sysid ';
  }
  if (isset($options['status'])) {
    $query_array[':status'] = $options['status'];
    $query .= 'AND status = :status ';
  }
  $query .= 'ORDER BY wid, weight';
  $results = db_query($query, $query_array);
  return $results->fetchAll();
}

function workflow_get_workflow_states_by_wid($wid, $options = array()) {
  $query = 'SELECT * FROM {workflow_states} WHERE 1=1 ';
  $query_array = array();
  if (isset($options['sysid'])) {
    $query_array[':sysid'] = $options['sysid'];
    $query .= 'AND sysid = :sysid ';
  }
  if (isset($options['status'])) {
    $query_array[':status'] = $options['status'];
    $query .= 'AND status = :status ';
  }
  $query_array[':wid'] = $wid;
  $query .= 'AND wid = :wid ORDER BY wid, weight';
  $results = db_query($query, $query_array);
  return $results->fetchAll();
  // TODO consider merging this with workflow_get_workflow_states() and pass wid as option.
}

/**
 * Given a sid, return a state. Sids are a unique id.
 */
function workflow_get_workflow_states_by_sid($sid, $options = array()) {
  $results = db_query('SELECT * FROM {workflow_states} WHERE sid = :sid', array(':sid' => $sid));
  return $results->fetchObject();
}

/**
 * Given a sid, delete the state and all associated data.
 */
function workflow_delete_workflow_states_by_sid($sid, $new_sid = FALSE) {
  // Notify interested modules. We notify first to allow access to data before we zap it.
  module_invoke_all('workflow', 'state delete', $sid, NULL, NULL);
  // Re-parent any nodes that we don't want to orphan.
  if ($new_sid) {
    global $user;
    // A candidate for the batch API.
    // Future updates should seriously consider setting this with batch.
    $node = new stdClass();
    $node->workflow_stamp = REQUEST_TIME;
    foreach (workflow_get_workflow_node_by_sid($sid) as $data) {
      $node->nid = $data->nid;
      $node->workflow = $sid;
      $data = array(
        'nid' => $node->nid,
        'sid' => $new_sid,
        'uid' => $user->uid,
        'stamp' => $node->workflow_stamp,
      );
      workflow_update_workflow_node($data, $sid, t('Previous state deleted'));
    }
  }
  // Find out which transitions this state is involved in.
  $preexisting = array();
  foreach (workflow_get_workflow_transitions_by_sid_involved($sid) as $data) {
    $preexisting[$data->sid][$data->target_sid] = TRUE;
  }
  // Delete the transitions.
  foreach ($preexisting as $from => $array) {
    foreach (array_keys($array) as $target_id) {
      if ($transition = workflow_get_workflow_transitions_by_sid_target_sid($from, $target_id)) {
        workflow_delete_workflow_transitions_by_tid($transition->tid);
      }
    }
  }
  // Delete any lingering node to state values.
  workflow_delete_workflow_node_by_sid($sid);
  // Delete the state. -- We don't actually delete, just deactivate.
  // This is a matter up for some debate, to delete or not to delete, since this
  // causes name conflicts for states. In the meantime, we just stick with what we know.
  // db_delete('workflow_states')->condition('sid', $sid)->execute();
  db_update('workflow_states')->fields(array('status' => 0,))->condition('sid', $sid, '=')->execute();
}

/**
 * Given data, update or insert into workflow_states.
 */
function workflow_update_workflow_states(&$data) {
  $data = (object) $data;
  if (!isset($data->sysid)) {
    $data->sysid = 0;
  }
  if (!isset($data->status)) {
    $data->status = 1;
  }
  if (isset($data->sid) && count(workflow_get_workflow_states_by_sid($data->sid)) > 0) {
    drupal_write_record('workflow_states', $data, 'sid');
  }
  else {
    drupal_write_record('workflow_states', $data);
  }
}

/**
 * Functions related to table workflow_scheduled_transition.
 */

/**
 * Given a node, get all scheduled transitions for it.
 */
function workflow_get_workflow_scheduled_transition_by_nid($nid) {
  $results = db_query('SELECT * FROM {workflow_scheduled_transition} WHERE nid = :nid ORDER BY scheduled ASC', array(':nid' => $nid));
  return $results->fetchAll();
}

/**
 * Given a timeframe, get all scheduled transistions.
 */
function workflow_get_workflow_scheduled_transition_by_between($start = 0, $end = REQUEST_TIME) {
  $results = db_query('SELECT * FROM {workflow_scheduled_transition} WHERE scheduled > :start AND scheduled < :end ORDER BY scheduled ASC', array(':start' => $start, ':end' => $end));
  return $results->fetchAll();
}

/**
 * Given a node, delete transitions for it.
 */
function workflow_delete_workflow_scheduled_transition_by_nid($nid) {
  return db_delete('workflow_scheduled_transition')->condition('nid', $nid)->execute();
}

/**
 * Get allowable transitions for a given workflow state. Typical use:
 *
 * global $user;
 * $possible = workflow_allowable_transitions($sid, 'to', $user->roles);
 *
 * If the state ID corresponded to the state named "Draft", $possible now
 * contains the states that the current user may move to from the Draft state.
 *
 * @param $sid
 *   The ID of the state in question.
 * @param $dir
 *   The direction of the transition: 'to' or 'from' the state denoted by $sid.
 *   When set to 'to' all the allowable states that may be moved to are
 *   returned; when set to 'from' all the allowable states that may move to the
 *   current state are returned.
 * @param mixed $roles
 *   Array of ints (and possibly the string 'author') representing the user's
 *   roles. If the string 'ALL' is passed (instead of an array) the role
 *   constraint is ignored (this is the default for backwards compatibility).
 *
 * @return
 *   Associative array of states ($sid => $state_name pairs), excluding current state.
 */
function workflow_allowable_transitions($sid, $dir = 'to', $roles = 'ALL') {
  $transitions = array();
  if ($dir == 'to') {
    $field = 'target_sid';
    $field_where = 'sid';
  }
  else {
    $field = 'sid';
    $field_where = 'target_sid';
  }
  // Yes. This is a monster. This should, in all absolute seriousness, be cut down to size.
  // I also REALLY do not like the insecure field pieces in here.
  $results = db_query(''
      . '(SELECT t.tid, t.' . $field . ' as state_id, s.state AS state_name, s.weight AS state_weight '
        . 'FROM {workflow_transitions} t '
        . 'INNER JOIN {workflow_states} s ON s.sid = t.' . $field . ' '
        . 'WHERE t.' . $field_where . ' = :sid AND s.status = 1 '
        . 'ORDER BY state_weight) '
    . 'UNION '
      . '(SELECT s.sid as tid, s.sid as state_id, s.state as state_name, s.weight as state_weight '
        . 'FROM {workflow_states} s '
        . 'WHERE s.sid = :sid AND s.status = 1) '
    . 'ORDER BY state_weight, state_id',
    array(':sid' => $sid));
  foreach ($results as $transition) {
    if ($roles == 'ALL'  // Superuser.
      || $sid == $transition->state_id // Include current state for same-state transitions.
      || workflow_transition_allowed($transition->tid, $roles)) {
      $transitions[] = $transition;
    }
  }
  return $transitions;
}

/**
 * Insert a new scheduled transistion.
 * Only one transistion at a time (for now).
 */
function workflow_insert_workflow_scheduled_transition($data) {
  $data = (object) $data;
  workflow_delete_workflow_scheduled_transition_by_nid($data->nid);
  drupal_write_record('workflow_scheduled_transition', $data);
}

/**
 * Functions related to table workflow_node_history.
 */

/**
 * Get all recored history for a node id.
 * Since this may return a lot of data, a limit is included to allow for only one result.
 */
function workflow_get_workflow_node_history_by_nid($nid, $limit = NULL) {
  if (empty($limit)) {
    $limit = variable_get('workflow_states_per_page', 20);
  }
  $results = db_query('SELECT * FROM {workflow_node_history} h ' .
    'LEFT JOIN {users} u ON h.uid = u.uid ' .
    'WHERE nid = :nid ORDER BY h.hid DESC', array(':nid' => $nid));
  if ($limit == 1) {
    return $results->fetchObject();
  }
  return $results->fetchAll();
}

/**
 * Given a user id, re-assign history to the new user account. Called by user_delete().
 */
function workflow_update_workflow_node_history_uid($uid, $new_value) {
  return db_update('workflow_node_history')->fields(array('uid' => $new_value,))->condition('uid', $uid, '=')->execute();
}

/**
 * Given data, insert a new history. Always insert.
 */
function workflow_insert_workflow_node_history($data) {
  $data = (object) $data;
  if (isset($data->hid)) {
    unset($data->hid);
  }
  drupal_write_record('workflow_node_history', $data);
}

/**
 * Functions related to table workflow_node.
 */

/**
 * Given a node id, find out what it's current state is. Unique (for now).
 */
function workflow_get_workflow_node_by_nid($nid) {
  $results = db_query('SELECT * FROM {workflow_node} WHERE nid = :nid', array(':nid' => $nid));
  return $results->fetchObject();
}

/**
 * Given a sid, find out the nodes associated.
 */
function workflow_get_workflow_node_by_sid($sid) {
  $results = db_query('SELECT * FROM {workflow_node} WHERE sid = :sid', array(':sid' => $sid));
  return $results->fetchAll();
}

/**
 * Given nid, update the new stamp. This probably can be refactored. Called by workflow_execute_transition().
 */
function workflow_update_workflow_node_stamp($nid, $new_stamp) { // TODO refactor into a correct insert / update.
  return db_update('workflow_node')->fields(array('stamp' => $new_stamp,))->condition('nid', $nid, '=')->execute();
}

/**
 * Given data, update the new user account.  Called by user_delete().
 */
function workflow_update_workflow_node_uid($uid, $new_uid) {
  return db_update('workflow_node')->fields(array('uid' => $new_uid,))->condition('uid', $uid, '=')->execute();
}

/**
 * Given nid, delete associated workflow data.
 */
function workflow_delete_workflow_node_by_nid($nid) {
  return db_delete('workflow_node')->condition('nid', $nid)->execute();
}

/**
 * Given sid, delete associated workflow data.
 */
function workflow_delete_workflow_node_by_sid($sid) {
  return db_delete('workflow_node')->condition('sid', $sid)->execute();
}

/**
 * Given data, insert the node association.
 */
function workflow_update_workflow_node($data, $old_sid, $comment = NULL) {
  $data = (object) $data;
  if (isset($data->nid) && workflow_get_workflow_node_by_nid($data->nid)) {
    drupal_write_record('workflow_node', $data, 'nid');
  }
  else {
    drupal_write_record('workflow_node', $data);
  }
  // Write to history for this node.
  $data = array(
    'nid' => $data->nid,
    'old_sid' => $old_sid,
    'sid' => $data->sid,
    'uid' => $data->uid,
    'stamp' => $data->stamp,
    'comment' => $comment,
  );
  workflow_insert_workflow_node_history($data);
}