state_flow.module in State Machine 7
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',
);
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.'),
),
);
}
/**
* 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_directory().
*/
function state_flow_ctools_plugin_directory($module, $plugin) {
if ($module == 'state_flow') {
return 'plugins/' . $plugin;
}
}
/**
* Implements hook_ctools_plugin_type().
*/
function state_flow_ctools_plugin_type() {
$plugins = array(
'plugins' => array(
'cache' => TRUE,
'use hooks' => TRUE,
),
);
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) {
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',
'file' => 'state_flow.inc',
'path' => $path,
),
);
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->state_flow_ignore_state)) {
$state_flow = state_flow_load_state_machine($node);
//Check to see if we should go through workflow
if (empty($node->stateflow_skip_workflow)) {
$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('publish');
}
}
}
}
/**
* Implements hook_node_insert().
*/
function state_flow_node_insert($node) {
global $user;
$state_flow = state_flow_load_state_machine($node);
$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);
//Check to see if we should go through workflow
if (empty($node->stateflow_skip_workflow)) {
if (!empty($node->state_flow_revert_draft) && $state_flow
->get_current_state() !== 'draft') {
$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) {
$result = db_delete('node_revision_states')
->condition('nid', $node->nid)
->execute();
$result = db_delete('node_revision_states_history')
->condition('nid', $node->nid)
->execute();
}
/**
* Implements hook_node_revision_delete().
*/
function state_flow_node_revision_delete($node) {
$result = db_delete('node_revision_states')
->condition('vid', $node->vid)
->execute();
$result = 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.
$access = user_access('manage content workflow', $account);
// 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($object, $state) {
// Load related objects
$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);
}
}
/**
* 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;
}
/**
* Load the state_flow state_machine for the given node.
*/
function state_flow_load_state_machine($node, $reset = FALSE) {
$objects =& drupal_static(__FUNCTION__);
if (!isset($objects[$node->vid]) || $reset) {
ctools_include('plugins');
$machine_type = variable_get('state_flow_' . $node->type, 'state_flow');
$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->state_flow_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';
}
}
/**
* 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 (isset($current_rev->path)) {
$path += $current_rev->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->state_flow_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
$res = db_update('node')
->fields(array(
'changed' => $current_timestamp,
))
->condition('vid', $new_rev->vid)
->execute();
$res = 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
$res = 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
$res = db_update('node_revision_states_history')
->fields(array(
'vid' => $new_rev->vid,
))
->condition('vid', $current_vid)
->execute();
// 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;
}
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_directory | Implements hook_ctools_plugin_directory(). |
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_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_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_update | Implements hook_node_update(). |
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_state_flow_plugins | Implements hook_state_flow_plugins(). |
state_flow_views_api | Implements hook_views_api(). |