shop, prepare food, eat, and clean up.'); case strstr($section, 'admin/workflow/actions') && (sizeof($section) == 22): return t('Use this page to set actions to happen when transitions occur. To %conf_actions, use the actions module.', array('%conf_actions' => l('configure actions', 'admin/actions'))); } } /** * Implementation of hook_perm(). */ function workflow_perm() { return array('administer workflow'); } /** * Implementation of hook_menu(). */ function workflow_menu($may_cache) { $items = array(); $access = user_access('administer workflow'); if ($may_cache) { $items[] = array('path' => 'admin/workflow', 'title' => t('workflow'), 'access' => $access, 'callback' => 'workflow_overview'); $items[] = array('path' => 'admin/workflow/edit', 'title' => t('Edit workflow'), 'access' => $access, 'type' => MENU_CALLBACK, 'callback' => 'workflow_edit'); $items[] = array('path' => 'admin/workflow/list', 'title' => t('List'), 'access' => $access, 'weight' => -10, 'callback' => 'workflow_page', 'type' => MENU_DEFAULT_LOCAL_TASK); $items[] = array('path' => 'admin/workflow/add', 'title' => t('Add workflow'), 'access' => $access, 'weight' => -8, 'callback' => 'workflow_add_form', 'type' => MENU_LOCAL_TASK); $items[] = array('path' => 'admin/workflow/state', 'title' => t('Add state'), 'access' => $access, 'type' => MENU_CALLBACK, 'callback' => 'workflow_state_add'); $items[] = array('path' => 'admin/workflow/delete', 'title' => t('Delete workflow'), 'access' => $access, 'type' => MENU_CALLBACK, 'callback' => 'workflow_delete'); $items[] = array('path' => 'admin/workflow/actions', 'title' => t('Workflow actions'), 'access' => $access, 'type' => MENU_CALLBACK, 'callback' => 'workflow_actions_form'); $items[] = array('path' => 'admin/workflow/actions/remove', 'title' => t('Workflow actions'), 'access' => $access, 'type' => MENU_CALLBACK, 'callback' => '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) { drupal_set_title(t('workflow')); $node = node_load($nid); $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: ') . check_plain($states[$current]) . "

\n"; $choices = workflow_field_choices($node); $min = $states[$current] == t('(creation)') ? 1 : 2; if (count($choices) >= $min) { // bail out if user has no new target state(s) ksort($choices); $wid = workflow_get_workflow_for_type($node->type); $name = check_plain(workflow_get_name($wid)); $form = array(); workflow_node_form($form, t('Change %s state', array('%s' => $name)), $name, $current, $choices); $form['node'] = array( '#type' => 'value', '#value' => $node, ); $form['submit'] = array( '#type' => 'submit', '#value' => t('Submit') ); $output .= drupal_get_form('workflow_tab', $form); } $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_submit($form_id, $form_values) { $node = $form_values['node']; $sid = $form_values['workflow']; // make sure new state is a valid choice if (array_key_exists($sid, workflow_field_choices($node))) { workflow_execute_transition($node, $sid, $form_values['workflow_comment']); // do transition } 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); 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))) { workflow_execute_transition($node, $sid, $node->workflow_comment); // do transition } 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) { if (sizeof($choices) == 1) { $form['workflow'][$name] = array( '#type' => 'hidden', '#value' => $current ); } else { $form['workflow'][$name] = array( '#type' => 'radios', '#title' => $title, '#options' => $choices, '#name' => $name, '#parents' => array('workflow'), '#default_value' => $current ); $form['workflow']['workflow_comment'] = array( '#type' => 'textarea', '#title' => t('Comment'), '#description' => t('A comment to put in the workflow log.'), ); } } /** * 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); $min = $states[$current] == t('(creation)') ? 1 : 2; if (count($choices) < $min) { // bail out if user has no new target state(s) 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, ); } workflow_node_form($form, $name, $name, $current, $choices); } } /** * 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) { _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 = check_plain(node_get_name($node->type)); //module_invoke($node->type, 'node_name', $node); $state_name = check_plain($state_name); watchdog('workflow', t('State of %type %node_title set to %state_name', array('%type' => $type, '%node_title' => theme('placeholder', $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); } /** * 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 ($user->uid == $node->uid && $node->uid > 0) { // if the node author $roles += array('author' => 'author'); } if ($user->uid == 1) { // if the superuser $roles = 'ALL'; } $transitions = workflow_allowable_transitions($current_sid, 'to', $roles); if ($current_sid != _workflow_creation_state($wid)) { // include current state if not (creation) $transitions = array($current_sid => $states[$current_sid]) + $transitions; } 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'), '#size' => '16', '#maxlength' => '254', '#default_value' => $name ); $form['submit'] = array('#type' => 'submit', '#value' => t('Add Workflow')); $output = drupal_get_form('workflow_add_form', $form); return $output; } function workflow_add_form_validate($form_id, $form_values) { if ($form_values['wf_name'] == '') { form_set_error('', t('Please provide a nonblank name for the new workflow.')); } } function workflow_add_form_submit($form_id, $form_values) { if (array_key_exists('wf_name', $form_values) && $form_values['wf_name'] != '') { $wf_name = $form_values['wf_name']; workflow_create($wf_name); drupal_set_message(t('The workflow %wf was created. You should now add states to your workflow.', array('%wf' => theme('placeholder', $wf_name)))); drupal_goto('admin/workflow'); } } /** * Create the form for confirmation of deleting a workflow. * * @param $wid * The ID of the workflow. * * @return * HTML form. * */ function workflow_delete($wid, $sid = NULL) { if (isset($sid)) { return workflow_state_delete_form($wid, $sid); } $wf = workflow_get_name($wid); $form = array(); $form['wid'] = array('#type' => 'value', '#value' => $wid); $output = confirm_form('workflow_delete_confirm', $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' => theme('placeholder', $wf))), $_GET['destination'] ? $_GET['destination'] : 'admin/workflow', t('This action cannot be undone.'), t('Delete'), t('Cancel') ); return $output; } function workflow_delete_confirm_submit($form_id, $form_values) { if ($form_values['confirm'] == 1) { $wid = $form_values['wid']; $wf = workflow_get_name($wid); workflow_deletewf($wid); drupal_set_message(t('The workflow %wf was deleted.', array('%wf' => theme('placeholder', $wf)))); drupal_goto('admin/workflow'); } } /** * View workflow permissions by role * * @param $wid * The ID of the workflow */ function workflow_permissions($wid) { $name = workflow_get_name($wid); $result = db_query("SELECT sid, state FROM {workflow_states} WHERE wid = %d", $wid); $states = array(); while ($data = db_fetch_object($result)) { $states[$data->sid] = $data->state; } $all = array(); $result = db_query("SELECT * FROM {workflow_transitions}"); while ($data = db_fetch_object($result)) { if (isset($states[$data->sid])) { foreach (explode(',', $data->roles) as $role) { $all[$role][] = array($data->sid, $data->target_sid); } } } $output = ''; $role_names = user_roles(); $header = array(t('From'), '', t('To')); foreach ($all as $role_id => $transitions) { $rows = array(); $role = $role_id == 'author' ? 'author' : $role_names[$role_id]; foreach ($transitions as $transition) { list($from, $to) = $transition; $rows[] = array($states[$from], WORKFLOW_ARROW, $states[$to]); } $output .= '

' . t("%role may do these transitions:", array('%role' => $role)) . '

'; if ($rows) { $output .= theme('table', $header, $rows); } else { $output .= t('None'); } } return $output; } /** * Menu callback to edit a workflow's properties. * * @param $wid * The ID of the workflow. * * @return * HTML form. * */ function workflow_edit($wid) { // $name = workflow_get_name($wid); $workflow = db_fetch_object(db_query("SELECT * FROM {workflows} WHERE wid = %d", $wid)); $form['wf_name'] = array( '#type' => 'textfield', '#default_value' => $workflow->name, '#title' => t('Workflow Name'), '#size' => '16', '#maxlength' => '254', ); $form['tab'] = array( '#type' => 'fieldset', '#title' => t('Workflow tab permissions'), '#collapsible' => TRUE, '#collapsed' => FALSE, ); $form['tab']['tab_roles'] = array( '#type' => 'checkboxes', '#options' => workflow_get_roles(), '#default_value' => explode(',', $workflow->tab_roles), '#description' => t('Select any roles that should have access to the workflow tab on nodes that have a workflow.'), ); $form['transitions'] = workflow_transition_grid_form($wid); $form['submit'] = array('#type' => 'submit', '#value' => t('Save')); $form['wid'] = array('#type' => 'value', '#value' => $wid); $form['#theme'] = 'workflow_edit'; $form['transitions']['#tree'] = TRUE; $output = drupal_get_form('workflow_edit', $form); $output .= workflow_permissions($wid); return $output; } function theme_workflow_edit($form) { $output = form_render($form['wf_name']); $wid = $form['wid']['#value']; $states = workflow_get_states($wid); if ($states) { $roles = workflow_get_roles(); $header = array(array('data' => t('From / To ') . ' ' . WORKFLOW_ARROW)); $rows = array(); foreach ($states as $state_id => $name) { if ($name != t('(creation)')) { // don't allow transition TO (creation) $header[] = array('data' => t($name)); } $row = array(array('data' => $name)); foreach ($states as $nested_state_id => $nested_name) { if ($nested_name == t('(creation)')) { continue; // don't allow transition TO (creation) } if ($nested_state_id != $state_id) { // need to render checkboxes for transition from $state to $nested_state $from = $state_id; $to = $nested_state_id; $cell = ''; foreach ($roles as $rid => $role_name) { $cell .= form_render($form['transitions'][$from][$to][$rid]); //$cell .= form_render($form['transitions']["transitions][$from][$to][$rid"]); } $row[] = array('data' => $cell); } else { $row[] = array('data' => ''); } } $rows[] = $row; } $output .= theme('table', $header, $rows); } else { $output = 'There are no states defined for this workflow.'; } $output .= form_render($form); return $output; } function workflow_edit_validate($form_id, $form_values) { $wid = $form_values['wid']; if (array_key_exists('wf_name', $form_values) && $form_values['wf_name'] != '') { // validate: make sure workflow name is not a duplicate $dupe_name = db_result(db_query("SELECT COUNT(*) FROM {workflows} WHERE name='%s' AND wid!=%d", $form_values['wf_name'], $wid)); if ($dupe_name) { form_set_error('', t('Warning: Another workflow with this name already exists.')); } // validate: make sure 'author' is checked for (creation) -> [something] $states = workflow_get_states($wid); $author_creates = 0; foreach ($form_values['transitions'] as $id => $trans) { if ($states[$id] == t('(creation)')) { foreach ($trans as $t) { if ($t['author'] == 1) { $author_creates++; } } } } if ($author_creates == 0) { drupal_set_message(t('Warning: Please give the author permission to go from (creation) to at least one state.'), 'warning'); } } else { form_set_error('', t('Please provide a name for the workflow.')); } } function workflow_edit_submit($form_id, $form_values) { workflow_update($form_values['wid'], $form_values['wf_name'], array_filter($form_values['tab_roles'])); workflow_update_transitions($form_values['transitions']); drupal_set_message(t('Workflow updated.')); return('admin/workflow'); } /** * Menu callback to create form to add a workflow state. * * @param $wid * The ID of the workflow. * * @return * HTML form. * */ function workflow_state_add ($wid) { $form['state_name'] = array( '#type' => 'textfield', '#title' => t('State Name'), '#size' => '16', '#maxlength' => '254', ); $form['submit'] = array('#type' => 'submit', '#value' => t('Save')); $form['wid'] = array('#type' => 'value', '#value' => $wid); $output = drupal_get_form('workflow_state_add', $form); return $output; } function workflow_state_add_validate($form_id, $form_values) { if ($form_values['state_name'] == '') { form_set_error('', t('Please provide a nonblank name for the new state.')); } } function workflow_state_add_submit($form_id, $form_values) { workflow_state_create($form_values['wid'], $form_values['state_name']); drupal_set_message(t('The workflow state was created.')); return('admin/workflow'); } /** * Update the transitions for a workflow. * * @param array $transitions * Transitions, for example: * 18 => array( * 20 => array( * 'author' => 1, * 1 => 0, * 2 => 1, * ) * ) * means the transition from state 18 to state 20 can be executed by * the node author or a user in role 2. The $transitions array should * contain ALL transitions for the workflow. */ function workflow_update_transitions($transitions = array()) { foreach ($transitions as $from => $to_data) { foreach ($to_data as $to => $role_data) { foreach ($role_data as $role => $can_do) { if ($can_do) { workflow_transition_add_role($from, $to, $role); } else { workflow_transition_delete_role($from, $to, $role); } } } } db_query("DELETE FROM {workflow_transitions} WHERE roles=''"); } /** * Add a role to the list of those allowed for a given transition. * Add the transition if necessary. * @param int $from * @param int $to * @param mixed $role * Int (role ID) or string ('author'). */ function workflow_transition_add_role($from, $to, $role) { $tid = workflow_get_transition_id($from, $to); if ($tid) { $roles = db_result(db_query("SELECT roles FROM {workflow_transitions} WHERE tid=%d", $tid)); $roles = explode(',', $roles); if (array_search($role, $roles) === FALSE) { $roles[] = $role; db_query("UPDATE {workflow_transitions} SET roles='%s' WHERE tid=%d", implode(',', $roles), $tid); } } else { db_query("INSERT INTO {workflow_transitions} (tid, sid, target_sid, roles) VALUES (%d, %d, %d, '%s')", db_next_id('{workflow_transitions}_tid'), $from, $to, $role); } } /** * Remove a role from the list of those allowed for a given transition. * @param int $tid * @param mixed $role * Int (role ID) or string ('author'). */ function workflow_transition_delete_role($from, $to, $role) { $tid = workflow_get_transition_id($from, $to); if ($tid) { $roles = db_result(db_query("SELECT roles FROM {workflow_transitions} WHERE tid=%d", $tid)); $roles = explode(',', $roles); if (($i = array_search($role, $roles)) !== FALSE) { unset($roles[$i]); db_query("UPDATE {workflow_transitions} SET roles='%s' WHERE tid=%d", implode(',', $roles), $tid); } } } /** * Build the grid of transitions for defining a workflow. * * @param int $wid */ function workflow_transition_grid_form($wid) { $roles = workflow_get_roles(); $form = array(); $states = workflow_get_states($wid); if (!$states) { $form = array('#type' => 'markup', '#value' => t('There are no states defined for this workflow.')); return $form; } foreach ($states as $state_id => $name) { foreach ($states as $nested_state_id => $nested_name) { if ($nested_name == t('(creation)')) { continue; // don't allow transition TO (creation) } if ($nested_state_id != $state_id) { // need to generate checkboxes for transition from $state to $nested_state $from = $state_id; $to = $nested_state_id; foreach ($roles as $rid => $role_name) { $tid = workflow_get_transition_id($from, $to); $form[$from][$to][$rid] = array( '#type' => 'checkbox', '#title' => $role_name, '#default_value' => $tid ? workflow_transition_allowed($tid, $rid) : FALSE); } } } } return $form; } /** * See if a transition is allowed for a given role. * * @param int $tid * @param mixed $role * A single role (int or string 'author') or array of roles. * @return bool */ function workflow_transition_allowed($tid, $role = null) { $allowed = db_result(db_query( "SELECT roles FROM {workflow_transitions} WHERE tid=%d", $tid)); $allowed = explode(',', $allowed); if ($role) { if (!is_array($role)) { $role = array($role); } return array_intersect($role, $allowed) == TRUE; } else { return $allowed == TRUE; // allowed for anybody? } } /** * Create the main workflow page, which gives an overview * of workflows and workflow states. * * @return * HTML form. * */ function workflow_overview() { $result = db_query("SELECT * FROM {workflows}"); $header = array(array('data' => t('Name')), array('data' => 'Operations', 'colspan' => '4')); $row = array(); while ($data = db_fetch_object($result)) { $states = workflow_get_states($data->wid); $row[] = array( array('data' => $data->name), array('data' => l(t('add state'), "admin/workflow/state/$data->wid")), array('data' => $states && module_exist('actions') ? l(t('actions'), "admin/workflow/actions/$data->wid") : ''), array('data' => l(t('edit'), "admin/workflow/edit/$data->wid")), array('data' => l(t('delete'), "admin/workflow/delete/$data->wid"))); if ($states) { $cell = ''; foreach ($states as $sid => $state) { if (!workflow_is_system_state($state)) { $cell .= "
" . $state; $cell .= ""; $cell .= l(t('delete'), "admin/workflow/delete/$data->wid/$sid") . "
"; } } $row[] = array(array('data' => $cell, 'colspan' => '5')); } } if (!$row) { $output = '

' . t('No workflows have been added. Would you like to %add_a_workflow?', array('%add_a_workflow' => l(t('add a workflow'), 'admin/workflow/add'))) . '

'; } else { $output .= theme('table', $header, $row); } $output .= 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); $output = confirm_form('workflow_state_delete_confirm', $form, t('Are you sure you want to delete %title (and all its transitions)?', array('%title' => theme('placeholder', $states[$sid]))), $_GET['destination'] ? $_GET['destination'] : 'admin/workflow', t('This action cannot be undone.'), t('Delete'), t('Cancel') ); return $output; } function workflow_state_delete_confirm_submit($form_id, $form_values) { $states = workflow_get_states($form_values['wid']); $state = $states[$form_values['sid']]; if ($form_values['confirm'] == 1) { workflow_state_delete($form_values['sid']); drupal_set_message(t('The workflow state %title was deleted.', array('%title' => theme('placeholder', $state)))); } return('admin/workflow'); } function workflow_types_form() { $workflows = workflow_get_all(); if (count($workflows) == 0) { return ''; } $workflows[0] = t('None'); $form = array(); $form['workflow_mapping'] = array(); $form['workflow_mapping']['#theme'] = 'workflow_types_form'; $type_map = array(); $result = db_query("SELECT * FROM {workflow_type_map}"); while ($data = db_fetch_object($result)) { $type_map[$data->type] = $data->wid; } $options = $workflows; $nodetypes = node_get_types(); foreach ($nodetypes as $type => $name) { $value = isset($type_map[$type]) ? $type_map[$type] : 0; $form['workflow_mapping'][$type] = array( '#type' => 'select', '#prefix' => $name, '#options' => $options, '#default_value' => $value ); } $form['workflow_mapping']['submit'] = array('#type' => 'submit', '#value' => t('Save Workflow Mapping')); $form['nodetypes'] = array('#type' => 'value', '#value' => $nodetypes); return drupal_get_form('workflow_type_form', $form); } function theme_workflow_types_form($form) { $output = '
' . t('Each node type may have a separate workflow:') . '
'; $header = array( array('data' => t('Node Type')), array('data' => t('Workflow')) ); $row = array(); foreach (element_children($form) as $key) { $name = $form[$key]['#prefix']; unset($form[$key]['#prefix']); $row[] = array( array('data' => $name), array('data' => form_render($form[$key])) ); } $output .= theme('table', $header, $row); return $output; } function workflow_type_form_submit($form_id, $form_values) { workflow_types_save($form_values); drupal_set_message(t('The workflow mapping was saved.')); return('admin/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('workflow_actions_remove_confirm', $form, t('Are you sure you want to delete the action %title?', array('%title' => theme('placeholder', $actions[$aid]['description']))), $_GET['destination'] ? $_GET['destination'] : 'admin/workflow/actions', 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' => theme('placeholder', $actions[$aid])))); return('/admin/workflow/actions/'. $form_values['wid']); } } function workflow_actions_form($wid) { if (!module_exist('actions')) { drupal_set_message(t('Before you can assign actions you must install and enable the actions module.'), 'error'); drupal_goto('admin/workflow'); } $result = workflow_actions_form_build($wid); $form = $result[1]; $output = workflow_actions_form_build($wid, $form); return $output[0]; } function workflow_actions_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(3); return("admin/workflow/actions/$wid"); } function workflow_actions_form_build($wid, $form = NULL) { if (!isset($form)) { $form = array(); $render = FALSE; } else { $render =TRUE; } $output = ''; $states = array(); $all_states = workflow_get_states($wid); foreach ($all_states as $key => $val) { $states[$key] = $val; } $actions = actions_actions_map(actions_get_all_actions()); $options = array(t('None')); foreach ($actions as $aid => $action) { $options[$aid] = $action['description']; } $header = array( array('data' => t('Transition'), 'colspan' => '3'), array('data' => t('Actions')) ); $row = array(); foreach ($states as $sid => $state) { $allowable_to = workflow_allowable_transitions($sid); $actions_this_tid = array(); // we'll create a row for each allowable transition foreach ($allowable_to as $to_sid => $name) { $inner_row = array(); $tid = workflow_get_transition_id($sid, $to_sid); // get and list the actions that are already assigned to this transition $actions_this_tid = workflow_get_actions($tid); $available_options = $options; foreach ($actions_this_tid as $aid => $act_name) { $inner_row[] = array( array('data' => $act_name), array('data' => l(t('remove'), "admin/workflow/actions/remove/$wid/$tid/$aid")) ); unset($available_options[$aid]); } // list possible actions that may be assigned if (count($available_options) > 1) { if ($render) { $inner_row[] = array( array('data' => drupal_get_form('workflow_actions_'. $tid, $form[$tid], 'workflow_actions'), 'colspan' => '2')); } else { $form[$tid] = array(); $form[$tid]['action'] = array( '#prefix' => '', '#type' => 'select', '#options' => $available_options, '#default_value' => t('None') ); $form[$tid]['submit'] = array( '#prefix' => '
', '#suffix' => '', '#suffix' => '
', '#type' => 'submit', '#value' => t('Add') ); $form[$tid]['tid'] = array('#type' => 'value', '#value' => $tid); } } $action_table = theme('table', array(), $inner_row); $row[] = array( array('data' => $state), array('data' => WORKFLOW_ARROW), array('data' => $name), array('data' => $action_table) ); } } if (count($row) == 0) { $output .= t('You must first %link before you can assign actions.', array('%link' => l(t('set up transitions'), 'admin/workflow/edit/' . $wid))); } else { $output .= theme('table', $header, $row); } return array($output, $form); } /** * 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 * FROM {workflows}"); 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); db_query("INSERT INTO {workflow_states} (sid, wid, state, sysid) VALUES (%d, %d, '%s', %d)", db_next_id('{workflow_states}_sid'), $wid, t('(creation)'), WORKFLOW_CREATION); } /** * 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); watchdog('workflow', t('Deleted workflow') . " '$wf'."); } /** * 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 sid", intval($wid)); while ($data = db_fetch_object($result)) { $states[$data->sid] = $data->state; } return $states; } /** * Add a workflow state the database. * * @param $wid * The ID of the workflow. * @param $name * A string representing the workflow state, e.g., 'published'. * * @return * The ID of the workflow state */ function workflow_state_create($wid, $name) { $wid = intval($wid); $sid = db_next_id('{workflow_states}_sid'); db_query("INSERT INTO {workflow_states} (sid, wid, state, sysid) VALUES (%d, %d, '%s', %d)", $sid, $wid, $name, 0); return $sid; } /** * 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; 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 WHERE nid = %d", $sid, $user->uid, $node->nid); } else { db_query("INSERT INTO {workflow_node} (nid, sid, uid) VALUES (%d, %d, %d)", $node->nid, $sid, $user->uid); } _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, time()); } /** * 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; 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; } }