workflow.module 76.6 KB
Newer Older
jvandyk's avatar
jvandyk committed
1
<?php
jvandyk's avatar
jvandyk committed
2
// $Id$
jvandyk's avatar
jvandyk committed
3

4
define('WORKFLOW_CREATION', 1);
5
define('WORKFLOW_DELETION', -1);
6
define('WORKFLOW_CREATION_DEFAULT_WEIGHT', -50);
7
define('WORKFLOW_ARROW', '&#8594;');
jvandyk's avatar
jvandyk committed
8
9

/**
10
11
 * Implementation of hook_help().
 */
jvandyk's avatar
jvandyk committed
12
13
function workflow_help($section) {
  switch ($section) {
14
    case strstr($section, 'admin/build/workflow/edit'):
15
      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 editor role may move a node from Review state to the Published state, check the box next to editor. The author role is built in and refers to the user who authored the node. For a summary of which role may do which transition, look at the bottom of this page.');
16
    case 'admin/build/workflow/add':
17
      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.');
18
    case strstr($section, 'admin/build/workflow/state') && !strstr($section, 'delete'):
jvandyk's avatar
jvandyk committed
19
      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 food</em>, <em>eat</em>, and <em>clean up</em>.');
20
    case strstr($section, 'admin/build/workflow/actions') && (count($section) == 22):
21
      return t('Use this page to set actions to happen when transitions occur. To <a href="@link">configure actions</a>, use the actions module.', array('@link' => url('admin/actions')));
jvandyk's avatar
jvandyk committed
22
23
24
25
  }
}

/**
26
27
 * Implementation of hook_perm().
 */
jvandyk's avatar
jvandyk committed
28
function workflow_perm() {
29
  return array('administer workflow', 'schedule workflow transitions');
jvandyk's avatar
jvandyk committed
30
31
32
}

/**
33
34
 * Implementation of hook_menu().
 */
jvandyk's avatar
jvandyk committed
35
36
function workflow_menu($may_cache) {
  $items = array();
jvandyk's avatar
jvandyk committed
37
  $access = user_access('administer workflow');
jvandyk's avatar
jvandyk committed
38
39

  if ($may_cache) {
40
41
    $items[] = array('path' => 'admin/build/workflow',
      'title'    => t('Workflow'),
42
      'access'   => $access,
43
      'callback' => 'workflow_overview',
44
      'description' => t('Allows the creation and assignment of arbitrary workflows to content types.'));
45

46
    $items[] = array('path' => 'admin/build/workflow/edit',
jvandyk's avatar
jvandyk committed
47
48
      'title'    => t('Edit workflow'),
      'type'     => MENU_CALLBACK,
49
50
      'callback' => 'drupal_get_form',
      'callback arguments' => array('workflow_edit_form'));
jvandyk's avatar
jvandyk committed
51

52
    $items[] = array('path' => 'admin/build/workflow/list',
jvandyk's avatar
jvandyk committed
53
      'title'    => t('List'),
54
55
56
57
      'weight'   => -10,
      'callback' => 'workflow_page',
      'type'     => MENU_DEFAULT_LOCAL_TASK);

58
    $items[] = array('path' => 'admin/build/workflow/add',
jvandyk's avatar
jvandyk committed
59
60
      'title'    => t('Add workflow'),
      'weight'   => -8,
61
62
      'callback' => 'drupal_get_form',
      'callback arguments' => array('workflow_add_form'),
63
      'type'     => MENU_LOCAL_TASK);
jvandyk's avatar
jvandyk committed
64

65
    $items[] = array('path' => 'admin/build/workflow/state',
jvandyk's avatar
jvandyk committed
66
67
      'title'    => t('Add state'),
      'type'     => MENU_CALLBACK,
68
69
      'callback' => 'drupal_get_form',
      'callback arguments' => array('workflow_state_add_form'));
70

71
72
73
74
75
    $items[] = array('path' => 'admin/build/workflow/state/delete',
      'title'    => t('Delete State'),
      'type'     => MENU_CALLBACK,
      'callback' => 'drupal_get_form',
      'callback arguments' => array('workflow_state_delete_form'));
jvandyk's avatar
jvandyk committed
76

77
    $items[] = array('path' => 'admin/build/workflow/delete',
jvandyk's avatar
jvandyk committed
78
79
      'title'    => t('Delete workflow'),
      'type'     => MENU_CALLBACK,
80
      'callback' => 'drupal_get_form',
81
82
      'callback arguments' => array('workflow_delete_form')
    );
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109

    // Add triggers page with multilevel local tasks.
    $items[] = array(
      'path' => 'admin/build/trigger/workflow',
      'title' => t('Workflow'),
      'callback' => 'actions_assign',
      'callback arguments' => array('workflow'),
      'access' => $access,
      'type' => MENU_LOCAL_TASK,
    );
    if (workflow_get_states()) {
      // Add a subtab for each workflow.
      $result = db_query("SELECT wid, name FROM {workflows}");
      $default_assigned = FALSE;
      while ($data = db_fetch_object($result)) {
        $items[] = array(
          'path' => 'admin/build/trigger/workflow/'. $data->wid,
          'title' => check_plain($data->name),
          'callback' => 'actions_assign',
          'callback arguments' => array('workflow'),
          'access' => $access,
          'type' => MENU_LOCAL_TASK,
//          'type' => $default_assigned ? MENU_LOCAL_TASK : MENU_DEFAULT_LOCAL_TASK,
        );
        $default_assigned = TRUE;
      }
    }
jvandyk's avatar
jvandyk committed
110
  }
111
112
113
114
  else {
    if (arg(0) == 'node' && is_numeric(arg(1))) {
      $node = node_load(arg(1));
      $wid = workflow_get_workflow_for_type($node->type);
115
116
      // If a workflow has been assigned to this node type.
      if ($wid) {
117
        global $user;
118
119
120
121
        $roles = array_keys($user->roles);
        if ($node->uid == $user->uid) {
          $roles = array_merge(array('author'), $roles);
        }
122
123
124
        $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",
125
          'title'    => t('Workflow'),
126
127
128
          'access'   => array_intersect($roles, $allowed_roles) || user_access('administer nodes'),
          'type'     => MENU_LOCAL_TASK,
          'weight'   => 2,
129
          'callback' => 'workflow_tab_page',
130
          'callback arguments' => arg(1),
131
        );
132
133
134
      }
    }
  }
jvandyk's avatar
jvandyk committed
135
136
137
  return $items;
}

138
139
140
141
142
143
144
/**
 * Menu callback. Displays the workflow information for a node.
 *
 * @param $nid
 *   Node ID of node for which workflow information will be displayed.
 * @return unknown
 */
145
146
function workflow_tab_page($nid) {
  $node = node_load($nid);
147
  drupal_set_title(check_plain($node->title));
148
  $wid = workflow_get_workflow_for_type($node->type);
149
  $states_per_page = variable_get('workflow_states_per_page', 20);
150
151
152
153
154
155
156
157
158
  $result = db_query("SELECT sid, state FROM {workflow_states} WHERE status = 1 ORDER BY sid");
  while ($data = db_fetch_object($result)) {
    $states[$data->sid] = $data->state;
  }
  $deleted_states = array();
  $result = db_query("SELECT sid, state FROM {workflow_states} WHERE status = 0 ORDER BY sid");
  while ($data = db_fetch_object($result)) {
    $deleted_states[$data->sid] = $data->state;
  }
159
  $current = workflow_node_current_state($node);
jvandyk's avatar
jvandyk committed
160

161
162
  // theme_workflow_current_state() must run state through check_plain().
  $output = '<p>'. t('Current state: !state', array('!state' => theme('workflow_current_state', $states[$current]))) . "</p>\n";
163
164
  $output .= drupal_get_form('workflow_tab_form', $node, $wid, $states, $current);

165
  $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 hid DESC", $states_per_page, 0, NULL, $nid);
166
167
  $rows = array();
  while ($history = db_fetch_object($result)) {
jvandyk's avatar
jvandyk committed
168
    if ($history->sid == $current && !isset($deleted_states[$history->sid]) && !isset($current_themed)) {
169
170
      // Theme the current state differently so it stands out.
      $state_name = theme('workflow_current_state', $states[$history->sid]);
jvandyk's avatar
jvandyk committed
171
172
173
      // Make a note that we have themed the current state; other times in the history
      // of this node where the node was in this state do not need to be specially themed.
      $current_themed = TRUE;
174
175
176
177
178
179
180
181
    }
    elseif (isset($deleted_states[$history->sid])) {
      // The state has been deleted, but we include it in the history.
      $state_name = theme('workflow_deleted_state', $deleted_states[$history->sid]);
      $footer_needed = TRUE;
    }
    else {
      // Regular state.
182
      $state_name = check_plain(t($states[$history->sid]));
183
184
185
186
187
188
189
    }

    if (isset($deleted_states[$history->old_sid])) {
      $old_state_name = theme('workflow_deleted_state', $deleted_states[$history->old_sid]);
      $footer_needed = TRUE;
    }
    else {
190
      $old_state_name = check_plain(t($states[$history->old_sid]));
191
    }
192
    $rows[] = theme('workflow_history_table_row', $history, $old_state_name, $state_name);
193
  }
jvandyk's avatar
jvandyk committed
194
  $output .= theme('workflow_history_table', $rows, !empty($footer_needed));
195
196
197
  $output .= theme('pager', $states_per_page);
  return $output;
}
198

199
200
/*
 * Theme one workflow history table row.
201
202
203
 *
 * $old_state_name and $state_name must be run through check_plain(t())
 * before calling this function.
204
205
206
207
 */
function theme_workflow_history_table_row($history, $old_state_name, $state_name) {
  return array(
    format_date($history->stamp),
208
209
    $old_state_name,
    $state_name,
210
211
212
213
214
215
216
217
218
219
220
    theme('username', $history),
    filter_xss($history->comment, array('a', 'em', 'strong')),
  );
}

/*
 * Theme entire workflow history table.
 */
function theme_workflow_history_table($rows, $footer) {
  $output = theme('table', array(t('Date'), t('Old State'), t('New State'), t('By'), t('Comment')), $rows, array('class' => 'workflow_history'), t('Workflow History'));
  if ($footer) {
221
222
    $output .= t('*State is no longer available.');
  }
223
224
  return $output;
}
225

226
227
228
/**
 * Theme the current state in the workflow history table.
 */
229
function theme_workflow_current_state($state_name) {
jvandyk's avatar
jvandyk committed
230
  return '<strong>'. check_plain(t($state_name)) .'</strong>';
231
232
233
234
235
236
}

/**
 * Theme a deleted state in the workflow history table.
 */
function theme_workflow_deleted_state($state_name) {
jvandyk's avatar
jvandyk committed
237
  return check_plain(t($state_name)) .'*';
238
239
}

240
241
242
243
244
245
246
247
248
249
250
251
252
253
/**
 * Creates form definition of the workflow editing form.
 *
 * @param $node
 *   Node for which workflow information will be displayed.
 * @param $wid
 *   The workflow ID.
 * @param unknown_type $states
 *   Array of states for the workflow.
 * @param $current
 *   The current state that this node is in.
 * @return
 *   Form definition.
 */
254
255
function workflow_tab_form(&$node, $wid, $states, $current) {
  $form = array();
256
  $choices = workflow_field_choices($node);
257

258
  $min = $states[$current] == t('(creation)') ? 1 : 2;
259
260
  // Only build form if user has possible target state(s).
  if (count($choices) >= $min) {
261
    $wid = workflow_get_workflow_for_type($node->type);
jvandyk's avatar
jvandyk committed
262
    $name = check_plain(t(workflow_get_name($wid)));
263
    // See if scheduling information is present.
264
265
266
267
268
269
270
271
    if ($node->_workflow_scheduled_timestamp && $node->_workflow_scheduled_sid) {
      global $user;
      if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) {
        $timezone = $user->timezone;
      }
      else {
        $timezone = variable_get('date_default_timezone', 0);
      }
272
273
      // The default value should be the upcoming sid.
      $current = $node->_workflow_scheduled_sid;
274
275
276
      $timestamp = $node->_workflow_scheduled_timestamp;
      $comment = $node->_workflow_scheduled_comment;
    }
277

278
    workflow_node_form($form, t('Change %s state', array('%s' => $name)), $name, $current, $choices, $timestamp, $comment);
279

280
281
282
283
284
285
286
287
288
289
    $form['node'] = array(
      '#type' => 'value',
      '#value' => $node,
    );

    $form['submit'] = array(
      '#type' => 'submit',
      '#value' => t('Submit')
    );
  }
290

291
  return $form;
292
293
}

294
295
296
/**
 * Submit handler for workflow page.
 */
297
function workflow_tab_form_submit($form_id, $form_values) {
298
  $node = $form_values['node'];
299
300

  // Mockup a node so we don't need to repeat the code for processing this.
301
302
303
304
305
  $node->workflow = $form_values['workflow'];
  $node->workflow_comment = $form_values['workflow_comment'];
  $node->workflow_scheduled = $form_values['workflow_scheduled'];
  $node->workflow_scheduled_date = $form_values['workflow_scheduled_date'];
  $node->workflow_scheduled_hour = $form_values['workflow_scheduled_hour'];
306
307

  // Call node_save() to make sure all saving properties run on this node.
308
  node_save($node);
309

310
  // Redirect user from tab page to node itself.
311
312
313
  return 'node/' . $node->nid;
}

jvandyk's avatar
jvandyk committed
314
/**
jvandyk's avatar
jvandyk committed
315
 * Implementation of hook_nodeapi().
316
 */
jvandyk's avatar
jvandyk committed
317
function workflow_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
jvandyk's avatar
jvandyk committed
318
319
  switch ($op) {

320
321
322
323
324
325
326
327
328
329
330
    case 'load':
      // Add workflow information to the node.
      $node->_workflow = workflow_node_current_state($node);

      // Add scheduling information to the node.
      $res = db_query('SELECT * FROM {workflow_scheduled_transition} WHERE nid = %d', $node->nid);
      if ($row = db_fetch_object($res)) {
        $node->_workflow_scheduled_sid = $row->sid;
        $node->_workflow_scheduled_timestamp = $row->scheduled;
        $node->_workflow_scheduled_comment = $row->comment;
      }
331
    break;
jvandyk's avatar
jvandyk committed
332
333

    case 'insert':
jvandyk's avatar
jvandyk committed
334
335
336
337
338
339
340
341
342
      // If the state is not specified, use first valid state.
      // For example, a new node must move from (creation) to some
      // initial state.
      if (empty($node->workflow)) {
        $choices = workflow_field_choices($node);
        $keys = array_keys($choices);
        $sid = array_shift($keys);
      }
      // Note no break; fall through to 'update' case.
jvandyk's avatar
jvandyk committed
343
    case 'update':
344
      // Do nothing if there is no workflow for this node type.
jvandyk's avatar
jvandyk committed
345
346
347
      $wid = workflow_get_workflow_for_type($node->type);
      if (!$wid) {
        break;
jvandyk's avatar
jvandyk committed
348
      }
jvandyk's avatar
jvandyk committed
349

350
      // Get new state from value of workflow form field, stored in $node->workflow.
jvandyk's avatar
jvandyk committed
351
352
      if (!isset($sid)) {
        $sid = $node->workflow;
jvandyk's avatar
jvandyk committed
353
354
      }

jvandyk's avatar
jvandyk committed
355
      workflow_transition($node, $sid);
356
      break;
jvandyk's avatar
jvandyk committed
357

358
    case 'delete':
jvandyk's avatar
jvandyk committed
359
      db_query("DELETE FROM {workflow_node} WHERE nid = %d", $node->nid);
360
      _workflow_write_history($node, WORKFLOW_DELETION, t('Node deleted'));
361
      break;
jvandyk's avatar
jvandyk committed
362
363
  }
}
364

jvandyk's avatar
jvandyk committed
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
/**
 * Implementation of hook_comment().
 */
function workflow_comment($a1, $op) {
  if (($op == 'insert' || $op == 'update') && isset($a1['workflow'])) {
    $node = node_load($a1['nid']);
    $sid = $a1['workflow'];
    $node->workflow_comment = $a1['workflow_comment'];
    workflow_transition($node, $sid);
  }
}

/**
 * Validate target state and either execute a transition immediately or schedule
 * a transition to be executed later by cron.
 *
 * @param $node
 * @param $sid
 *   An integer; the target state ID.
 */
function workflow_transition($node, $sid) {
  // Make sure new state is a valid choice.
  if (array_key_exists($sid, workflow_field_choices($node))) {
    if (!$node->workflow_scheduled) {
      // It's an immediate change. Do the transition.
      workflow_execute_transition($node, $sid, $node->workflow_comment);
    }
    else {
      // Schedule the the time to change the state.
      $comment = $node->workflow_comment;
      $old_sid = workflow_node_current_state($node);

      if ($node->workflow_scheduled_date['day'] < 10) {
        $node->workflow_scheduled_date['day'] = '0' .
        $node->workflow_scheduled_date['day'];
      }

      if ($node->workflow_scheduled_date['month'] < 10) {
        $node->workflow_scheduled_date['month'] = '0' .
        $node->workflow_scheduled_date['month'];
      }

      if (!$node->workflow_scheduled_hour) {
        $node->workflow_scheduled_hour = '00:00';
      }

      $scheduled = $node->workflow_scheduled_date['year'] . $node->workflow_scheduled_date['month'] . $node->workflow_scheduled_date['day'] . ' ' . $node->workflow_scheduled_hour . 'Z';
      if ($scheduled = strtotime($scheduled)) {
        // Adjust for user and site timezone settings.
        global $user;
        if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) {
          $timezone = $user->timezone;
        }
        else {
          $timezone = variable_get('date_default_timezone', 0);
        }
        $scheduled = $scheduled - $timezone;

        // Clear previous entries and insert.
        db_query("DELETE FROM {workflow_scheduled_transition} WHERE nid = %d", $node->nid);
        db_query("INSERT INTO {workflow_scheduled_transition} VALUES (%d, %d, %d, %d, '%s')", $node->nid, $old_sid, $sid, $scheduled, $comment);

        drupal_set_message(t("@node_title is scheduled for state change on !scheduled_date", array( "@node_title" => $node->title, "!scheduled_date" => format_date($scheduled))));
      }
    }
  }
}

433
/**
434
 * Add the actual form widgets for workflow change to a form definition.
435
 *
436
437
438
439
440
441
442
443
 * @param $form
 *   A form definition array.
 * @param $name
 *   The name of the workflow.
 * @param $current
 *   The state ID of the current state.
 * @param $choices
 *   An array of possible target states.
444
 */
445
function workflow_node_form(&$form, $title, $name, $current, $choices, $timestamp = NULL, $comment = NULL) {
446
  // No sense displaying choices if there is only one choice.
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
  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
    );
462

jvandyk's avatar
jvandyk committed
463
464
465
466
    // Display scheduling form only if a node is being edited and user has
    // permission. State change cannot be scheduled at node creation because
    // that leaves the node in the (creation) state.
    if (!(arg(0) == 'node' && arg(1) == 'add') && user_access('schedule workflow transitions')) {
467
468
469
      $scheduled = $timestamp ? 1 : 0;
      $timestamp = $scheduled ? $timestamp : time();

470
      $form['workflow']['workflow_scheduled'] = array(
471
472
        '#type' => 'radios',
        '#title' => t('Schedule'),
473
        '#options' => array(
474
475
476
477
478
479
          t('Immediately'),
          t('Schedule for state change at:'),
        ),
        '#default_value' => $scheduled,
      );

480
      $form['workflow']['workflow_scheduled_date'] = array(
481
        '#type' => 'date',
jvandyk's avatar
jvandyk committed
482
483
484
485
486
        '#default_value' => array(
          'day'   => format_date($timestamp, 'custom', 'j'),
          'month' => format_date($timestamp, 'custom', 'n'),
          'year'  => format_date($timestamp, 'custom', 'Y')
        ),
487
      );
488

489
      $hours = format_date($timestamp, 'custom', 'H:i');
490
      $form['workflow']['workflow_scheduled_hour'] = array(
491
        '#type' => 'textfield',
492
        '#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()),
493
494
495
        '#default_value' => $scheduled ? $hours : NULL,
      );
    }
496

497
498
499
500
    $form['workflow']['workflow_comment'] = array(
      '#type' => 'textarea',
      '#title' => t('Comment'),
      '#description' => t('A comment to put in the workflow log.'),
501
      '#default_value' => $comment,
jvandyk's avatar
jvandyk committed
502
      '#rows' => 2,
503
504
    );
  }
505
506
}

jvandyk's avatar
jvandyk committed
507
/**
508
 * Modify the node form to add the workflow field.
jvandyk's avatar
jvandyk committed
509
 */
510
function workflow_form_alter($form_id, &$form) {
jvandyk's avatar
jvandyk committed
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
  // Ignore all forms except comment forms and node editing forms.
  if ($form_id == 'comment_form' || isset($form['type']) && $form['#base'] == 'node_form') {
    if (isset($form['#node'])) {
      $node = $form['#node'];
      // Abort if user does not want to display workflow form on node editing form.
      if (!in_array('node', variable_get('workflow_' . $node->type, array('node')))) {
        return;
      }
    }
    else {
      $type = db_result(db_query("SELECT type FROM {node} WHERE nid = %d", $form['nid']['#value']));
      // Abort if user does not want to display workflow form on node editing form.
      if (!in_array('comment', variable_get('workflow_' . $type, array('node')))) {
        return;
      }
      $node = node_load($form['nid']['#value']);
    }
528
    $choices = workflow_field_choices($node);
529
    $wid = workflow_get_workflow_for_type($node->type);
530
    $states = workflow_get_states($wid);
531
532
    $current = workflow_node_current_state($node);
    $min = $states[$current] == t('(creation)') ? 1 : 2;
533
534
    // Bail out if user has no new target state(s).
    if (count($choices) < $min) {
535
536
      return;
    }
Mark Fredrickson's avatar
Mark Fredrickson committed
537

jvandyk's avatar
jvandyk committed
538
    $name = check_plain(t(workflow_get_name($wid)));
jvandyk's avatar
jvandyk committed
539

540
541
542
    // 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.
543
544
545
546
    if (!isset($choices[$current])) {
      $array = array_keys($choices);
      $current = $array[0];
    }
jvandyk's avatar
jvandyk committed
547

548
549
550
551
552
553
    if (sizeof($choices) > 1) {
      $form['workflow'] = array(
        '#type' => 'fieldset',
        '#title' => $name,
        '#collapsible' => TRUE,
        '#collapsed' => FALSE,
Mark Fredrickson's avatar
Mark Fredrickson committed
554
        '#weight' => 10,
555
556
      );
    }
557
558

    // See if scheduling information is present.
559
560
561
562
563
564
565
566
    if ($node->_workflow_scheduled_timestamp && $node->_workflow_scheduled_sid) {
      global $user;
      if (variable_get('configurable_timezones', 1) && $user->uid && strlen($user->timezone)) {
        $timezone = $user->timezone;
      }
      else {
        $timezone = variable_get('date_default_timezone', 0);
      }
567
568
      // The default value should be the upcoming sid.
      $current = $node->_workflow_scheduled_sid;
569
570
571
572
573
      $timestamp = $node->_workflow_scheduled_timestamp;
      $comment = $node->_workflow_scheduled_comment;
    }

    workflow_node_form($form, $name, $name, $current, $choices, $timestamp, $comment);
jvandyk's avatar
jvandyk committed
574
575
576
577
578
579
  }
}

/**
 * Execute a transition (change state of a node).
 *
580
581
582
583
584
 * @param $node
 * @param $sid
 *   Target state ID.
 * @param $comment
 *   A comment for the node's workflow history.
585
586
 * @param $force
 *   If set to TRUE, workflow permissions will be ignored.
587
588
 * @return
 *   ID of new state.
jvandyk's avatar
jvandyk committed
589
 */
590
function workflow_execute_transition(&$node, $sid, $comment = NULL, $force = FALSE) {
jvandyk's avatar
jvandyk committed
591
  $old_sid = workflow_node_current_state($node);
592
593
  if ($old_sid == $sid) {
    // Stop if not going to a different state.
594
    // Write comment into history though.
595
596
597
    if ($comment && !$node->_workflow_scheduled_comment) {
      $node->workflow_stamp = time();
      db_query("UPDATE {workflow_node} SET stamp = %d WHERE nid = %d", $node->workflow_stamp, $node->nid);
598
      $result = module_invoke_all('workflow', 'transition pre', $old_sid, $sid, $node);
599
600
      _workflow_write_history($node, $sid, $comment);
    }
601
      $result = module_invoke_all('workflow', 'transition post', $old_sid, $sid, $node);
jvandyk's avatar
jvandyk committed
602
603
604
    return;
  }

605
  $tid = workflow_get_transition_id($old_sid, $sid);
606
  if (!$tid) {
607
    watchdog('workflow', t('Attempt to go to nonexistent transition (from %old to %new)', array('%old' => $old_sid, '%new' => $sid)), WATCHDOG_ERROR);
608
609
610
    return;
  }

jvandyk's avatar
jvandyk committed
611
612
  // Make sure this transition is valid and allowed for the current user.
  global $user;
613
614
  // Check allowability of state change if user is not superuser (might be cron).
  if (($user->uid != 1) && !$force) {
615
    if (!workflow_transition_allowed($tid, array_merge(array_keys($user->roles), array('author')))) {
616
      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);
jvandyk's avatar
jvandyk committed
617
618
619
620
      return;
    }
  }

jvandyk's avatar
jvandyk committed
621
  // Invoke a callback indicating a transition is about to occur. Modules
jvandyk's avatar
jvandyk committed
622
623
624
  // may veto the transition by returning FALSE.
  $result = module_invoke_all('workflow', 'transition pre', $old_sid, $sid, $node);

625
626
  // Stop if a module says so.
  if (in_array(FALSE, $result)) {
jvandyk's avatar
jvandyk committed
627
    watchdog('workflow', t('Transition vetoed by module.'));
jvandyk's avatar
jvandyk committed
628
629
    return;
  }
jvandyk's avatar
jvandyk committed
630

631
632
  // Change the state.
  _workflow_node_to_state($node, $sid, $comment);
633
  $node->_workflow = $sid;
jvandyk's avatar
jvandyk committed
634

635
  // Register state change with watchdog.
jvandyk's avatar
jvandyk committed
636
  $state_name = db_result(db_query('SELECT state FROM {workflow_states} WHERE sid = %d', $sid));
637
  $type = node_get_types('name', $node->type);
638
  watchdog('workflow', t('State of @type %node_title set to @state_name', array('@type' => $type, '%node_title' => $node->title, '@state_name' => $state_name)), WATCHDOG_NOTICE, l('view', 'node/' . $node->nid));
639

jvandyk's avatar
jvandyk committed
640
  // Notify modules that transition has occurred. Actions should take place
jvandyk's avatar
jvandyk committed
641
642
  // in response to this callback, not the previous one.
  module_invoke_all('workflow', 'transition post', $old_sid, $sid, $node);
643
644

  // Clear any references in the scheduled listing.
645
  db_query('DELETE FROM {workflow_scheduled_transition} WHERE nid = %d', $node->nid);
jvandyk's avatar
jvandyk committed
646
647
}

648
/**
649
 * Implementation of hook-action_info().
650
 */
651
652
function workflow_action_info() {
  return array(
653
    'workflow_select_next_state_action' => array(
654
655
656
657
658
659
      'type' => 'node',
      'description' => t('Change workflow state of post to next state'),
      'configurable' => FALSE,
      'hooks' => array(
        'nodeapi' => array('presave'),
        'comment' => array('insert', 'update'),
660
        'workflow' => array('any'),
661
662
      ),
    ),
663
664
665
666
667
668
669
    'workflow_select_given_state_action' => array(
      'type' => 'node',
      'description' => t('Change workflow state of post to new state'),
      'configurable' => TRUE,
      'hooks' => array(
        'nodeapi' => array('presave'),
        'comment' => array('insert', 'update'),
670
        'workflow' => array('any'),
671
672
      ),
    ),
673
674
  );
}
jvandyk's avatar
jvandyk committed
675

676
/**
677
 * Implementation of a Drupal action. Move a node to the next state in the workfow.
678
 */
679
function workflow_select_next_state_action($node, $context) {
680
  // If this action is being fired because it's attached to a workflow transition
681
682
  // then the node's new state (now its current state) should be in $node->workflow
  // because that is where the value from the workflow form field is stored;
683
684
685
686
687
688
  // 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;
jvandyk's avatar
jvandyk committed
689

690
691
692
693
694
695
  // Get the node's new state.
  $choices = workflow_field_choices($node);
  foreach ($choices as $sid => $name) {
    if (isset($flag)) {
      $new_state = $sid;
      $new_state_name = $name;
696
      break;
697
698
699
700
701
    }
    if ($sid == $current_state) {
      $flag = TRUE;
    }
  }
702

703
704
  // Fire the transition
  workflow_execute_transition($node, $new_state);
705
706
707
708
709
710
711
712
713
}

/**
 * Implementation of a Drupal action. Move a node to a specified state.
 */
function workflow_select_given_state_action($node, $context) {
  $comment = t($context['workflow_comment'], array('%title' => check_plain($node->title), '%state' => check_plain($context['state_name'])));
  workflow_execute_transition($node, $context['target_sid'], $comment, $context['force']);
}
714

715
function workflow_select_given_state_action_form($context) {
716
  $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");
717
718
719
  $previous_workflow = '';
  $options = array();
  while ($data = db_fetch_object($result)) {
jvandyk's avatar
jvandyk committed
720
    $options[$data->name][$data->sid] = check_plain(t($data->state));
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
  }
  $form['target_sid'] = array(
    '#type' => 'select',
    '#title' => t('Target state'),
    '#description' => t('Please select that state that should be assigned when this action runs.'),
    '#default_value' => isset($context['target_sid']) ? $context['target_sid'] : '',
    '#options' => $options,
  );
  $form['force'] = array(
    '#type' => 'checkbox',
    '#title' => t('Force transition'),
    '#description' => t('If this box is checked, the new state will be assigned even if workflow permissions disallow it.'),
    '#default_value' => isset($context['force']) ? $context['force'] : '',
  );
  $form['workflow_comment'] = array(
    '#type' => 'textfield',
    '#title' => t('Message'),
    '#description' => t('This message will be written into the workflow history log when the action runs. You may include the following variables: %state, %title'),
    '#default_value' => isset($context['workflow_history']) ? $context['workflow_history'] : t('Action set %title to %state.'),
  );
  return $form;
}

function workflow_select_given_state_action_submit($form_id, $form_values) {
  $state_name = db_result(db_query("SELECT state FROM {workflow_states} WHERE sid = %d", $form_values['target_sid']));
  return array(
    'target_sid' => $form_values['target_sid'],
jvandyk's avatar
jvandyk committed
748
    'state_name' => check_plain(t($state_name)),
749
750
751
    'force' => $form_values['force'],
    'workflow_comment' => $form_values['workflow_comment'],
  );
752
753
}

754

jvandyk's avatar
jvandyk committed
755
756
757
758
759
760
761
762
763
/**
 * 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);
764
765
  if (!$wid) {
    // No workflow for this type.
jvandyk's avatar
jvandyk committed
766
    return array();
767
  }
jvandyk's avatar
jvandyk committed
768
769
770
  $states = workflow_get_states($wid);
  $roles = array_keys($user->roles);
  $current_sid = workflow_node_current_state($node);
771
772
773

  // If user is node author or this is a new page, give the authorship role.
  if (($user->uid == $node->uid && $node->uid > 0) || (arg(0) == 'node' && arg(1) == 'add')) {
jvandyk's avatar
jvandyk committed
774
775
    $roles += array('author' => 'author');
  }
776
777
  if ($user->uid == 1) {
    // Superuser is special.
jvandyk's avatar
jvandyk committed
778
779
780
    $roles = 'ALL';
  }
  $transitions = workflow_allowable_transitions($current_sid, 'to', $roles);
781
782
783

  // Include current state if it is not the (creation) state.
  if ($current_sid == _workflow_creation_state($wid)) {
784
    unset($transitions[$current_sid]);
jvandyk's avatar
jvandyk committed
785
786
787
788
789
790
791
792
  }
  return $transitions;
}

/**
 * Get the current state of a given node.
 *
 * @param object $node
793
 * @return string state ID
jvandyk's avatar
jvandyk committed
794
795
 */
function workflow_node_current_state($node) {
796
797
  $sid = db_result(db_query('SELECT sid FROM {workflow_node} WHERE nid = %d', $node->nid));

jvandyk's avatar
jvandyk committed
798
799
800
  if (!$sid) {
    $wid = workflow_get_workflow_for_type($node->type);
    $sid = _workflow_creation_state($wid);
jvandyk's avatar
jvandyk committed
801
  }
jvandyk's avatar
jvandyk committed
802
803
  return $sid;
}
jvandyk's avatar
jvandyk committed
804

jvandyk's avatar
jvandyk committed
805
function _workflow_creation_state($wid) {
806
  static $cache;
807
  if (!isset($cache[$wid])) {
808
    $result = db_result(db_query("SELECT sid FROM {workflow_states} WHERE wid = %d AND sysid = %d", $wid, WORKFLOW_CREATION));
809
810
    $cache[$wid] = $result;
  }
811

812
  return $cache[$wid];
jvandyk's avatar
jvandyk committed
813
814
815
}

/**
jvandyk's avatar
jvandyk committed
816
 * Implementation of hook_workflow().
817
 */
jvandyk's avatar
jvandyk committed
818
819
function workflow_workflow($op, $old_state, $new_state, $node) {
  switch ($op) {
820
    case 'transition pre':
821
      break;
jvandyk's avatar
jvandyk committed
822

823
    case 'transition post':
824
825
826
827
828
      // A transition has occurred; fire off actions associated with this transition.
      // Can't fire actions if actions module is not enabled.
      if (!function_exists('actions_do')) {
        break;
      }
829
      $tid = workflow_get_transition_id($old_state, $new_state);
830
      $op = 'workflow-'. $node->type .'-'. $tid;
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
      $aids = _actions_get_hook_aids('workflow', $op);
      if ($aids) {
        $context = array(
          'hook' => 'workflow',
          'op' => $op,
        );

        // We need to get the expected object if the action's type is not 'node'.
        // We keep the object in $objects so we can reuse it if we have multiple actions
        // that make changes to an object.
        foreach ($aids as $aid => $action_info) {
          if ($action_info['type'] != 'node') {
            if (!isset($objects[$action_info['type']])) {
              $objects[$action_info['type']] = _actions_normalize_node_context($action_info['type'], $node);
           }
           // Since we know about the node, we pass that info along to the action.
           $context['node'] = $node;
           $result = actions_do($aid, $objects[$action_info['type']], $context);
         }
         else {
           actions_do($aid, $node, $context);
          }
853
        }
jvandyk's avatar
jvandyk committed
854
      }
855
      break;
jvandyk's avatar
jvandyk committed
856
  }
jvandyk's avatar
jvandyk committed
857
858
859
}

/**
jvandyk's avatar
jvandyk committed
860
 * Create the form for adding/editing a workflow.
jvandyk's avatar
jvandyk committed
861
 *
jvandyk's avatar
jvandyk committed
862
863
864
865
 * @param $name
 *   Name of the workflow if editing.
 * @param $add
 *   Boolean, if true edit workflow name.
jvandyk's avatar
jvandyk committed
866
867
 *
 * @return
jvandyk's avatar
jvandyk committed
868
 *   HTML form.
jvandyk's avatar
jvandyk committed
869
870
 *
 */
jvandyk's avatar
jvandyk committed
871
872
873
874
875
876
function workflow_add_form($name = NULL) {
  $form = array();
  $form['wf_name'] = array(
    '#type' => 'textfield',
    '#title' => t('Workflow Name'),
    '#maxlength' => '254',
877
    '#default_value' => $name
jvandyk's avatar
jvandyk committed
878
879
    );
  $form['submit'] = array('#type' => 'submit', '#value' => t('Add Workflow'));
880
881

  return $form;
jvandyk's avatar
jvandyk committed
882
}
jvandyk's avatar
jvandyk committed
883

jvandyk's avatar
jvandyk committed
884
function workflow_add_form_validate($form_id, $form_values) {
885
886
  $workflow_name = $form_values['wf_name'];
  $workflows = array_flip(workflow_get_all());
887
  // Make sure a nonblank workflow name is provided.
888
889
890
  if ($workflow_name == '') {
    form_set_error('wf_name', t('Please provide a nonblank name for the new workflow.'));
  }
891
  // Make sure workflow name is not a duplicate.
892
893
894
  if (array_key_exists($workflow_name, $workflows)) {
    form_set_error('wf_name', t('A workflow with the name %name already exists. Please enter another name for your new workflow.',
      array('%name' => $workflow_name)));
jvandyk's avatar
jvandyk committed
895
896
  }
}
jvandyk's avatar
jvandyk committed
897

jvandyk's avatar
jvandyk committed
898
function workflow_add_form_submit($form_id, $form_values) {
899
900
901
902
903
904
  $workflow_name = $form_values['wf_name'];
  if (array_key_exists('wf_name', $form_values) && $workflow_name != '')  {
    workflow_create($workflow_name);
    watchdog('workflow', t('Created workflow %name', array('%name' => $workflow_name)));
    drupal_set_message(t('The workflow %name was created. You should now add states to your workflow.',
      array('%name' => $workflow_name)), 'warning');
905
    return ('admin/build/workflow');
jvandyk's avatar
jvandyk committed
906
  }
jvandyk's avatar
jvandyk committed
907
908
909
910
911
912
913
914
915
916
917
918
}

/**
 * Create the form for confirmation of deleting a workflow.
 *
 * @param $wid
 *   The ID of the workflow.
 *
 * @return
 *   HTML form.
 *
 */
919
function workflow_delete_form($wid, $sid = NULL) {
jvandyk's avatar
jvandyk committed
920
  if (isset($sid)) {
jvandyk's avatar
jvandyk committed
921
    return workflow_state_delete_form($wid, $sid);
jvandyk's avatar
jvandyk committed
922
  }
923

jvandyk's avatar
jvandyk committed
924
925
  $form = array();
  $form['wid'] = array('#type' => 'value', '#value' => $wid);
926
927
928
929
930
931
932
933
  return confirm_form(
    $form,
    t('Are you sure you want to delete %title? All nodes that have a workflow state associated with this workflow will have those workflow states removed.', array('%title' => workflow_get_name($wid))),
    $_GET['destination'] ? $_GET['destination'] : 'admin/build/workflow',
    t('This action cannot be undone.'),
    t('Delete'),
    t('Cancel')
  );
jvandyk's avatar
jvandyk committed
934
935
}

936
function workflow_delete_form_submit($form_id, $form_values) {
jvandyk's avatar
jvandyk committed
937
  if ($form_values['confirm'] == 1) {
938
939
940
941
    $workflow_name = workflow_get_name($form_values['wid']);
    workflow_deletewf($form_values['wid']);
    watchdog('workflow', t('Deleted workflow %name with all its states', array('%name' => $workflow_name)));
    drupal_set_message(t('The workflow %name with all its states was deleted.', array('%name' => $workflow_name)));
942
    return ('admin/build/workflow');
jvandyk's avatar
jvandyk committed
943
944
945
  }
}

946
947
948
949
950
951
952
953
954
/**
 * View workflow permissions by role
 *
 * @param $wid
 *   The ID of the workflow
 */
function workflow_permissions($wid) {
  $name = workflow_get_name($wid);
  $all = array();
955
956
957
958
959
960
961
962
963
  $roles = array('author' => t('author')) + user_roles();
  foreach ($roles as $role => $value) {
    $all[$role]['name'] = $value;
  }
  $result = db_query(
    "SELECT t.roles, s1.state AS state_name, s2.state AS target_state_name "
  . "FROM {workflow_transitions} t "
  . "INNER JOIN {workflow_states} s1 ON s1.sid = t.sid "
  . "INNER JOIN {workflow_states} s2 ON s2.sid = t.target_sid "
964
  . "WHERE s1.wid = %d AND s1.status = 1 "
965
966
  . "ORDER BY s1.weight ASC , s1.state ASC , s2.weight ASC , s2.state ASC",
    $wid);
967
  while ($data = db_fetch_object($result)) {
968
    foreach (explode(',', $data->roles) as $role) {
jvandyk's avatar
jvandyk committed
969
      $all[$role]['transitions'][] = array(check_plain(t($data->state_name)), WORKFLOW_ARROW, check_plain(t($data->target_state_name)));
970
971
    }
  }
972

973
974
  $output = '';
  $header = array(t('From'), '', t('To'));
975
  foreach ($all as $role => $value) {
976
    if ($role == 'author') {
jvandyk's avatar
jvandyk committed
977
      $output .= '<h3>'. t("The author of the post may do these transitions:") .'</h3>';
978
979
980
981
    }
    else {
      $output .= '<h3>'. t("The role %role may do these transitions:", array('%role' => $value['name'])) .'</h3>';
    }
982
983
    if ($value['transitions']) {
      $output .= theme('table', $header, $value['transitions']) . '<p></p>';
984
985
    }
    else {
986
      $output .= '<table><tbody><tr class="even"><td>' . t('None') . '</td><td></tr></tbody></table><p></p>';
987
988
989
990
991
    }
  }
  return $output;
}

jvandyk's avatar
jvandyk committed
992
993
994
995
996
997
998
999
1000
/**
 * Menu callback to edit a workflow's properties.
 *
 * @param $wid
 *   The ID of the workflow.
 *
 * @return
 *   HTML form.
 */