Hot-fix a Workbench Moderated Node with a Pre-existing Draft

If you use the Workbench Moderation module, you have undoubtedly created drafts for new or updated content. These drafts can often get stuck in the editorial workflow as they await approval from editors and other stakeholders. However, sometimes the content on the currently published version of the node suddenly needs to be updated before the draft finishes making it through the editorial process. You don't want to blow away all of the hard work you invested in creating the revisions stored in your draft, but you also need to make a change to the live content now!

For the developers out there, just imagine if you were using Git and you could simply stash your changes, make the urgent changes, push them out, and then re-apply your stash and continue work. Then imagine we take that same methodology and apply it to the Workbench Moderation module.

We first need a way to edit the currently published or live version of the node, which the traditional node edit page no longer provides us access to once we have created a draft. So we set up a new page to render an edit page for the live version of the node, which we will call the Hot Fix page.


/**
 * Implements hook_menu().
 */
function workbench_moderation_hotfix_menu() {
  $items['node/%node/hotfix'] = array(
    'title' => 'Hot Fix',
    'page callback' => 'workbench_moderation_hotfix_node_page_hotfix',
    'page arguments' => array(1),
    'access callback' => 'workbench_moderation_hotfix_node_page_hotfix_access',
    'access arguments' => array(1),
    'weight' => 0,
    'type' => MENU_LOCAL_TASK,
    'context' => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
    'file' => 'workbench_moderation.node.inc',
    'file path' => drupal_get_path('module', 'workbench_moderation'),
  );
  return $items;
}

/**
 * Access callback for hotfix node edit page.
 *
 * @param object $node
 *   A standard Drupal node.
 */
function workbench_moderation_hotfix_node_page_hotfix_access($node) {
  $update = node_access('update', $node);
  $node_draft = workbench_moderation_node_current_load($node);
  $node_live = workbench_moderation_node_live_load($node);
  // Only allow users with edit access to use this page when a draft exists.
  if ($update && $node_draft->vid != $node_live->vid) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Callback for hotfix node edit page.
 *
 * Renders a node edit form for the latest published revision instead of
 * the latest draft.
 *
 * @param object $node
 *   A standard Drupal node.
 */
function workbench_moderation_hotfix_node_page_hotfix($node) {
  // Get the current live node.
  $node_live = workbench_moderation_node_live_load($node);
  // Set the hotfix flag.
  $node_live->hotfix = TRUE;
  // Load the latest node revision draft information.
  $hotfix_draft_data = variable_get('workbench_moderation_hotfix_draft_data', array());
  // Get the revision of the latest draft.
  $node_draft = workbench_moderation_node_current_load($node);
  // Store the revision id of the last draft.
  $hotfix_draft_data[$node_draft->nid] = $node_draft->vid;
  // Save the information.
  variable_set('workbench_moderation_hotfix_draft_data', $hotfix_draft_data);
  // Add required includes.
  module_load_include('inc', 'node', 'node.pages');
  // Build the form.
  $form = drupal_get_form($node_live->type . '_node_form', $node_live);
  // Inform the user how hotfixes work and warn them of the workflow bypass.
  $message = t('Hotfixes bypass the tradional workflow system and go directly to a published state. Pre-existing drafts will be maintained to allowed for continued editing of unfinished content.');
  drupal_set_message($message, 'warning', FALSE);
  // Return and render the form.
  return drupal_render($form);
}

Now that we have that setup, we need to handle the submission of this form. We can handle that with a simple hook_node_presave implementation.


/**
 * Implements hook_node_presave().
 *
 * Intercepts the saving of hotfix nodes and documents what was done
 * in the node revision history and adjusts the workflow state.
 */
function workbench_moderation_hotfix_node_presave($node) {
  // Verify that a hotfix is being saved.
  if (!empty($node->hotfix) && $node->hotfix) {
    // Log information about why the new revision was created.
    $log_message = t('Automatically published as a hotfix.');
    if (!empty($node->log)) {
      $node->log = sprintf('%s%s%s', $node->log, PHP_EOL, $log_message);
    }
    else {
      $node->log = $log_message;
    }
    $node->workbench_moderation_state_new = 'published';
  }
}

With our new hot-fixed changes successfully saved, we now turn our attention to ensuring that we don't lose all of the work we put into our draft. To do this next step, we need to install the Hook Post Action module, which offers us hook_node_postupdate. This hook provides us a trigger that fires after the node has been written to the database, ensuring that our hot-fix revision is fully saved and a new revision id has been added to the database. With the revision saved we can then go back, retrieve the previous draft, and save it to the node as a "new" draft, thus restoring our draft and putting things back to the way we found them!


/**
 * Implements hook_node_postupdate().
 *
 * Creates a new draft of a node that had a hotfix revision saved. This keeps
 * the draft created before the hotfix appearing on the node edit page as the
 * most recent draft.
 */
function workbench_moderation_hotfix_node_postupdate($node) {
  // Verify that a hotfix is being saved.
  if (!empty($node->hotfix) && $node->hotfix) {
    // Get hotfix draft revision data.
    $hotfix_draft_data = variable_get('workbench_moderation_hotfix_draft_data', array());
    // Verify that a revision id was set for this node.
    if (!empty($hotfix_draft_data[$node->nid])) {
      // Obtain the vid of the last draft.
      $vid = $hotfix_draft_data[$node->nid];
      // Remove the value from the array.
      unset($hotfix_draft_data[$node->nid]);
      // Store the updated array.
      variable_set('workbench_moderation_hotfix_draft_data', $hotfix_draft_data);
      // Load the node at it's last draft state.
      $node_draft = node_load($node->nid, $vid);
      // Log information about the new draft revision.
      $replacements = array(
        '@vid' => $node_draft->vid,
      );
      $node_draft->log = t('Recreation of draft revision @vid after hotfix.', $replacements);
      // Force the draft to be saved as a new revision.
      $node_draft->revision = TRUE;
      // Get the moderation state of the previous draft.
      $moderation_state = $node_draft->workbench_moderation['my_revision']->state;
      // Set a fallback moderation state of one could not be found.
      if (empty($moderation_state)) {
        $moderation_state = 'authoring';
      }
      // Set the moderation state of the new draft to the state of previous draft.
      $node_draft->workbench_moderation_state_new = $moderation_state;
      // Resave the node with previous draft node state.
      node_save($node_draft);
    }
  }
}

Finally, we need to do a few alterations to the node edit form when performing these hot fix edits to ensure that the UI makes sense.


/**
 * Implements hook_form_alter().
 *
 * Modifies the node edit form when it is being used in the hotfix mode by
 * hiding workflow elements, removing un-needed buttons, and re-labeling
 * buttons for greater usability.
 */
function workbench_moderation_hotfix_form_alter(&$form, &$form_state, $form_id) {
  // Hotfix node form alterations.
  if (strpos($form_id, '_node_form') !== FALSE) {
    if (!empty($form_state['node']->hotfix) && $form_state['node']->hotfix) {
      // Alter the save button text.
      $form['actions']['submit']['#value'] = t('Publish Hotfix');
      // Add a hotfix class so workflow elements can be hidden.
      $form['#attributes']['class'][] = 'hotfix';
    }
  }
}

Now we have an already great module in Workbench Moderation with the added functionality of being able to hot-fix content when a draft already exists. Next we will be creating revision branches and rebasing them. Ok, maybe not, but a developer can dream right?

To download the module, head over to the sandbox project page for Workbench Moderation Hotfix. Feel free to help us make it better or submit feature ideas while you are there.

In the name of full transparency, this module has been tested, but not used extensively yet, so use with caution! As always, test things out in a development environment before you go using something in production!