state_flow.module in State Machine 7.2
Same filename and directory in other branches
An implementation of node revision workflow for Drupal based on the State Machine system.
File
modules/state_flow/state_flow.moduleView source
<?php
/**
* @file
* An implementation of node revision workflow for Drupal based on the
* State Machine system.
*/
/**
* Implements hook_menu().
*/
function state_flow_menu() {
$items = array();
$items['node/%node/workflow'] = array(
'title' => 'Workflow',
'description' => 'Information about the workflow status of this content',
'type' => MENU_LOCAL_TASK,
'page callback' => 'state_flow_events',
'page arguments' => array(
1,
),
'access callback' => 'state_flow_menu_node_access',
'access arguments' => array(
1,
),
'weight' => 10,
'file' => 'state_flow.pages.inc',
);
$items['node/%node/revisions/%/edit'] = array(
'title' => 'Edit an earlier revision',
'load arguments' => array(
3,
),
'page callback' => 'node_page_edit',
'page arguments' => array(
1,
TRUE,
),
'access callback' => 'node_access',
'access arguments' => array(
'update',
1,
),
'file' => 'node.pages.inc',
'file path' => drupal_get_path('module', 'node'),
);
$items['node/%node/revisions/%/workflow'] = array(
'title' => 'Transition a revision to a new workflow state',
'load arguments' => array(
3,
),
'page callback' => 'drupal_get_form',
'page arguments' => array(
'state_flow_events_revision',
1,
5,
),
'access callback' => 'state_flow_events_revisions_access',
'access arguments' => array(
1,
5,
),
'file' => 'state_flow.pages.inc',
'type' => MENU_CALLBACK,
);
$items['admin/content/content-revisions'] = array(
'title' => 'Content Revisions',
'page callback' => 'drupal_get_form',
'page arguments' => array(
'state_flow_content_page',
),
'access arguments' => array(
'administer content revisions',
),
'type' => MENU_NORMAL_ITEM | MENU_LOCAL_TASK,
'file' => 'state_flow.admin.inc',
);
return $items;
}
/**
* Implements hook_permission().
*/
function state_flow_permission() {
return array(
'manage content workflow' => array(
'title' => t('Manage content workflow'),
'description' => t('Manage the content workflow pages and operations.'),
),
'administer content revisions' => array(
'title' => t('Administer content revisions'),
),
);
}
/**
* Implements hook_admin_paths().
*/
function state_flow_admin_paths() {
if (variable_get('node_admin_theme')) {
$paths = array(
'node/*/workflow' => TRUE,
'node/*/revisions/*/edit' => TRUE,
'node/*/revisions/*/workflow' => TRUE,
'node/*/revisions/*/workflow/*' => TRUE,
);
return $paths;
}
}
/**
* Implements hook_ctools_plugin_type().
*/
function state_flow_ctools_plugin_type() {
$plugins = array(
'plugins' => array(
'cache' => TRUE,
'use hooks' => TRUE,
'info file' => TRUE,
'alterable' => TRUE,
'classes' => array(
'handler',
),
),
);
return $plugins;
}
/**
* Implements hook_entity_property_info_alter().
*
* Adds a "state" property on nodes that are configured with state flow.
*/
function state_flow_entity_property_info_alter(&$info) {
if (isset($info['node'])) {
if (!array_key_exists('bundles', $info['node'])) {
return;
}
foreach ($info['node']['bundles'] as $entity_type => $entity_info) {
if (variable_get('state_flow_' . $entity_type, '')) {
$info['node']['bundles'][$entity_type]['properties']['state'] = array(
'label' => t('Workflow state'),
'description' => t('The current workflow state for this node revision.'),
'getter callback' => 'state_flow_entity_get_state',
);
}
}
}
}
/**
* Implements hook_state_flow_plugins().
*/
function state_flow_state_flow_plugins() {
$info = array();
$path = drupal_get_path('module', 'state_flow') . '/plugins';
$info['state_flow'] = array(
'handler' => array(
'class' => 'StateFlow',
'path' => $path,
'file' => 'state_flow.inc',
),
);
return $info;
}
/**
* Implements hook_views_api().
*/
function state_flow_views_api() {
return array(
'api' => 2,
'path' => drupal_get_path('module', 'state_flow') . '/includes/views',
);
}
/**
* Implements hook_node_presave().
*/
function state_flow_node_presave($node) {
// If the node is not new and is not marked to be ignored by
// state_flow_promote_node_revision(), then check its current state.
if (!empty($node->nid) && empty($node->stateflow_ignore_state)) {
$state_flow = state_flow_load_state_machine($node);
//Check to see if we should go through workflow for new nodes
if (!state_flow_skip_workflow($node)) {
if (!$state_flow
->ignore()) {
$state = $state_flow
->get_current_state();
if ($state == 'published') {
// If the node being updated is in the published state, then ensure that
// changes are saved to a new revision.
$node->revision = TRUE;
}
else {
if ($state != 'draft') {
// If the node being updated is not in the draft state, then mark this
// node to be reverted to draft state.
$node->state_flow_revert_draft = TRUE;
}
}
}
}
else {
if ($node->status) {
$state_flow
->fire_event($state_flow
->skip_to_publish(), 1, 'Workflow skipped.');
}
}
}
}
/**
* Implements hook_node_insert().
*/
function state_flow_node_insert($node) {
global $user;
$state_flow = state_flow_load_state_machine($node);
if (!$state_flow
->ignore()) {
$state_flow
->persist();
$state_flow
->write_history($user->uid);
}
}
/**
* Implements hook_node_update().
*/
function state_flow_node_update($node) {
global $user;
$state_flow = state_flow_load_state_machine($node);
$options = $state_flow
->get_states_options();
if (!state_flow_skip_workflow($node)) {
if (!$state_flow
->ignore()) {
if (!empty($node->state_flow_revert_draft) && $state_flow
->get_current_state() !== 'draft' && in_array("draft", $options)) {
$state_flow
->fire_event('to draft');
}
else {
$state_flow
->persist();
if (!empty($node->revision)) {
$state_flow
->write_history($user->uid);
}
}
state_flow_prevent_live_revision($node);
}
}
}
/**
* Implements hook_node_delete().
*/
function state_flow_node_delete($node) {
db_delete('node_revision_states')
->condition('nid', $node->nid)
->execute();
db_delete('node_revision_states_history')
->condition('nid', $node->nid)
->execute();
}
/**
* Implements hook_node_revision_delete().
*/
function state_flow_node_revision_delete($node) {
db_delete('node_revision_states')
->condition('vid', $node->vid)
->execute();
db_delete('node_revision_states_history')
->condition('vid', $node->vid)
->execute();
}
/**
* Menu access callback for accessing the node workflow pages.
*/
function state_flow_menu_node_access($node, $account = NULL) {
global $user;
// If no user account is given, then use the current user.
if (empty($account)) {
$account = $user;
}
// If the user has the "manage content workflow" permission, then allow access
// to workflow pages.
$has_access = user_access('manage content workflow', $account);
//check to see if node type is ignored
$is_ignored = state_flow_load_state_machine($node)
->ignore();
if ($has_access && !$is_ignored) {
$access = TRUE;
}
else {
$access = FALSE;
}
// Allow other modules to alter this decision
drupal_alter('state_flow_menu_node_access', $access, $node, $account);
return $access;
}
/**
* Menu access callback for the node revision workflow transition page.
*/
function state_flow_events_revisions_access($node, $event_name = NULL) {
return !empty($event_name) ? state_flow_access($node, $event_name) : state_flow_menu_node_access($node);
}
/**
* Determine whether a user has permission to transition a node with an event.
*/
function state_flow_access($node, $event_name, $account = NULL) {
global $user;
// If no user account is given, then use the current user.
if (empty($account)) {
$account = $user;
}
// If the user cannot edit the node, then deny access to any events.
if (!state_flow_menu_node_access($node, $account)) {
return FALSE;
}
// Load the state machine for the node and test whether the event is allowed.
$state_flow = state_flow_load_state_machine($node);
$state_event = $state_flow ? $state_flow
->get_event($event_name) : FALSE;
return $state_event ? $state_event
->validate() : FALSE;
}
/**
* Getter callback for the "state" property on node bundles using workflow.
*/
function state_flow_entity_get_state($data, $options, $name, $type, $info) {
$state_flow = state_flow_load_state_machine($data);
return $state_flow
->get_current_state();
}
/**
* Inform external systems about a workflow transition.
*/
function state_flow_invoke_event_handlers(StateMachine $state_machine, $event_key, $uid, $log) {
// Load related objects
$object = $state_machine
->get_object();
if (is_object($object)) {
$state = $state_machine
->get_current_state();
$node = node_load($object->nid, $object->vid);
$author = !empty($node->uid) ? user_load($node->uid) : drupal_anonymous_user();
// Invoke the Rules state_flow_event_fired event.
if ($node && module_exists('rules')) {
rules_invoke_event('state_flow_event_fired', $node, $author, $state);
}
// Make sure Apachesolr gets update
if ($node && module_exists('apachesolr')) {
module_invoke('apachesolr', 'entity_update', $node, 'node');
}
module_invoke_all('state_flow_event_fired', $node, $event_key, $uid, $log);
}
}
/**
* Retrieve the states history for a node.
*/
function state_flow_get_history($nid) {
$history = db_query('
SELECT nrsh.*, u.uid, u.name AS user_name
FROM {node_revision_states_history} nrsh
LEFT JOIN {users} u ON u.uid = nrsh.uid
WHERE nrsh.nid = :nid
ORDER BY nrsh.timestamp DESC', array(
':nid' => $nid,
))
->fetchAll();
return $history;
}
/**
* Get all of the states for all content types
*/
function state_flow_get_all_states() {
static $states = array();
if (empty($states)) {
$i = 0;
foreach (node_type_get_types() as $type_key => $type) {
// Fake a node object
$node = new stdClass();
$node->vid = $i;
$i++;
$node->type = $type_key;
$state_machine = state_flow_load_state_machine($node);
if (!$state_machine
->ignore()) {
$states += $state_machine
->get_states_options();
}
}
}
return $states;
}
/**
* Get all of the events for all content types
*/
function state_flow_get_all_events() {
static $events = array();
if (empty($events)) {
$i = 0;
foreach (node_type_get_types() as $type_key => $type) {
// Fake a node object
$node = new stdClass();
$node->vid = $i;
$i++;
$node->type = $type_key;
$state_machine = state_flow_load_state_machine($node);
if (!$state_machine
->ignore()) {
foreach ($state_machine
->get_all_events() as $key => $value) {
$events[$key] = $key;
}
}
}
}
return $events;
}
/**
* Load the state_flow state_machine for the given node.
*
* @param $node StdClass
* @param $reset Boolean
* @return StateFlow
*/
function state_flow_load_state_machine($node, $reset = FALSE) {
$objects =& drupal_static(__FUNCTION__);
if (!isset($objects[$node->vid]) || $reset) {
ctools_include('plugins');
$machine_type = 'state_flow';
//allow other modules to invoke other machine types
drupal_alter('state_flow_machine_type', $machine_type, $node);
$plugin = ctools_get_plugins('state_flow', 'plugins', $machine_type);
if (!empty($plugin)) {
$class = ctools_plugin_get_class($plugin, 'handler');
$state_flow_object = new $class($node);
$objects[$node->vid] = $state_flow_object;
}
}
return $objects[$node->vid];
}
/**
* Checks whether the version of the node being saved is in the published state,
* and if not, re-saves the latest published revision.
*
* To prevent field content of a draft node revision from being used as the
* published version, we need to re-save the current published version after
* any draft revision is saved.
*
* In Drupal 7, the old approach of "munging the node vid" is not compatible
* with fields. See: http://drupal.org/node/1184318
*/
function state_flow_prevent_live_revision($node) {
// If this node is marked to be ignored by state_flow_promote_node_revision(),
// then skip handling it.
if (!empty($node->stateflow_ignore_state)) {
return;
}
// If the revision being saved is not the current published version, then
// ensure that the published version is re-saved to make it the most recent.
$published_revision = state_flow_live_revision($node->nid);
if (!empty($published_revision[0]->vid) && $published_revision[0]->vid != $node->vid) {
state_flow_promote_node_revision($published_revision[0], $node->nid, $published_revision[0]->vid);
// When a draft is saved and does not become the current revision, then
// redirect the user to the revision saved. This hijacks the redirection by
// drupal_goto().
$_GET['destination'] = 'node/' . $node->nid . '/revisions/' . $node->vid . '/view';
// Save the vid for the draft revision back to the node in case other modules need it.
$node->new_draft_vid = $node->vid;
}
}
/**
* Promote a node revision to be the most current by loading and re-saving it.
* If the given node revision is not the most recent, then re-save it as a new
* revision. Also update related metadata from the node, node_revision,
* node_revision_states, and node_revision_states_history tables. Finally,
* delete the original revision if a new revision is created.
*/
function state_flow_promote_node_revision($rev_state_rec, $nid, $current_vid) {
// Load data about the current revision
$current_rev = node_load($nid, $current_vid);
$current_timestamp = !empty($current_rev->revision_timestamp) ? $current_rev->revision_timestamp : REQUEST_TIME;
// From workbench_moderation:
// Path module is stupid and doesn't load its data in node_load.
if (module_exists('path') && isset($current_rev->nid)) {
$path = array();
$conditions = array(
'source' => 'node/' . $current_rev->nid,
'language' => isset($current_rev->language) ? $current_rev->language : LANGUAGE_NONE,
);
$path = path_load($conditions);
if ($path === FALSE) {
$path = array();
}
if (module_exists('pathauto')) {
if (isset($current_rev->path) && isset($path['pathauto'])) {
$path['pathauto'] += $current_rev->path['pathauto'];
}
}
if (isset($current_rev->path)) {
$path += $current_rev->path;
}
if (!empty($path)) {
$current_rev->path = $path;
}
}
// Determine the latest revision of this node
$latest_vid = db_query('
SELECT nr.vid
FROM {node_revision} nr
WHERE nr.nid = :nid
ORDER BY nr.vid DESC
LIMIT 0, 1', array(
':nid' => $nid,
))
->fetchField();
// Re-save the node. Create a new revision if the given revision is not the
// most recent.
$current_rev->revision = $latest_vid > $current_vid ? TRUE : FALSE;
$current_rev->stateflow_ignore_state = TRUE;
node_save($current_rev);
// node_save() has updated the $current_rev object, so it is the new revision.
$new_rev = $current_rev;
// Set the node.changed and the node_revision.timestamp value to the
// timestamp of the published revision
db_update('node')
->fields(array(
'changed' => $current_timestamp,
))
->condition('vid', $new_rev->vid)
->execute();
db_update('node_revision')
->fields(array(
'timestamp' => $current_timestamp,
))
->condition('vid', $new_rev->vid)
->execute();
// If a new revision was created, update state_flow records for the revision.
if ($new_rev->revision) {
// Update the node_revision_states record for the new published revision
// to match the old revision
db_update('node_revision_states')
->fields(array(
'state' => $rev_state_rec->state,
'status' => $rev_state_rec->status,
'timestamp' => $rev_state_rec->timestamp,
))
->condition('vid', $new_rev->vid)
->execute();
// Delete any node_revision_states_history records associated with the new
// revision (created during hook_node_insert) which will refer to the new
// version as a draft
db_delete('node_revision_states_history')
->condition('vid', $new_rev->vid)
->execute();
// Change all node_revision_states_history records for the old revision
// to be associated with the new revision
db_update('node_revision_states_history')
->fields(array(
'vid' => $new_rev->vid,
))
->condition('vid', $current_vid)
->execute();
// Notify other modules about the vid change
module_invoke_all('state_flow_change_vid', $current_vid, $new_rev);
// Delete the old published revision (that has been cloned)
node_revision_delete($current_vid);
}
}
/**
* Helper function to return all node_revision_states records for a node.
*/
function state_flow_get_revisions($nid) {
$revisions = db_query('
SELECT *
FROM {node_revision_states}
WHERE nid = :nid
ORDER BY vid DESC', array(
':nid' => $nid,
))
->fetchAll();
return $revisions;
}
/**
* Helper function to return node_revision_states records for all published
* revisions of a node.
*/
function state_flow_live_revision($nid) {
$state = variable_get('state_flow_published_state', 'published');
$revision_state = db_query('
SELECT *
FROM {node_revision_states}
WHERE nid = :nid
AND status = 1
AND state = :state
ORDER BY vid DESC
LIMIT 0, 1', array(
':nid' => $nid,
':state' => $state,
))
->fetchAll();
return $revision_state;
}
/**
* Implements hook_node_revision_filters()
*/
function state_flow_node_revision_filters() {
$filters = array();
if ($states = state_flow_get_all_states()) {
$options = array_combine(array_keys($states), array_keys($states));
}
else {
$options = array();
}
array_unshift($options, 'any');
$filters['state'] = array(
'form' => array(
'#type' => 'select',
'#title' => t('State'),
'#options' => $options,
),
);
return $filters;
}
/**
* Implements hook_query_node_revision_alter()
*/
function state_flow_query_node_revision_alter(QueryAlterableInterface $query) {
// Get the filter form the session
$filters = $query
->getMetaData('filters');
if ($filter = isset($filters['state']) ? $filters['state'] : NULL) {
$query
->join('node_revision_states', 'nrs', 'nr.vid = nrs.vid');
$query
->condition('nrs.state', $filter);
}
}
/**
* Implements hook_node_revision_operations().
*/
function state_flow_node_revision_operations() {
$operations = array();
$events = state_flow_get_all_events();
foreach ($events as $event) {
$operations["change_state_{$event}"] = array(
'label' => t('Transition Action: @event', array(
'@event' => $event,
)),
'callback' => 'state_flow_node_revision_operation_change_state',
'callback arguments' => array(
'args' => array(
'event' => $event,
),
),
);
}
return $operations;
}
/**
* Operation callback to change state of a node
*/
function state_flow_node_revision_operation_change_state($nodes, $args) {
$event = $args['event'];
// We use batch processing to prevent timeout when updating a large number
// of nodes.
if (count($nodes) > 10) {
$batch = array(
'operations' => array(
array(
'state_flow_node_revision_operation_change_state_batch_process',
array(
$nodes,
$args,
),
),
),
'finished' => 'state_flow_node_revision_operation_change_state_batch_finished',
'title' => t('Processing'),
// We use a single multi-pass operation, so the default
// 'Remaining x of y operations' message will be confusing here.
'progress_message' => '',
'error_message' => t('The update has encountered an error.'),
);
batch_set($batch);
}
else {
$message = array();
foreach ($nodes as $info) {
$messages[] = state_flow_operation_change_helper($info, $event);
}
$message = theme('item_list', array(
'items' => $messages,
));
$message = format_plural(count($nodes), '1 item successfully processed:', '@count items successfully processed:');
drupal_set_message($message);
}
}
function state_flow_operation_change_helper($info, $event) {
global $user;
$allowed_events = array();
$node = node_load($info['nid'], $info['vid']);
$state_machine = state_flow_load_state_machine($node);
if (!$state_machine
->ignore()) {
$allowed_events = $state_machine
->get_available_events();
}
if (in_array($event, array_keys($allowed_events))) {
$state_machine
->fire_event($event, $user->uid, 'State changed via Bulk Node Operation');
return t('@title has been transitioned to @state', array(
'@title' => $node->title,
'@state' => $state_machine
->get_label_for_current_state(),
));
}
}
/**
* State Change Mass Update Batch operation
*/
function state_flow_node_revision_operation_change_state_batch_process($nodes, $updates, &$context) {
if (!isset($context['sandbox']['progress'])) {
$context['sandbox']['progress'] = 0;
$context['sandbox']['max'] = count($nodes);
$context['sandbox']['nodes'] = $nodes;
}
// Process nodes by groups of 5.
$count = min(5, count($context['sandbox']['nodes']));
for ($i = 1; $i <= $count; $i++) {
// For each nid, load the node, reset the values, and save it.
$info = array_shift($context['sandbox']['nodes']);
// Store result for post-processing in the finished callback.
$context['results'][] = state_flow_operation_change_helper($info, $updates['event']);
// Update our progress information.
$context['sandbox']['progress']++;
}
// Inform the batch engine that we are not finished,
// and provide an estimation of the completion level we reached.
if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
$context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
}
}
/**
* Node Mass Update Batch 'finished' callback.
*/
function state_flow_node_revision_operation_change_state_batch_finished($success, $results, $operations) {
if ($success) {
drupal_set_message(t('The update has been performed.'));
}
else {
drupal_set_message(t('An error occurred and processing did not complete.'), 'error');
$message = format_plural(count($results), '1 item successfully processed:', '@count items successfully processed:');
$message .= theme('item_list', array(
'items' => $results,
));
drupal_set_message($message);
}
}
/**
* Guard condition callback
*
* @param string $event
* @return bool
*/
function state_flow_guard_schedule($event) {
if (module_exists('state_flow_schedule')) {
return state_flow_schedule_guard_permission($event, 'schedule content workflow');
}
}
/**
* Determine whether the workflow should be skipped on a node
*
* @param unknown_type $node
*/
function state_flow_skip_workflow($node) {
if (!isset($node->stateflow_skip_workflow)) {
return FALSE;
}
return $node->stateflow_skip_workflow;
}
/**
* Implements hook_node_revision_status()
*
* @param $node
* THe node revision object
*
* @return string
* The display label of the status
*/
function state_flow_node_revision_status($node) {
return !state_flow_load_state_machine($node)
->ignore() ? state_flow_load_state_machine($node)
->get_label_for_current_state() : '- None -';
}
Functions
Name | Description |
---|---|
state_flow_access | Determine whether a user has permission to transition a node with an event. |
state_flow_admin_paths | Implements hook_admin_paths(). |
state_flow_ctools_plugin_type | Implements hook_ctools_plugin_type(). |
state_flow_entity_get_state | Getter callback for the "state" property on node bundles using workflow. |
state_flow_entity_property_info_alter | Implements hook_entity_property_info_alter(). |
state_flow_events_revisions_access | Menu access callback for the node revision workflow transition page. |
state_flow_get_all_events | Get all of the events for all content types |
state_flow_get_all_states | Get all of the states for all content types |
state_flow_get_history | Retrieve the states history for a node. |
state_flow_get_revisions | Helper function to return all node_revision_states records for a node. |
state_flow_guard_schedule | Guard condition callback |
state_flow_invoke_event_handlers | Inform external systems about a workflow transition. |
state_flow_live_revision | Helper function to return node_revision_states records for all published revisions of a node. |
state_flow_load_state_machine | Load the state_flow state_machine for the given node. |
state_flow_menu | Implements hook_menu(). |
state_flow_menu_node_access | Menu access callback for accessing the node workflow pages. |
state_flow_node_delete | Implements hook_node_delete(). |
state_flow_node_insert | Implements hook_node_insert(). |
state_flow_node_presave | Implements hook_node_presave(). |
state_flow_node_revision_delete | Implements hook_node_revision_delete(). |
state_flow_node_revision_filters | Implements hook_node_revision_filters() |
state_flow_node_revision_operations | Implements hook_node_revision_operations(). |
state_flow_node_revision_operation_change_state | Operation callback to change state of a node |
state_flow_node_revision_operation_change_state_batch_finished | Node Mass Update Batch 'finished' callback. |
state_flow_node_revision_operation_change_state_batch_process | State Change Mass Update Batch operation |
state_flow_node_revision_status | Implements hook_node_revision_status() |
state_flow_node_update | Implements hook_node_update(). |
state_flow_operation_change_helper | |
state_flow_permission | Implements hook_permission(). |
state_flow_prevent_live_revision | Checks whether the version of the node being saved is in the published state, and if not, re-saves the latest published revision. |
state_flow_promote_node_revision | Promote a node revision to be the most current by loading and re-saving it. If the given node revision is not the most recent, then re-save it as a new revision. Also update related metadata from the node, node_revision, node_revision_states, and… |
state_flow_query_node_revision_alter | Implements hook_query_node_revision_alter() |
state_flow_skip_workflow | Determine whether the workflow should be skipped on a node |
state_flow_state_flow_plugins | Implements hook_state_flow_plugins(). |
state_flow_views_api | Implements hook_views_api(). |