draft, review, and published. You probably would want the draft state to be able to move to the review state.'); case 'admin/workflow/add': return t('To get started, provide a name for your workflow. This name will be used as a label when the workflow status is shown during node editing.'); case strstr($section, 'admin/workflow/state'): return t('Enter the name for a state in your workflow. For example, if you were doing a meal workflow it may include states like shop, prepare food, eat, and clean up.'); case strstr($section, 'admin/workflow/actions'): 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'))); case strstr($section, 'admin/workflow/edit'): return t('Use the checkboxes below to enable certain transitions for certain users. IMPORTANT: You MUST enable some transition from the (creation) state for every role!'); } } /** * 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_page'); $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'), 'access' => $access, 'weight' => -8, 'type' => MENU_LOCAL_TASK); } return $items; } /** * Implementation of hook_nodeapi(). */ function workflow_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) { switch ($op) { // To have workflow field automatically inserted into forms, uncomment: /**/ case 'form post': return workflow_field($node); /**/ case 'load': $node->_workflow = workflow_node_current_state($node); break; case 'insert': case 'update': // stop if no workflow for this node type if (!$wid = workflow_get_workflow_for_type($node->type)) { break; } // get new state $sid = $node->_workflow; if (!$sid && $op == 'insert') { // if not specified, use first valid $sid = array_shift(array_keys(workflow_field_choices($node))); } // make sure new state is a valid choice if (array_key_exists($sid, workflow_field_choices($node))) { workflow_execute_transition($node, $sid); // do transition } break; case 'delete': db_query("DELETE FROM {workflow_node} WHERE nid = %d", $node->nid); break; } } /** * Get a workflow state select field for a node. * * @param object $node * @return array */ function workflow_field($node) { $wid = workflow_get_workflow_for_type($node->type); $choices = workflow_field_choices($node); if (count($choices) > 1) { if ($node->_workflow) { $sid = $node->_workflow; } return form_select(workflow_get_name($wid), '_workflow', $sid, $choices); } } /** * Get a Formproc-style workflow field for a node. * * This function requires the Formproc module to be useful. * * @param object &$node * @return array */ function workflow_formproc_field($node) { $choices = workflow_field_choices($node); if (count($choices) > 1) { $wid = workflow_get_workflow_for_type($node->type); return array( 'type' => 'select', 'name' => '_workflow', 'label' => workflow_get_name($wid), 'choices' => $choices, ); } else { return false; } } /** * Execute a transition (change state of a node). * * @param object $node * @param int $sid * ID of new state. */ function workflow_execute_transition($node, $sid) { $old_sid = workflow_node_current_state($node); if ($old_sid == $sid) { // stop if not going to a different state 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) if ($tid = workflow_get_transition_id($old_sid, $sid)) { if (!workflow_transition_allowed($tid, array_keys($user->roles))) { return; } } else { 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)) { break; } // stop if a module says so _workflow_node_to_state($node, $sid); // change the state // Register state change with watchdog $state_name = db_result(db_query("SELECT state FROM {workflow_states} WHERE sid = %d", $sid)); $type = module_invoke($node->type, 'node_name', $node); watchdog('workflow', "State of $node->title ($type) set to $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); } /** * 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) { return false; } // no workflow for this type $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 bool */ function workflow_node_current_state($node) { $sid = db_result(db_query("SELECT sid FROM {workflow_node} WHERE nid=%d ". "ORDER BY start DESC LIMIT 1", $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 && 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; } } /** * Display one of the various workflow admin pages. * * @param $op * The operation, aka the "add" in admin/workflow/add * @param string $arg3, $arg4, ... */ function workflow_page($op = NULL, $arg3 = null, $arg4 = null, $arg5 = null, $arg6 = null) { $edit = array_key_exists('edit', $_POST) ? $_POST['edit'] : array(); $output = ''; switch ($op) { case 'add': if (array_key_exists('wf_name', $edit) && $edit['wf_name'] != '') { $wf_name = $edit['wf_name']; workflow_create($wf_name); drupal_set_message(t("The workflow '$wf_name' was created. You should now add states to your workflow.")); drupal_goto('admin/workflow'); } else { $output = workflow_add_form($edit); } break; case 'edit': $wid = $arg3; if (array_key_exists('wf_name', $edit) && $edit['wf_name'] != '') { $error = false; // validate: make sure workflow name is not a duplicate $dupe_name = db_result(db_query("SELECT COUNT(*) FROM {workflows} WHERE name='%s' && wid!=%d", $edit['wf_name'], $wid)); if ($dupe_name) { drupal_set_message(t('Warning: Another workflow with this name already exists.'), 'warning'); } workflow_update_name($wid, $edit['wf_name']); // validate: make sure 'author' is checked for (creation) -> [something] $states = workflow_get_states($wid); $author_creates = 0; foreach ($edit['transitions'] as $id => $trans) { if ($states[$id] == '(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'); } workflow_update_transitions($edit['transitions']); drupal_set_message(t('Workflow updated.')); drupal_goto('admin/workflow'); } $edit['wf_name'] = workflow_get_name($wid); $output = form_textfield('Workflow Name', 'wf_name', $edit['wf_name'], 16, 254); $output .= workflow_transition_grid($wid, $edit); $output .= form_submit(t('Save')); $output = form($output); break; case 'state': $wid = $arg3; if (array_key_exists('state_name', $edit) && $edit['state_name'] != '' && is_numeric($wid)) { $state_name = $edit['state_name']; workflow_state_create($wid, $state_name); drupal_set_message(t("The workflow state '$state_name' was created.")); drupal_goto('admin/workflow'); } else { $output = workflow_state_add_form($wid, $edit); } break; case 'delete': $wid = $arg3; $sid = $arg4; if ($edit['confirm']) { if ($sid) { $states = workflow_get_states($wid); $state = $states[$sid]; $output = workflow_state_delete($sid); drupal_set_message(t("The workflow state '$state' was deleted.")); } else { $wf = workflow_get_name($wid); $output = workflow_deletewf($wid); drupal_set_message(t('The workflow') . " '$wf' " . t('was deleted.')); } drupal_goto('admin/workflow'); } else { if ($sid) { $output = workflow_state_delete_form($wid, $sid); } else { $output = workflow_delete_form($wid); } } break; case 'actions': if (!is_numeric($arg3)) { drupal_goto('admin/workflow'); } $wid = intval($arg3); if ($arg4 == 'remove' && is_numeric($arg5)) { $tid = intval($arg5); $aid = $arg6; $actions = workflow_get_actions($tid); workflow_actions_remove($tid, $aid); drupal_set_message(t('The action') . " '$actions[$aid]' " . t('has been removed.')); drupal_goto("admin/workflow/actions/$wid"); } elseif (isset($edit['action']) && $edit['action']) { $tid = intval($edit['tid']); $aid = actions_key_lookup($edit['action']); workflow_actions_save($tid, $aid); } $workflow_name = workflow_get_name($wid); $output = "
| " . $state; $cell .= " | "; $cell .= l(t('delete'), "admin/workflow/delete/$data->wid/$sid") . " |
' . 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; } /** * Create the form for adding/editing a workflow. * * @param $edit * * @return * HTML form. * */ function workflow_add_form($edit, $add = TRUE) { $output = form_textfield('Workflow Name', 'wf_name', $edit['wf_name'], 16, 254); if ($add) { $output .= form_submit(t('Add Workflow')); } else { $output .= form_submit(t('Save')); } return form($output); } /** * Create the form for confirmation of deleting a workflow. * * @param $wid * The ID of the workflow. * * @return * HTML form. * */ function workflow_delete_form($wid) { $wf = workflow_get_name($wid); $output .= form_item(t('Confirm deletion'), t("Really delete workflow '%wf' (and all the states it contains)? All nodes that have a workflow state associated with this workflow will have those workflow states removed.", array('%wf' => $wf))); $output .= form_hidden('confirm', 1); $output .= form_submit(t('Delete')); return form($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 adding a workflow state. * * @param $wid * The ID of the workflow. * @param $edit * * @return * HTML form. * */ function workflow_state_add_form($wid, $edit) { $output = form_textfield('State Name', 'state_name', $edit['state_name'], 16, 254); $output .= form_hidden('wid', $wid); $output .= form_submit('Add State'); return form($output); } /** * 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); $output .= form_item(t('Confirm deletion'), t("Really delete workflow state '%state' (and all of its transitions)?", array('%state' => $states[$sid]))); $output .= form_hidden('confirm', 1); $output .= form_submit(t('Delete')); return form($output); } function workflow_types_form() { $workflows = workflow_get_all(); $workflows[0] = t('None'); $header = array( array('data' => t('Node Type')), array('data' => t('Workflow')) ); $row = array(); $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_list(); foreach ($nodetypes as $type) { $value = $type_map[$type]; $row[] = array( array('data' => node_invoke($type, 'node_name')), array('data' => form_select('', $type, $value, $options)) ); } $output .= theme('table', $header, $row); $output .= form_hidden('update', '1'); $output .= form_item('', '', t('Each node type may have a separate workflow.')); $output .= form_submit(t('Save Workflow Mapping')); return form($output); } function workflow_actions_form($wid, $edit) { $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/$wid/remove/$tid/$aid")) ); unset($available_options[$aid]); } // list possible actions that may be assigned if (count($available_options) > 1) { $inner_row[] = array( array('data' => form_select('', 'action', t('None'), $available_options)), array('data' => form_hidden('tid', $tid) . form_submit(t('Add'))) ); } $action_table = form(theme('table', array(), $inner_row)); $row[] = array( array('data' => $state), array('data' => ' --> '), array('data' => $name), array('data' => $action_table) ); } } $output .= theme('table', $header, $row); return $output; } /** * 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'); 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_state'), $wid, t('(creation)'), WORKFLOW_CREATION); } /** * Save a workflow's name in the database. * * @param $name * The name of the workflow. * */ function workflow_update_name($wid, $name) { db_query("UPDATE {workflows} SET name = '%s' WHERE wid = %d", $name, $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) { $wid = intval($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) { $result = db_query("SELECT * 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_state'); 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)) { $transitions[$t->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 $edit */ function workflow_types_save($edit) { $nodetypes = node_list(); db_query("DELETE FROM {workflow_type_map}"); foreach ($nodetypes as $type) { db_query("INSERT INTO {workflow_type_map} (type, wid) VALUES ('%s', %d)", $type, intval($edit[$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(); $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 && 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) { global $user; db_query("INSERT INTO {workflow_node} (nid, sid, uid) VALUES (%d, %d, %d)", $node->nid, $sid, $user->uid); } ?>