shop, prepare food, eat, and clean up.'); case strstr($section, 'admin/build/workflow/actions') && (sizeof($section) == 22): return t('Use this page to set actions to happen when transitions occur. To configure actions, use the actions module.', array('@link' => url('admin/actions'))); } } /** * Implementation of hook_perm(). */ function workflow_perm() { return array('administer workflow', 'schedule workflow transitions'); } /** * Implementation of hook_menu(). */ function workflow_menu($may_cache) { $items = array(); $access = user_access('administer workflow'); if ($may_cache) { $items[] = array('path' => 'admin/build/workflow', 'title' => t('Workflow'), 'access' => $access, 'callback' => 'workflow_overview', 'description' => t('Allows the creation and assignment of arbitrary workflows to node types.')); $items[] = array('path' => 'admin/build/workflow/edit', 'title' => t('Edit workflow'), 'type' => MENU_CALLBACK, 'callback' => 'workflow_edit_page'); $items[] = array('path' => 'admin/build/workflow/list', 'title' => t('List'), 'weight' => -10, 'callback' => 'workflow_page', 'type' => MENU_DEFAULT_LOCAL_TASK); $items[] = array('path' => 'admin/build/workflow/add', 'title' => t('Add workflow'), 'weight' => -8, 'callback' => 'drupal_get_form', 'callback arguments' => array('workflow_add_form'), 'type' => MENU_LOCAL_TASK); $items[] = array('path' => 'admin/build/workflow/state', 'title' => t('Add state'), 'type' => MENU_CALLBACK, 'callback' => 'drupal_get_form', 'callback arguments' => array('workflow_state_add_form')); $items[] = array('path' => 'admin/build/workflow/state/delete', 'title' => t('Delete State'), 'type' => MENU_CALLBACK, 'callback' => 'drupal_get_form', 'callback arguments' => array('workflow_state_delete_form')); $items[] = array('path' => 'admin/build/workflow/delete', 'title' => t('Delete workflow'), 'type' => MENU_CALLBACK, 'callback' => 'drupal_get_form', 'callback arguments' => array('workflow_delete_form')); $items[] = array('path' => 'admin/build/workflow/actions', 'title' => t('Workflow actions'), 'type' => MENU_CALLBACK, 'callback' => 'workflow_actions_page'); $items[] = array('path' => 'admin/build/workflow/actions/remove', 'title' => t('Workflow actions'), 'type' => MENU_CALLBACK, 'callback' => 'drupal_get_form', 'callback arguments' => array('workflow_actions_remove_form')); } else { if (arg(0) == 'node' && is_numeric(arg(1))) { $node = node_load(arg(1)); $wid = workflow_get_workflow_for_type($node->type); if ($wid) { // workflow exists for this type global $user; $roles = array_keys($user->roles); if ($node->uid == $user->uid) { $roles = array_merge(array('author'), $roles); } $workflow = db_fetch_object(db_query("SELECT * FROM {workflows} WHERE wid = %d", $wid)); $allowed_roles = $workflow->tab_roles ? explode(',', $workflow->tab_roles) : array(); $items[] = array('path' => "node/$node->nid/workflow", 'title' => t('Workflow'), 'access' => array_intersect($roles, $allowed_roles) || user_access('administer nodes'), 'type' => MENU_LOCAL_TASK, 'weight' => 2, 'callback' => 'workflow_tab_page', 'callback arguments' => arg(1), ); } } } return $items; } function workflow_tab_page($nid) { $node = node_load($nid); drupal_set_title(check_plain($node->title)); $wid = workflow_get_workflow_for_type($node->type); $states_per_page = 20; $states = workflow_get_states($wid) + array(t('(creation)')); $current = workflow_node_current_state($node); $output = '
'. t('Current state: @state', array('@state' => $states[$current])) . "
\n"; $output .= drupal_get_form('workflow_tab_form', $node, $wid, $states, $current); $result = pager_query("SELECT h.*, u.name FROM {workflow_node_history} h LEFT JOIN {users} u ON h.uid = u.uid WHERE nid = %d ORDER BY stamp DESC", $states_per_page, 0, NULL, $nid); $rows = array(); while ($history = db_fetch_object($result)) { $rows[] = array( format_date($history->stamp), check_plain($states[$history->old_sid]), check_plain($states[$history->sid]), theme('username', $history), check_plain($history->comment) ); } $output .= theme('table', array(t('Date'), t('Old State'), t('New State'), t('By'), t('Comment')), $rows, array('class' => 'workflow_history'), t('Workflow History')); $output .= theme('pager', $states_per_page); return $output; } function workflow_tab_form(&$node, $wid, $states, $current) { $form = array(); $choices = workflow_field_choices($node); // Bail out if user doesn't have any target workflow state. if (count($choices) >= 1) { ksort($choices); $wid = workflow_get_workflow_for_type($node->type); $name = check_plain(workflow_get_name($wid)); // see if scheduling information is present if ($node->_workflow_scheduled_timestamp && $node->_workflow_scheduled_sid) { global $user; if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) { $timezone = $user->timezone; } else { $timezone = variable_get('date_default_timezone', 0); } $current = $node->_workflow_scheduled_sid; // the default value should be the upcoming sid $timestamp = $node->_workflow_scheduled_timestamp; $comment = $node->_workflow_scheduled_comment; } workflow_node_form($form, t('Change %s state', array('%s' => $name)), $name, $current, $choices, $timestamp, $comment); $form['node'] = array( '#type' => 'value', '#value' => $node, ); $form['submit'] = array( '#type' => 'submit', '#value' => t('Submit') ); } return $form; } function workflow_tab_form_submit($form_id, $form_values) { $node = $form_values['node']; // mockup a node so we don't need to repeat the code for processing this $node->workflow = $form_values['workflow']; $node->workflow_comment = $form_values['workflow_comment']; $node->workflow_scheduled = $form_values['workflow_scheduled']; $node->workflow_scheduled_date = $form_values['workflow_scheduled_date']; $node->workflow_scheduled_hour = $form_values['workflow_scheduled_hour']; // call node save to make sure all saving properties run on this node node_save($node); return 'node/' . $node->nid; } /** * Implementation of hook_nodeapi(). * Summary of nodeapi ops we can see (Drupal 4.7): * * preview submit submit preview * (from (from (from (from * add add) add) view edit edit) edit) * -------- -------- -------- ----- ------- -------- -------- * load load load load * prepare prepare prepare prepare prepare prepare * validate validate validate validate * view view view * submit submit * insert update * */ function workflow_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) { switch ($op) { case 'load': $node->_workflow = workflow_node_current_state($node); // scheduling information $res = db_query('SELECT * FROM {workflow_scheduled_transition} WHERE nid = %d', $node->nid); if ($row = db_fetch_object($res)) { $node->_workflow_scheduled_sid = $row->sid; $node->_workflow_scheduled_timestamp = $row->scheduled; $node->_workflow_scheduled_comment = $row->comment; } break; case 'insert': case 'update': // stop if no workflow for this node type $wid = workflow_get_workflow_for_type($node->type); if (!$wid) { break; } // get new state $sid = $node->workflow; if (!$sid && $op == 'insert') { // if not specified, use first valid $choices = workflow_field_choices($node); $keys = array_keys($choices); $sid = array_shift($keys); } // make sure new state is a valid choice if (array_key_exists($sid, workflow_field_choices($node))) { // check to see if this is an immediate change or a scheduled change if (!$node->workflow_scheduled) { workflow_execute_transition($node, $sid, $node->workflow_comment); // do transition } else { // schedule the the time to change the state $nid = $node->nid; $comment = $node->workflow_comment; $old_sid = workflow_node_current_state($node); if ($node->workflow_scheduled_date['day'] < 10) { $node->workflow_scheduled_date['day'] = '0' . $node->workflow_scheduled_date['day']; } if ($node->workflow_scheduled_date['month'] < 10) { $node->workflow_scheduled_date['month'] = '0' . $node->workflow_scheduled_date['month']; } if (!$node->workflow_scheduled_hour) { $node->workflow_scheduled_hour = '00:00'; } $scheduled = $node->workflow_scheduled_date['year'] . $node->workflow_scheduled_date['month'] . $node->workflow_scheduled_date['day'] . ' ' . $node->workflow_scheduled_hour . 'Z'; if ($scheduled = strtotime($scheduled)) { // adjust for user and site timezone settings global $user; if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) { $timezone = $user->timezone; } else { $timezone = variable_get('date_default_timezone', 0); } $scheduled = $scheduled - $timezone; // clear previous entries and insert db_query("DELETE FROM {workflow_scheduled_transition} WHERE nid = $nid"); db_query('INSERT INTO {workflow_scheduled_transition} VALUES (%d, %d, %d, %d,\'%s\')', $nid, $old_sid, $sid, $scheduled, $comment); drupal_set_message("$node->title is scheduled for state change on " . format_date($scheduled)); } } } break; case 'delete': db_query("DELETE FROM {workflow_node} WHERE nid = %d", $node->nid); break; } } /** * Add the actual form widgets for workflow change to the passed in form. * * @param array $form * @param string $name * @param string $current * @param array $choices */ function workflow_node_form(&$form, $title, $name, $current, $choices, $timestamp = NULL, $comment = NULL) { if (sizeof($choices) == 1) { $form['workflow'][$name] = array( '#type' => 'hidden', '#value' => $current, '#parents' => array('workflow'), ); } else { $form['workflow'][$name] = array( '#type' => 'radios', '#title' => $title, '#options' => $choices, '#name' => $name, '#parents' => array('workflow'), '#default_value' => $current ); // HACK: using arg to see if we're on a node add page // i have to do this because current is never creation! // scheduling from transition is major bad news if (arg(1) != 'add' && user_access('schedule workflow transitions')) { $scheduled = $timestamp ? 1 : 0; $timestamp = $scheduled ? $timestamp : time(); $form['workflow']['workflow_scheduled'] = array ( '#type' => 'radios', '#title' => t('Schedule'), '#options' => array ( t('Immediately'), t('Schedule for state change at:'), ), '#default_value' => $scheduled, ); $form['workflow']['workflow_scheduled_date'] = array ( '#type' => 'date', '#default_value' => array('day' => format_date($timestamp, 'custom', 'j'), 'month' => format_date($timestamp, 'custom', 'n'), 'year' => format_date($timestamp, 'custom', 'Y')), ); $hours = format_date($timestamp, 'custom', 'H:i'); $form['workflow']['workflow_scheduled_hour'] = array ( '#type' => 'textfield', '#description' => t('Please enter a time in 24 hour (eg. HH:MM) format. If no date is included, the default will be midnight on the specified date. The current time is: ') . format_date(time()), '#default_value' => $scheduled ? $hours : NULL, ); } $form['workflow']['workflow_comment'] = array( '#type' => 'textarea', '#title' => t('Comment'), '#description' => t('A comment to put in the workflow log.'), '#default_value' => $comment, ); } } /** * Generate a forms API compliant workflow field. * * @param object &$node * @return array */ function workflow_form_alter($form_id, &$form) { if (isset($form['type']) && $form['type']['#value'] .'_node_form' == $form_id) { $node = $form['#node']; $choices = workflow_field_choices($node); $wid = workflow_get_workflow_for_type($node->type); $states = workflow_get_states($wid) + array(t('(creation)')); $current = workflow_node_current_state($node); // Bail out if user doesn't have any target workflow state. if (count($choices) < 1) { return; } ksort($choices); $name = check_plain(workflow_get_name($wid)); // if the current node state is not one of the choices, autoselect first choice // we know all states in $choices are states that user has permission to // go to because workflow_field_choices() has already checked that if (!isset($choices[$current])) { $array = array_keys($choices); $current = $array[0]; } if (sizeof($choices) > 1) { $form['workflow'] = array( '#type' => 'fieldset', '#title' => $name, '#collapsible' => TRUE, '#collapsed' => FALSE, '#weight' => 10, ); } // see if scheduling information is present if ($node->_workflow_scheduled_timestamp && $node->_workflow_scheduled_sid) { global $user; if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) { $timezone = $user->timezone; } else { $timezone = variable_get('date_default_timezone', 0); } $current = $node->_workflow_scheduled_sid; // the default value should be the upcoming sid $timestamp = $node->_workflow_scheduled_timestamp; $comment = $node->_workflow_scheduled_comment; } workflow_node_form($form, $name, $name, $current, $choices, $timestamp, $comment); } } /** * Execute a transition (change state of a node). * * @param object $node * @param int $sid * @return int ID of new state. */ function workflow_execute_transition($node, $sid, $comment = NULL) { $old_sid = workflow_node_current_state($node); if ($old_sid == $sid) { // stop if not going to a different state // Write comment into history though. if ($comment && !$node->_workflow_scheduled_comment) { $node->workflow_stamp = time(); db_query("UPDATE {workflow_node} SET stamp = %d WHERE nid = %d", $node->workflow_stamp, $node->nid); _workflow_write_history($node, $sid, $comment); } return; } // Make sure this transition is valid and allowed for the current user. global $user; if ($user->uid > 1) { // allow any state change for superuser (might be cron) $tid = workflow_get_transition_id($old_sid, $sid); if (!$tid) { watchdog('workflow', t('Attempt to go to nonexistent transition (from %old to %new)', array('%old' => $old_sid, '%new' => $sid), WATCHDOG_ERROR)); return; } if (!workflow_transition_allowed($tid, array_merge(array_keys($user->roles), array('author')))) { watchdog('workflow', t('User %user not allowed to go from state %old to %new)', array('%user' => $user->name, '%old' => $old_sid, '%new' => $sid), WATCHDOG_NOTICE)); return; } } // Invoke a callback indicating a transition is about to occur. Modules // may veto the transition by returning FALSE. $result = module_invoke_all('workflow', 'transition pre', $old_sid, $sid, $node); if (in_array(FALSE, $result)) { // stop if a module says so return; } _workflow_node_to_state($node, $sid, $comment); // change the state // Register state change with watchdog $state_name = db_result(db_query("SELECT state FROM {workflow_states} WHERE sid = %d", $sid)); $type = node_get_types('name', $node->type); watchdog('workflow', t('State of @type %node_title set to @state_name', array('@type' => $type, '%node_title' => $node->title, '@state_name' => $state_name)), WATCHDOG_NOTICE, l('view', $node->nid)); // Notify modules that transition has occurred. Actions should take place // in response to this callback, not the previous one. module_invoke_all('workflow', 'transition post', $old_sid, $sid, $node); // clear any references in the scheduled listing db_query('DELETE FROM {workflow_scheduled_transition} WHERE nid = %d', $node->nid); } /** * Implementation of a Drupal action. * Changes the workflow state of a node to the next state of the workflow. * */ function action_workflow_execute_transition($op, $edit = array(), &$node) { switch($op) { case 'do': // if this action is being fired because it's attached to a workflow transition // then the node's new state (now it's current state) should be in $node->workflow; // otherwise the current state is placed in $node->_workflow by our nodeapi load if (!isset($node->workflow) && !isset($node->_workflow)) { watchdog('workflow', t('Unable to get current workflow state of node %nid.', array('%nid' => $node->nid))); return; } $current_state = isset($node->workflow) ? $node->workflow : $node->_workflow; // get the node's new state //$new_state = $edit['target_state']; // change to specific state not yet implemented $new_state = ''; if ($new_state == '') { $choices = workflow_field_choices($node); foreach ($choices as $sid => $name) { if (isset($flag)) { $new_state = $sid; $new_state_name = $name; break; } if ($sid == $current_state) { $flag = TRUE; } } } // fire the transition watchdog('action', t('Changing workflow state of node id %id to %state', array('%id' => intval($node->nid), '%state' => check_plain($new_state_name)))); workflow_execute_transition($node, $new_state); watchdog('action', t('Changed workflow state of node id %id to %state', array('%id' => intval($node->nid), '%state' => check_plain($new_state_name)))); break; case 'metadata': return array( 'description' => t('Change workflow state of a node to next state'), 'type' => t('Workflow'), 'batchable' => TRUE, 'configurable' => FALSE, ); // return an HTML config form for the action case 'form': return ''; // validate the HTML form case 'validate': return TRUE; // process the HTML form to store configuration case 'submit': return ''; } } /** * Get the states one can move to for a given node. * * @param object $node * @return array */ function workflow_field_choices($node) { global $user; $wid = workflow_get_workflow_for_type($node->type); if (!$wid) { // no workflow for this type return array(); } $states = workflow_get_states($wid); $roles = array_keys($user->roles); $current_sid = workflow_node_current_state($node); // if the node author or this is a new page, give the authorship role if (($user->uid == $node->uid && $node->uid > 0) || (arg(0) == 'node' && arg(1) == 'add')) { $roles += array('author' => 'author'); } if ($user->uid == 1) { // if the superuser $roles = 'ALL'; } $transitions = workflow_allowable_transitions($current_sid, 'to', $roles); return $transitions; } /** * Get the current state of a given node. * * @param object $node * @return string state ID */ function workflow_node_current_state($node) { $sid = db_result(db_query("SELECT sid FROM {workflow_node} WHERE nid=%d ", $node->nid)); if (!$sid) { $wid = workflow_get_workflow_for_type($node->type); $sid = _workflow_creation_state($wid); } return $sid; } function _workflow_creation_state($wid) { return db_result(db_query("SELECT sid FROM {workflow_states} WHERE ". "wid=%d AND sysid=%d", $wid, WORKFLOW_CREATION)); } /** * Implementation of hook_workflow(). */ function workflow_workflow($op, $old_state, $new_state, $node) { switch ($op) { case 'transition pre': break; case 'transition post': // a transition has occurred; fire off actions associated with this transition // an SQL guru could clean this up with a complicated JOIN $tid = workflow_get_transition_id($old_state, $new_state); if ($tid) { $actions_this_tid = workflow_get_actions($tid); if ($actions_this_tid && function_exists('actions_do')) { actions_do(array_keys($actions_this_tid), $node); } } break; } } /** * Create the form for adding/editing a workflow. * * @param $name * Name of the workflow if editing. * @param $add * Boolean, if true edit workflow name. * * @return * HTML form. * */ function workflow_add_form($name = NULL) { $form = array(); $form['wf_name'] = array( '#type' => 'textfield', '#title' => t('Workflow Name'), '#maxlength' => '254', '#default_value' => $name ); $form['submit'] = array('#type' => 'submit', '#value' => t('Add Workflow')); return $form; } function workflow_add_form_validate($form_id, $form_values) { $workflow_name = $form_values['wf_name']; $workflows = array_flip(workflow_get_all()); // Make sure a nonblank workflow name is provided if ($workflow_name == '') { form_set_error('wf_name', t('Please provide a nonblank name for the new workflow.')); } // Make sure workflow name is not a duplicate if (array_key_exists($workflow_name, $workflows)) { form_set_error('wf_name', t('A workflow with the name %name already exists. Please enter another name for your new workflow.', array('%name' => $workflow_name))); } } function workflow_add_form_submit($form_id, $form_values) { $workflow_name = $form_values['wf_name']; if (array_key_exists('wf_name', $form_values) && $workflow_name != '') { workflow_create($workflow_name); watchdog('workflow', t('Created workflow %name', array('%name' => $workflow_name))); drupal_set_message(t('The workflow %name was created. You should now add states to your workflow.', array('%name' => $workflow_name)), 'warning'); return('admin/build/workflow'); } } /** * Create the form for confirmation of deleting a workflow. * * @param $wid * The ID of the workflow. * * @return * HTML form. * */ function workflow_delete_form($wid, $sid = NULL) { if (isset($sid)) { return workflow_state_delete_form($wid, $sid); } $form = array(); $form['wid'] = array('#type' => 'value', '#value' => $wid); return confirm_form( $form, t('Are you sure you want to delete %title? All nodes that have a workflow state associated with this workflow will have those workflow states removed.', array('%title' => workflow_get_name($wid))), $_GET['destination'] ? $_GET['destination'] : 'admin/build/workflow', t('This action cannot be undone.'), t('Delete'), t('Cancel') ); } function workflow_delete_form_submit($form_id, $form_values) { if ($form_values['confirm'] == 1) { $workflow_name = workflow_get_name($form_values['wid']); workflow_deletewf($form_values['wid']); watchdog('workflow', t('Deleted workflow %name with all its states', array('%name' => $workflow_name))); drupal_set_message(t('The workflow %name with all its states was deleted.', array('%name' => $workflow_name))); return('admin/build/workflow'); } } /** * View workflow permissions by role * * @param $wid * The ID of the workflow */ function workflow_permissions($wid) { $name = workflow_get_name($wid); $all = array(); $roles = array('author' => t('author')) + user_roles(); foreach ($roles as $role => $value) { $all[$role]['name'] = $value; } $result = db_query( "SELECT t.roles, s1.state AS state_name, s2.state AS target_state_name " . "FROM {workflow_transitions} t " . "INNER JOIN {workflow_states} s1 ON s1.sid = t.sid " . "INNER JOIN {workflow_states} s2 ON s2.sid = t.target_sid " . "WHERE s1.wid = %d " . "ORDER BY s1.weight ASC , s1.state ASC , s2.weight ASC , s2.state ASC", $wid); while ($data = db_fetch_object($result)) { foreach (explode(',', $data->roles) as $role) { $all[$role]['transitions'][] = array($data->state_name, WORKFLOW_ARROW, $data->target_state_name); } } $output = ''; $header = array(t('From'), '', t('To')); foreach ($all as $role => $value) { $output .= '' . t('None') . ' |
' . t('No workflows have been added. Would you like to add a workflow?', array('@link' => url('admin/build/workflow/add'))) . '
'; } $output .= drupal_get_form('workflow_types_form'); return $output; } /** * Tell caller whether a state is a protected system state, such as the creation state. * * @param $state * The name of the state to test * * @return * boolean * */ function workflow_is_system_state($state) { static $states; if (!isset($states)) { $states = array(t('(creation)') => TRUE); } return isset($states[$state]); } /** * Create the form for confirmation of deleting a workflow state. * * @param $wid * integer The ID of the workflow. * @param $sid * The ID of the workflow state. * * @return * HTML form. * */ function workflow_state_delete_form($wid, $sid) { $states = workflow_get_states($wid); $form = array(); $form['wid'] = array('#type' => 'value', '#value' => $wid); $form['sid'] = array('#type' => 'value', '#value' => $sid); return confirm_form( $form, t('Are you sure you want to delete %title (and all its transitions)?', array('%title' => $states[$sid])), $_GET['destination'] ? $_GET['destination'] : 'admin/build/workflow', t('This action cannot be undone.'), t('Delete'), t('Cancel') ); } function workflow_state_delete_form_submit($form_id, $form_values) { $states = workflow_get_states($form_values['wid']); $state_name = $states[$form_values['sid']]; if ($form_values['confirm'] == 1) { workflow_state_delete($form_values['sid']); watchdog('workflow', t('Deleted workflow state %name', array('%name' => $state_name))); drupal_set_message(t('The workflow state %name was deleted.', array('%name' => $state_name))); } return('admin/build/workflow'); } function workflow_types_form() { $form = array(); $workflows = array('<' . t('None') . '>') + workflow_get_all(); if (count($workflows) == 0) { return $form; } $type_map = array(); $result = db_query("SELECT wid, type FROM {workflow_type_map}"); while ($data = db_fetch_object($result)) { $type_map[$data->type] = $data->wid; } $form['#theme'] = 'workflow_types_form'; $form['help'] = array( '#type' => 'item', '#value' => t('Each node type may have a separate workflow:'), ); foreach (node_get_types('names') as $type => $name) { $form[$type] = array( '#type' => 'select', '#title' => $name, '#options' => $workflows, '#default_value' => isset($type_map[$type]) ? $type_map[$type] : 0 ); } $form['submit'] = array( '#type' => 'submit', '#value' => t('Save Workflow Mapping') ); return $form; } function theme_workflow_types_form($form) { $header = array(t('Node Type'), t('Workflow')); $row = array(); foreach (element_children($form) as $key) { if ($form[$key]['#type'] == 'select') { $name = $form[$key]['#title']; unset($form[$key]['#title']); $row[] = array($name, drupal_render($form[$key])); } } $output = drupal_render($form['help']); $output .= theme('table', $header, $row); return $output . drupal_render($form); } function workflow_types_form_submit($form_id, $form_values) { workflow_types_save($form_values); drupal_set_message(t('The workflow mapping was saved.')); return('admin/build/workflow'); } function workflow_actions_remove_form($wid, $tid, $aid) { $form = array(); $form['wid'] = array('#type' => 'value', '#value' => $wid); $form['tid'] = array('#type' => 'value', '#value' => $tid); $form['aid'] = array('#type' => 'value', '#value' => $aid); $actions = actions_get_all_actions(); $output = confirm_form( $form, t('Are you sure you want to delete the action %title?', array('%title' => $actions[$aid]['description'])), $_GET['destination'] ? $_GET['destination'] : 'admin/build/workflow/actions/'. $wid, t('You can add it again later if you wish.'), t('Delete'), t('Cancel') ); return $output; } function workflow_actions_remove_confirm_submit($form_id, $form_values) { if ($form_values['confirm'] == 1) { $aid = $form_values['aid']; $actions = actions_actions_map(actions_get_all_actions()); workflow_actions_remove($form_values['tid'], $aid); watchdog('workflow', t('Action %action deleted', array('%action' => check_plain($actions[$aid])))); drupal_set_message(t('Action %action deleted.', array('%action' => $actions[$aid]))); return('admin/workflow/actions/'. $form_values['wid']); } } function workflow_actions_page($wid) { if (!module_exists('actions')) { drupal_set_message(t('Before you can assign actions you must install and enable the actions module.'), 'error'); drupal_goto('admin/build/workflow'); } $options = array(t('None')); foreach (actions_actions_map(actions_get_all_actions()) as $aid => $action) { $options[$action['type']][$aid] = $action['description']; } $header = array( array('data' => t('Transition'), 'colspan' => '3'), array('data' => t('Actions')) ); $rows = array(); $states = workflow_get_states($wid); foreach ($states as $sid => $state) { // we'll create a row for each allowable transition $allowable_to = workflow_allowable_transitions($sid); foreach ($allowable_to as $to_sid => $name) { $tid = workflow_get_transition_id($sid, $to_sid); $rows[] = array( array('data' => $state), array('data' => WORKFLOW_ARROW), array('data' => $name), array('data' => drupal_get_form('workflow_actions_form', $wid, $tid, $options)) ); } } if (count($rows) == 0) { $output = t('You must first set up transitions before you can assign actions.', array('@link' => url('admin/build/workflow/edit/' . $wid))); } else { $output = theme('table', $header, $rows); } return $output; } function workflow_actions_form($wid, $tid, $available_options) { $form['tid'] = array( '#type' => 'hidden', '#value' => $tid ); $form['#multistep'] = true; // get and list the actions that are already assigned to this transition $actions_this_tid = workflow_get_actions($tid); foreach ($actions_this_tid as $aid => $act_name) { $form['remove'][$aid] = array( '#type' => 'item', '#title' => $act_name, '#value' => l(t('remove'), "admin/build/workflow/actions/remove/$wid/$tid/$aid"), ); unset($available_options[md5($aid)]); } // list possible actions that may be assigned if (count($available_options) > 1) { $form['action'] = array( '#type' => 'select', '#options' => $available_options, ); $form['submit'] = array( '#type' => 'submit', '#value' => t('Add') ); } return $form; } function theme_workflow_actions_form($form) { $row = array(); // put each existing action and remove link on it's own row foreach (element_children($form['remove']) as $key) { $name = $form['remove'][$key]['#title']; unset($form['remove'][$key]['#title']); $row[] = array($name, drupal_render($form['remove'][$key])); } // and if there are form items for a "new" action put that on a row if (isset($form['action'])) { $row[] = array(drupal_render($form['action']), drupal_render($form['submit'])); } $output .= theme('table', array(), $row); return $output . drupal_render($form); } function workflow_actions_form_submit($form, $form_values) { $tid = $form_values['tid']; $aid = actions_key_lookup($form_values['action']); workflow_actions_save($tid, $aid); // the form does weird things if we don't do a drupal_goto() $wid = arg(4); return "admin/build/workflow/actions/$wid"; } function workflow_actions_remove_form_submit($form_id, $form_values) { if ($form_values['confirm'] == 1) { $aid = $form_values['aid']; $actions = actions_get_all_actions(); $action = $actions[$aid]['description']; workflow_actions_remove($form_values['tid'], $aid); watchdog('workflow', t('Action %action deleted', array('%action' => $action))); drupal_set_message(t('Action %action deleted.', array('%action' => $action))); } return 'admin/build/workflow/actions/'. $form_values['wid']; } /** * Given the ID of a workflow, return its name. * * @param integer $wid * The ID of the workflow. * * @return string * The name of the workflow. * */ function workflow_get_name($wid) { $name = db_result(db_query("SELECT name FROM {workflows} WHERE wid = %d", $wid)); return $name; } /** * Get ID of a workflow for a node type. * * @return int * The ID of the workflow or FALSE if none. * */ function workflow_get_workflow_for_type($type) { $wid = db_result(db_query("SELECT wid FROM {workflow_type_map} WHERE type = '%s'", $type)); return $wid > 0 ? $wid : FALSE; } /** * Get names and IDS of all workflows from the database. * * @return * An array of workflows keyed by ID. * */ function workflow_get_all() { $workflows = array(); $result = db_query("SELECT wid, name FROM {workflows} ORDER BY name ASC"); while ($data = db_fetch_object($result)) { $workflows[$data->wid] = $data->name; } return $workflows; } /** * Create a workflow and its (creation) state. * * @param $name * The name of the workflow. * */ function workflow_create($name) { $wid = db_next_id('{workflows}_wid'); db_query("INSERT INTO {workflows} (wid, name) VALUES (%d, '%s')", $wid, $name); workflow_state_save(array('wid' => $wid, 'state' => t('(creation)'), 'sysid' => WORKFLOW_CREATION, 'weight' => WORKFLOW_CREATION_DEFAULT_WEIGHT)); return $wid; } /** * Save a workflow's name in the database. * * @param $name * The name of the workflow. * */ function workflow_update($wid, $name, $tab_roles) { db_query("UPDATE {workflows} SET name = '%s', tab_roles = '%s' WHERE wid = %d", $name, implode(',', $tab_roles), $wid); } /** * Delete a workflow from the database. Deletes all states, * transitions and node type mappings too. Removes workflow state * information from nodes participating in this workflow. * * @param $wid * The ID of the workflow. * */ function workflow_deletewf($wid) { $wf = workflow_get_name($wid); $result = db_query('SELECT sid FROM {workflow_states} WHERE wid = %d', $wid); while ($data = db_fetch_object($result)) { // delete the state and any associated transitions and actions workflow_state_delete($data->sid); db_query('DELETE FROM {workflow_node} WHERE sid = %d', $data->sid); } workflow_types_delete($wid); db_query('DELETE FROM {workflows} WHERE wid = %d', $wid); } /** * Load workflow states for a workflow from the database. * * @param $wid * The ID of the workflow. * * @return * An array of workflow states keyed by state ID. */ function workflow_get_states($wid) { $states = array(); $result = db_query("SELECT sid, state FROM {workflow_states} WHERE wid = %d ORDER BY weight, state", intval($wid)); while ($data = db_fetch_object($result)) { $states[$data->sid] = $data->state; } return $states; } /** * Given the ID of a workflow, return an array with all its attributes. * * @param $sid * The ID of the workflow state. * * @return * An array with all attributes of the state. */ function workflow_get_state($sid) { $state = array(); $result = db_query('SELECT wid, state, weight, sysid FROM {workflow_states} WHERE sid = %d', intval($sid)); while ($data = db_fetch_object($result)) { $state['wid'] = $data->wid; $state['state'] = $data->state; $state['weight'] = $data->weight; $state['sysid'] = $data->sysid; } return $state; } /** * Add or update a workflow state to the database. * * @param $edit * An array containing values for the new or updated workflow state. * * @return * The ID of the new or updated workflow state. */ function workflow_state_save($edit) { $defaults = array('weight' => 0, 'sysid' => 0); $edit = array_merge($defaults, $edit); if (!isset($edit['sid'])) { $edit['sid'] = db_next_id('{workflow_states}_sid'); db_query("INSERT INTO {workflow_states} (sid, wid, state, sysid, weight) VALUES (%d, %d, '%s', %d, %d)", $edit['sid'], $edit['wid'], $edit['state'], $edit['sysid'], $edit['weight']); } else { db_query("UPDATE {workflow_states} SET wid = %d, state = '%s', sysid = %d, weight = %d WHERE sid = %d", $edit['wid'], $edit['state'], $edit['sysid'], $edit['weight'], $edit['sid']); } return $edit['sid']; } /** * Legacy code for workflow_state_create(). */ function workflow_state_create($wid, $name) { return workflow_state_save(array('wid' => $wid, 'state' => $name)); } /** * Delete a workflow state from the database, including any * transitions the state was involved in and any associations * with actions that were made to that transition. * * @param $sid * The ID of the state to delete. */ function workflow_state_delete($sid) { // find out which transitions this state is involved in $preexisting = array(); $result = db_query("SELECT sid, target_sid FROM {workflow_transitions} WHERE sid = %d OR target_sid = %d", $sid, $sid); while ($data = db_fetch_object($result)) { $preexisting[$data->sid][$data->target_sid] = TRUE; } // delete the transitions and associated actions foreach ($preexisting as $from => $array) { foreach (array_keys($array) as $target_id) { $tid = workflow_get_transition_id($from, $target_id); workflow_transition_delete($tid); } } // delete the state db_query("DELETE FROM {workflow_states} WHERE sid = %d", intval($sid)); } /** * Delete a transition (and any associated actions). * * @param $tid * The ID of the transition. */ function workflow_transition_delete($tid) { $actions = workflow_get_actions($tid); foreach (array_keys($actions) as $aid) { workflow_actions_remove($tid, $aid); } db_query("DELETE FROM {workflow_transitions} WHERE tid = %d", $tid); } /** * Get allowable transitions for a given workflow state. * * @param $sid * The ID of the state in question. * @param $dir * The direction of the transition: 'to' or 'from' the state denoted by $sid. * When set to 'to' all the allowable states that may be moved to are * returned; when set to 'from' all the allowable states that may move to the * current state are returned. * @param mixed $roles * Array of ints (and possibly the string 'author') representing the user's * roles. If the string 'ALL' is passed (instead of an array) the role * constraint is ignored (this is the default for backwards compatibility). * * @return * Associative array of states (sid=>name pairs), excluding current state. * */ function workflow_allowable_transitions($sid, $dir = 'to', $roles = 'ALL') { $transitions = array(); $field = $dir == 'to' ? 'target_sid' : 'sid'; $field_where = $dir != 'to' ? 'target_sid' : 'sid'; $result = db_query("SELECT t.tid, t.%s as state_id, s.state as state_name FROM " . "{workflow_transitions} t INNER JOIN {workflow_states} s ON s.sid = " . "t.%s WHERE t.%s = %d", $field, $field, $field_where, $sid); while ($t = db_fetch_object($result)) { if ($roles == 'ALL' || workflow_transition_allowed($t->tid, $roles)) { //$state_id = str_pad($t->state_id, 5, '0', STR_PAD_LEFT); $state_id = $t->state_id; $transitions[$state_id] = $t->state_name; } } return $transitions; } /** * Save mapping of workflow to node type. E.g., "the story node type * is using the Foo workflow." * * @param $form_values */ function workflow_types_save($form_values) { $nodetypes = node_get_types(); db_query("DELETE FROM {workflow_type_map}"); foreach ($nodetypes as $type => $name) { db_query("INSERT INTO {workflow_type_map} (type, wid) VALUES ('%s', %d)", $type, intval($form_values[$type])); } } function workflow_types_delete($wid) { db_query("DELETE FROM {workflow_type_map} WHERE wid = %d", $wid); } /** * Get the actions associated with a given transition. * @param int $tid * @return array * Actions as aid=>description pairs. */ function workflow_get_actions($tid) { $actions = array(); if (!function_exists('actions_do')) { return $actions; } $result = db_query("SELECT a.aid, a.description FROM {actions} a INNER JOIN {workflow_actions} w ON a.aid = w.aid WHERE w.tid = %d", $tid); while ($data = db_fetch_object($result)) { $actions[$data->aid] = $data->description; } return $actions; } /** * Get the tid of a transition, if it exists. * * @param int $from * ID (sid) of originating state. * @param int $to * ID (sid) of target state. * @return int * Tid or FALSE if no such transition exists. */ function workflow_get_transition_id($from, $to) { return db_result(db_query("SELECT tid FROM {workflow_transitions} WHERE sid=%d AND target_sid=%d", $from, $to)); } function workflow_actions_save($tid, $aid) { actions_register($aid, 'workflow', $tid); $data = db_fetch_object(db_query("SELECT tid FROM {workflow_actions} WHERE tid = %d AND aid = '%s'", $tid, $aid)); if ($data) return; db_query("INSERT INTO {workflow_actions} (tid, aid, weight) VALUES (%d, '%s', %d)", $tid, $aid, 0); } function workflow_actions_remove($tid, $aid) { actions_unregister($aid, 'workflow', $tid); db_query("DELETE FROM {workflow_actions} WHERE tid = %d AND aid = '%s'", $tid, $aid); } /** * Put a node into a state. * No permission checking here; only call this from other functions that know * what they're doing. * * @see workflow_execute_transition() * * @param object $node * @param int $sid */ function _workflow_node_to_state($node, $sid, $comment = NULL) { global $user; $node->workflow_stamp = time(); if (db_result(db_query("SELECT nid FROM {workflow_node} WHERE nid = %d", $node->nid))) { db_query("UPDATE {workflow_node} SET sid = %d, uid = %d, stamp = %d WHERE nid = %d", $sid, $user->uid, $node->workflow_stamp, $node->nid); } else { db_query("INSERT INTO {workflow_node} (nid, sid, uid, stamp) VALUES (%d, %d, %d, %d)", $node->nid, $sid, $user->uid, $node->workflow_stamp); } _workflow_write_history($node, $sid, $comment); } function _workflow_write_history($node, $sid, $comment) { global $user; db_query("INSERT INTO {workflow_node_history} (nid, old_sid, sid, uid, comment, stamp) VALUES (%d, %d, %d, %d, '%s', %d)", $node->nid, $node->_workflow, $sid, $user->uid, $comment, $node->workflow_stamp); } /** * Function to get a list of roles. Used kind of often. */ function workflow_get_roles() { static $roles = NULL; if (!$roles) { $result = db_query('SELECT * FROM {role} ORDER BY name'); $roles = array('author' => 'author'); while ($data = db_fetch_object($result)) { $roles[$data->rid] = $data->name; } } return $roles; } /** * Implementation of hook_views_tables() */ function workflow_views_tables() { $table = array( 'name' => 'workflow_node', 'provider' => 'workflow', 'join' => array( 'left' => array( 'table' => 'node', 'field' => 'nid', ), 'right' => array( 'field' => 'nid', ), ), "filters" => array( 'sid' => array( 'name' => t('Workflow: state'), 'operator' => 'views_handler_operator_andor', 'list' => 'workflow_handler_filter_sid', 'help' => t('Include only nodes in the selected workflow states.'), ), ) ); $tables[$table['name']] = $table; $table = array( 'name' => 'workflow_states', 'provider' => 'workflow', 'join' => array( 'left' => array( 'table' => 'workflow_node', 'field' => 'sid', ), 'right' => array( 'field' => 'sid', ), ), "sorts" => array( 'weight' => array( 'name' => t('Workflow: state'), 'field' => array('weight', 'state'), 'help' => t('Order nodes by workflow state.'), ), ), "fields" => array( 'state' => array( 'name' => t('Workflow: state'), 'sortable' => TRUE, 'help' => t('Display the workflow state of the node.'), ), ), ); $tables[$table['name']] = $table; $table = array( 'name' => 'workflow_node_history', 'provider' => 'workflow', 'join' => array( 'left' => array( 'table' => 'workflow_node', 'field' => 'nid', ), 'right' => array( 'field' => 'nid', ), 'extra' => array( 'stamp = workflow_node.stamp' => NULL, ), ), "fields" => array( 'comment' => array( 'name' => t('Workflow: comment'), 'sortable' => false, 'help' => t('Display the most recent workflow comment of the node.'), ), ), ); $tables[$table['name']] = $table; return $tables; } /** * Implementation of hook_views_arguments() */ function workflow_views_arguments() { $arguments = array( 'workflow_state' => array( 'name' => t('Workflow: state'), 'handler' => 'workflow_handler_arg_sid', 'help' => t('The work flow argument allows users to filter a view by workflow state.'), ), ); return $arguments; } /** * Handler to provide a list of workflow states for the filter list */ function workflow_handler_filter_sid() { $result = db_query("SELECT sid, state FROM {workflow_states} ORDER BY weight, state"); while ($data = db_fetch_object($result)) { $states[$data->sid] = $data->state; } return $states; } /** * Handler to deal with sid as an argument. */ function workflow_handler_arg_sid($op, &$query, $argtype, $arg = '') { switch($op) { case 'summary': $query->add_table('workflow_states', TRUE); $fieldinfo['field'] = "workflow_states.sid"; $query->add_field('sid', 'workflow_states'); $query->add_field('state', 'workflow_states'); $query->add_where('workflow_node.sid IS NOT NULL'); return $fieldinfo; break; case 'filter': $query->add_table('workflow_states', TRUE); if (is_numeric($arg)) { $query->add_where("workflow_states.sid = %d", $arg); } else { $query->add_where("workflow_states.state = '%s'", $arg); } break; case 'link': return l($query->state, "$arg/$query->sid"); case 'title': $state = db_fetch_object(db_query("SELECT state FROM {workflow_states} WHERE sid=%d", $query)); return $state->state; } } /** * Implementation of hook_cron */ function workflow_cron() { $clear_cache = FALSE; //if the time now is greater than the time to publish a node, publish it $nodes = db_query('SELECT * FROM {workflow_scheduled_transition} s WHERE s.scheduled > 0 AND s.scheduled < %d', time()); while ($row = db_fetch_object($nodes)) { $node = node_load($row->nid); // make sure transition is still valid if ($node->_workflow == $row->old_sid) { // do transistion workflow_execute_transition($node, $row->sid, $row->comment); watchdog('content', t('%type: scheduled transition of %title.', array('%type' => t($node->type), '%title' => $node->title)), WATCHDOG_NOTICE, l(t('view'), 'node/'. $node->nid)); $clear_cache = TRUE; } } if ($clear_cache) { // clear the cache so an anonymous poster can see the node being published or unpublished cache_clear_all(); } }