/[drupal]/contributions/modules/workflow/workflow.module
ViewVC logotype

Contents of /contributions/modules/workflow/workflow.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.84 - (show annotations) (download) (as text)
Thu Sep 24 23:02:50 2009 UTC (2 months ago) by eaton
Branch: MAIN
CVS Tags: HEAD
Changes since 1.83: +12 -5 lines
File MIME type: text/x-php
Migrating changes from the DRUPAL-6--1 branch to HEAD.
1 <?php
2 // $Id: workflow.module,v 1.83 2009/01/01 21:09:16 jvandyk Exp $
3
4 /**
5 * @file
6 * Support workflows made up of arbitrary states.
7 */
8
9 define('WORKFLOW_CREATION', 1);
10 define('WORKFLOW_CREATION_DEFAULT_WEIGHT', -50);
11 define('WORKFLOW_DELETION', 0);
12 define('WORKFLOW_ARROW', '&#8594;');
13
14 /**
15 * Implementation of hook_help().
16 */
17 function workflow_help($path, $arg) {
18 switch ($path) {
19 case 'admin/build/workflow/edit/%':
20 return t('You are currently viewing the possible transitions to and from workflow states. The state is shown in the left column; the state to be moved to is to the right. For each transition, check the box next to the role(s) that may initiate the transition. For example, if only the "production editor" role may move a node from Review state to the Published state, check the box next to "production editor". The author role is built in and refers to the user who authored the node.');
21 case 'admin/build/workflow/add':
22 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.');
23 case 'admin/build/workflow/state':
24 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 <em>shop</em>, <em>prepare</em>, <em>eat</em>, and <em>clean up</em>.');
25 case 'admin/build/trigger/workflow':
26 return t('Use this page to set actions to happen when transitions occur. To configure actions, use the <a href="@link">actions settings page</a>.', array('@link' => url('admin/settings/actions')));
27 }
28 }
29
30 /**
31 * Implementation of hook_perm().
32 */
33 function workflow_perm() {
34 return array('administer workflow', 'schedule workflow transitions', 'access workflow summary views');
35 }
36
37 /**
38 * Implementation of hook_menu().
39 */
40 function workflow_menu() {
41 $items['admin/build/workflow'] = array(
42 'title' => 'Workflow',
43 'access arguments' => array('administer workflow'),
44 'page callback' => 'workflow_overview',
45 'description' => 'Allows the creation and assignment of arbitrary workflows to node types.',
46 'file' => 'workflow.admin.inc',
47 );
48 $items['admin/build/workflow/edit/%workflow'] = array(
49 'title' => 'Edit workflow',
50 'type' => MENU_CALLBACK,
51 'access arguments' => array('administer workflow'),
52 'page callback' => 'drupal_get_form',
53 'page arguments' => array('workflow_edit_form', 4),
54 'file' => 'workflow.admin.inc',
55 );
56 $items['admin/build/workflow/list'] = array(
57 'title' => 'List',
58 'weight' => -10,
59 'access arguments' => array('administer workflow'),
60 'page callback' => 'workflow_overview',
61 'file' => 'workflow.admin.inc',
62 'type' => MENU_DEFAULT_LOCAL_TASK,
63 );
64 $items['admin/build/workflow/add'] = array(
65 'title' => 'Add workflow',
66 'weight' => -8,
67 'access arguments' => array('administer workflow'),
68 'page callback' => 'drupal_get_form',
69 'page arguments' => array('workflow_add_form'),
70 'file' => 'workflow.admin.inc',
71 'type' => MENU_LOCAL_TASK,
72 );
73 $items['admin/build/workflow/state'] = array(
74 'title' => 'Add state',
75 'type' => MENU_CALLBACK,
76 'access arguments' => array('administer workflow'),
77 'page callback' => 'drupal_get_form',
78 'page arguments' => array('workflow_state_add_form'),
79 'file' => 'workflow.admin.inc',
80 );
81 $items['admin/build/workflow/state/delete'] = array(
82 'title' => 'Delete State',
83 'type' => MENU_CALLBACK,
84 'access arguments' => array('administer workflow'),
85 'page callback' => 'drupal_get_form',
86 'page arguments' => array('workflow_state_delete_form'),
87 'file' => 'workflow.admin.inc',
88 );
89 $items['admin/build/workflow/delete'] = array(
90 'title' => 'Delete workflow',
91 'type' => MENU_CALLBACK,
92 'access arguments' => array('administer workflow'),
93 'page callback' => 'drupal_get_form',
94 'page arguments' => array('workflow_delete_form'),
95 'file' => 'workflow.admin.inc',
96 );
97 $items['node/%node/workflow'] = array(
98 'title' => 'Workflow',
99 'type' => MENU_LOCAL_TASK,
100 'access callback' => 'workflow_node_tab_access',
101 'access arguments' => array(1),
102 'page callback' => 'workflow_tab_page',
103 'page arguments' => array(1),
104 'file' => 'workflow.pages.inc',
105 'weight' => 2,
106 );
107 return $items;
108 }
109
110 /**
111 * Menu access control callback. Determine access to Workflow tab.
112 */
113 function workflow_node_tab_access($node = NULL) {
114 global $user;
115 $wid = workflow_get_workflow_for_type($node->type);
116 if ($wid === FALSE) {
117 // No workflow associated with this node type.
118 return FALSE;
119 }
120 $roles = array_keys($user->roles);
121 if ($node->uid == $user->uid) {
122 $roles = array_merge(array('author'), $roles);
123 }
124 $workflow = db_fetch_object(db_query("SELECT * FROM {workflows} WHERE wid = %d", $wid));
125 $allowed_roles = $workflow->tab_roles ? explode(',', $workflow->tab_roles) : array();
126
127 if (user_access('administer nodes') || array_intersect($roles, $allowed_roles)) {
128 return TRUE;
129 }
130 else {
131 return FALSE;
132 }
133 }
134
135 /**
136 * Implementation of hook_theme().
137 */
138 function workflow_theme() {
139 return array(
140 'workflow_edit_form' => array(
141 'arguments' => array(
142 'form' => array(),
143 ),
144 ),
145 'workflow_types_form' => array(
146 'arguments' => array(
147 'form' => array(),
148 ),
149 ),
150 'workflow_actions_form' => array(
151 'arguments' => array(
152 'form' => array()
153 ),
154 ),
155 'workflow_history_table_row' => array(
156 'arguments' => array(
157 'history' => NULL,
158 'old_state_name' => NULL,
159 'state_name' => null
160 ),
161 ),
162 'workflow_history_table' => array(
163 'arguments' => array(
164 'rows' => array(),
165 'footer' => NULL,
166 ),
167 ),
168 'workflow_current_state' => array(
169 'arguments' => array(
170 'state_name' => NULL,
171 ),
172 ),
173 'workflow_deleted_state' => array(
174 'arguments' => array(
175 'state_name' => NULL,
176 ),
177 ),
178 'workflow_permissions' => array(
179 'arguments' => array(
180 'header' => array(),
181 'all' => array(),
182 ),
183 ),
184 );
185 }
186
187 /**
188 * Implementation of hook_views_api().
189 */
190 function workflow_views_api() {
191 return array(
192 'api' => 2,
193 'path' => drupal_get_path('module', 'workflow') .'/includes',
194 );
195 }
196
197 /**
198 * Implementation of hook_nodeapi().
199 */
200 function workflow_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
201 switch ($op) {
202 case 'load':
203 $node->_workflow = workflow_node_current_state($node);
204
205 // Add scheduling information.
206 $res = db_query('SELECT * FROM {workflow_scheduled_transition} WHERE nid = %d', $node->nid);
207 if ($row = db_fetch_object($res)) {
208 $node->_workflow_scheduled_sid = $row->sid;
209 $node->_workflow_scheduled_timestamp = $row->scheduled;
210 $node->_workflow_scheduled_comment = $row->comment;
211 }
212 break;
213
214 case 'insert':
215 // If the state is not specified, use first valid state.
216 // For example, a new node must move from (creation) to some
217 // initial state.
218 if (empty($node->workflow)) {
219 $choices = workflow_field_choices($node);
220 $keys = array_keys($choices);
221 $sid = array_shift($keys);
222 }
223 // Note no break; fall through to 'update' case.
224 case 'update':
225 // Do nothing if there is no workflow for this node type.
226 $wid = workflow_get_workflow_for_type($node->type);
227 if (!$wid) {
228 break;
229 }
230
231 // Get new state from value of workflow form field, stored in $node->workflow.
232 if (!isset($sid)) {
233 $sid = $node->workflow;
234 }
235
236 workflow_transition($node, $sid);
237 break;
238
239 case 'delete':
240 db_query("DELETE FROM {workflow_node} WHERE nid = %d", $node->nid);
241 _workflow_write_history($node, WORKFLOW_DELETION, t('Node deleted'));
242 // Delete any scheduled transitions for this node.
243 db_query("DELETE FROM {workflow_scheduled_transition} WHERE nid = %d", $node->nid);
244 break;
245 }
246 }
247
248 /**
249 * Implementation of hook_comment().
250 */
251 function workflow_comment($a1, $op) {
252 if (($op == 'insert' || $op == 'update') && isset($a1['workflow'])) {
253 $node = node_load($a1['nid']);
254 $sid = $a1['workflow'];
255 $node->workflow_comment = $a1['workflow_comment'];
256 workflow_transition($node, $sid);
257 }
258 }
259
260 /**
261 * Validate target state and either execute a transition immediately or schedule
262 * a transition to be executed later by cron.
263 *
264 * @param $node
265 * @param $sid
266 * An integer; the target state ID.
267 */
268 function workflow_transition($node, $sid) {
269 // Make sure new state is a valid choice.
270 if (array_key_exists($sid, workflow_field_choices($node))) {
271 if (!$node->workflow_scheduled) {
272 // It's an immediate change. Do the transition.
273 workflow_execute_transition($node, $sid, isset($node->workflow_comment) ? $node->workflow_comment : NULL);
274 }
275 else {
276 // Schedule the the time to change the state.
277 $comment = $node->workflow_comment;
278 $old_sid = workflow_node_current_state($node);
279
280 if ($node->workflow_scheduled_date['day'] < 10) {
281 $node->workflow_scheduled_date['day'] = '0' .
282 $node->workflow_scheduled_date['day'];
283 }
284
285 if ($node->workflow_scheduled_date['month'] < 10) {
286 $node->workflow_scheduled_date['month'] = '0' .
287 $node->workflow_scheduled_date['month'];
288 }
289
290 if (!$node->workflow_scheduled_hour) {
291 $node->workflow_scheduled_hour = '00:00';
292 }
293
294 $scheduled = $node->workflow_scheduled_date['year'] . $node->workflow_scheduled_date['month'] . $node->workflow_scheduled_date['day'] . ' ' . $node->workflow_scheduled_hour . 'Z';
295 if ($scheduled = strtotime($scheduled)) {
296 // Adjust for user and site timezone settings.
297 global $user;
298 if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) {
299 $timezone = $user->timezone;
300 }
301 else {
302 $timezone = variable_get('date_default_timezone', 0);
303 }
304 $scheduled = $scheduled - $timezone;
305
306 // Clear previous entries and insert.
307 db_query("DELETE FROM {workflow_scheduled_transition} WHERE nid = %d", $node->nid);
308 db_query("INSERT INTO {workflow_scheduled_transition} VALUES (%d, %d, %d, %d, '%s')", $node->nid, $old_sid, $sid, $scheduled, $comment);
309
310 // Get name of state.
311 $state_name = db_result(db_query('SELECT state FROM {workflow_states} WHERE sid = %d', $sid));
312 watchdog('workflow', '@node_title scheduled for state change to %state_name on !scheduled_date', array('@node_title' => $node->title, '%state_name' => $state_name, '!scheduled_date' => format_date($scheduled)), WATCHDOG_NOTICE, l('view', "node/$node->nid/workflow"));
313 drupal_set_message(t('@node_title is scheduled for state change to %state_name on !scheduled_date', array('@node_title' => $node->title, '%state_name' => $state_name, '!scheduled_date' => format_date($scheduled))));
314 }
315 }
316 }
317 }
318
319 /**
320 * Form builder. Add form widgets for workflow change to $form.
321 *
322 * This builder is factored out of workflow_form_alter() because
323 * it is also used on the Workflow tab.
324 *
325 * @param $form
326 * An existing form definition array.
327 * @param $name
328 * The name of the workflow.
329 * @param $current
330 * The state ID of the current state, used as the default value.
331 * @param $choices
332 * An array of possible target states.
333 */
334 function workflow_node_form(&$form, $form_state, $title, $name, $current, $choices, $timestamp = NULL, $comment = NULL) {
335 // No sense displaying choices if there is only one choice.
336 if (sizeof($choices) == 1) {
337 $form['workflow'][$name] = array(
338 '#type' => 'hidden',
339 '#value' => $current
340 );
341 }
342 else {
343 $form['workflow'][$name] = array(
344 '#type' => 'radios',
345 '#title' => $title,
346 '#options' => $choices,
347 '#name' => $name,
348 '#parents' => array('workflow'),
349 '#default_value' => $current
350 );
351
352 // Display scheduling form only if a node is being edited and user has
353 // permission. State change cannot be scheduled at node creation because
354 // that leaves the node in the (creation) state.
355 if (!(arg(0) == 'node' && arg(1) == 'add') && user_access('schedule workflow transitions')) {
356 $scheduled = $timestamp ? 1 : 0;
357 $timestamp = $scheduled ? $timestamp : time();
358
359 $form['workflow']['workflow_scheduled'] = array(
360 '#type' => 'radios',
361 '#title' => t('Schedule'),
362 '#options' => array(
363 t('Immediately'),
364 t('Schedule for state change at:'),
365 ),
366 '#default_value' => isset($form_state['values']['workflow_scheduled']) ? $form_state['values']['workflow_scheduled'] : $scheduled,
367 );
368
369 $form['workflow']['workflow_scheduled_date'] = array(
370 '#type' => 'date',
371 '#default_value' => array(
372 'day' => isset($form_state['values']['workflow_scheduled_date']['day']) ? $form_state['values']['workflow_scheduled_date']['day'] : format_date($timestamp, 'custom', 'j'),
373 'month' => isset($form_state['values']['workflow_scheduled_date']['month']) ? $form_state['values']['workflow_scheduled_date']['month'] :format_date($timestamp, 'custom', 'n'),
374 'year' => isset($form_state['values']['workflow_scheduled_date']['year']) ? $form_state['values']['workflow_scheduled_date']['year'] : format_date($timestamp, 'custom', 'Y')
375 ),
376 );
377
378 $hours = format_date($timestamp, 'custom', 'H:i');
379 $form['workflow']['workflow_scheduled_hour'] = array(
380 '#type' => 'textfield',
381 '#description' => t('Please enter a time in 24 hour (eg. HH:MM) format. If no time is included, the default will be midnight on the specified date. The current time is: ') . format_date(time()),
382 '#default_value' => $scheduled ? (isset($form_state['values']['workflow_scheduled_hour']) ? $form_state['values']['workflow_scheduled_hour'] : $hours) : NULL,
383 );
384 }
385 if (isset($form['#tab'])) {
386 $determiner = 'comment_log_tab';
387 }
388 else {
389 $determiner = 'comment_log_node';
390 }
391 $form['workflow']['workflow_comment'] = array(
392 '#type' => $form['#wf']->options[$determiner] ? 'textarea': 'hidden',
393 '#title' => t('Comment'),
394 '#description' => t('A comment to put in the workflow log.'),
395 '#default_value' => $comment,
396 '#rows' => 2,
397 );
398 }
399 }
400
401 /**
402 * Implementation of hook_form_alter().
403 *
404 * @param object &$node
405 * @return array
406 */
407 function workflow_form_alter(&$form, $form_state, $form_id) {
408 // Ignore all forms except comment forms and node editing forms.
409 if ($form_id == 'comment_form' || (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id)) {
410 if (isset($form['#node'])) {
411 $node = $form['#node'];
412 // Abort if no workflow is assigned to this node type.
413 if (!in_array('node', variable_get('workflow_' . $node->type, array('node')))) {
414 return;
415 }
416 }
417 else {
418 $type = db_result(db_query("SELECT type FROM {node} WHERE nid = %d", $form['nid']['#value']));
419 // Abort if user does not want to display workflow form on node editing form.
420 if (!in_array('comment', variable_get('workflow_' . $type, array('node')))) {
421 return;
422 }
423 $node = node_load($form['nid']['#value']);
424 }
425 $choices = workflow_field_choices($node);
426 $wid = workflow_get_workflow_for_type($node->type);
427 $states = workflow_get_states($wid);
428 // If this is a preview, the current state should come from
429 // the form values, not the node, as the user may have changed
430 // the state.
431 $current = isset($form_state['values']['workflow']) ? $form_state['values']['workflow'] : workflow_node_current_state($node);
432 $min = $states[$current] == t('(creation)') ? 1 : 2;
433 // Stop if user has no new target state(s) to choose.
434 if (count($choices) < $min) {
435 return;
436 }
437 $workflow = workflow_load($wid);
438 $form['#wf'] = $workflow;
439
440 // If the current node state is not one of the choices, autoselect first choice.
441 // We know all states in $choices are states that user has permission to
442 // go to because workflow_field_choices() has already checked that.
443 if (!isset($choices[$current])) {
444 $array = array_keys($choices);
445 $current = $array[0];
446 }
447
448 if (sizeof($choices) > 1) {
449 $form['workflow'] = array(
450 '#type' => 'fieldset',
451 '#title' => check_plain($workflow->name),
452 '#collapsible' => TRUE,
453 '#collapsed' => FALSE,
454 '#weight' => 10,
455 );
456 }
457
458 $timestamp = NULL;
459 $comment = '';
460
461 // See if scheduling information is present.
462 if (isset($node->_workflow_scheduled_timestamp) && isset($node->_workflow_scheduled_sid)) {
463 // The default value should be the upcoming sid.
464 $current = $node->_workflow_scheduled_sid;
465 $timestamp = $node->_workflow_scheduled_timestamp;
466 $comment = $node->_workflow_scheduled_comment;
467 }
468
469 if (isset($form_state['values']['workflow_comment'])) {
470 $comment = $form_state['values']['workflow_comment'];
471 }
472
473 workflow_node_form($form, $form_state, '', '', $current, $choices, $timestamp, $comment);
474 }
475 }
476
477 /**
478 * Execute a transition (change state of a node).
479 *
480 * @param $node
481 * @param $sid
482 * Target state ID.
483 * @param $comment
484 * A comment for the node's workflow history.
485 * @param $force
486 * If set to TRUE, workflow permissions will be ignored.
487 *
488 * @return int
489 * ID of new state.
490 */
491 function workflow_execute_transition($node, $sid, $comment = NULL, $force = FALSE) {
492 global $user;
493 $old_sid = workflow_node_current_state($node);
494 if ($old_sid == $sid) {
495 // Stop if not going to a different state.
496 // Write comment into history though.
497 if ($comment && !$node->_workflow_scheduled_comment) {
498 $node->workflow_stamp = time();
499 db_query("UPDATE {workflow_node} SET stamp = %d WHERE nid = %d", $node->workflow_stamp, $node->nid);
500 $result = module_invoke_all('workflow', 'transition pre', $old_sid, $sid, $node);
501 _workflow_write_history($node, $sid, $comment);
502 }
503 $result = module_invoke_all('workflow', 'transition post', $old_sid, $sid, $node);
504 return;
505 }
506
507 $tid = workflow_get_transition_id($old_sid, $sid);
508 if (!$tid && !$force) {
509 watchdog('workflow', 'Attempt to go to nonexistent transition (from %old to %new)', array('%old' => $old_sid, '%new' => $sid, WATCHDOG_ERROR));
510 return;
511 }
512 // Make sure this transition is valid and allowed for the current user.
513 // Check allowability of state change if user is not superuser (might be cron).
514 if (($user->uid != 1) && !$force) {
515 if (!workflow_transition_allowed($tid, array_merge(array_keys($user->roles), array('author')))) {
516 watchdog('workflow', 'User %user not allowed to go from state %old to %new', array('%user' => $user->name, '%old' => $old_sid, '%new' => $sid, WATCHDOG_NOTICE));
517 return;
518 }
519 }
520 // Invoke a callback indicating a transition is about to occur. Modules
521 // may veto the transition by returning FALSE.
522 $result = module_invoke_all('workflow', 'transition pre', $old_sid, $sid, $node);
523
524 // Stop if a module says so.
525 if (in_array(FALSE, $result)) {
526 watchdog('workflow', 'Transition vetoed by module.');
527 return;
528 }
529
530 // If the node does not have an existing $node->_workflow property, save
531 // the $old_sid there so _workflow_write_history() can log it.
532 if (!isset($node->_workflow)) {
533 $node->_workflow = $old_sid;
534 }
535 // Change the state.
536 _workflow_node_to_state($node, $sid, $comment);
537 $node->_workflow = $sid;
538
539 // Register state change with watchdog.
540 $state_name = db_result(db_query('SELECT state FROM {workflow_states} WHERE sid = %d', $sid));
541 $type = node_get_types('name', $node->type);
542 watchdog('workflow', '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/' . $node->nid));
543
544 // Notify modules that transition has occurred. Actions should take place
545 // in response to this callback, not the previous one.
546 module_invoke_all('workflow', 'transition post', $old_sid, $sid, $node);
547
548 // Clear any references in the scheduled listing.
549 db_query('DELETE FROM {workflow_scheduled_transition} WHERE nid = %d', $node->nid);
550 }
551
552 /**
553 * Implementation of hook_action_info().
554 */
555 function workflow_action_info() {
556 return array(
557 'workflow_select_next_state_action' => array(
558 'type' => 'node',
559 'description' => t('Change workflow state of post to next state'),
560 'configurable' => FALSE,
561 'hooks' => array(
562 'nodeapi' => array('presave'),
563 'comment' => array('insert', 'update'),
564 'workflow' => array('any'),
565 ),
566 ),
567 'workflow_select_given_state_action' => array(
568 'type' => 'node',
569 'description' => t('Change workflow state of post to new state'),
570 'configurable' => TRUE,
571 'hooks' => array(
572 'nodeapi' => array('presave'),
573 'comment' => array('insert', 'update'),
574 'workflow' => array('any'),
575 ),
576 ),
577 );
578 }
579
580 /**
581 * Implementation of a Drupal action. Move a node to the next state in the workfow.
582 */
583 function workflow_select_next_state_action($node, $context) {
584 // If this action is being fired because it's attached to a workflow transition
585 // then the node's new state (now its current state) should be in $node->workflow
586 // because that is where the value from the workflow form field is stored;
587 // otherwise the current state is placed in $node->_workflow by our nodeapi load.
588 if (!isset($node->workflow) && !isset($node->_workflow)) {
589 watchdog('workflow', 'Unable to get current workflow state of node %nid.', array('%nid' => $node->nid));
590 return;
591 }
592 $current_state = isset($node->workflow) ? $node->workflow : $node->_workflow;
593
594 // Get the node's new state.
595 $choices = workflow_field_choices($node);
596 foreach ($choices as $sid => $name) {
597 if (isset($flag)) {
598 $new_state = $sid;
599 $new_state_name = $name;
600 break;
601 }
602 if ($sid == $current_state) {
603 $flag = TRUE;
604 }
605 }
606
607 // Fire the transition.
608 workflow_execute_transition($node, $new_state);
609 }
610
611 /**
612 * Implementation of a Drupal action. Move a node to a specified state.
613 */
614 function workflow_select_given_state_action($node, $context) {
615 $comment = t($context['workflow_comment'], array('%title' => check_plain($node->title), '%state' => check_plain($context['state_name'])));
616 workflow_execute_transition($node, $context['target_sid'], $comment, $context['force']);
617 }
618
619 /**
620 * Configuration form for "Change workflow state of post to new state" action.
621 *
622 * @see workflow_select_given_state_action()
623 */
624 function workflow_select_given_state_action_form($context) {
625 $result = db_query("SELECT * FROM {workflow_states} ws LEFT JOIN {workflows} w ON ws.wid = w.wid WHERE ws.sysid = 0 AND ws.status = 1 ORDER BY ws.wid, ws.weight");
626 $previous_workflow = '';
627 $options = array();
628 while ($data = db_fetch_object($result)) {
629 $options[$data->name][$data->sid] = $data->state;
630 }
631 $form['target_sid'] = array(
632 '#type' => 'select',
633 '#title' => t('Target state'),
634 '#description' => t('Please select that state that should be assigned when this action runs.'),
635 '#default_value' => isset($context['target_sid']) ? $context['target_sid'] : '',
636 '#options' => $options,
637 );
638 $form['force'] = array(
639 '#type' => 'checkbox',
640 '#title' => t('Force transition'),
641 '#description' => t('If this box is checked, the new state will be assigned even if workflow permissions disallow it.'),
642 '#default_value' => isset($context['force']) ? $context['force'] : '',
643 );
644 $form['workflow_comment'] = array(
645 '#type' => 'textfield',
646 '#title' => t('Message'),
647 '#description' => t('This message will be written into the workflow history log when the action runs. You may include the following variables: %state, %title'),
648 '#default_value' => isset($context['workflow_history']) ? $context['workflow_history'] : t('Action set %title to %state.'),
649 );
650 return $form;
651 }
652
653 /**
654 * Submit handler for "Change workflow state of post to new state" action
655 * configuration form.
656 *
657 * @see workflow_select_given_state_action_form()
658 */
659 function workflow_select_given_state_action_submit($form_id, $form_state) {
660 $state_name = db_result(db_query("SELECT state FROM {workflow_states} WHERE sid = %d", $form_state['values']['target_sid']));
661 return array(
662 'target_sid' => $form_state['values']['target_sid'],
663 'state_name' => $state_name,
664 'force' => $form_state['values']['force'],
665 'workflow_comment' => $form_state['values']['workflow_comment'],
666 );
667 }
668
669 /**
670 * Get the states current user can move to for a given node.
671 *
672 * @param object $node
673 * The node to check.
674 * @return
675 * Array of transitions.
676 */
677 function workflow_field_choices($node) {
678 global $user;
679 $wid = workflow_get_workflow_for_type($node->type);
680 if (!$wid) {
681 // No workflow for this type.
682 return array();
683 }
684 $states = workflow_get_states($wid);
685 $roles = array_keys($user->roles);
686 $current_sid = workflow_node_current_state($node);
687
688 // If user is node author or this is a new page, give the authorship role.
689 if (($user->uid == $node->uid && $node->uid > 0) || (arg(0) == 'node' && arg(1) == 'add')) {
690 $roles += array('author' => 'author');
691 }
692 if ($user->uid == 1) {
693 // Superuser is special.
694 $roles = 'ALL';
695 }
696 $transitions = workflow_allowable_transitions($current_sid, 'to', $roles);
697
698 // Include current state if it is not the (creation) state.
699 if ($current_sid == _workflow_creation_state($wid)) {
700 unset($transitions[$current_sid]);
701 }
702 return $transitions;
703 }
704
705 /**
706 * Get the current state of a given node.
707 *
708 * @param $node
709 * The node to check.
710 * @return
711 * The ID of the current state.
712 */
713 function workflow_node_current_state($node) {
714 $sid = FALSE;
715
716 if (!empty($node->nid)) {
717 $sid = db_result(db_query('SELECT sid FROM {workflow_node} WHERE nid = %d', $node->nid));
718 }
719
720 if (!$sid && !empty($node->type)) {
721 // No current state. Use creation state.
722 $wid = workflow_get_workflow_for_type($node->type);
723 $sid = _workflow_creation_state($wid);
724 }
725 return $sid;
726 }
727
728 /**
729 * Return the ID of the creation state for this workflow.
730 *
731 * @param $wid
732 * The ID of the workflow.
733 */
734 function _workflow_creation_state($wid) {
735 static $cache;
736 if (!isset($cache[$wid])) {
737 $result = db_result(db_query("SELECT sid FROM {workflow_states} WHERE wid = %d AND sysid = %d", $wid, WORKFLOW_CREATION));
738 $cache[$wid] = $result;
739 }
740
741 return $cache[$wid];
742 }
743
744 /**
745 * Implementation of hook_workflow().
746 *
747 * @param $op
748 * The current workflow operation: 'transition pre' or 'transition post'.
749 * @param $old_state
750 * The state ID of the current state.
751 * @param $new_state
752 * The state ID of the new state.
753 * @param $node
754 * The node whose workflow state is changing.
755 */
756 function workflow_workflow($op, $old_state, $new_state, $node) {
757 switch ($op) {
758 case 'transition pre':
759 // The workflow module does nothing during this operation.
760 // But your module's implementation of the workflow hook could
761 // return FALSE here and veto the transition.
762 break;
763
764 case 'transition post':
765 // A transition has occurred; fire off actions associated with this transition.
766 // Can't fire actions if trigger module is not enabled.
767 if (!module_exists('trigger')) {
768 break;
769 }
770 $tid = workflow_get_transition_id($old_state, $new_state);
771 $op = 'workflow-'. $node->type .'-'. $tid;
772 $aids = _trigger_get_hook_aids('workflow', $op);
773 if ($aids) {
774 $context = array(
775 'hook' => 'workflow',
776 'op' => $op,
777 );
778
779 // We need to get the expected object if the action's type is not 'node'.
780 // We keep the object in $objects so we can reuse it if we have multiple actions
781 // that make changes to an object.
782 foreach ($aids as $aid => $action_info) {
783 if ($action_info['type'] != 'node') {
784 if (!isset($objects[$action_info['type']])) {
785 $objects[$action_info['type']] = _trigger_normalize_node_context($action_info['type'], $node);
786 }
787 // Since we know about the node, we pass that info along to the action.
788 $context['node'] = $node;
789 $result = actions_do($aid, $objects[$action_info['type']], $context);
790 }
791 else {
792 actions_do($aid, $node, $context);
793 }
794 }
795 }
796 break;
797 }
798 }
799
800 /**
801 * Load function.
802 *
803 * @param $wid
804 * The ID of the workflow to load.
805 * @return $workflow
806 * Object representing the workflow.
807 */
808 function workflow_load($wid) {
809 $workflow = db_fetch_object(db_query('SELECT * FROM {workflows} WHERE wid = %d', $wid));
810 $workflow->options = unserialize($workflow->options);
811 return $workflow;
812 }
813
814 /**
815 * Update the transitions for a workflow.
816 *
817 * @param array $transitions
818 * Transitions, for example:
819 * 18 => array(
820 * 20 => array(
821 * 'author' => 1,
822 * 1 => 0,
823 * 2 => 1,
824 * )
825 * )
826 * means the transition from state 18 to state 20 can be executed by
827 * the node author or a user in role 2. The $transitions array should
828 * contain ALL transitions for the workflow.
829 */
830 function workflow_update_transitions($transitions = array()) {
831 // Empty string is sometimes passed in instead of an array.
832 if (!$transitions) {
833 return;
834 }
835
836 foreach ($transitions as $from => $to_data) {
837 foreach ($to_data as $to => $role_data) {
838 foreach ($role_data as $role => $can_do) {
839 if ($can_do) {
840 workflow_transition_add_role($from, $to, $role);
841 }
842 else {
843 workflow_transition_delete_role($from, $to, $role);
844 }
845 }
846 }
847 }
848 db_query("DELETE FROM {workflow_transitions} WHERE roles = ''");
849 }
850
851 /**
852 * Add a role to the list of those allowed for a given transition.
853 *
854 * Add the transition if necessary.
855 *
856 * @param int $from
857 * @param int $to
858 * @param mixed $role
859 * Int (role ID) or string ('author').
860 */
861 function workflow_transition_add_role($from, $to, $role) {
862 $transition = array(
863 'sid' => $from,
864 'target_sid' => $to,
865 'roles' => $role,
866 );
867 $tid = workflow_get_transition_id($from, $to);
868 if ($tid) {
869 $roles = db_result(db_query("SELECT roles FROM {workflow_transitions} WHERE tid=%d", $tid));
870 $roles = explode(',', $roles);
871 if (array_search($role, $roles) === FALSE) {
872 $roles[] = $role;
873 $transition['roles'] = implode(',', $roles);
874 $transition['tid'] = $tid;
875 drupal_write_record('workflow_transitions', $transition, 'tid');
876 }
877 }
878 else {
879 drupal_write_record('workflow_transitions', $transition);
880 }
881 }
882
883 /**
884 * Remove a role from the list of those allowed for a given transition.
885 *
886 * @param int $tid
887 * @param mixed $role
888 * Int (role ID) or string ('author').
889 */
890 function workflow_transition_delete_role($from, $to, $role) {
891 $tid = workflow_get_transition_id($from, $to);
892 if ($tid) {
893 $roles = db_result(db_query("SELECT roles FROM {workflow_transitions} WHERE tid=%d", $tid));
894 $roles = explode(',', $roles);
895 if (($i = array_search($role, $roles)) !== FALSE) {
896 unset($roles[$i]);
897 db_query("UPDATE {workflow_transitions} SET roles='%s' WHERE tid=%d", implode(',', $roles), $tid);
898 }
899 }
900 }
901
902 /**
903 * See if a transition is allowed for a given role.
904 *
905 * @param int $tid
906 * @param mixed $role
907 * A single role (int or string 'author') or array of roles.
908 * @return
909 * TRUE if the role is allowed to do the transition.
910 */
911 function workflow_transition_allowed($tid, $role = NULL) {
912 $allowed = db_result(db_query("SELECT roles FROM {workflow_transitions} WHERE tid = %d", $tid));
913 $allowed = explode(',', $allowed);
914 if ($role) {
915 if (!is_array($role)) {
916 $role = array($role);
917 }
918 return array_intersect($role, $allowed) == TRUE;
919 }
920 }
921
922 /**
923 * Tell caller whether a state is a protected system state, such as the creation state.
924 *
925 * @param $state
926 * The name of the state to test
927 * @return
928 * TRUE if the state is a system state.
929 */
930 function workflow_is_system_state($state) {
931 static $states;
932 if (!isset($states)) {
933 $states = array(t('(creation)') => TRUE);
934 }
935 return isset($states[$state]);
936 }
937
938 /**
939 * Given the ID of a workflow, return its name.
940 *
941 * @param integer $wid
942 * The ID of the workflow.
943 * @return string
944 * The name of the workflow.
945 */
946 function workflow_get_name($wid) {
947 return db_result(db_query("SELECT name FROM {workflows} WHERE wid = %d", $wid));
948 }
949
950 /**
951 * Get ID of a workflow for a node type.
952 *
953 * @param $type
954 * Machine readable node type name, e.g. 'story'.
955 * @return int
956 * The ID of the workflow or FALSE if no workflow is mapped to this type.
957 */
958 function workflow_get_workflow_for_type($type) {
959 static $cache;
960 if(!isset($cache[$type])) {
961 $wid = db_result(db_query("SELECT wid FROM {workflow_type_map} WHERE type = '%s'", $type));
962 $cache[$type] = $wid;
963 }
964 else {
965 $wid = $cache[$type];
966 }
967 return $wid > 0 ? $wid : FALSE;
968 }
969
970 /**
971 * Get names and IDs of all workflows from the database.
972 *
973 * @return
974 * An array of workflows keyed by workflow ID.
975 */
976 function workflow_get_all() {
977 $workflows = array();
978 $result = db_query("SELECT wid, name FROM {workflows} ORDER BY name ASC");
979 while ($data = db_fetch_object($result)) {
980 $workflows[$data->wid] = $data->name;
981 }
982 return $workflows;
983 }
984
985 /**
986 * Create a workflow and its (creation) state.
987 *
988 * @param $name
989 * The name of the workflow.
990 */
991 function workflow_create($name) {
992 $workflow = array(
993 'name' => $name,
994 'options' => serialize(array('comment_log_node' => 1, 'comment_log_tab' => 1)),
995 );
996 drupal_write_record('workflows', $workflow);
997 workflow_state_save(array(
998 'wid' => $workflow['wid'],
999 'state' => t('(creation)'),
1000 'sysid' => WORKFLOW_CREATION,
1001 'weight' => WORKFLOW_CREATION_DEFAULT_WEIGHT));
1002 // Workflow creation affects tabs (local tasks), so force menu rebuild.
1003 menu_rebuild();
1004 return $workflow['wid'];
1005 }
1006
1007 /**
1008 * Save a workflow's name in the database.
1009 *
1010 * @param $wid
1011 * The ID of the workflowl
1012 * @param $name
1013 * The name of the workflow.
1014 * @param $tab_roles
1015 * Array of role IDs allowed to see the workflow tab.
1016 * @param $options
1017 * Array of key-value pairs that constitute various settings for
1018 * this workflow. An example is whether to show the comment form
1019 * on the workflow tab page or not.
1020 */
1021 function workflow_update($wid, $name, $tab_roles, $options) {
1022 db_query("UPDATE {workflows} SET name = '%s', tab_roles = '%s', options = '%s' WHERE wid = %d", $name, implode(',', $tab_roles), serialize($options), $wid);
1023 // Workflow name change affects tabs (local tasks), so force menu rebuild.
1024 menu_rebuild();
1025 }
1026
1027 /**
1028 * Delete a workflow from the database. Deletes all states,
1029 * transitions and node type mappings, too. Removes workflow state
1030 * information from nodes participating in this workflow.
1031 *
1032 * @param $wid
1033 * The ID of the workflow.
1034 */
1035 function workflow_deletewf($wid) {
1036 $wf = workflow_get_name($wid);
1037 $result = db_query('SELECT sid FROM {workflow_states} WHERE wid = %d', $wid);
1038 while ($data = db_fetch_object($result)) {
1039 // Delete the state and any associated transitions and actions.
1040 workflow_state_delete($data->sid);
1041 db_query('DELETE FROM {workflow_node} WHERE sid = %d', $data->sid);
1042 }
1043 db_query("DELETE FROM {workflow_type_map} WHERE wid = %d", $wid);
1044 db_query('DELETE FROM {workflows} WHERE wid = %d', $wid);
1045 // Notify any interested modules.
1046 module_invoke_all('workflow', 'workflow delete', $wid, NULL, NULL);
1047 // Workflow deletion affects tabs (local tasks), so force menu rebuild.
1048 cache_clear_all('*', 'cache_menu', TRUE);
1049 menu_rebuild();
1050 }
1051
1052 /**
1053 * Load workflow states for a workflow from the database.
1054 * If $wid is not passed, all states for all workflows are given.
1055 * States that have been deleted are not included.
1056 *
1057 * @param $wid
1058 * The ID of the workflow.
1059 *
1060 * @return
1061 * An array of workflow states keyed by state ID.
1062 */
1063 function workflow_get_states($wid = NULL) {
1064 $states = array();
1065 if (isset($wid)) {
1066 $result = db_query("SELECT sid, state FROM {workflow_states} WHERE wid = %d AND status = 1 ORDER BY weight, sid", $wid);
1067 while ($data = db_fetch_object($result)) {
1068 $states[$data->sid] = $data->state;
1069 }
1070 }
1071 else {
1072 $result = db_query("SELECT ws.sid, ws.state, w.name FROM {workflow_states} ws INNER JOIN {workflows} w ON ws.wid = w.wid WHERE status = 1 ORDER BY sid");
1073 while ($data = db_fetch_object($result)) {
1074 $states[$data->sid] = $data->name .': '. $data->state;
1075 }
1076 }
1077
1078 return $states;
1079 }
1080
1081 /**
1082 * Given the ID of a workflow state, return a keyed array representing the state.
1083 *
1084 * Note: this will retrieve states that have been deleted (their status key
1085 * will be set to 0).
1086 *
1087 * @param $sid
1088 * The ID of the workflow state.
1089 * @return
1090 * A keyed array with all attributes of the state.
1091 */
1092 function workflow_get_state($sid) {
1093 $state = array();
1094 $result = db_query('SELECT wid, state, weight, sysid, status FROM {workflow_states} WHERE sid = %d', $sid);
1095 // State IDs are unique, so there should be only one row.
1096 $data = db_fetch_object($result);
1097 $state['wid'] = $data->wid;
1098 $state['state'] = $data->state;
1099 $state['weight'] = $data->weight;
1100 $state['sysid'] = $data->sysid;
1101 $state['status'] = $data->status;
1102 return $state;
1103 }
1104
1105 /**
1106 * Add or update a workflow state to the database.
1107 *
1108 * @param $edit
1109 * An array containing values for the new or updated workflow state.
1110 * @return
1111 * The ID of the new or updated workflow state.
1112 */
1113 function workflow_state_save($state) {
1114 if (!isset($state['sid'])) {
1115 drupal_write_record('workflow_states', $state);
1116 }
1117 else {
1118 drupal_write_record('workflow_states', $state, 'sid');
1119 }
1120
1121 return $state['sid'];
1122 }
1123
1124 /**
1125 * Delete a workflow state from the database, including any
1126 * transitions the state was involved in and any associations
1127 * with actions that were made to that transition.
1128 *
1129 * @param $sid
1130 * The ID of the state to delete.
1131 * @param $new_sid
1132 * Deleting a state will leave any nodes to which that state is assigned
1133 * without a state. If $new_sid is given, it will be assigned to those
1134 * orphaned nodes
1135 */
1136 function workflow_state_delete($sid, $new_sid = NULL) {
1137 if ($new_sid) {
1138 // Assign nodes to new state so they are not orphaned.
1139 // A candidate for the batch API.
1140 $node = new stdClass();
1141 $node->workflow_stamp = time();
1142 $result = db_query("SELECT nid FROM {workflow_node} WHERE sid = %d", $sid);
1143 while ($data = db_fetch_object($result)) {
1144 $node->nid = $data->nid;
1145 $node->_workflow = $sid;
1146 _workflow_write_history($node, $new_sid, t('Previous state deleted'));
1147 db_query("UPDATE {workflow_node} SET sid = %d WHERE nid = %d AND sid = %d", $new_sid, $data->nid, $sid);
1148 }
1149 }
1150 else {
1151 // Go ahead and orphan nodes.
1152 db_query('DELETE from {workflow_node} WHERE sid = %d', $sid);
1153 }
1154
1155 // Find out which transitions this state is involved in.
1156 $preexisting = array();
1157 $result = db_query("SELECT sid, target_sid FROM {workflow_transitions} WHERE sid = %d OR target_sid = %d", $sid, $sid);
1158 while ($data = db_fetch_object($result)) {
1159 $preexisting[$data->sid][$data->target_sid] = TRUE;
1160 }
1161
1162 // Delete the transitions and associated actions, if any.
1163 foreach ($preexisting as $from => $array) {
1164 foreach (array_keys($array) as $target_id) {
1165 $tid = workflow_get_transition_id($from, $target_id);
1166 workflow_transition_delete($tid);
1167 }
1168 }
1169
1170 // Delete the state.
1171 db_query("UPDATE {workflow_states} SET status = 0 WHERE sid = %d", $sid);
1172 // Notify interested modules.
1173 module_invoke_all('workflow', 'state delete', $sid, NULL, NULL);
1174 }
1175
1176 /**
1177 * Delete a transition (and any associated actions).
1178 *
1179 * @param $tid
1180 * The ID of the transition.
1181 */
1182 function workflow_transition_delete($tid) {
1183 $actions = workflow_get_actions($tid);
1184 foreach ($actions as $aid) {
1185 workflow_actions_remove($tid, $aid);
1186 }
1187 db_query("DELETE FROM {workflow_transitions} WHERE tid = %d", $tid);
1188 // Notify interested modules.
1189 module_invoke_all('workflow', 'transition delete', $tid, NULL, NULL);
1190 }
1191
1192 /**
1193 * Get allowable transitions for a given workflow state. Typical use:
1194 *
1195 * global $user;
1196 * $possible = workflow_allowable_transitions($sid, 'to', $user->roles);
1197