You are here

workflow.module in Workflow 5

File

workflow.module
View source
<?php

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

/**
 * Implementation of hook_help().
 */
function workflow_help($section) {
  switch ($section) {
    case strstr($section, 'admin/build/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/build/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/build/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/build/workflow/actions') && sizeof($section) == 22:
      return t('Use this page to set actions to happen when transitions occur. To <a href="@link">configure actions</a>, use the actions module.', array(
        '@link' => url('admin/actions'),
      ));
  }
}

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

/**
 * Implementation of hook_menu().
 */
function workflow_menu($may_cache) {
  $items = array();
  $access = user_access('administer workflow');
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/build/workflow',
      'title' => t('Workflow'),
      'access' => $access,
      'callback' => 'workflow_overview',
      'description' => t('Allows the creation and assignment of arbitrary workflows to node types.'),
    );
    $items[] = array(
      'path' => 'admin/build/workflow/edit',
      'title' => t('Edit workflow'),
      'type' => MENU_CALLBACK,
      'callback' => 'workflow_edit_page',
    );
    $items[] = array(
      'path' => 'admin/build/workflow/list',
      'title' => t('List'),
      'weight' => -10,
      'callback' => 'workflow_page',
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/build/workflow/add',
      'title' => t('Add workflow'),
      'weight' => -8,
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'workflow_add_form',
      ),
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/build/workflow/state',
      'title' => t('Add state'),
      'type' => MENU_CALLBACK,
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'workflow_state_add_form',
      ),
    );
    $items[] = array(
      'path' => 'admin/build/workflow/state/delete',
      'title' => t('Delete State'),
      'type' => MENU_CALLBACK,
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'workflow_state_delete_form',
      ),
    );
    $items[] = array(
      'path' => 'admin/build/workflow/delete',
      'title' => t('Delete workflow'),
      'type' => MENU_CALLBACK,
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'workflow_delete_form',
      ),
    );
    $items[] = array(
      'path' => 'admin/build/workflow/actions',
      'title' => t('Workflow actions'),
      'type' => MENU_CALLBACK,
      'callback' => 'workflow_actions_page',
    );
    $items[] = array(
      'path' => 'admin/build/workflow/actions/remove',
      'title' => t('Workflow actions'),
      'type' => MENU_CALLBACK,
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        '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) {
  $node = node_load($nid);
  drupal_set_title(check_plain($node->title));
  $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>' . t('Current state: @state', array(
    '@state' => $states[$current],
  )) . "</p>\n";
  $output .= drupal_get_form('workflow_tab_form', $node, $wid, $states, $current);
  $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_form(&$node, $wid, $states, $current) {
  $form = array();
  $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)
    $wid = workflow_get_workflow_for_type($node->type);
    $name = check_plain(workflow_get_name($wid));

    // see if scheduling information is present
    if ($node->_workflow_scheduled_timestamp && $node->_workflow_scheduled_sid) {
      global $user;
      if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) {
        $timezone = $user->timezone;
      }
      else {
        $timezone = variable_get('date_default_timezone', 0);
      }
      $current = $node->_workflow_scheduled_sid;

      // the default value should be the upcoming sid
      $timestamp = $node->_workflow_scheduled_timestamp;
      $comment = $node->_workflow_scheduled_comment;
    }
    workflow_node_form($form, t('Change %s state', array(
      '%s' => $name,
    )), $name, $current, $choices, $timestamp, $comment);
    $form['node'] = array(
      '#type' => 'value',
      '#value' => $node,
    );
    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Submit'),
    );
  }
  return $form;
}
function workflow_tab_form_submit($form_id, $form_values) {
  $node = $form_values['node'];

  // mockup a node so we don't need to repeat the code for processing this
  $node->workflow = $form_values['workflow'];
  $node->workflow_comment = $form_values['workflow_comment'];
  $node->workflow_scheduled = $form_values['workflow_scheduled'];
  $node->workflow_scheduled_date = $form_values['workflow_scheduled_date'];
  $node->workflow_scheduled_hour = $form_values['workflow_scheduled_hour'];

  // call node save to make sure all saving properties run on this node
  node_save($node);
  return 'node/' . $node->nid;
}

/**
 * Implementation of hook_nodeapi().
 * Summary of nodeapi ops we can see (Drupal 4.7):
 *
 *          preview  submit                 submit   preview
 *          (from    (from                  (from    (from
 * add      add)     add)     view  edit    edit)    edit)
 * -------- -------- -------- ----- ------- -------- --------
 *                            load  load    load     load
 * prepare  prepare  prepare        prepare prepare  prepare
 *          validate validate               validate validate
 *          view              view                   view
 *                   submit                 submit
 *                   insert                 update
 *
 */
function workflow_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
  switch ($op) {
    case 'load':
      $node->_workflow = workflow_node_current_state($node);

      // scheduling information
      $res = db_query('SELECT * FROM {workflow_scheduled_transition} WHERE nid = %d', $node->nid);
      if ($row = db_fetch_object($res)) {
        $node->_workflow_scheduled_sid = $row->sid;
        $node->_workflow_scheduled_timestamp = $row->scheduled;
        $node->_workflow_scheduled_comment = $row->comment;
      }
      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))) {

        // check to see if this is an immediate change or a scheduled change
        if (!$node->workflow_scheduled) {
          workflow_execute_transition($node, $sid, $node->workflow_comment);

          // do transition
        }
        else {

          // schedule the the time to change the state
          $nid = $node->nid;
          $comment = $node->workflow_comment;
          $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 (!$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 && strlen($user->timezone)) {
              $timezone = $user->timezone;
            }
            else {
              $timezone = variable_get('date_default_timezone', 0);
            }
            $scheduled = $scheduled - $timezone;

            // clear previous entries and insert
            db_query("DELETE FROM {workflow_scheduled_transition} WHERE nid = {$nid}");
            db_query('INSERT INTO {workflow_scheduled_transition} VALUES (%d, %d, %d, %d,\'%s\')', $nid, $old_sid, $sid, $scheduled, $comment);
            drupal_set_message(t("@node_title is scheduled for state change on !scheduled_date", array(
              "@node_title" => $node->title,
              "!scheduled_date" => format_date($scheduled),
            )));
          }
        }
      }
      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, $timestamp = NULL, $comment = NULL) {
  if (sizeof($choices) == 1) {
    $form['workflow'][$name] = array(
      '#type' => 'hidden',
      '#value' => $current,
    );
  }
  else {
    $form['workflow'][$name] = array(
      '#type' => 'radios',
      '#title' => $title,
      '#options' => $choices,
      '#name' => $name,
      '#parents' => array(
        'workflow',
      ),
      '#default_value' => $current,
    );

    // HACK: using arg to see if we're on a node add page
    // i have to do this because current is never creation!
    // scheduling from transition is major bad news
    if (arg(1) != 'add' && user_access('schedule workflow transitions')) {
      $scheduled = $timestamp ? 1 : 0;
      $timestamp = $scheduled ? $timestamp : time();
      $form['workflow']['workflow_scheduled'] = array(
        '#type' => 'radios',
        '#title' => t('Schedule'),
        '#options' => array(
          t('Immediately'),
          t('Schedule for state change at:'),
        ),
        '#default_value' => $scheduled,
      );
      $form['workflow']['workflow_scheduled_date'] = array(
        '#type' => 'date',
        '#default_value' => array(
          'day' => format_date($timestamp, 'custom', 'j'),
          'month' => format_date($timestamp, 'custom', 'n'),
          '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(time()),
        '#default_value' => $scheduled ? $hours : NULL,
      );
    }
    $form['workflow']['workflow_comment'] = array(
      '#type' => 'textarea',
      '#title' => t('Comment'),
      '#description' => t('A comment to put in the workflow log.'),
      '#default_value' => $comment,
    );
  }
}

/**
 * Generate a forms API compliant workflow field.
 *
 * @param object &$node
 * @return array
 */
function workflow_form_alter($form_id, &$form) {
  if (isset($form['type']) && $form['#base'] == 'node_form') {
    $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;
    }
    $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];
    }
    if (sizeof($choices) > 1) {
      $form['workflow'] = array(
        '#type' => 'fieldset',
        '#title' => $name,
        '#collapsible' => TRUE,
        '#collapsed' => FALSE,
        '#weight' => 10,
      );
    }

    // see if scheduling information is present
    if ($node->_workflow_scheduled_timestamp && $node->_workflow_scheduled_sid) {
      global $user;
      if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) {
        $timezone = $user->timezone;
      }
      else {
        $timezone = variable_get('date_default_timezone', 0);
      }
      $current = $node->_workflow_scheduled_sid;

      // the default value should be the upcoming sid
      $timestamp = $node->_workflow_scheduled_timestamp;
      $comment = $node->_workflow_scheduled_comment;
    }
    workflow_node_form($form, $name, $name, $current, $choices, $timestamp, $comment);
  }
}

/**
 * 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 && !$node->_workflow_scheduled_comment) {
      $node->workflow_stamp = time();
      db_query("UPDATE {workflow_node} SET stamp = %d WHERE nid = %d", $node->workflow_stamp, $node->nid);
      $result = module_invoke_all('workflow', 'transition pre', $old_sid, $sid, $node);
      _workflow_write_history($node, $sid, $comment);
    }
    $result = module_invoke_all('workflow', 'transition post', $old_sid, $sid, $node);
    return;
  }

  // Make sure this transition is valid and allowed for the current user.
  global $user;
  if ($user->uid > 1 || $user->uid == 0) {

    // 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 = node_get_types('name', $node->type);
  watchdog('workflow', t('State of @type %node_title set to @state_name', array(
    '@type' => $type,
    '%node_title' => $node->title,
    '@state_name' => $state_name,
  )), WATCHDOG_NOTICE, l('view', 'node/' . $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);

  // clear any references in the scheduled listing
  db_query('DELETE FROM {workflow_scheduled_transition} WHERE nid = %d', $node->nid);
}

/**
 * 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 the 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) {

    // 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)
    unset($transitions[$current_sid]);
  }
  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) {
  static $cache;
  if (!isset($cache[$wid])) {
    $result = db_result(db_query("SELECT sid FROM {workflow_states} WHERE " . "wid=%d AND sysid=%d", $wid, WORKFLOW_CREATION));
    $cache[$wid] = $result;
  }
  return $cache[$wid];
}

/**
 * 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'),
    '#maxlength' => '254',
    '#default_value' => $name,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Add Workflow'),
  );
  return $form;
}
function workflow_add_form_validate($form_id, $form_values) {
  $workflow_name = $form_values['wf_name'];
  $workflows = array_flip(workflow_get_all());

  // Make sure a nonblank workflow name is provided
  if ($workflow_name == '') {
    form_set_error('wf_name', t('Please provide a nonblank name for the new workflow.'));
  }

  // Make sure workflow name is not a duplicate
  if (array_key_exists($workflow_name, $workflows)) {
    form_set_error('wf_name', t('A workflow with the name %name already exists. Please enter another name for your new workflow.', array(
      '%name' => $workflow_name,
    )));
  }
}
function workflow_add_form_submit($form_id, $form_values) {
  $workflow_name = $form_values['wf_name'];
  if (array_key_exists('wf_name', $form_values) && $workflow_name != '') {
    workflow_create($workflow_name);
    watchdog('workflow', t('Created workflow %name', array(
      '%name' => $workflow_name,
    )));
    drupal_set_message(t('The workflow %name was created. You should now add states to your workflow.', array(
      '%name' => $workflow_name,
    )), 'warning');
    return 'admin/build/workflow';
  }
}

/**
 * Create the form for confirmation of deleting a workflow.
 *
 * @param $wid
 *   The ID of the workflow.
 *
 * @return
 *   HTML form.
 *
 */
function workflow_delete_form($wid, $sid = NULL) {
  if (isset($sid)) {
    return workflow_state_delete_form($wid, $sid);
  }
  $form = array();
  $form['wid'] = array(
    '#type' => 'value',
    '#value' => $wid,
  );
  return confirm_form($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' => workflow_get_name($wid),
  )), $_GET['destination'] ? $_GET['destination'] : 'admin/build/workflow', t('This action cannot be undone.'), t('Delete'), t('Cancel'));
}
function workflow_delete_form_submit($form_id, $form_values) {
  if ($form_values['confirm'] == 1) {
    $workflow_name = workflow_get_name($form_values['wid']);
    workflow_deletewf($form_values['wid']);
    watchdog('workflow', t('Deleted workflow %name with all its states', array(
      '%name' => $workflow_name,
    )));
    drupal_set_message(t('The workflow %name with all its states was deleted.', array(
      '%name' => $workflow_name,
    )));
    return 'admin/build/workflow';
  }
}

/**
 * View workflow permissions by role
 *
 * @param $wid
 *   The ID of the workflow
 */
function workflow_permissions($wid) {
  $name = workflow_get_name($wid);
  $all = array();
  $roles = array(
    'author' => t('author'),
  ) + user_roles();
  foreach ($roles as $role => $value) {
    $all[$role]['name'] = $value;
  }
  $result = db_query("SELECT t.roles, s1.state AS state_name, s2.state AS target_state_name " . "FROM {workflow_transitions} t " . "INNER JOIN {workflow_states} s1 ON s1.sid = t.sid " . "INNER JOIN {workflow_states} s2 ON s2.sid = t.target_sid " . "WHERE s1.wid = %d " . "ORDER BY s1.weight ASC , s1.state ASC , s2.weight ASC , s2.state ASC", $wid);
  while ($data = db_fetch_object($result)) {
    foreach (explode(',', $data->roles) as $role) {
      $all[$role]['transitions'][] = array(
        $data->state_name,
        WORKFLOW_ARROW,
        $data->target_state_name,
      );
    }
  }
  $output = '';
  $header = array(
    t('From'),
    '',
    t('To'),
  );
  foreach ($all as $role => $value) {
    $output .= '<h3>' . t("%role may do these transitions:", array(
      '%role' => $value['name'],
    )) . '</h3>';
    if ($value['transitions']) {
      $output .= theme('table', $header, $value['transitions']) . '<p></p>';
    }
    else {
      $output .= '<table><tbody><tr class="even"><td>' . t('None') . '</td><td></tr></tbody></table><p></p>';
    }
  }
  return $output;
}

/**
 * Menu callback to edit a workflow's properties.
 *
 * @param $wid
 *   The ID of the workflow.
 *
 * @return
 *   HTML form.
 *
 */
function workflow_edit_page($wid) {
  $output = drupal_get_form('workflow_edit_form', $wid);
  $output .= workflow_permissions($wid);
  return $output;
}
function workflow_edit_form($wid) {
  $form = array();

  #$form['#theme'] = 'workflow_edit_form';
  $workflow = db_fetch_object(db_query("SELECT * FROM {workflows} WHERE wid = %d", $wid));
  $form['wid'] = array(
    '#type' => 'value',
    '#value' => $workflow->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($workflow->wid);
  $form['transitions']['#tree'] = TRUE;
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save'),
  );
  return $form;
}
function theme_workflow_edit_form($form) {
  $output = drupal_render($form['wf_name']);
  $wid = $form['wid']['#value'];
  $states = workflow_get_states($wid);
  drupal_set_title(t('Edit workflow %name', array(
    '%name' => workflow_get_name($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 != t('(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 == t('(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 .= drupal_render($form['transitions'][$from][$to][$rid]);

            //$cell .= drupal_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 .= drupal_render($form);
  return $output;
}
function workflow_edit_form_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
    $workflow_name = $form_values['wf_name'];
    $workflows = array_flip(workflow_get_all());
    if (array_key_exists($workflow_name, $workflows) && $wid != $workflows[$workflow_name]) {
      form_set_error('wf_name', t('A workflow with the name %name already exists. Please enter another name for this workflow.', array(
        '%name' => $workflow_name,
      )));
    }
  }
  else {
    form_set_error('wf_name', t('Please provide a nonblank name for this workflow.'));
  }

  // Make sure 'author' is checked for (creation) -> [something]
  $creation_id = _workflow_creation_state($wid);
  if (is_array($form_values['transitions'][$creation_id])) {
    foreach ($form_values['transitions'][$creation_id] as $to => $roles) {
      if ($roles['author']) {
        $author_has_permission = true;
        break;
      }
    }
  }
  if (!$author_has_permission) {
    form_set_error('transitions', t('Please give the author permission to go from %creation to at least one state!', array(
      '%creation' => '(creation)',
    )));
  }
}
function workflow_edit_form_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('The workflow was updated.'));
  return 'admin/build/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_form($wid, $sid = NULL) {
  $form = array();
  $form['wid'] = array(
    '#type' => 'value',
    '#value' => $wid,
  );
  if (isset($sid)) {
    $state = workflow_get_state($sid);
    if (isset($state)) {
      drupal_set_title(t('Edit workflow state %state', array(
        '%state' => $state['state'],
      )));
      $form['sid'] = array(
        '#type' => 'value',
        '#value' => $sid,
      );
    }
  }

  // If we don't have a state or db_fetch_array returned FALSE, load defaults.
  if (!isset($state) || $state === FALSE) {
    $state = array(
      'state' => '',
      'weight' => 0,
    );
    drupal_set_title(t('Add a new state to workflow %workflow', array(
      '%workflow' => workflow_get_name($wid),
    )));
  }
  $form['state'] = array(
    '#type' => 'textfield',
    '#title' => t('State name'),
    '#default_value' => $state['state'],
    '#size' => '16',
    '#maxlength' => '254',
    '#required' => TRUE,
    '#description' => 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>.'),
  );
  $form['weight'] = array(
    '#type' => 'weight',
    '#title' => t('Weight'),
    '#default_value' => $state['weight'],
    '#description' => t('In listings, the heavier states will sink and the lighter states will be positioned nearer the top.'),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save'),
  );
  return $form;
}
function workflow_state_add_form_validate($form_id, $form_values) {
  $state_name = $form_values['state'];
  $wf_states = array_flip(workflow_get_states($form_values['wid']));
  if (array_key_exists('sid', $form_values)) {

    // Validate changes to existing state:
    // Make sure a nonblank state name is provided
    if ($state_name == '') {
      form_set_error('state', t('Please provide a nonblank name for this state.'));
    }

    // Make sure changed state name is not a duplicate
    if (array_key_exists($state_name, $wf_states) && $form_values['sid'] != $wf_states[$state_name]) {
      form_set_error('state', t('A state with the name %state already exists in this workflow. ' . 'Please enter another name for this state.', array(
        '%state' => $state_name,
      )));
    }
  }
  else {

    // Validate new state:
    // Make sure a nonblank state name is provided
    if ($state_name == '') {
      form_set_error('state', t('Please provide a nonblank name for the new state.'));
    }

    // Make sure state name is not a duplicate
    if (array_key_exists($state_name, $wf_states)) {
      form_set_error('state', t('A state with the name %state already exists in this workflow. ' . 'Please enter another name for your new state.', array(
        '%state' => $state_name,
      )));
    }
  }
}
function workflow_state_add_form_submit($form_id, $form_values) {
  workflow_state_save($form_values);
  if (array_key_exists('sid', $form_values)) {
    drupal_set_message(t('The workflow state was updated.'));
  }
  else {
    watchdog('workflow', t('Created workflow state %name', array(
      '%name' => $form_values['state'],
    )));
    drupal_set_message(t('The workflow state %name was created.', array(
      '%name' => $form_values['state'],
    )));
  }
  return 'admin/build/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()) {

  // empty string is sometimes passed in instead of an array
  if (!$transitions) {
    return;
  }
  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}_tid'), $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) {
  $form = array();
  $roles = workflow_get_roles();
  $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() {
  $workflows = workflow_get_all();
  $row = array();
  foreach ($workflows as $wid => $name) {
    $links = array(
      'workflow_overview_add_state' => array(
        'title' => t('Add state'),
        'href' => "admin/build/workflow/state/{$wid}",
      ),
      'workflow_overview_actions' => array(
        'title' => t('Actions'),
        'href' => "admin/build/workflow/actions/{$wid}",
      ),
      'workflow_overview_edit' => array(
        'title' => t('Edit'),
        'href' => "admin/build/workflow/edit/{$wid}",
      ),
      'workflow_overview_delete' => array(
        'title' => t('Delete'),
        'href' => "admin/build/workflow/delete/{$wid}",
      ),
    );
    $states = workflow_get_states($wid);
    if (!(module_exists('actions') && $states)) {
      unset($links['workflow_overview_actions']);
    }
    $row[] = array(
      $name,
      theme('links', $links),
    );
    $subrows = array();
    foreach ($states as $sid => $statename) {
      if (!workflow_is_system_state(t($statename))) {
        $statelinks = array(
          'workflow_overview_edit_state' => array(
            'title' => t('Edit'),
            'href' => "admin/build/workflow/state/{$wid}/{$sid}",
          ),
          'workflow_overview_delete_state' => array(
            'title' => t('Delete'),
            'href' => "admin/build/workflow/state/delete/{$wid}/{$sid}",
          ),
        );
      }
      $subrows[] = array(
        t($statename),
        theme('links', $statelinks),
      );
      unset($statelinks);
    }
    $subheader_state = array(
      'data' => t('State'),
      'style' => 'width: 30%',
    );
    $subheader_operations = array(
      'data' => t('Operations'),
      'style' => 'width: 70%',
    );
    $subheader_style = array(
      'style' => 'width: 100%; margin: 3px 20px 20px;',
    );
    $subtable = theme('table', array(
      $subheader_state,
      $subheader_operations,
    ), $subrows, $subheader_style);
    $row[] = array(
      array(
        'data' => $subtable,
        'colspan' => '2',
      ),
    );
  }
  if ($row) {
    $output = theme('table', array(
      t('Workflow'),
      t('Operations'),
    ), $row);
  }
  else {
    $output = '<p>' . t('No workflows have been added. Would you like to <a href="@link">add a workflow</a>?', array(
      '@link' => url('admin/build/workflow/add'),
    )) . '</p>';
  }
  $output .= drupal_get_form('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,
  );
  return confirm_form($form, t('Are you sure you want to delete %title (and all its transitions)?', array(
    '%title' => $states[$sid],
  )), $_GET['destination'] ? $_GET['destination'] : 'admin/build/workflow', t('This action cannot be undone.'), t('Delete'), t('Cancel'));
}
function workflow_state_delete_form_submit($form_id, $form_values) {
  $states = workflow_get_states($form_values['wid']);
  $state_name = $states[$form_values['sid']];
  if ($form_values['confirm'] == 1) {
    workflow_state_delete($form_values['sid']);
    watchdog('workflow', t('Deleted workflow state %name', array(
      '%name' => $state_name,
    )));
    drupal_set_message(t('The workflow state %name was deleted.', array(
      '%name' => $state_name,
    )));
  }
  return 'admin/build/workflow';
}
function workflow_types_form() {
  $form = array();
  $workflows = array(
    '<' . t('None') . '>',
  ) + workflow_get_all();
  if (count($workflows) == 0) {
    return $form;
  }
  $type_map = array();
  $result = db_query("SELECT wid, type FROM {workflow_type_map}");
  while ($data = db_fetch_object($result)) {
    $type_map[$data->type] = $data->wid;
  }
  $form['#theme'] = 'workflow_types_form';
  $form['help'] = array(
    '#type' => 'item',
    '#value' => t('Each node type may have a separate workflow:'),
  );
  foreach (node_get_types('names') as $type => $name) {
    $form[$type] = array(
      '#type' => 'select',
      '#title' => $name,
      '#options' => $workflows,
      '#default_value' => isset($type_map[$type]) ? $type_map[$type] : 0,
    );
  }
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save Workflow Mapping'),
  );
  return $form;
}
function theme_workflow_types_form($form) {
  $header = array(
    t('Node Type'),
    t('Workflow'),
  );
  $row = array();
  foreach (element_children($form) as $key) {
    if ($form[$key]['#type'] == 'select') {
      $name = $form[$key]['#title'];
      unset($form[$key]['#title']);
      $row[] = array(
        $name,
        drupal_render($form[$key]),
      );
    }
  }
  $output = drupal_render($form['help']);
  $output .= theme('table', $header, $row);
  return $output . drupal_render($form);
}
function workflow_types_form_submit($form_id, $form_values) {
  workflow_types_save($form_values);
  drupal_set_message(t('The workflow mapping was saved.'));
  return 'admin/build/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($form, t('Are you sure you want to delete the action %title?', array(
    '%title' => $actions[$aid]['description'],
  )), $_GET['destination'] ? $_GET['destination'] : 'admin/build/workflow/actions/' . $wid, 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' => $actions[$aid],
    )));
    return 'admin/workflow/actions/' . $form_values['wid'];
  }
}
function workflow_actions_page($wid) {
  if (!module_exists('actions')) {
    drupal_set_message(t('Before you can assign actions you must install and enable the actions module.'), 'error');
    drupal_goto('admin/build/workflow');
  }
  $options = array(
    t('None'),
  );
  foreach (actions_actions_map(actions_get_all_actions()) as $aid => $action) {
    $options[$action['type']][$aid] = $action['description'];
  }
  $header = array(
    array(
      'data' => t('Transition'),
      'colspan' => '3',
    ),
    array(
      'data' => t('Actions'),
    ),
  );
  $rows = array();
  $states = workflow_get_states($wid);
  foreach ($states as $sid => $state) {

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

      // Don't allow actions on same state transaction... Should we allow it?
      if ($to_sid == $sid) {
        continue;
      }
      $tid = workflow_get_transition_id($sid, $to_sid);
      $rows[] = array(
        array(
          'data' => $state,
        ),
        array(
          'data' => WORKFLOW_ARROW,
        ),
        array(
          'data' => $name,
        ),
        array(
          'data' => drupal_get_form('workflow_actions_form', $wid, $tid, $options),
        ),
      );
    }
  }
  if (count($rows) == 0) {
    $output = t('You must first <a href="@link">set up transitions</a> before you can assign actions.', array(
      '@link' => url('admin/build/workflow/edit/' . $wid),
    ));
  }
  else {
    $output = theme('table', $header, $rows);
  }
  return $output;
}
function workflow_actions_form($wid, $tid, $available_options) {
  $form['tid'] = array(
    '#type' => 'hidden',
    '#value' => $tid,
  );
  $form['#multistep'] = true;

  // get and list the actions that are already assigned to this transition
  $actions_this_tid = workflow_get_actions($tid);
  foreach ($actions_this_tid as $aid => $act_name) {
    $form['remove'][$aid] = array(
      '#type' => 'item',
      '#title' => $act_name,
      '#value' => l(t('remove'), "admin/build/workflow/actions/remove/{$wid}/{$tid}/{$aid}"),
    );
    unset($available_options[md5($aid)]);
  }

  // list possible actions that may be assigned
  if (count($available_options) > 1) {
    $form['action'] = array(
      '#type' => 'select',
      '#options' => $available_options,
    );
    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Add'),
    );
  }
  return $form;
}
function theme_workflow_actions_form($form) {
  $row = array();

  // put each existing action and remove link on it's own row
  foreach (element_children($form['remove']) as $key) {
    $name = $form['remove'][$key]['#title'];
    unset($form['remove'][$key]['#title']);
    $row[] = array(
      $name,
      drupal_render($form['remove'][$key]),
    );
  }

  // and if there are form items for a "new" action put that on a row
  if (isset($form['action'])) {
    $row[] = array(
      drupal_render($form['action']),
      drupal_render($form['submit']),
    );
  }
  $output .= theme('table', array(), $row);
  return $output . drupal_render($form);
}
function workflow_actions_form_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(4);
  return "admin/build/workflow/actions/{$wid}";
}
function workflow_actions_remove_form_submit($form_id, $form_values) {
  if ($form_values['confirm'] == 1) {
    $aid = $form_values['aid'];
    $actions = actions_get_all_actions();
    $action = $actions[$aid]['description'];
    workflow_actions_remove($form_values['tid'], $aid);
    watchdog('workflow', t('Action %action deleted', array(
      '%action' => $action,
    )));
    drupal_set_message(t('Action %action deleted.', array(
      '%action' => $action,
    )));
  }
  return 'admin/build/workflow/actions/' . $form_values['wid'];
}

/**
 * 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) {
  static $cache;
  if (!isset($cache[$type])) {
    $wid = db_result(db_query("SELECT wid FROM {workflow_type_map} WHERE type = '%s'", $type));
    $cache[$type] = $wid;
  }
  else {
    $wid = $cache[$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 wid, name FROM {workflows} ORDER BY name ASC");
  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}_wid');
  db_query("INSERT INTO {workflows} (wid, name) VALUES (%d, '%s')", $wid, $name);
  workflow_state_save(array(
    'wid' => $wid,
    'state' => t('(creation)'),
    'sysid' => WORKFLOW_CREATION,
    'weight' => WORKFLOW_CREATION_DEFAULT_WEIGHT,
  ));
  return $wid;
}

/**
 * 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);
}

/**
 * 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 weight, state", intval($wid));
  while ($data = db_fetch_object($result)) {
    $states[$data->sid] = $data->state;
  }
  return $states;
}

/**
 * Given the ID of a workflow, return an array with all its attributes.
 *
 * @param $sid
 *   The ID of the workflow state.
 *
 * @return
 *   An array with all attributes of the state.
 */
function workflow_get_state($sid) {
  $state = array();
  $result = db_query('SELECT wid, state, weight, sysid FROM {workflow_states} WHERE sid = %d', intval($sid));
  while ($data = db_fetch_object($result)) {
    $state['wid'] = $data->wid;
    $state['state'] = $data->state;
    $state['weight'] = $data->weight;
    $state['sysid'] = $data->sysid;
  }
  return $state;
}

/**
 * Add or update a workflow state to the database.
 *
 * @param $edit
 *   An array containing values for the new or updated workflow state.
 *
 * @return
 *   The ID of the new or updated workflow state.
 */
function workflow_state_save($edit) {
  $defaults = array(
    'weight' => 0,
    'sysid' => 0,
  );
  $edit = array_merge($defaults, $edit);
  if (!isset($edit['sid'])) {
    $edit['sid'] = db_next_id('{workflow_states}_sid');
    db_query("INSERT INTO {workflow_states} (sid, wid, state, sysid, weight) VALUES (%d, %d, '%s', %d, %d)", $edit['sid'], $edit['wid'], $edit['state'], $edit['sysid'], $edit['weight']);
  }
  else {
    db_query("UPDATE {workflow_states} SET wid = %d, state = '%s', sysid = %d, weight = %d WHERE sid = %d", $edit['wid'], $edit['state'], $edit['sysid'], $edit['weight'], $edit['sid']);
  }
  return $edit['sid'];
}

/**
 * Legacy code for workflow_state_create().
 */
function workflow_state_create($wid, $name) {
  return workflow_state_save(array(
    'wid' => $wid,
    'state' => $name,
  ));
}

/**
 * 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, s.weight as state_weight FROM " . "{workflow_transitions} t INNER JOIN {workflow_states} s ON s.sid = " . "t.%s WHERE t.%s = %d) " . "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 = %d) " . "ORDER BY state_weight", $field, $field, $field_where, $sid, $sid);
  while ($t = db_fetch_object($result)) {
    if ($roles == 'ALL' || $t->tid == $t->state_id || 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 AND 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;
  $node->workflow_stamp = time();
  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, stamp = %d WHERE nid = %d", $sid, $user->uid, $node->workflow_stamp, $node->nid);
  }
  else {
    db_query("INSERT INTO {workflow_node} (nid, sid, uid, stamp) VALUES (%d, %d, %d, %d)", $node->nid, $sid, $user->uid, $node->workflow_stamp);
  }
  _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, $node->workflow_stamp);
}

/**
 * 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',
        'list-type' => 'list',
        'value-type' => 'array',
        '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;
  $table = array(
    'name' => 'workflow_node_history',
    'provider' => 'workflow',
    'join' => array(
      'left' => array(
        'table' => 'workflow_node',
        'field' => 'nid',
      ),
      'right' => array(
        'field' => 'nid',
      ),
      'extra' => array(
        'stamp = workflow_node.stamp' => NULL,
      ),
    ),
    "fields" => array(
      'comment' => array(
        'name' => t('Workflow: comment'),
        'sortable' => false,
        'help' => t('Display the most recent workflow comment 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;
  }
}

/**
 * Implementation of hook_cron
 */
function workflow_cron() {
  $clear_cache = FALSE;

  //if the time now is greater than the time to publish a node, publish it
  $nodes = db_query('SELECT * FROM {workflow_scheduled_transition} s WHERE s.scheduled > 0 AND s.scheduled < %d', time());
  while ($row = db_fetch_object($nodes)) {
    $node = node_load($row->nid);

    // make sure transition is still valid
    if ($node->_workflow == $row->old_sid) {

      // do transistion
      workflow_execute_transition($node, $row->sid, $row->comment);
      watchdog('content', t('%type: scheduled transition of %title.', array(
        '%type' => t($node->type),
        '%title' => $node->title,
      )), WATCHDOG_NOTICE, l(t('view'), 'node/' . $node->nid));
      $clear_cache = TRUE;
    }
  }
  if ($clear_cache) {

    // clear the cache so an anonymous poster can see the node being published or unpublished
    cache_clear_all();
  }
}

/**
 * Implementation of hook_token_values()
 */
function workflow_token_values($type, $object = NULL) {
  $values = array();
  switch ($type) {
    case "node":
    case 'workflow':
      $node = (object) $object;
      if ($wid = workflow_get_workflow_for_type($node->type)) {
        $values['workflow-name'] = workflow_get_name($wid);
        $states = workflow_get_states($wid);
      }
      else {
        break;
      }
      $result = db_query_range("SELECT h.* FROM {workflow_node_history} h WHERE nid = %d ORDER BY stamp DESC", $node->nid, 0, 1);
      if ($row = db_fetch_object($result)) {
        $account = user_load(array(
          'uid' => $row->uid,
        ));
        $comment = $row->comment;
      }
      $values['workflow-current-state-name'] = $states[$row->sid];
      $values['workflow-old-state-name'] = $states[$row->old_sid];
      $values['workflow-current-state-date-iso'] = date('Ymdhis', $row->stamp);
      $values['workflow-current-state-date-tstamp'] = $row->stamp;
      $values['workflow-current-state-date-formatted'] = date('M d, Y h:i:s', $row->stamp);
      $values['workflow-current-state-updating-user-name'] = $account->uid ? check_plain($account->name) : variable_get('anonymous', 'Anonymous');
      $values['workflow-current-state-updating-user-uid'] = $account->uid;
      $values['workflow-current-state-updating-user-mail'] = $account->uid ? check_plain($account->mail) : '';
      $values['workflow-current-state-log-entry'] = filter_xss($row->comment, array(
        'a',
        'em',
        'strong',
      ));
      break;
  }
  return $values;
}

/**
 * Implementation of hook_token_list()
 */
function workflow_token_list($type = 'all') {
  if ($type == 'workflow' || $type == 'node' || $type == 'all') {
    $tokens['workflow']['workflow-name'] = 'Name of workflow appied to this node';
    $tokens['workflow']['workflow-current-state-name'] = 'Current state of content';
    $tokens['workflow']['workflow-old-state-name'] = 'Old state of content';
    $tokens['workflow']['workflow-current-state-date-iso'] = 'Date of last state change (ISO)';
    $tokens['workflow']['workflow-current-state-date-tstamp'] = 'Date of last state change (timestamp)';
    $tokens['workflow']['workflow-current-state-date-formatted'] = 'Date of last state change (formated - M d, Y h:i:s)';
    $tokens['workflow']['workflow-current-state-updating-user-name'] = 'Username of last state changer';
    $tokens['workflow']['workflow-current-state-updating-user-uid'] = 'uid of last state changer';
    $tokens['workflow']['workflow-current-state-updating-user-mail'] = 'email of last state changer';
    $tokens['workflow']['workflow-current-state-log-entry'] = 'Last workflow comment log';
    $tokens['node'] = $tokens['workflow'];
  }
  return $tokens;
}

Functions

Namesort descending Description
action_workflow_execute_transition Implementation of a Drupal action. Changes the workflow state of a node to the next state of the workflow.
theme_workflow_actions_form
theme_workflow_edit_form
theme_workflow_types_form
workflow_actions_form
workflow_actions_form_submit
workflow_actions_page
workflow_actions_remove
workflow_actions_remove_confirm_submit
workflow_actions_remove_form
workflow_actions_remove_form_submit
workflow_actions_save
workflow_add_form Create the form for adding/editing a workflow.
workflow_add_form_submit
workflow_add_form_validate
workflow_allowable_transitions Get allowable transitions for a given workflow state.
workflow_create Create a workflow and its (creation) state.
workflow_cron Implementation of hook_cron
workflow_deletewf 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.
workflow_delete_form Create the form for confirmation of deleting a workflow.
workflow_delete_form_submit
workflow_edit_form
workflow_edit_form_submit
workflow_edit_form_validate
workflow_edit_page Menu callback to edit a workflow's properties.
workflow_execute_transition Execute a transition (change state of a node).
workflow_field_choices Get the states one can move to for a given node.
workflow_form_alter Generate a forms API compliant workflow field.
workflow_get_actions Get the actions associated with a given transition.
workflow_get_all Get names and IDS of all workflows from the database.
workflow_get_name Given the ID of a workflow, return its name.
workflow_get_roles Function to get a list of roles. Used kind of often.
workflow_get_state Given the ID of a workflow, return an array with all its attributes.
workflow_get_states Load workflow states for a workflow from the database.
workflow_get_transition_id Get the tid of a transition, if it exists.
workflow_get_workflow_for_type Get ID of a workflow for a node type.
workflow_handler_arg_sid Handler to deal with sid as an argument.
workflow_handler_filter_sid Handler to provide a list of workflow states for the filter list
workflow_help Implementation of hook_help().
workflow_is_system_state Tell caller whether a state is a protected system state, such as the creation state.
workflow_menu Implementation of hook_menu().
workflow_nodeapi Implementation of hook_nodeapi(). Summary of nodeapi ops we can see (Drupal 4.7):
workflow_node_current_state Get the current state of a given node.
workflow_node_form Add the actual form widgets for workflow change to the passed in form.
workflow_overview Create the main workflow page, which gives an overview of workflows and workflow states.
workflow_perm Implementation of hook_perm().
workflow_permissions View workflow permissions by role
workflow_state_add_form Menu callback to create form to add a workflow state.
workflow_state_add_form_submit
workflow_state_add_form_validate
workflow_state_create Legacy code for workflow_state_create().
workflow_state_delete 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.
workflow_state_delete_form Create the form for confirmation of deleting a workflow state.
workflow_state_delete_form_submit
workflow_state_save Add or update a workflow state to the database.
workflow_tab_form
workflow_tab_form_submit
workflow_tab_page
workflow_token_list Implementation of hook_token_list()
workflow_token_values Implementation of hook_token_values()
workflow_transition_add_role Add a role to the list of those allowed for a given transition. Add the transition if necessary.
workflow_transition_allowed See if a transition is allowed for a given role.
workflow_transition_delete Delete a transition (and any associated actions).
workflow_transition_delete_role Remove a role from the list of those allowed for a given transition.
workflow_transition_grid_form Build the grid of transitions for defining a workflow.
workflow_types_delete
workflow_types_form
workflow_types_form_submit
workflow_types_save Save mapping of workflow to node type. E.g., "the story node type is using the Foo workflow."
workflow_update Save a workflow's name in the database.
workflow_update_transitions Update the transitions for a workflow.
workflow_views_arguments Implementation of hook_views_arguments()
workflow_views_tables Implementation of hook_views_tables()
workflow_workflow Implementation of hook_workflow().
_workflow_creation_state
_workflow_node_to_state Put a node into a state. No permission checking here; only call this from other functions that know what they're doing.
_workflow_write_history

Constants