draft") and * "Publish this" ("review -> live"). * The alternative buttons appear on: * a) the node edit form (node/%/edit) * b) the comment edit form, if enabled * c) the workflow tab, if enabled (node/%/workflow) * * Re 2) * This module defines a replacement token [node:workflow-state-age], which * when used in a scheduled rule set, make it easier to invoke actions when * a workflow state was NOT changed after a specified elapsed time. * See drupal.org/project/workflow_extensions for full instructions on * how to do this using Rules. */ define('WORKFLOW_EXTENSIONS_UI_RADIOS', 1); // the original Workflow style define('WORKFLOW_EXTENSIONS_UI_BUTTONS', 2); // single-action buttons define('WORKFLOW_EXTENSIONS_UI_DROPDOWN', 3); // dropdown selector + update /** * Implementation of hook_perm(). */ function workflow_extensions_perm() { return array( 'change workflow state via node edit form', 'edit workflow log', 'view workflow state change block even when state cannot be changed'); } /** * Implementation of hook_menu(). */ function workflow_extensions_menu() { $items = array(); $items['workflow-log/%workflow_state_transition_record'] = array( // maps to function workflow_state_transition_record_load() 'title' => 'Edit workflow log comment', 'description' => 'Edit workflow state transition comment.', 'page callback' => 'drupal_get_form', 'page arguments' => array('workflow_extensions_workflow_comment_edit_form', 1), 'access arguments' => array('edit workflow log') ); $items['admin/settings/workflow_extensions'] = array( 'title' => 'Workflow extensions', 'description' => 'Configure workflow form style (buttons and labels).', 'page callback' => 'drupal_get_form', 'page arguments' => array('workflow_extensions_admin_configure'), 'access arguments' => array('administer site configuration'), 'file' => 'workflow_extensions.admin.inc' ); return $items; } /** * Load function belonging to the above menu option 'workflow-log/%workflow-state-transition-record'. * Maps to this function just like 'node/%node' maps to node_load(). * * @param $hid * The ID of the workflow state transition record to load. * @return * object representing one row from the {workflow_node_history} table */ function workflow_state_transition_record_load($hid) { $workflow_state_transition_record = db_fetch_object(db_query('SELECT * FROM {workflow_node_history} WHERE hid = %d', $hid)); return $workflow_state_transition_record; } /** * Display a text area populated with the selected workflow log comment and * allow the user to modify and save it. */ function workflow_extensions_workflow_comment_edit_form($form_state, $workflow_state_transition_record) { $form = array(); $form['hid'] = array('#type' => 'value', '#value' => $workflow_state_transition_record->hid); $form['nid'] = array('#type' => 'value', '#value' => $workflow_state_transition_record->nid); $form['workflow']['workflow_comment'] = array( '#type' => 'textarea', '#title' => t('Comment'), '#description' => t('Modify this workflow state transition comment and press submit.'), '#default_value' => $workflow_state_transition_record->comment, '#rows' => 2, ); $form['submit'] = array( '#type' => 'submit', '#value' => t('Submit') ); return $form; } /** * Submit handler for the workflow transition comment edit form. * * @see workflow_extensions_workflow_comment_edit_form() */ function workflow_extensions_workflow_comment_edit_form_submit($form, &$form_state) { $hid = $form_state['values']['hid']; $comment = $form_state['values']['workflow_comment']; db_query("UPDATE {workflow_node_history} SET comment = '%s' WHERE hid = %d", $comment, $hid); $nid = $form_state['values']['nid']; // Whatever is set here, is overriden by the "?destination=..." parameter, if present $form_state['redirect'] = module_exists('views') && views_get_view('workflow_history') ? "workflow-history/$nid" : (workflow_node_tab_access(node_load($nid)) ? "node/$nid/workflow" : "node/$nid"); } /** * Implementation of hook_form_alter(). * * 1) Remove the Workflow radio buttons and replace each state transition by a * submit button with a configurable, explanatory label. * To allow saving of edits to the node without a state transition, display * an additional button "Save, don't submit" (or similar). * 2) If on the create/edit form suppress the workflow fieldset if the user * does not have the relevant permission * 3) If the schedule state transition form is enabled, suppress it until the * user clicks the radio button to reveal it. * 4) Add an (extra) form validator */ function workflow_extensions_form_alter(&$form, &$form_state, $form_id) { // If there's no Workflow fieldset, then we have nothing further to do. if (!isset($form['#wf'])) { // was $form['workflow'] but need to exclude content type edit form return; } // 4) Add form validator if (empty($form['#validate'])) { $form['#validate'] = array(); } elseif (!is_array($form['#validate'])) { $form['#validate'] = array($form['#validate']); } $form['#validate'][] = 'workflow_extensions_workflow_form_validate'; // 2) Suppress Workflow fieldset on the create/edit form if not permitted. if (isset($form['#id']) && $form['#id'] == 'node-form' && !user_access('change workflow state via node edit form')) { unset($form['workflow']); return; } // The next if-statement implements point 3) above if (variable_get('workflow_extensions_display_schedule_toggle', TRUE)) { $path = drupal_get_path('module', 'workflow_extensions'); drupal_add_js($path . '/workflow_schedule.js', 'module'); } // The rest of this function implements point 1) $workflow_name = workflow_extensions_extract_workflow_name($form); $workflow_radios = $form['workflow'][$workflow_name]; if (is_array($workflow_radios) && isset($workflow_radios['#options'])) { // Use the form to work out the potential state transitions for this user. $states = $workflow_radios['#options']; // $states will be empty when creating node if (!empty($states)) { $title = variable_get('workflow_extensions_change_state_form_title', ''); if (!empty($title)) { if (trim($title) == '') { unset($form['workflow'][$workflow_name]['#title']); } else { $form['workflow'][$workflow_name]['#title'] = workflow_extensions_replace_tokens_raw($title); } } if (empty($form['#submit'])) { // This seems to happen as a side-effect of using hook_forms(), i.e // the case where workflow_extensions_change_state_form() is called, // for instance when the state change form appears in a block or View. // In the latter case, we also create ourselves an option to redirect // to page different from the default (which is node/%), as set in // workflow_tab_form_submit(). $form['#submit'] = array('workflow_tab_form_submit', 'workflow_extensions_form_redirect'); } switch (variable_get('workflow_extensions_ui_style', WORKFLOW_EXTENSIONS_UI_BUTTONS)) { case WORKFLOW_EXTENSIONS_UI_BUTTONS: if (count($states) > 1) { _workflow_extensions_replace_with_buttons($form, $workflow_name); } break; case WORKFLOW_EXTENSIONS_UI_DROPDOWN: _workflow_extensions_replace_with_dropdown($form, $workflow_name); // no break; default: // radios if (isset($form['submit'])) { $submit_label = variable_get('workflow_extensions_change_state_button_label', ''); if (!empty($submit_label)) { $form['submit']['#value'] = workflow_extensions_replace_tokens_raw($submit_label); } } break; } } } } /** * Implementation of hook_forms(); * * Called as a result of the fact that there are no form handlers for the * unique form_id's generated in workflow_extensions_change_state_form(). * Here we map these form_id's back to the same 'workflow_tab_form'. This * allows us to have multiple copies of the same form on the same page. * Note: first of the args is typically the node object. */ function workflow_extensions_forms($form_id, $args) { if (strpos($form_id, 'workflow_tab_form_nid') === 0) { $form = array( $form_id => array( 'callback' => 'workflow_tab_form', 'callback arguments' => array() ) ); return $form; } } /** * Validate the workflow form, in particular the state transition comment and * the scheduled state transition time (the date is a drop-down so requires no * validation). */ function workflow_extensions_workflow_form_validate($form, &$form_state) { $nid = substr($form['#id'], strrpos($form['#id'], '-') + 1); $title = isset($form_state['values']['title']) ? $form_state['values']['title'] : $form_state['values']['node']->title; if (!empty($form_state['values']['workflow_scheduled_hour'])) { if (!strtotime($form_state['values']['workflow_scheduled_hour'])) { form_set_error(is_numeric($nid) ? "workflow_scheduled_hour_$nid" : 'workflow_scheduled_hour', t('%title: scheduled time is not in the correct format.', array('%title' => $title))); } } if (!variable_get('workflow_extensions_allow_blank_comments', TRUE)) { if (!isset($form_state['values']['nid']) && !isset($form_state['values']['node'])) { // Bypass node creation return; } $comment = $form_state['values']['workflow_comment']; if (!trim($comment)) { form_set_error(is_numeric($nid) ? "workflow_comment_$nid" : 'workflow_comment', t('%title: please enter a non-blank comment for the workflow log.', array('%title' => $title))); } } } /** * Handler for the single-action submit buttons on the edit form. * Does NOT get called when radio buttons or drop-down are used. */ function workflow_extensions_form_submit($form, &$form_state) { // In the original form_submit handler that we pass control to next, the // selected workflow state is taken from form_state['values']['workflow']. So // that's the entry we need to set here in accordance with the clicked button. // See node_form_submit() -> node_submit() for the edit form. // See workflow_tab_form_submit() for the Workflow tab. $form_state['values']['workflow'] = $form_state['clicked_button']['#to_state']; // ... now proceed to next handler, e.g. node_form_submit for a normal save } /** * If the workflow state was not transitioned via either the node edit form or * the Workflow tab, then redirect to a configurable page, e.g. some View. */ function workflow_extensions_form_redirect($form, &$form_state) { if (arg(0) != 'node') { $path = variable_get('workflow_extensions_redirect_page', ''); // @todo: make this a UI config option $form_state['redirect'] = empty($path) ? $_GET['q'] : $path; // @todo: save query-string too (Views pager) } } function _workflow_extensions_replace_with_buttons(&$form, $workflow_name) { $current_sid = $form['workflow'][$workflow_name]['#default_value']; if (function_exists('workflow_get_state_name')) { $current_state_name = workflow_get_state_name($current_sid); } else { $current_state = workflow_get_state($current_sid); $current_state_name = $current_state['state']; } // We need a node-context for token replacement. When on the Workflow tab // form, the node object will already have been loaded on the form. // When creating content (node/add/) we only have limited data. In // the remaining cases we load the node from the cache based on the nid // found on the form. $form_id = $form['form_id']['#value']; if (strpos($form_id, 'workflow_tab_form') === 0) { $node = $form['node']['#value']; } elseif (is_numeric($nid = $form['nid']['#value'])) { $node = node_load($nid); } else { // Creating new content, nid not yet known $node = $form['#node']; } $states = $form['workflow'][$workflow_name]['#options']; $submit_handlers = _workflow_extensions_assign_handlers($form); foreach ($states as $sid => $to_state_name) { if ($sid != $current_sid) { // Create button for transition from current_sid to destination state. $button = array(); $button['#value'] = workflow_extensions_get_transition_label($form['#wf']->wid, $current_state_name, workflow_get_state_name($sid), $node); $button['#type'] = 'submit'; $button['#to_state'] = $sid; if (isset($form['buttons']['submit']['#weight'])) { // node form $button['#weight'] = $form['buttons']['submit']['#weight'] + 1; } elseif (isset($form['submit']['#weight'])) { // comment form $button['#weight'] = $form['submit']['#weight']; } $button['#submit'] = $submit_handlers; $form['buttons']["submit_to_$to_state_name"] = $button; } } // Get rid of workflow radio buttons that live inside the fieldset unset($form['workflow'][$workflow_name]); // If after this the fieldset is empty, remove it altogher if (!isset($form['workflow']['workflow_scheduled']) && (!isset($form['workflow']['workflow_comment']) || $form['workflow']['workflow_comment']['#type'] == 'hidden')) { unset($form['workflow']); } // With the existing Save button now impotent to submit a workflow // transition, we can re-purpose it for saving all other edits to the // node without changing the workflow state. // This does not make sense for the Workflow tab form though, as there is // nothing to save but a state change. In this case we simply remove the // Save button. if ($form_id == 'comment_form') { $form['buttons']['submit'] = $form['submit']; $form['buttons']['submit']['#submit'] = $form['#submit']; $form['buttons']['submit']['#weight']--; // left-most } if (strpos($form_id, 'workflow_tab_form') !== 0 && ($label = variable_get('workflow_extensions_default_save_button_label', ''))) { $form['buttons']['submit']['#value'] = workflow_extensions_replace_state_name_tokens($label, $current_state_name); } unset($form['submit']); // the button //unset($form['#submit']); // don't remove handler, [#1097328] } function _workflow_extensions_replace_with_dropdown(&$form, $workflow_name) { $form['workflow'][$workflow_name]['#type'] = 'select'; $form['workflow'][$workflow_name]['#name'] = 'workflow'; } /** * Implementation of hook_block(). */ function workflow_extensions_block($op = 'list', $delta = 0) { if (!module_exists('workflow')) { return; } if ($op == 'list') { $block[0]['info'] = t('Workflow state change form'); return $block; } elseif ($op == 'view' && arg(0) == 'node') { $node = node_load(arg(1)); $block['content'] = workflow_extensions_change_state_form($node); $block['subject'] = ''; return $block; } } /** * Use this function in a code-snippet to output a workflow state change form * on any page. May be used multiple times on the same page (for different * nodes), e.g. using the 'Views PHP' module, as a unique form_id is generated * for each occurrence of the form. * * @param $node */ function workflow_extensions_change_state_form($node) { if (!$node || !($wid = workflow_get_workflow_for_type($node->type))) { return ''; } $choices = workflow_field_choices($node); if (count($choices) == 1) { if (user_access('view workflow state change block even when state cannot be changed')) { // Generate single-option form without Submit button return drupal_get_form('workflow_extensions_single_state_form', workflow_get_name($wid), $choices); } return ''; // not allowed to view state } $result = db_query("SELECT sid, state FROM {workflow_states} WHERE status = 1 ORDER BY sid"); while ($row = db_fetch_object($result)) { $workflow_states[$row->sid] = check_plain(t($row->state)); } $sid_current = workflow_node_current_state($node); require_once drupal_get_path('module', 'workflow') . '/workflow.pages.inc'; $output = drupal_get_form('workflow_tab_form_nid_'. $node->nid, $node, $wid, $workflow_states, $sid_current); return $output; } /** * Form used to display the current state, without a submit button to change it. * @param $form_state * @param $workflow_name * @param $current_state, an array of one element, indexed by its sid */ function workflow_extensions_single_state_form($form_state, $workflow_name, $current_state) { $sids = array_keys($current_state); $form['workflow']['#title'] = $workflow_name; $form['workflow'][$workflow_name] = array( '#type' => 'radios', // may be overridden by workflow_extensions_form_alter() '#options' => $current_state, '#default_value' => $sids[0] ); return $form; } function workflow_extensions_extract_workflow_name($form) { // Current and allowed next states for this user and node live in // $form['workflow'][$workflow_name]['#options]. // At the time of writing, the workflow.module (6.x.1-4) contained a bug that // resulted in the $workflow_name being passed as blank. Luckily we can work // around this and also be compatible with later versions that don't have the // bug. if (isset($form['workflow'][''])) { return ''; } return isset($form['#wf']->name) ? $form['#wf']->name : $form['workflow']['#title']; } function _workflow_extensions_assign_handlers($form) { $original_handlers = $form['#submit']; // e.g. 'workflow_tab_form_submit' or 'menu_node_form_submit' return ($form['#id'] == 'node-form') // 'menu_node_form_submit', 'upload_node_form_submit' ? array('workflow_extensions_form_submit', 'node_form_submit') // node_form_submit() will add original handlers : array_merge(array('workflow_extensions_form_submit'), $original_handlers); // [#1346078] ? //$original_handlers = array_merge($original_handlers, $form['buttons']['submit']['#submit']); // return array_merge(array('workflow_extensions_form_submit'), $original_handlers); } /** * Implementation of hook_token_list(). * * Note: [workflow-new-state-name] is in fact a pseudo-token, but the user * doesn't have to know that! */ function workflow_extensions_token_list($context = 'all') { if (module_exists('workflow') && in_array($context, array('workflow', 'node', 'all'))) { $tokens['workflow']['workflow-new-state-name'] = 'New state of content'; $tokens['workflow']['workflow-state-age'] = 'Seconds elapsed since last state change'; } if ($context == 'node'|| $context == 'all') { $tokens['node']['mod-since-seconds'] = 'Seconds elapsed since last modification'; } return $tokens; } /** * Implementation of hook_token_values(). * * Returning [workflow-state-age] for both node and workflow contexts as there * seems to be an issue with using Workflow state as the data type argument in * a Rule set. Such a Rule set won't show as available in a scheduled triggered * rule. The Content (ie node) data type must be used instead. */ function workflow_extensions_token_values($context, $object = NULL) { $values = array(); switch ($context) { case 'node': case 'workflow': if (isset($object)) { $node = (object)$object; if (module_exists('workflow')) { $stamp = db_result(db_query_range("SELECT stamp FROM {workflow_node_history} WHERE nid = %d ORDER BY stamp DESC", $node->nid, 0, 1)); $values['workflow-state-age'] = $stamp ? (time() - $stamp) : 0; } $values['mod-since-seconds'] = $node->changed ? (time() - $node->changed) : 0; } break; } return $values; } /** * Return the name for the workflow transition identified by the supplied * from-state and to-state names. * * @param int $wid, workflow identifier, maybe NULL (but then the combination * of $from_state_name and $to_state_name must be unique across all workflows) * @param string $from_state_name * @param string $to_state_name * @param object $node, context for token replacement; if omitted an attempt * will be made to load the node based on the nid in the URL. This will fail * when creating new content, in which case a partial node must be supplied. */ function workflow_extensions_get_transition_label($wid, $from_state_name, $to_state_name, $node = NULL) { if (module_exists('workflow_named_transitions')) { $transitions = workflow_named_transitions_get_transitions($wid); foreach ($transitions as $transition) { if ($transition['from_state'] == $from_state_name && $transition['to_state'] == $to_state_name) { return workflow_extensions_replace_state_name_tokens($transition['label'], $to_state_name, $node); } } // No label defined, fall through as if module 'workflow_named_transitions' // wasn't installed } $tokenized_label = variable_get('workflow_extensions_change_state_button_label', ''); // Don't think we need to check_markup(). Only users with 'administer site // configuration' permission can set the label pattern, so it's up to them to // use or not use HTML and/or javascript. return workflow_extensions_replace_state_name_tokens($tokenized_label, $to_state_name, $node); } function workflow_extensions_replace_state_name_tokens($tokenized_label, $to_state_name = NULL, $node = NULL) { if (empty($tokenized_label)) { return t('Move to "@state_name"', array('@state_name' => $to_state_name)); } $label = workflow_extensions_replace_tokens_raw($tokenized_label, $node); if (!empty($to_state_name)) { // Once the real tokens have been replaced, replace the pseudo-token // [workflow-new-state-name] $label = str_replace('[workflow-new-state-name]', $to_state_name, $label); } return $label; } function workflow_extensions_replace_tokens_raw($tokenized_label, $node = NULL) { if (module_exists('token')) { global $user; $objects['global'] = NULL; $objects['user'] = $user; if ($node == NULL && arg(0) == 'node' && is_numeric(arg(1))) { $node = node_load(arg(1)); // node/% } $objects['node'] = $objects['workflow'] = $node; return token_replace_multiple($tokenized_label, $objects); } return $tokenized_label; } /** * Implementation of hook_views_api(). */ function workflow_extensions_views_api() { return array( 'api' => views_api_version(), 'path' => drupal_get_path('module', 'workflow_extensions') .'/views' ); }