state_flow.module in State Machine 7.3
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',
);
// This item required to have a valid path scheme for entity_translation.
$items['node/%node/revisions/%state_flow_revision_node'] = array(
'title' => 'Revision ',
'title callback' => 'state_flow_revision_menu_item_title_callback',
'title arguments' => array(
3,
),
'load arguments' => array(
3,
),
'page callback' => 'node_show',
'page arguments' => array(
1,
TRUE,
),
'access callback' => '_node_revision_access',
'access arguments' => array(
1,
),
);
$items['node/%node/revisions/%state_flow_revision_node/edit'] = array(
'title' => 'Edit revision',
'type' => MENU_LOCAL_TASK,
'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/%state_flow_revision_node/workflow'] = array(
'title' => 'Transition a revision to a new workflow state',
'type' => MENU_CALLBACK,
'load arguments' => array(
3,
),
'page callback' => 'drupal_get_form',
'page arguments' => array(
'state_flow_entity_events_revision',
1,
'node',
5,
array(),
),
'access callback' => 'state_flow_events_revisions_access',
'access arguments' => array(
1,
5,
),
'file' => 'state_flow_entity.forms.inc',
'file path' => drupal_get_path('module', 'state_flow_entity'),
);
$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;
}
/**
* Menu argument loader.
*/
function state_flow_revision_node_load($vid) {
$nid = arg(1);
if (empty($vid) || empty($nid)) {
return FALSE;
}
return node_load($nid, $vid, TRUE);
}
/**
* Menu title callback loader.
*/
function state_flow_revision_menu_item_title_callback($node) {
return t('Revision !vid', array(
'!vid' => $node->vid,
));
}
/**
* Implements hook_menu_alter().
*/
function state_flow_menu_alter(&$items) {
// Hijack the node/X/edit page to ensure that the right revision is displayed
if (isset($items['node/%node/edit'])) {
$items['node/%node/edit']['page callback'] = 'state_flow_node_edit_page_override';
$items['node/%node/edit']['title'] = t('Edit Draft');
}
// Ensure viewing a revision is the default task for the revisions route.
if (isset($items['node/%node/revisions/%/view'])) {
$items['node/%node/revisions/%/view']['type'] = MENU_DEFAULT_LOCAL_TASK;
}
// Ensure our access callback is used.
if (isset($items['node/%/revisions-state-flow-states'])) {
$items['node/%/revisions-state-flow-states']['access callback'] = 'state_flow_revisions_node_tab_access';
}
}
/**
* Implements hook_module_implements_alter().
*
* Needed for state_flow_menu_alter() to reach Views-generated
* menu entries.
*/
function state_flow_module_implements_alter(&$implementations, $hook) {
if ($hook == 'menu_alter') {
$group = $implementations['state_flow'];
unset($implementations['state_flow']);
$implementations['state_flow'] = $group;
}
}
/**
* Disable Revisions tab if there are no state flow changes.
*
* From Views, it's not possible for performance reasons.
* But here, with a lightweight query, we can predict if it would
* have results or not.
*/
function state_flow_revisions_node_tab_access() {
// The menu_get_object() would cause infinite recursion.
$nid = (int) arg(1);
$count = db_query("SELECT COUNT(*) FROM {state_flow_states} WHERE entity_type = 'node' and entity_id = :nid", array(
':nid' => $nid,
))
->fetchField();
if ($count > 0) {
$args = func_get_args();
$callback = isset($args[0][0]) && is_callable($args[0][0]) ? $args[0][0] : 'views_check_perm';
$params = isset($args[0][1]) ? $args[0][1] : array(
'administer nodes',
);
return call_user_func_array($callback, $params);
}
return FALSE;
}
/**
* Overrides the node/%/edit page to ensure the proper revision is shown.
*
* @param $node
* The node being acted upon.
* @return
* A node editing form.
*/
function state_flow_node_edit_page_override($node) {
// Check to see if this is an existing node.
if (isset($node->nid)) {
// Load the node moderation data.
$machine = state_flow_entity_load_state_machine($node, 'node');
$active_revision_id = $machine
->get_active_revision();
if (!empty($active_revision_id)) {
// We ONLY edit the active revision.
$active_revision = node_load($node->nid, $machine->object->active_revision_id);
if (!empty($active_revision)) {
$node = $active_revision;
}
else {
watchdog('state_flow', 'Node @title @nid has corrupted {state_flow_states} rows.', array(
'@title' => $node->title,
'@nid' => $node->nid,
), WATCHDOG_ERROR);
drupal_set_message(t('Workflow data is corrupted, report this incident to the site administrators.'), 'error');
}
}
}
// Ensure we have the editing code.
module_load_include('inc', 'node', 'node.pages');
return node_page_edit($node);
}
/**
* 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,
'node/*/state-flow-history' => TRUE,
'node/*/revisions-state-flow-states' => TRUE,
);
return $paths;
}
}
/**
* Implements hook_entity_property_info_alter().
*
* Adds a "state" property on nodes that are configured with state flow.
*
* @see entity_api module.
*
* @todo, this should move to state_flow_entity, probably
*/
function state_flow_entity_property_info_alter(&$info) {
if (!empty($info['node']['bundles'])) {
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_entity_info_alter().
*/
function state_flow_entity_info_alter(&$entity_info) {
// Add state_flow_information to the node entity type.
if (!empty($entity_info['node'])) {
$entity_info['node']['state_flow_entity'] = array(
// @todo, there will likely be more properties needed here.
'revision_workflow_path' => 'node/%entity_id/revisions/%revision_id/workflow',
'revision_delete_path' => 'node/%entity_id/revisions/%revision_id/delete',
'revision_edit_path' => 'node/%entity_id/revisions/%revision_id/edit',
);
// Add revision handling path schemes for entity translation.
if (module_exists('entity_translation')) {
if (!isset($entity_info['node']['translation']['entity_translation'])) {
$entity_info['node']['translation']['entity_translation'] = array();
}
$entity_info['node']['state_flow_entity']['revision_translate_path'] = 'node/%entity_id/revisions/%revision_id/translate';
$et_info =& $entity_info['node']['translation']['entity_translation'];
$et_info['path schemes']['state_flow'] = array(
'admin theme' => 1,
'base path' => 'node/%node/revisions/%state_flow_revision_node',
'view path' => 'node/%node/revisions/%/view',
'edit path' => 'node/%node/revisions/%state_flow_revision_node/edit',
'path wildcard' => '%node',
'edit tabs' => TRUE,
);
}
}
}
/**
* Implements hook_state_flow_entity_plugins().
*/
function state_flow_state_flow_entity_plugins() {
$info = array();
$workflow_options = array(
'states' => array(
'draft' => array(
'label' => t('Draft'),
),
'published' => array(
'label' => t('Published'),
'on_enter' => 'on_enter_published',
'on_exit' => 'on_exit_published',
),
'unpublished' => array(
'label' => t('Unpublished'),
'on_enter' => 'on_enter_unpublished',
),
),
'events' => array(
'keep in draft' => array(
'label' => t('Keep in Draft'),
'origin' => 'draft',
'target' => 'draft',
),
'publish' => array(
'label' => t('Publish'),
'origin' => array(
'draft',
'published',
),
'target' => 'published',
),
'unpublish' => array(
'label' => t('Unpublish'),
'origin' => array(
'draft',
'published',
),
'target' => 'unpublished',
'permission' => 'publish and unpublish content',
),
'to draft' => array(
'label' => t('To Draft'),
'origin' => array(
'unpublished',
'published',
),
'target' => 'draft',
),
),
);
$info['state_flow_node'] = array(
'handler' => array(
'class' => 'StateFlowNode',
'file' => 'state_flow_node.inc',
'path' => drupal_get_path('module', 'state_flow') . '/plugins',
'parent' => 'state_flow_entity',
'workflow_options' => $workflow_options,
'entity_type' => 'node',
'event_form_options' => array(),
),
);
return $info;
}
/**
* Implements hook_views_api().
*/
function state_flow_views_api() {
return array(
'api' => 3,
'path' => drupal_get_path('module', 'state_flow') . '/includes/views',
);
}
/**
* Implements hook_node_revision_delete().
*/
function state_flow_node_revision_delete($node) {
// Delete history and update active revision for this entity.
$machine = state_flow_entity_load_state_machine($node, 'node');
$machine
->delete_state_flow_revision($node->vid, 'node');
}
/**
* 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_entity_load_state_machine($node, '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.
$machine = state_flow_entity_load_state_machine($node, 'node');
$state_event = $machine ? $machine
->get_event($event_name) : FALSE;
return $state_event ? $state_event
->validate() : FALSE;
}
/**
* Getter callback for the "state" property on node bundles using workflow.
*
* @see entity_api module.
*/
function state_flow_entity_get_state($data, $options, $name, $type, $info) {
$machine = state_flow_entity_load_state_machine($data, 'node');
return $machine
->get_current_state();
}
/**
* 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;
$machine = state_flow_entity_load_state_machine($node, 'node');
$states += $machine
->get_states_options();
}
}
return $states;
}
/**
* Get all of the states for all content types as option list array.
*/
function state_flow_get_all_state_options() {
$states = state_flow_get_all_states();
array_walk($states, function (&$item) {
$item = $item['label'];
});
return $states;
}
/**
* Get all of the events for all content types.
*
* @todo, this will not play well at all with having more than one workflow available.
*/
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;
$machine = state_flow_entity_load_state_machine($node, 'node');
$events += array_keys($machine
->get_all_events());
}
}
return $events;
}
/**
* Retrieve the states history for an entity.
*
* @param integer node id
* @return array of objects
*/
function state_flow_get_all_history($entity_id) {
$history = db_query('
SELECT sfh.*, u.uid, u.name AS user_name
FROM {state_flow_history} sfh
LEFT JOIN {users} u ON u.uid = sfh.uid
WHERE sfh.entity_id = :entity_id
ORDER BY sfh.timestamp DESC', array(
':entity_id' => $entity_id,
))
->fetchAll();
return $history;
}
/**
* Implements hook_node_revision_filters()
*/
function state_flow_node_revision_filters() {
$filters = array();
$states = state_flow_get_all_states();
$options = array_combine(array_keys($states), array_keys($states));
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('state_flow_states', 'sfs', 'nr.vid = sfs.revision_id AND entity_type=\'node\'');
$query
->condition('sfs.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;
$node = node_load($info['nid'], $info['vid']);
$machine = state_flow_entity_load_state_machine($node, 'node');
$allowed_events = $machine
->get_available_events();
if (in_array($event, $allowed_events)) {
$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);
// 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);
}
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function state_flow_form_node_form_alter(&$form, &$form_state, $form_id) {
// Add the event form.
// @todo, should this be added to all node forms? Should that be a setting
// in the state flow plugin.
$node = $form_state['node'];
$machine = state_flow_entity_load_state_machine($node, 'node');
// Only alter the form if this entity has a state machine and is not ignored.
if (!empty($machine) && !$machine
->ignore()) {
module_load_include('inc', 'state_flow_entity', 'state_flow_entity.forms');
$form['options']['state_flow']['#type'] = 'fieldset';
// Use a select list.
$form_options = array(
'event_element_type' => 'select',
);
$event_form = _state_flow_entity_events_revision(array(), $form_state, $node, 'node', NULL, $form_options);
if (!empty($form['options']['#access'])) {
$form['options']['state_flow'] += $event_form;
}
elseif (!empty($form['revision_information']['#access'])) {
$form['revision_information']['state_flow'] = $event_form;
}
else {
$form['state_flow'] = $event_form;
$form['state_flow']['#weight'] = 99;
}
// Remove node revision log field.
$form['revision_information']['log']['#access'] = FALSE;
// If this is the published revision we create a new revision by default.
$form['revision_information']['revision']['#default_value'] = !$machine
->object_is_new() && $machine
->get_active_revision() == $node->vid && !empty($node->status);
// Hide Published checkbox.
$form['options']['status']['#disabled'] = TRUE;
if ($machine
->object_is_new()) {
$form['options']['status']['#access'] = FALSE;
}
}
}
/**
* Implements hook_node_validate().
*/
function state_flow_node_validate($node, $form, &$form_state) {
if (!empty($form_state['values']['state_flow'])) {
// Validate fields.
$machine = $form_state['values']['state_flow'];
$machine
->history_entity_form_field_validate($form, $form_state);
// Validate event value.
$event = $machine
->get_event($form_state['values']['event']);
if (!is_object($event) || !method_exists($event, 'get_options')) {
$event_item = NULL;
if (isset($form['options']['state_flow']) && isset($form['options']['state_flow']['event'])) {
$event_item = $form['options']['state_flow']['event'];
}
else {
if (isset($form['revision_information']['state_flow']) && isset($form['revision_information']['state_flow']['event'])) {
$event_item = $form['revision_information']['state_flow']['event'];
}
else {
if (isset($form['state_flow']) && isset($form['state_flow']['event'])) {
$event_item = $form['state_flow']['event'];
}
}
}
if (empty($event_item)) {
form_set_error('event', t('@title value is unknown.', array(
'@title' => $form_state['values']['event'],
)));
}
else {
form_set_error('event', t('@title value is invalid.', array(
'@title' => $event_item['#title'],
)));
}
}
}
}
/**
* Implements hook_node_submit().
*/
function state_flow_node_submit($node, $form, &$form_state) {
if (!empty($form_state['values']['state_flow'])) {
/** @var StateFlowNode $machine */
$machine = $form_state['values']['state_flow'];
/** @var StateMachine_Event $event */
$event = $machine
->get_event($form_state['values']['event']);
if (!is_object($event) || !method_exists($event, 'get_options')) {
watchdog('state_flow', 'Invalid event value "@value" submitted in node form.', array(
'@value' => $form_state['values']['event'],
), WATCHDOG_ERROR);
return;
}
$machine
->history_entity_form_submit_build_entity($form, $form_state);
// If this isn't the active published revision we redirect to the revision
// view page - this should avoid confusion when working with revisions.
if (!empty($node->nid) && !empty($node->vid) && !$machine
->isActivePublishedRevision() && empty($_GET['destination'])) {
// We set the get param because we can't set $form_state['redirect'] - the
// node form handler overwrites this value by default.
$_GET['destination'] = 'node/' . $node->nid . '/revisions/' . $node->vid . '/view';
}
}
}
/**
* 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_state_flow_entity_machine_type_alter().
*/
function state_flow_state_flow_entity_machine_type_alter(&$machine_type, $entity, $entity_type) {
// If the entity is a node. Use the state_flow_node plugin.
if ($entity_type === 'node') {
$machine_type = 'state_flow_node';
}
}