<?php
// $Id: workflow.module,v 1.45 2006/06/13 11:53:58 jvandyk Exp $

define('WORKFLOW_CREATION', 1);
define('WORKFLOW_ARROW', '&#8594;');

/**
 * Implementation of hook_help().
 */
function workflow_help($section) {
  switch ($section) {
    case 'admin/modules#description':
      return t('Enables users to manage content using workflow states.');
    case strstr($section, 'admin/workflow/edit'):
      return t('You are currently viewing the possible transitions to and from workflow states. The state is shown in the left column; the state to be moved to is to the right. For each transition, check the box next to the role(s) that may initiate the transition. For example, if only the hulking_editor role may move a node from Review state to the Published state, check the box next to hulking_editor. The author role is built in and refers to the user who authored the node.');
    case 'admin/workflow/add':
      return t('To get started, provide a name for your workflow. This name will be used as a label when the workflow status is shown during node editing.');
    case strstr($section, 'admin/workflow/state'):
      return t('Enter the name for a state in your workflow. For example, if you were doing a meal workflow it may include states like <em>shop</em>, <em>prepare food</em>, <em>eat</em>, and <em>clean up</em>.');
    case strstr($section, 'admin/workflow/actions') && (sizeof($section) == 22):
      return t('Use this page to set actions to happen when transitions occur. To %conf_actions, use the actions module.', array('%conf_actions' => l('configure actions', 'admin/actions')));
  }
}

/**
 * Implementation of hook_perm().
 */
function workflow_perm() {
  return array('administer workflow');
}

/**
 * Implementation of hook_menu().
 */
function workflow_menu($may_cache) {
  $items = array();
  $access = user_access('administer workflow');

  if ($may_cache) {
    $items[] = array('path' => 'admin/workflow',
      'title'    => t('workflow'),
      'access'   => $access,
      'callback' => 'workflow_overview');

    $items[] = array('path' => 'admin/workflow/edit',
      'title'    => t('Edit workflow'),
      'access'   => $access,
      'type'     => MENU_CALLBACK,
      'callback' => 'workflow_edit');

    $items[] = array('path' => 'admin/workflow/list',
      'title'    => t('List'),
      'access'   => $access,
      'weight'   => -10,
      'callback' => 'workflow_page',
      'type'     => MENU_DEFAULT_LOCAL_TASK);

    $items[] = array('path' => 'admin/workflow/add',
      'title'    => t('Add workflow'),
      'access'   => $access,
      'weight'   => -8,
      'callback' => 'workflow_add_form',
      'type'     => MENU_LOCAL_TASK);

    $items[] = array('path' => 'admin/workflow/state',
      'title'    => t('Add state'),
      'access'   => $access,
      'type'     => MENU_CALLBACK,
      'callback' => 'workflow_state_add');

    $items[] = array('path' => 'admin/workflow/delete',
      'title'    => t('Delete workflow'),
      'access'   => $access,
      'type'     => MENU_CALLBACK,
      'callback' => 'workflow_delete');

    $items[] = array('path' => 'admin/workflow/actions',
      'title'    => t('Workflow actions'),
      'access'   => $access,
      'type'     => MENU_CALLBACK,
      'callback' => 'workflow_actions_form');

    $items[] = array('path' => 'admin/workflow/actions/remove',
      'title'    => t('Workflow actions'),
      'access'   => $access,
      'type'     => MENU_CALLBACK,
      'callback' => 'workflow_actions_remove_form');
  }
  else {
    if (arg(0) == 'node' && is_numeric(arg(1))) {
      $node = node_load(arg(1));
      $wid = workflow_get_workflow_for_type($node->type);
      if ($wid) { // workflow exists for this type
        global $user;
        $roles = array_keys($user->roles);
        if ($node->uid == $user->uid) {
          $roles = array_merge(array('author'), $roles);
        }
        $workflow = db_fetch_object(db_query("SELECT * FROM {workflows} WHERE wid = %d", $wid));
        $allowed_roles = $workflow->tab_roles ? explode(',', $workflow->tab_roles) : array();
        $items[] = array('path' => "node/$node->nid/workflow",
          'title'    => t('workflow'),
          'access'   => array_intersect($roles, $allowed_roles) || user_access('administer nodes'),
          'type'     => MENU_LOCAL_TASK,
          'weight'   => 2,
          'callback' => 'workflow_tab_page', 
          'callback arguments' => arg(1),
         );
      }
    }
  }
  return $items;
}

function workflow_tab_page($nid) {
  drupal_set_title(t('workflow'));
  $node = node_load($nid);
  $wid = workflow_get_workflow_for_type($node->type);
  $states_per_page = 20;
  $states = workflow_get_states($wid) + array(t('(creation)'));
  $current = workflow_node_current_state($node);

  $output = "<p>Current state: " . check_plain($states[$current]) . "</p>\n";

  $choices = workflow_field_choices($node);
  $min = $states[$current] == t('(creation)') ? 1 : 2;
  if (count($choices) >= $min) { // bail out if user has no new target state(s)
    ksort($choices);
    $wid = workflow_get_workflow_for_type($node->type);
    $name = check_plain(workflow_get_name($wid));

    $form = array();
    workflow_node_form($form, t('Change %s state', array('%s' => $name)), $name, $current, $choices);
    $form['node'] = array(
      '#type' => 'value',
      '#value' => $node,
    );

    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Submit')
    );

    $output .= drupal_get_form('workflow_tab', $form);
  }

  $result = pager_query("SELECT h.*, u.name FROM {workflow_node_history} h LEFT JOIN {users} u ON h.uid = u.uid WHERE nid = %d ORDER BY stamp DESC", $states_per_page, 0, NULL, $nid);
  $rows = array();
  while ($history = db_fetch_object($result)) {
    $rows[] = array(
      format_date($history->stamp),
      check_plain($states[$history->old_sid]), 
      check_plain($states[$history->sid]), 
      theme('username', $history), 
      check_plain($history->comment)
    );
  }
  
  $output .= theme('table', array(t('Date'), t('Old State'), t('New State'), t('By'), t('Comment')), $rows, array('class' => 'workflow_history'), t('Workflow History'));
  $output .= theme('pager', $states_per_page);
  return $output;
}

function workflow_tab_submit($form_id, $form_values) {
  $node = $form_values['node'];
  $sid = $form_values['workflow'];

  // make sure new state is a valid choice
  if (array_key_exists($sid, workflow_field_choices($node))) {
    workflow_execute_transition($node, $sid, $form_values['workflow_comment']); // do transition
  }
  return 'node/' . $node->nid;
}

/**
 * Implementation of hook_nodeapi().
 * TODO: update this table for 4.7 as it is out of date.
 * Summary of callbacks we can see (Drupal 4.5.1):
 *
 * add             edit           submit (from edit) preview (from edit)
 *                 load           load               load
 * validate        validate       validate           validate
 * form pre        form pre       fields             view
 * form post       form post      update             form pre
 * form admin      form admin                        form post
 * form param      form param                        form admin
 *                                                   form param
 */
function workflow_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
  switch ($op) {

  // need to introduce setting [x] add workflow field to node form
  // made obsolete by formapi; now we use workflow_form_alter()
  //case 'form':
  //  return workflow_field_form($node);

  case 'load':
    $node->_workflow = workflow_node_current_state($node);
      break;

    case 'insert':
    case 'update':
      // stop if no workflow for this node type
      $wid = workflow_get_workflow_for_type($node->type);
      if (!$wid) {
        break;
      }

      // get new state
      $sid = $node->workflow;
      if (!$sid && $op == 'insert') { // if not specified, use first valid
        $choices = workflow_field_choices($node);
        $keys = array_keys($choices);
        $sid = array_shift($keys);
      }

      // make sure new state is a valid choice
      if (array_key_exists($sid, workflow_field_choices($node))) {
        workflow_execute_transition($node, $sid, $node->workflow_comment); // do transition
      }
      break;

    case 'delete':
      db_query("DELETE FROM {workflow_node} WHERE nid = %d", $node->nid);
      break;
  }
}

/**
 * Add the actual form widgets for workflow change to the passed in form.
 *
 * @param array $form 
 * @param string $name
 * @param string $current
 * @param array $choices
 */
function workflow_node_form(&$form, $title, $name, $current, $choices) {
  $form['workflow'][$name] = array(
    '#type' => 'radios',
    '#title' => $title,
    '#options' => $choices,
    '#name' => $name,
    '#parents' => array('workflow'),
    '#default_value' => $current
  );
  $form['workflow']['workflow_comment'] = array(
    '#type' => 'textarea',
    '#title' => t('Comment'),
    '#description' => t('A comment to put in the workflow log.'),
  );
}

/**
 * Generate a forms API compliant workflow field.
 *
 * @param object &$node
 * @return array
 */
function workflow_form_alter($form_id, &$form) {
  if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) {
    $node = $form['#node'];
    $choices = workflow_field_choices($node);
    $wid = workflow_get_workflow_for_type($node->type);
    $states = workflow_get_states($wid) + array(t('(creation)'));
    $current = workflow_node_current_state($node);
    $min = $states[$current] == t('(creation)') ? 1 : 2;
    if (count($choices) < $min) { // bail out if user has no new target state(s)
      return;
    }
    ksort($choices);
    $name = check_plain(workflow_get_name($wid));

    // if the current node state is not one of the choices, autoselect 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];
    }

    $form['workflow'] = array(
      '#type' => 'fieldset',
      '#title' => $name,
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
    );
    workflow_node_form($form, $name, $name, $current, $choices);
  }
}

/**
 * Execute a transition (change state of a node).
 *
 * @param object $node
 * @param int $sid
 * @return int ID of new state.
 */
function workflow_execute_transition($node, $sid, $comment = NULL) {
  $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) {
      _workflow_write_history($node, $sid, $comment);
    }
    return;
  }

  // Make sure this transition is valid and allowed for the current user.
  global $user;
  if ($user->uid > 1) { // allow any state change for superuser (might be cron)
    $tid = workflow_get_transition_id($old_sid, $sid);
    if (!$tid) {
      watchdog('workflow', t('Attempt to go to nonexistent transition (from %old to %new)', array('%old' => $old_sid, '%new' => $sid), WATCHDOG_ERROR));
      return;
    }
    if (!workflow_transition_allowed($tid, array_merge(array_keys($user->roles), array('author')))) {
      watchdog('workflow', t('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);

  if (in_array(FALSE, $result)) { // stop if a module says so
    return;
  }

  _workflow_node_to_state($node, $sid, $comment); // change the state

  // Register state change with watchdog
  $state_name = db_result(db_query("SELECT state FROM {workflow_states} WHERE sid = %d", $sid));
  $type = check_plain(node_get_name($node->type)); //module_invoke($node->type, 'node_name', $node);
  $state_name = check_plain($state_name);
  watchdog('workflow', "State of $type <em>$node->title</em> set to $state_name", WATCHDOG_NOTICE, l('view', $node->nid));

  // Notify modules that transition has occurred. Actions should take place
  // in response to this callback, not the previous one.
  module_invoke_all('workflow', 'transition post', $old_sid, $sid, $node);
}

/**
 * Implementation of a Drupal action.
 * Changes the workflow state of a node to the next state of the workflow.
 *
 */
function action_workflow_execute_transition($op, $edit = array(), &$node) {
  switch($op) {
    case 'do':
      // if this action is being fired because it's attached to a workflow transition
      // then the node's new state (now it's current state) should be in $node->workflow;
      // otherwise the current state is placed in $node->_workflow by our nodeapi load
      if (!isset($node->workflow) && !isset($node->_workflow)) {
        watchdog('workflow', t('Unable to get current workflow state of node %nid.', array('%nid' => $node->nid)));
        return;
      }
      $current_state = isset($node->workflow) ? $node->workflow : $node->_workflow;

      // get the node's new state
      //$new_state = $edit['target_state']; // change to specific state not yet implemented
      $new_state = '';
      if ($new_state == '') {
        $choices = workflow_field_choices($node);
        foreach ($choices as $sid => $name) {
          if (isset($flag)) {
            $new_state = $sid;
            $new_state_name = $name;
            break;
          }
          if ($sid == $current_state) {
            $flag = TRUE;
          }
        }
      }

      // fire the transition
      watchdog('action', t('Changing workflow state of node id %id to %state', array('%id' => intval($node->nid), '%state' => check_plain($new_state_name))));
      workflow_execute_transition($node, $new_state);
      watchdog('action', t('Changed workflow state of node id %id to %state', array('%id' => intval($node->nid), '%state' => check_plain($new_state_name))));
      break;

    case 'metadata':
      return array(
        'description' => t('Change workflow state of a node to next state'),
        'type' => t('Workflow'),
        'batchable' => true,
        'configurable' => false,
      );

    // return an HTML config form for the action
    case 'form':
      return '';

    // validate the HTML form
    case 'validate':
      return TRUE;

    // process the HTML form to store configuration
    case 'submit':
      return '';
  }
}

/**
 * Get the states one can move to for a given node.
 *
 * @param object $node
 * @return array
 */
function workflow_field_choices($node) {
  global $user;
  $wid = workflow_get_workflow_for_type($node->type);
  if (!$wid) { // no workflow for this type
    return array();
  }
  $states = workflow_get_states($wid);
  $roles = array_keys($user->roles);
  $current_sid = workflow_node_current_state($node);
  if ($user->uid == $node->uid && $node->uid > 0) { // if the node author
    $roles += array('author' => 'author');
  }
  if ($user->uid == 1) { // if the superuser
    $roles = 'ALL';
  }
  $transitions = workflow_allowable_transitions($current_sid, 'to', $roles);

  if ($current_sid != _workflow_creation_state($wid)) { // include current state if not (creation)
    $transitions = array($current_sid => $states[$current_sid]) + $transitions;
  }
  return $transitions;
}

/**
 * Get the current state of a given node.
 *
 * @param object $node
 * @return string state ID
 */
function workflow_node_current_state($node) {
  $sid = db_result(db_query("SELECT sid FROM {workflow_node} WHERE nid=%d ", $node->nid));

  if (!$sid) {
    $wid = workflow_get_workflow_for_type($node->type);
    $sid = _workflow_creation_state($wid);
  }
  return $sid;
}

function _workflow_creation_state($wid) {
  return db_result(db_query("SELECT sid FROM {workflow_states} WHERE ".
    "wid=%d && sysid=%d", $wid, WORKFLOW_CREATION));
}

/**
 * Implementation of hook_workflow().
 */
function workflow_workflow($op, $old_state, $new_state, $node) {
  switch ($op) {
    case 'transition pre':
    break;

    case 'transition post':
      // a transition has occurred; fire off actions associated with this transition
      // an SQL guru could clean this up with a complicated JOIN
      $tid = workflow_get_transition_id($old_state, $new_state);
      if ($tid) {
        $actions_this_tid = workflow_get_actions($tid);
        if ($actions_this_tid && function_exists('actions_do')) {
          actions_do(array_keys($actions_this_tid), $node);
        }
      }
    break;
  }
}

/**
 * Create the form for adding/editing a workflow.
 *
 * @param $name
 *   Name of the workflow if editing.
 * @param $add
 *   Boolean, if true edit workflow name.
 *
 * @return
 *   HTML form.
 *
 */
function workflow_add_form($name = NULL) {
  $form = array();
  $form['wf_name'] = array(
    '#type' => 'textfield',
    '#title' => t('Workflow Name'),
    '#size' => '16',
    '#maxlength' => '254',
    '#default_value' => $name
    );
  $form['submit'] = array('#type' => 'submit', '#value' => t('Add Workflow'));
  $output = drupal_get_form('workflow_add_form', $form);
  return $output;
}

function workflow_add_form_validate($form_id, $form_values) {
  if ($form_values['wf_name'] == '') {
    form_set_error('', t('Please provide a nonblank name for the new workflow.'));
  }
}

function workflow_add_form_submit($form_id, $form_values) {
  if (array_key_exists('wf_name', $form_values) && $form_values['wf_name'] != '')  {
    $wf_name = $form_values['wf_name'];
    workflow_create($wf_name);
    drupal_set_message(t('The workflow %wf was created. You should now add states to your workflow.', array('%wf' => theme('placeholder', $wf_name))));
    drupal_goto('admin/workflow');
  }
}

/**
 * Create the form for confirmation of deleting a workflow.
 *
 * @param $wid
 *   The ID of the workflow.
 *
 * @return
 *   HTML form.
 *
 */
function workflow_delete($wid, $sid = NULL) {
  if (isset($sid)) {
    return workflow_state_delete_form($wid, $sid);
  }
  $wf = workflow_get_name($wid);
  $form = array();
  $form['wid'] = array('#type' => 'value', '#value' => $wid);
  $output = confirm_form('workflow_delete_confirm', $form, t('Are you sure you want to delete %title? All nodes that have a workflow state associated with this workflow will have those workflow states removed.', array('%title' => theme('placeholder', $wf))), $_GET['destination'] ? $_GET['destination'] : 'admin/workflow', t('This action cannot be undone.'), t('Delete'), t('Cancel')  );
  return $output;
}

function workflow_delete_confirm_submit($form_id, $form_values) {
  if ($form_values['confirm'] == 1) {
    $wid = $form_values['wid'];
    $wf = workflow_get_name($wid);
    workflow_deletewf($wid);
    drupal_set_message(t('The workflow %wf was deleted.', array('%wf' => theme('placeholder', $wf))));
    drupal_goto('admin/workflow');
  }
}

/**
 * View workflow permissions by role
 *
 * @param $wid
 *   The ID of the workflow
 */
function workflow_permissions($wid) {
  $name = workflow_get_name($wid);
  $result = db_query("SELECT sid, state FROM {workflow_states} WHERE wid = %d", $wid);
  $states = array();
  while ($data = db_fetch_object($result)) {
    $states[$data->sid] = $data->state;
  }

  $all = array();
  $result = db_query("SELECT * FROM {workflow_transitions}");
  while ($data = db_fetch_object($result)) {
    if (isset($states[$data->sid])) {
      foreach (explode(',', $data->roles) as $role) {
        $all[$role][] = array($data->sid, $data->target_sid);
      }
    }
  }

  $output = '';
  $role_names = user_roles();
  $header = array(t('From'), '', t('To'));
  foreach ($all as $role_id => $transitions) {
    $rows = array();
    $role = $role_id == 'author' ? 'author' : $role_names[$role_id];
    foreach ($transitions as $transition) {
      list($from, $to) = $transition;
      $rows[] = array($states[$from], WORKFLOW_ARROW, $states[$to]);
    }
   $output .= '<h3>' . t("%role may do these transitions:", array('%role' => $role)) . '</h3>';
   if ($rows) {
       $output .= theme('table', $header, $rows);
    }
    else {
      $output .= t('None');
    }
  }
  return $output;
}

/**
 * Menu callback to edit a workflow's properties.
 *
 * @param $wid
 *   The ID of the workflow.
 *
 * @return
 *   HTML form.
 *
 */
function workflow_edit($wid) {
//  $name = workflow_get_name($wid);
  $workflow = db_fetch_object(db_query("SELECT * FROM {workflows} WHERE wid = %d", $wid));
  $form['wf_name'] = array(
    '#type' => 'textfield',
    '#default_value' => $workflow->name,
    '#title' => t('Workflow Name'),
    '#size' => '16',
    '#maxlength' => '254',
  );
  $form['tab'] = array(
    '#type' => 'fieldset',
    '#title' => t('Workflow tab permissions'),
    '#collapsible' => TRUE,
    '#collapsed' => FALSE,
  );

  $form['tab']['tab_roles'] = array(
    '#type' => 'checkboxes',
    '#options' => workflow_get_roles(),
    '#default_value' => explode(',', $workflow->tab_roles),
    '#description' => t('Select any roles that should have access to the workflow tab on nodes that have a workflow.'),
  );

  $form['transitions'] = workflow_transition_grid_form($wid);
  $form['submit'] = array('#type' => 'submit', '#value' => t('Save'));
  $form['wid'] = array('#type' => 'value', '#value' => $wid);
  $form['#theme'] = 'workflow_edit';
  $form['transitions']['#tree'] = true;

  $output = drupal_get_form('workflow_edit', $form);
  $output .= workflow_permissions($wid);
  return $output;
}

function theme_workflow_edit($form) {
  $output = form_render($form['wf_name']);
  $wid = $form['wid']['#value'];
  $states = workflow_get_states($wid);
  if ($states) {
    $roles = workflow_get_roles();
    $header = array(array('data' => t('From / To ') . '&nbsp;' . WORKFLOW_ARROW));
    $rows = array();
    foreach ($states as $state_id => $name) {
      if ($name != '(creation)') { // don't allow transition TO (creation)
        $header[] = array('data' => t($name));
      }
      $row = array(array('data' => $name));
      foreach ($states as $nested_state_id => $nested_name) {
        if ($nested_name == '(creation)') {
          continue; // don't allow transition TO (creation)
        }
        if ($nested_state_id != $state_id) {
          // need to render checkboxes for transition from $state to $nested_state
          $from = $state_id;
          $to = $nested_state_id;
          $cell = '';
          foreach ($roles as $rid => $role_name) {
            $cell .= form_render($form['transitions'][$from][$to][$rid]);
            //$cell .= form_render($form['transitions']["transitions][$from][$to][$rid"]);
          }
          $row[] = array('data' => $cell);
        }
        else {
          $row[] = array('data' => '');
        }
      }
      $rows[] = $row;
    }
    $output .= theme('table', $header, $rows);
  } else {
    $output = 'There are no states defined for this workflow.';
  }

  $output .= form_render($form);
  return $output;
}

function workflow_edit_validate($form_id, $form_values) {
  $wid = $form_values['wid'];
  if (array_key_exists('wf_name', $form_values) && $form_values['wf_name'] != '') {
    // validate: make sure workflow name is not a duplicate
    $dupe_name = db_result(db_query("SELECT COUNT(*) FROM {workflows} WHERE name='%s' && wid!=%d", $form_values['wf_name'], $wid));
    if ($dupe_name) {
      form_set_error('', t('Warning: Another workflow with this name already exists.'));
    }

    // validate: make sure 'author' is checked for (creation) -> [something]
    $states = workflow_get_states($wid);
    $author_creates = 0;
    foreach ($form_values['transitions'] as $id => $trans) {
      if ($states[$id] == '(creation)') {
        foreach ($trans as $t) {
          if ($t['author'] == 1) { $author_creates++; }
        }
      }
    }
    if ($author_creates == 0) {
      drupal_set_message(t('Warning: Please give the author permission to go from (creation) to at least one state.'), 'warning');
    }
  }
  else {
    form_set_error('', t('Please provide a name for the workflow.'));
  }
}

function workflow_edit_submit($form_id, $form_values) {
  workflow_update($form_values['wid'], $form_values['wf_name'], array_filter($form_values['tab_roles']));
  workflow_update_transitions($form_values['transitions']);
  drupal_set_message(t('Workflow updated.'));
  return('admin/workflow');
}

/**
 * Menu callback to create form to add a workflow state.
 *
 * @param $wid
 *   The ID of the workflow.
 *
 * @return
 *   HTML form.
 *
 */
function workflow_state_add ($wid) {
  $form['state_name'] = array(
    '#type'  => 'textfield',
    '#title' => t('State Name'),
    '#size'  => '16',
    '#maxlength' => '254',
  );
  $form['submit'] = array('#type' => 'submit', '#value' => t('Save'));
  $form['wid'] = array('#type' => 'value', '#value' => $wid);
  $output = drupal_get_form('workflow_state_add', $form);
  return $output;
}

function workflow_state_add_validate($form_id, $form_values) {
  if ($form_values['state_name'] == '') {
    form_set_error('', t('Please provide a nonblank name for the new state.'));
  }
}

function workflow_state_add_submit($form_id, $form_values) {
  workflow_state_create($form_values['wid'], $form_values['state_name']);
  drupal_set_message(t('The workflow state was created.'));
  return('admin/workflow');
}

/**
 * Update the transitions for a workflow.
 *
 * @param array $transitions
 *   Transitions, for example:
 *     18 => array(
 *       20 => array(
 *         'author' => 1,
 *         1        => 0,
 *         2        => 1,
 *       )
 *     )
 *   means the transition from state 18 to state 20 can be executed by
 *   the node author or a user in role 2. The $transitions array should
 *   contain ALL transitions for the workflow.
 */
function workflow_update_transitions($transitions = array()) {
  foreach ($transitions as $from => $to_data) {
    foreach ($to_data as $to => $role_data) {
      foreach ($role_data as $role => $can_do) {
        if ($can_do) {
          workflow_transition_add_role($from, $to, $role);
        }
        else {
          workflow_transition_delete_role($from, $to, $role);
        }
      }
    }
  }
  db_query("DELETE FROM {workflow_transitions} WHERE roles=''");
}

/**
 * Add a role to the list of those allowed for a given transition.
 * Add the transition if necessary.
 * @param int $from
 * @param int $to
 * @param mixed $role
 *   Int (role ID) or string ('author').
 */
function workflow_transition_add_role($from, $to, $role) {
  $tid = workflow_get_transition_id($from, $to);
  if ($tid) {
    $roles = db_result(db_query("SELECT roles FROM {workflow_transitions} WHERE tid=%d", $tid));
    $roles = explode(',', $roles);
    if (array_search($role, $roles) === FALSE) {
      $roles[] = $role;
      db_query("UPDATE {workflow_transitions} SET roles='%s' WHERE tid=%d", implode(',', $roles), $tid);
    }
  }
  else {
    db_query("INSERT INTO {workflow_transitions} (tid, sid, target_sid, roles) VALUES (%d, %d, %d, '%s')", db_next_id('workflow_transitions'), $from, $to, $role);
  }
}

/**
 * Remove a role from the list of those allowed for a given transition.
 * @param int $tid
 * @param mixed $role
 *   Int (role ID) or string ('author').
 */
function workflow_transition_delete_role($from, $to, $role) {
  $tid = workflow_get_transition_id($from, $to);
  if ($tid) {
    $roles = db_result(db_query("SELECT roles FROM {workflow_transitions} WHERE tid=%d", $tid));
    $roles = explode(',', $roles);
    if (($i = array_search($role, $roles)) !== FALSE) {
      unset($roles[$i]);
      db_query("UPDATE {workflow_transitions} SET roles='%s' WHERE tid=%d", implode(',', $roles), $tid);
    }
  }
}

/**
 * Build the grid of transitions for defining a workflow.
 *
 * @param int $wid
 */
function workflow_transition_grid_form($wid) {

  $roles = workflow_get_roles();

  $form = array();
  $states = workflow_get_states($wid);
  if (!$states) {
    $form = array('#type' => 'markup', '#value' => t('There are no states defined for this workflow.'));
    return $form;
  }
  foreach ($states as $state_id => $name) {
    foreach ($states as $nested_state_id => $nested_name) {
      if ($nested_name == t('(creation)')) {
        continue; // don't allow transition TO (creation)
      }
      if ($nested_state_id != $state_id) {
        // need to generate checkboxes for transition from $state to $nested_state
        $from = $state_id;
        $to = $nested_state_id;
        foreach ($roles as $rid => $role_name) {
          $tid = workflow_get_transition_id($from, $to);
          $form[$from][$to][$rid] = array(
            '#type' => 'checkbox',
            '#title' => $role_name,
            '#default_value' => $tid ? workflow_transition_allowed($tid, $rid) : false);
        }
      }
    }
  }
  return $form;
}

/**
 * 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 bool
 */
function workflow_transition_allowed($tid, $role = null) {
  $allowed = db_result(db_query(
    "SELECT roles FROM {workflow_transitions} WHERE tid=%d", $tid));
  $allowed = explode(',', $allowed);
  if ($role) {
    if (!is_array($role)) { $role = array($role); }
    return array_intersect($role, $allowed) ==  true;
  } else {
    return $allowed == true; // allowed for anybody?
  }
}

/**
 * Create the main workflow page, which gives an overview
 * of workflows and workflow states.
 *
 * @return
 *   HTML form.
 *
 */
function workflow_overview() {
  $result = db_query("SELECT * FROM {workflows}");
  $header = array(array('data' => t('Name')), array('data' => 'Operations', 'colspan' => '4'));
  $row = array();

  while ($data = db_fetch_object($result)) {
    $states = workflow_get_states($data->wid);
    $row[] = array(
      array('data' => $data->name),
      array('data' => l(t('add state'), "admin/workflow/state/$data->wid")),
      array('data' => $states ? l(t('actions'), "admin/workflow/actions/$data->wid") : ''),
      array('data' => l(t('edit'), "admin/workflow/edit/$data->wid")),
      array('data' => l(t('delete'), "admin/workflow/delete/$data->wid")));

    if ($states) {
      $cell = '';
      foreach ($states as $sid => $state) {
        if (!workflow_is_system_state($state)) {
          $cell .= "<table style='width: 100%'><tr><td>" . $state;
          $cell .= "</td><td style='text-align: right'>";
          $cell .= l(t('delete'), "admin/workflow/delete/$data->wid/$sid") . "</td></tr></table>";
        }
      }
      $row[] = array(array('data' => $cell, 'colspan' => '5'));
    }
  }

  if (!$row) {
    $output = '<p>' . t('No workflows have been added. Would you like to %add_a_workflow?', array('%add_a_workflow' => l(t('add a workflow'), 'admin/workflow/add'))) . '</p>';
  } else {
    $output .= theme('table', $header, $row);
  }

  $output .= workflow_types_form();

  return $output;
}

/**
 * Tell caller whether a state is a protected system state, such as the creation state.
 *
 * @param $state
 *   The name of the state to test
 *
 * @return
 *   boolean
 *
 */
function workflow_is_system_state($state) {
  static $states;
  if (!isset($states)) {
    $states = array(t('(creation)') => TRUE);
  }
  return isset($states[$state]);
}

/**
 * Create the form for confirmation of deleting a workflow state.
 *
 * @param $wid
 *   integer The ID of the workflow.
 * @param $sid
 *   The ID of the workflow state.
 *
 * @return
 *   HTML form.
 *
 */
function workflow_state_delete_form($wid, $sid) {
  $states = workflow_get_states($wid);
  $form = array();
  $form['wid'] = array('#type' => 'value', '#value' => $wid);
  $form['sid'] = array('#type' => 'value', '#value' => $sid);
  $output = confirm_form('workflow_state_delete_confirm', $form, t('Are you sure you want to delete %title (and all its transitions)?', array('%title' => theme('placeholder', $states[$sid]))), $_GET['destination'] ? $_GET['destination'] : 'admin/workflow', t('This action cannot be undone.'), t('Delete'), t('Cancel')  );
  return $output;
}

function workflow_state_delete_confirm_submit($form_id, $form_values) {
  $states = workflow_get_states($form_values['wid']);
  $state = $states[$form_values['sid']];
  if ($form_values['confirm'] == 1) {
    workflow_state_delete($form_values['sid']);
    drupal_set_message(t('The workflow state %title was deleted.', array('%title' => theme('placeholder', $state))));
  }
  return('admin/workflow');
}

function workflow_types_form() {
  $workflows = workflow_get_all();
  if (count($workflows) == 0) {
    return '';
  }
  $workflows[0] = t('None');
  $form = array();
  $form['workflow_mapping'] = array();
  $form['workflow_mapping']['#theme'] = 'workflow_types_form';

  $type_map = array();
  $result = db_query("SELECT * FROM {workflow_type_map}");
  while ($data = db_fetch_object($result)) {
    $type_map[$data->type] = $data->wid;
  }
  $options = $workflows;

  $nodetypes = node_get_types();
  foreach ($nodetypes as $type => $name) {
    $value = isset($type_map[$type]) ? $type_map[$type] : 0;
    $form['workflow_mapping'][$type] = array(
      '#type' => 'select',
      '#prefix' => $name,
      '#options' => $options,
      '#default_value' => $value
      );
  }
  $form['workflow_mapping']['submit'] = array('#type' => 'submit', '#value' => t('Save Workflow Mapping'));
  $form['nodetypes'] = array('#type' => 'value', '#value' => $nodetypes);
  return drupal_get_form('workflow_type_form', $form);
}

function theme_workflow_types_form($form) {
  $output = '<div class="form-item">' . t('Each node type may have a separate workflow:') . '</div>';
  $header = array(
    array('data' => t('Node Type')),
    array('data' => t('Workflow'))
  );
  $row = array();
  foreach (element_children($form) as $key) {
    $name = $form[$key]['#prefix'];
    unset($form[$key]['#prefix']);
    $row[] = array(
      array('data' => $name),
      array('data' => form_render($form[$key]))
    );
  }
  $output .= theme('table', $header, $row);
  return $output;
}

function workflow_type_form_submit($form_id, $form_values) {
  workflow_types_save($form_values);
  drupal_set_message(t('The workflow mapping was saved.'));
  return('admin/workflow');
}

function workflow_actions_remove_form($wid, $tid, $aid) {
  $form = array();
  $form['wid'] = array('#type' => 'value', '#value' => $wid);
  $form['tid'] = array('#type' => 'value', '#value' => $tid);
  $form['aid'] = array('#type' => 'value', '#value' => $aid);
  $actions = actions_get_all_actions();
  $output = confirm_form('workflow_actions_remove_confirm', $form, t('Are you sure you want to delete the action %title?', array('%title' => theme('placeholder', $actions[$aid]['description']))), $_GET['destination'] ? $_GET['destination'] : 'admin/workflow/actions', t('You can add it again later if you wish.'), t('Delete'), t('Cancel')  );
  return $output;
}

function workflow_actions_remove_confirm_submit($form_id, $form_values) {
  if ($form_values['confirm'] == 1) {
    $aid = $form_values['aid'];
    $actions = actions_actions_map(actions_get_all_actions());
    workflow_actions_remove($form_values['tid'], $aid);
    watchdog('workflow', t('Action %action deleted',  array('%action' => check_plain($actions[$aid]))));
    drupal_set_message(t('Action %action deleted.', array('%action' => theme('placeholder', $actions[$aid]))));
    return('/admin/workflow/actions/'. $form_values['wid']);
  }
}

function workflow_actions_form($wid) {
  if (!module_exist('actions')) {
    drupal_set_message(t('Before you can assign actions you must install and enable the actions module.'), 'error');
    drupal_goto('admin/workflow');
  }
  $result = workflow_actions_form_build($wid);
  $form = $result[1];
  $output = workflow_actions_form_build($wid, $form);
  return $output[0];
}

function workflow_actions_submit($form, $form_values) {
  $tid = $form_values['tid'];
  $aid = actions_key_lookup($form_values['action']);
  workflow_actions_save($tid, $aid);

  // the form does weird things if we don't do a drupal_goto()
  $wid = arg(3);
  return("admin/workflow/actions/$wid");
}

function workflow_actions_form_build($wid, $form = NULL) {
  if (!isset($form)) {
    $form = array();
    $render = false;
  }
  else {
    $render =true;
  }
  $output = '';
  $states = array();
  $all_states = workflow_get_states($wid);

  foreach ($all_states as $key => $val) {
    $states[$key] = $val;
  }

  $actions = actions_actions_map(actions_get_all_actions());
  $options = array(t('None'));
  foreach ($actions as $aid => $action) {
    $options[$aid] = $action['description'];
  }
  $header = array(
    array('data' => t('Transition'), 'colspan' => '3'),
    array('data' => t('Actions'))
    );
  $row = array();

  foreach ($states as $sid => $state) {

    $allowable_to = workflow_allowable_transitions($sid);
    $actions_this_tid = array();

    // we'll create a row for each allowable transition
    foreach ($allowable_to as $to_sid => $name) {
      $inner_row = array();
      $tid = workflow_get_transition_id($sid, $to_sid);

      // get and list the actions that are already assigned to this transition
      $actions_this_tid = workflow_get_actions($tid);
      $available_options = $options;
      foreach ($actions_this_tid as $aid => $act_name) {
        $inner_row[] = array(
          array('data' => $act_name),
          array('data' => l(t('remove'), "admin/workflow/actions/remove/$wid/$tid/$aid"))
          );
        unset($available_options[$aid]);
      }

      // list possible actions that may be assigned
      if (count($available_options) > 1) {
        if ($render) {
          $inner_row[] = array(
            array('data' => drupal_get_form('workflow_actions_'. $tid, $form[$tid], 'workflow_actions'), 'colspan' => '2'));
        }
        else {
          $form[$tid] = array();
          $form[$tid]['action'] = array(
            '#prefix' => '<table><tr><td>',
            '#suffix' => '</td>',
            '#type' => 'select',
            '#options' => $available_options,
            '#default_value' => t('None')
          );
          $form[$tid]['submit'] = array(
            '#prefix' => '<td>',
            '#suffix' => '</td></tr></table>',
            '#type' => 'submit',
            '#value' => t('Add')
          );
          $form[$tid]['tid'] = array('#type' => 'value', '#value' => $tid);
        }
      }
      $action_table = theme('table', array(), $inner_row);
      $row[] = array(
        array('data' => $state),
        array('data' => WORKFLOW_ARROW),
        array('data' => $name),
        array('data' => $action_table)
        );
    }
  }

  if (count($row) == 0) {
    $output .= t('You must first %link before you can assign actions.', array('%link' => l(t('set up transitions'), 'admin/workflow/edit/' . $wid)));
  }
  else {
    $output .= theme('table', $header, $row);
  }
  return array($output, $form);
}

/**
 * Given the ID of a workflow, return its name.
 *
 * @param integer $wid
 *   The ID of the workflow.
 *
 * @return string
 *   The name of the workflow.
 *
 */
function workflow_get_name($wid) {
  $name = db_result(db_query("SELECT name FROM {workflows} WHERE wid = %d", $wid));
  return $name;
}

/**
 * Get ID of a workflow for a node type.
 *
 * @return int
 *   The ID of the workflow or false if none.
 *
 */
function workflow_get_workflow_for_type($type) {
  $wid = db_result(db_query("SELECT wid FROM {workflow_type_map} WHERE type = '%s'", $type));
  return $wid > 0 ? $wid : false;
}

/**
 * Get names and IDS of all workflows from the database.
 *
 * @return
 *   An array of workflows keyed by ID.
 *
 */
function workflow_get_all() {
  $workflows = array();
  $result = db_query("SELECT * FROM {workflows}");
  while ($data = db_fetch_object($result)) {
    $workflows[$data->wid] = $data->name;
  }

  return $workflows;
}

/**
 * Create a workflow and its (creation) state.
 *
 * @param $name
 *   The name of the workflow.
 *
 */
function workflow_create($name) {
  $wid = db_next_id('workflows');
  db_query("INSERT INTO {workflows} (wid, name) VALUES (%d, '%s')", $wid, $name);
  db_query("INSERT INTO {workflow_states} (sid, wid, state, sysid) VALUES (%d, %d, '%s', %d)",
    db_next_id('workflow_state'), $wid, t('(creation)'), WORKFLOW_CREATION);
}

/**
 * Save a workflow's name in the database.
 *
 * @param $name
 *   The name of the workflow.
 *
 */
function workflow_update($wid, $name, $tab_roles) {
  db_query("UPDATE {workflows} SET name = '%s', tab_roles = '%s' WHERE wid = %d", $name, implode(',', $tab_roles), $wid);
}

/**
 * Delete a workflow from the database. Deletes all states,
 * transitions and node type mappings too. Removes workflow state
 * information from nodes participating in this workflow.
 *
 * @param $wid
 *   The ID of the workflow.
 *
 */
function workflow_deletewf($wid) {
  $wf = workflow_get_name($wid);
  $result = db_query("SELECT sid FROM {workflow_states} WHERE wid = %d", $wid);
  while ($data = db_fetch_object($result)) {
    // delete the state and any associated transitions and actions
    workflow_state_delete($data->sid);
    db_query("DELETE FROM {workflow_node} WHERE sid = %d", $data->sid);
  }
  workflow_types_delete($wid);
  db_query("DELETE FROM {workflows} WHERE wid = %d", $wid);
  watchdog('workflow', t('Deleted workflow') . " '$wf'.");
}

/**
 * Load workflow states for a workflow from the database.
 *
 * @param $wid
 *   The ID of the workflow.
 *
 * @return
 *   An array of workflow states keyed by state ID.
 */
function workflow_get_states($wid) {
  $states = array();
  $result = db_query("SELECT sid, state FROM {workflow_states} WHERE wid = %d ORDER BY sid", intval($wid));
  while ($data = db_fetch_object($result)) {
    $states[$data->sid] = $data->state;
  }

  return $states;
}

/**
 * Add a workflow state the database.
 *
 * @param $wid
 *   The ID of the workflow.
 * @param $name
 *   A string representing the workflow state, e.g., 'published'.
 *
 * @return
 *   The ID of the workflow state
 */
function workflow_state_create($wid, $name) {
  $wid = intval($wid);
  $sid = db_next_id('workflow_state');
  db_query("INSERT INTO {workflow_states} (sid, wid, state, sysid) VALUES (%d, %d, '%s', %d)",
    $sid, $wid, $name, 0);
  return $sid;
}

/**
 * Delete a workflow state from the database, including any
 * transitions the state was involved in and any associations
 * with actions that were made to that transition.
 *
 * @param $sid
 *   The ID of the state to delete.
 */
function workflow_state_delete($sid) {
  // find out which transitions this state is involved in
  $preexisting = array();
  $result = db_query("SELECT sid, target_sid FROM {workflow_transitions} WHERE sid = %d OR target_sid = %d", $sid, $sid);
  while ($data = db_fetch_object($result)) {
   $preexisting[$data->sid][$data->target_sid] = TRUE;
  }

  // delete the transitions and associated actions
  foreach ($preexisting as $from => $array) {
    foreach (array_keys($array) as $target_id) {
      $tid = workflow_get_transition_id($from, $target_id);
      workflow_transition_delete($tid);
    }
  }

  // delete the state
  db_query("DELETE FROM {workflow_states} WHERE sid = %d", intval($sid));
}

/**
 * Delete a transition (and any associated actions).
 *
 * @param $tid
 *   The ID of the transition.
 */
function workflow_transition_delete($tid) {
  $actions = workflow_get_actions($tid);
  foreach (array_keys($actions) as $aid) {
    workflow_actions_remove($tid, $aid);
  }
  db_query("DELETE FROM {workflow_transitions} WHERE tid = %d", $tid);
}

/**
 * Get allowable transitions for a given workflow 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=>name pairs), excluding current state.
 *
 */
function workflow_allowable_transitions($sid, $dir = 'to', $roles = 'ALL') {
  $transitions = array();
  $field       = $dir == 'to' ? 'target_sid' : 'sid';
  $field_where = $dir != 'to' ? 'target_sid' : 'sid';
  $result = db_query("SELECT t.tid, t.%s as state_id, s.state as state_name FROM "
    . "{workflow_transitions} t INNER JOIN {workflow_states} s ON s.sid = "
    . "t.%s WHERE t.%s = %d", $field, $field, $field_where, $sid);

  while ($t = db_fetch_object($result)) {
    if ($roles == 'ALL' || workflow_transition_allowed($t->tid, $roles)) {
      //$state_id = str_pad($t->state_id, 5, '0', STR_PAD_LEFT);
      $state_id = $t->state_id;
      $transitions[$state_id] = $t->state_name;
    }
  }
  return $transitions;
}

/**
 * Save mapping of workflow to node type. E.g., "the story node type
 * is using the Foo workflow."
 *
 * @param $form_values
 */
function workflow_types_save($form_values) {
  $nodetypes = node_get_types();
  db_query("DELETE FROM {workflow_type_map}");
  foreach ($nodetypes as $type => $name) {
    db_query("INSERT INTO {workflow_type_map} (type, wid) VALUES ('%s', %d)", $type, intval($form_values[$type]));
  }
}

function workflow_types_delete($wid) {
  db_query("DELETE FROM {workflow_type_map} WHERE wid = %d", $wid);
}

/**
 * Get the actions associated with a given transition.
 * @param int $tid
 * @return array
 *   Actions as aid=>description pairs.
 */
function workflow_get_actions($tid) {
  $actions = array();
  if (!function_exists('actions_do')) {
    return $actions;
  }
  $result = db_query("SELECT a.aid, a.description FROM {actions} a INNER JOIN {workflow_actions} w ON a.aid = w.aid WHERE w.tid = %d", $tid);
  while ($data = db_fetch_object($result)) {
    $actions[$data->aid] = $data->description;
  }
  return $actions;
}

/**
 * Get the tid of a transition, if it exists.
 *
 * @param int $from
 *   ID (sid) of originating state.
 * @param int $to
 *   ID (sid) of target state.
 * @return int
 *   Tid or FALSE if no such transition exists.
 */
function workflow_get_transition_id($from, $to) {
  return db_result(db_query("SELECT tid FROM {workflow_transitions} WHERE sid=%d && target_sid=%d", $from, $to));
}

function workflow_actions_save($tid, $aid) {
  actions_register($aid, 'workflow', $tid);
  $data = db_fetch_object(db_query("SELECT tid FROM {workflow_actions} WHERE tid = %d AND aid = '%s'", $tid, $aid));
  if ($data) return;
  db_query("INSERT INTO {workflow_actions} (tid, aid, weight) VALUES (%d, '%s', %d)", $tid, $aid, 0);
}

function workflow_actions_remove($tid, $aid) {
  actions_unregister($aid, 'workflow', $tid);
  db_query("DELETE FROM {workflow_actions} WHERE tid = %d AND aid = '%s'", $tid, $aid);
}

/**
 * Put a node into a state.
 * No permission checking here; only call this from other functions that know
 * what they're doing.
 *
 * @see workflow_execute_transition()
 *
 * @param object $node
 * @param int $sid
 */
function _workflow_node_to_state($node, $sid, $comment = NULL) {
  global $user;
  if (db_result(db_query("SELECT nid FROM {workflow_node} WHERE nid = %d", $node->nid))) {
   db_query("UPDATE {workflow_node} SET sid = %d, uid = %d WHERE nid = %d", $sid, $user->uid, $node->nid);
  }
  else {
    db_query("INSERT INTO {workflow_node} (nid, sid, uid) VALUES (%d, %d, %d)", $node->nid, $sid, $user->uid);
  }
  _workflow_write_history($node, $sid, $comment);
}

function _workflow_write_history($node, $sid, $comment) {
  global $user;
  db_query("INSERT INTO {workflow_node_history} (nid, old_sid, sid, uid, comment, stamp) VALUES (%d, %d, %d, %d, '%s', %d)", $node->nid, $node->_workflow, $sid, $user->uid, $comment, time());
}

/**
 * Function to get a list of roles. Used kind of often.
 */
function workflow_get_roles() {
  static $roles = NULL;
  if (!$roles) {
    $result = db_query('SELECT * FROM {role} ORDER BY name');
    $roles = array('author' => 'author');
    while ($data = db_fetch_object($result)) {
      $roles[$data->rid] = $data->name;
    }
  }
  return $roles;
}

/**
 * Implementation of hook_views_tables()
 */
function workflow_views_tables() {
  $table = array(
    'name' => 'workflow_node',
    'provider' => 'workflow',
    'join' => array(
      'left' => array(
        'table' => 'node',
        'field' => 'nid',
       ),
       'right' => array(
         'field' => 'nid',
       ),
     ),
    "filters" => array(
      'sid' => array(
        'name' => t('Workflow: state'),
        'operator' => 'views_handler_operator_andor',
        'list' => 'workflow_handler_filter_sid',
        'help' => t('Include only nodes in the selected workflow states.'),
      ),
    )
  );
  $tables[$table['name']] = $table;

  $table = array(
    'name' => 'workflow_states',
    'provider' => 'workflow',
    'join' => array(
      'left' => array(
        'table' => 'workflow_node',
        'field' => 'sid',
       ),
       'right' => array(
         'field' => 'sid',
       ),
     ),
    "sorts" => array(
      'weight' => array(
        'name' => t('Workflow: state'),
        'field' => array('weight', 'state'),
        'help' => t('Order nodes by workflow state.'),
      ),
    ),
    "fields" => array(
      'state' => array(
        'name' => t('Workflow: state'),
        'sortable' => true,
        'help' => t('Display the workflow state of the node.'),
      ),
    ),
  );
  $tables[$table['name']] = $table;

  return $tables;
}

/**
 * Implementation of hook_views_arguments()
 */
function workflow_views_arguments() {
  $arguments = array(
    'workflow_state' => array(
      'name' => t('Workflow: state'),
      'handler' => 'workflow_handler_arg_sid',
      'help' => t('The work flow argument allows users to filter a view by workflow state.'),
    ),
  );
  return $arguments;
}

/**
 * Handler to provide a list of workflow states for the filter list
 */
function workflow_handler_filter_sid() {
  $result = db_query("SELECT sid, state FROM {workflow_states} ORDER BY weight, state");
  while ($data = db_fetch_object($result)) {
    $states[$data->sid] = $data->state;
  }

  return $states;
}

/**
 * Handler to deal with sid as an argument.
 */
function workflow_handler_arg_sid($op, &$query, $argtype, $arg = '') {
  switch($op) {
    case 'summary':
      $query->add_table('workflow_states', true);
      $fieldinfo['field'] = "workflow_states.sid";
      $query->add_field('sid', 'workflow_states');
      $query->add_field('state', 'workflow_states');
      $query->add_where('workflow_node.sid IS NOT NULL');
      return $fieldinfo;
      break;
    case 'filter':
      $query->add_table('workflow_states', true);
      if (is_numeric($arg)) {
        $query->add_where("workflow_states.sid = %d", $arg);
      }
      else {
        $query->add_where("workflow_states.state = '%s'", $arg);
      }
      break;
    case 'link':
      return l($query->state, "$arg/$query->sid");
    case 'title':
      $state = db_fetch_object(db_query("SELECT state FROM {workflow_states} WHERE sid=%d", $query));
      return $state->state;
  }
}