workflow.module 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819
  1. <?php
  2. /**
  3. * @file
  4. * Support workflows made up of arbitrary states.
  5. */
  6. define('WORKFLOW_CREATION', 1);
  7. define('WORKFLOW_CREATION_DEFAULT_WEIGHT', -50);
  8. define('WORKFLOW_DELETION', 0);
  9. /**
  10. * Implements hook_permission().
  11. */
  12. function workflow_permission() {
  13. return array(
  14. 'schedule workflow transitions' => array(
  15. 'title' => t('Schedule workflow transitions'),
  16. 'description' => t('Schedule workflow transitions.'),
  17. ),
  18. 'show workflow state form' => array(
  19. 'title' => t('Show workflow state change on node view'),
  20. 'description' => t('Show workflow state change form on node viewing.'),
  21. ),
  22. );
  23. }
  24. /**
  25. * Implements hook_menu().
  26. */
  27. function workflow_menu() {
  28. $items['node/%node/workflow'] = array(
  29. 'title' => 'Workflow',
  30. 'type' => MENU_LOCAL_TASK,
  31. 'file' => 'workflow.pages.inc',
  32. 'access callback' => 'workflow_node_tab_access',
  33. 'access arguments' => array(1),
  34. 'page callback' => 'workflow_tab_page',
  35. 'page arguments' => array(1),
  36. 'weight' => 2,
  37. );
  38. return $items;
  39. }
  40. /**
  41. * Menu wild card loader: %workflow.
  42. * May be used by add on modules, such as workflow_admin_ui.
  43. */
  44. function workflow_load($wid) {
  45. return workflow_get_workflows_by_wid($wid);
  46. }
  47. /**
  48. * Implements hook_admin_paths_alter().
  49. * If node edits are done in admin mode, then workflow will be too.
  50. */
  51. function workflow_admin_paths_alter(&$paths) {
  52. if (isset($path['node/*/edit'])) {
  53. $path['node/*/workflow'] = $path['node/*/edit'];
  54. }
  55. }
  56. /**
  57. * Menu access control callback. Determine access to Workflow tab.
  58. */
  59. function workflow_node_tab_access($node = NULL) {
  60. global $user;
  61. if ($workflow = workflow_get_workflow_type_map_by_type($node->type)) {
  62. if ($workflow = workflow_get_workflows_by_wid($workflow->wid)) {
  63. $roles = array_keys($user->roles);
  64. if ($node->uid == $user->uid) {
  65. $roles = array_merge(array('author'), $roles);
  66. }
  67. $allowed_roles = $workflow->tab_roles ? explode(',', $workflow->tab_roles) : array();
  68. if (user_access('administer nodes') || array_intersect($roles, $allowed_roles)) {
  69. return TRUE;
  70. }
  71. else {
  72. return FALSE;
  73. }
  74. }
  75. }
  76. return FALSE;
  77. }
  78. /**
  79. * Implements hook_theme().
  80. */
  81. function workflow_theme() {
  82. return array(
  83. 'workflow_history_table_row' => array(
  84. 'variables' => array(
  85. 'history' => NULL,
  86. 'old_state_name' => NULL,
  87. 'state_name' => NULL
  88. ),
  89. ),
  90. 'workflow_history_table' => array(
  91. 'variables' => array(
  92. 'rows' => array(),
  93. 'footer' => NULL,
  94. ),
  95. ),
  96. 'workflow_history_current_state' => array(
  97. 'variables' => array(
  98. 'state_name' => NULL,
  99. 'state_system_name' => NULL,
  100. 'sid' => NULL,
  101. ),
  102. ),
  103. 'workflow_current_state' => array(
  104. 'variables' => array(
  105. 'state' => NULL,
  106. 'state_system_name' => NULL,
  107. 'sid' => NULL,
  108. ),
  109. ),
  110. 'workflow_deleted_state' => array(
  111. 'variables' => array(
  112. 'state_name' => NULL,
  113. 'state_system_name' => NULL,
  114. 'sid' => NULL,
  115. ),
  116. ),
  117. );
  118. }
  119. /**
  120. * Implements hook_cron().
  121. */
  122. function workflow_cron() {
  123. $clear_cache = FALSE;
  124. // If the time now is greater than the time to execute a transition, do it.
  125. foreach (workflow_get_workflow_scheduled_transition_by_between() as $row) {
  126. $node = node_load($row->nid);
  127. // If they didn't give a comment, create one.
  128. if (empty($row->comment)) {
  129. $row->comment = t('Scheduled by user @uid.', array('@uid' => $row->uid));
  130. }
  131. // Make sure transition is still valid; i.e., the node is
  132. // still in the state it was when the transition was scheduled.
  133. if ($node->workflow == $row->old_sid) {
  134. // Save the user who wanted this.
  135. $node->workflow_uid = $row->uid;
  136. // Do transition. Force it because user who scheduled was checked.
  137. workflow_execute_transition($node, $row->sid, $row->comment, TRUE);
  138. watchdog('content', '%type: scheduled transition of %title.',
  139. array('%type' => t($node->type), '%title' => $node->title), WATCHDOG_NOTICE, l(t('view'), 'node/' . $node->nid));
  140. $clear_cache = TRUE;
  141. }
  142. else {
  143. // Node is not in the same state it was when the transition
  144. // was scheduled. Defer to the node's current state and
  145. // abandon the scheduled transition.
  146. workflow_delete_workflow_scheduled_transition_by_nid($node->nid);
  147. }
  148. }
  149. if ($clear_cache) {
  150. // Clear the cache so that if the transition resulted in a node
  151. // being published, the anonymous user can see it.
  152. cache_clear_all();
  153. }
  154. }
  155. /**
  156. * Implements hook_user_delete().
  157. */
  158. function workflow_user_delete($account) {
  159. // Update tables for deleted account, move account to user 0 (anon.)
  160. // ALERT: This may cause previously non-anon posts to suddenly be accessible to anon.
  161. workflow_update_workflow_node_uid($account->uid, 0);
  162. workflow_update_workflow_node_history_uid($account->uid, 0);
  163. }
  164. /**
  165. * Node specific functions, remants of nodeapi.
  166. */
  167. /**
  168. * Implements hook_node_load().
  169. * @TODO: Consider replacing with hook_entity_load().
  170. */
  171. function workflow_node_load($nodes, $types) {
  172. // Get which types have workflows associated with them.
  173. $workflow_types = array_filter(workflow_get_workflow_type_map());
  174. foreach ($nodes as $node) {
  175. // If it's not a workflow type, quit immediately.
  176. if (!array_key_exists($node->type, $workflow_types)) {
  177. continue;
  178. }
  179. // ALERT: With the upgrade to Drupal 7, values stored on the node as _workflow_x
  180. // have been standardized to workflow_x, dropping the initial underscore.
  181. // Module maintainers integrating with workflow should keep that in mind.
  182. $last_history = workflow_get_recent_node_history($node->nid);
  183. // Nodes that existed before the workflow don't have any history.
  184. if ($last_history === FALSE) {
  185. $node->workflow = workflow_get_creation_state_by_type($node->type);
  186. $node->workflow_stamp = $node->created;
  187. continue;
  188. }
  189. else {
  190. $node->workflow = $last_history->sid;
  191. $node->workflow_stamp = $last_history->stamp;
  192. $node->workflow_state_name = $last_history->state_name;
  193. }
  194. if ($workflow_map = workflow_get_workflow_type_map_by_type($node->type)) {
  195. $node->workflow_wid = $workflow_map->wid;
  196. }
  197. // Add scheduling information.
  198. // Technically you could have more than one scheduled, but this will only add the soonest one.
  199. foreach (workflow_get_workflow_scheduled_transition_by_nid($node->nid) as $row) {
  200. $node->workflow_scheduled_sid = $row->sid;
  201. $node->workflow_scheduled_timestamp = $row->scheduled;
  202. $node->workflow_scheduled_comment = $row->comment;
  203. break;
  204. }
  205. }
  206. }
  207. /**
  208. * Implements hook_node_insert().
  209. */
  210. function workflow_node_insert($node) {
  211. // Skip if there are no workflows.
  212. if ($workflow = workflow_get_workflow_type_map_by_type($node->type)) {
  213. // If the state is not specified, use first valid state.
  214. // For example, a new node must move from (creation) to some
  215. // initial state.
  216. if (empty($node->workflow)) {
  217. $choices = workflow_field_choices($node);
  218. if ($choices) {
  219. $keys = array_keys($choices);
  220. $sid = array_shift($keys);
  221. }
  222. else {
  223. // This should never happen, but it did during testing.
  224. drupal_set_message(t('There are no workflow states available. Please notify your site administrator.'), 'error');
  225. return;
  226. }
  227. }
  228. if (!isset($sid)) {
  229. $sid = $node->workflow;
  230. }
  231. // And make the transition.
  232. workflow_transition($node, $sid);
  233. }
  234. }
  235. /**
  236. * Implements hook_node_update().
  237. */
  238. function workflow_node_update($node) {
  239. // Skip if there are no workflows.
  240. if ($workflow = workflow_get_workflow_type_map_by_type($node->type)) {
  241. // Get new state from value of workflow form field, stored in $node->workflow.
  242. if (!isset($sid)) {
  243. $sid = $node->workflow;
  244. }
  245. workflow_transition($node, $sid);
  246. }
  247. }
  248. /**
  249. * Implements hook_node_delete().
  250. */
  251. function workflow_node_delete($node) {
  252. $node->workflow_stamp = REQUEST_TIME;
  253. // Delete the association of node to state.
  254. workflow_delete_workflow_node_by_nid($node->nid);
  255. if (!empty($node->_worfklow)) {
  256. global $user;
  257. $data = array(
  258. 'nid' => $node->nid,
  259. 'old_sid' => $node->workflow,
  260. 'sid' => WORKFLOW_DELETION,
  261. 'uid' => $user->uid,
  262. 'stamp' => $node->workflow_stamp,
  263. 'comment' => t('Node deleted'),
  264. );
  265. workflow_insert_workflow_node_history($data);
  266. }
  267. // Delete any scheduled transitions for this node.
  268. workflow_delete_workflow_scheduled_transition_by_nid($node->nid);
  269. }
  270. /**
  271. * Implements hook_comment_insert().
  272. */
  273. function workflow_comment_insert($comment) {
  274. workflow_comment_update($comment);
  275. }
  276. /**
  277. * Implements hook_comment_update().
  278. */
  279. function workflow_comment_update($comment) {
  280. if (isset($comment->workflow)) {
  281. $node = node_load($comment->nid);
  282. $sid = $comment->workflow;
  283. $node->workflow_comment = $comment->workflow_comment;
  284. if (isset($comment->workflow_scheduled)) {
  285. $node->workflow_scheduled = $comment->workflow_scheduled;
  286. }
  287. if (isset($comment->workflow_scheduled_date)) {
  288. $node->workflow_scheduled_date = $comment->workflow_scheduled_date;
  289. }
  290. if (isset($comment->workflow_scheduled_hour)) {
  291. $node->workflow_scheduled_hour = $comment->workflow_scheduled_hour;
  292. }
  293. workflow_transition($node, $sid);
  294. }
  295. }
  296. /**
  297. * Implements hook_features_api().
  298. */
  299. function workflow_features_api() {
  300. return array(
  301. 'workflow' => array(
  302. 'name' => t('Workflow'),
  303. 'file' => drupal_get_path('module', 'workflow') . '/workflow.features.inc',
  304. 'default_hook' => 'workflow_default_workflows',
  305. 'feature_source' => TRUE,
  306. ),
  307. );
  308. }
  309. /**
  310. * Business related functions, the API.
  311. */
  312. /**
  313. * Validate target state and either execute a transition immediately or schedule
  314. * a transition to be executed later by cron.
  315. *
  316. * @param $node
  317. * @param $sid
  318. * An integer; the target state ID.
  319. * @param $false
  320. * Allows bypassing permissions, primarily for Rules.
  321. */
  322. function workflow_transition($node, $sid, $force = FALSE) {
  323. global $user;
  324. if ($force) {
  325. $choices = workflow_get_other_states_by_sid($sid);
  326. }
  327. else {
  328. $choices = workflow_field_choices($node, $force);
  329. }
  330. // Make sure new state is a valid choice.
  331. if (array_key_exists($sid, $choices)) {
  332. $node->workflow_scheduled = isset($node->workflow_scheduled) ? $node->workflow_scheduled : FALSE;
  333. if (!$node->workflow_scheduled) {
  334. // It's an immediate change. Do the transition.
  335. workflow_execute_transition($node, $sid, (isset($node->workflow_comment) ? $node->workflow_comment : NULL), $force);
  336. }
  337. else {
  338. // Schedule the the time to change the state.
  339. $old_sid = workflow_node_current_state($node);
  340. if ($node->workflow_scheduled_date['day'] < 10) {
  341. $node->workflow_scheduled_date['day'] = '0' .
  342. $node->workflow_scheduled_date['day'];
  343. }
  344. if ($node->workflow_scheduled_date['month'] < 10) {
  345. $node->workflow_scheduled_date['month'] = '0' .
  346. $node->workflow_scheduled_date['month'];
  347. }
  348. if (!isset($node->workflow_scheduled_hour)) {
  349. $node->workflow_scheduled_hour = '00:00';
  350. }
  351. $scheduled = $node->workflow_scheduled_date['year']
  352. . $node->workflow_scheduled_date['month']
  353. . $node->workflow_scheduled_date['day']
  354. . ' '
  355. . $node->workflow_scheduled_hour
  356. . ' '
  357. . $node->workflow_scheduled_timezone
  358. ;
  359. if ($scheduled = strtotime($scheduled)) {
  360. // Clear previous entries and insert.
  361. $data = array(
  362. 'nid' => $node->nid,
  363. 'old_sid' => $old_sid,
  364. 'sid' => $sid,
  365. 'uid' => $user->uid,
  366. 'scheduled' => $scheduled,
  367. 'comment' => $node->workflow_comment,
  368. );
  369. workflow_insert_workflow_scheduled_transition($data);
  370. // Get name of state.
  371. if ($state = workflow_get_workflow_states_by_sid($sid)) {
  372. $t_args = array(
  373. '@node_title' => $node->title,
  374. '%state_name' => t($state->state),
  375. '%scheduled_date' => format_date($scheduled),
  376. );
  377. watchdog('workflow', '@node_title scheduled for state change to %state_name on %scheduled_date', $t_args,
  378. WATCHDOG_NOTICE, l('view', 'node/' . $node->nid . '/workflow'));
  379. drupal_set_message(t('@node_title is scheduled for state change to %state_name on %scheduled_date',
  380. $t_args));
  381. }
  382. }
  383. }
  384. }
  385. }
  386. /**
  387. * Theme the current workflow state.
  388. */
  389. function theme_workflow_current_state($variables) {
  390. $state = $variables['state'];
  391. return '<div class="workflow-current-state '
  392. . 'workflow-current-sid-' . intval($variables['sid']) . ' '
  393. . drupal_html_class($state)
  394. . '">'
  395. . t('Current state: <span class="state">@state</span>', array('@state' => $state))
  396. . '</div>';
  397. }
  398. /**
  399. * Implements hook_node_view().
  400. *
  401. * @param object $node.
  402. * @param string $view_mode.
  403. * @param string $langcode.
  404. *
  405. * @return renderable content in $node->content array.
  406. */
  407. function workflow_node_view($node, $view_mode, $langcode) {
  408. if (!user_access('show workflow state form') || $node->status == 0) {
  409. return;
  410. }
  411. $workflow = workflow_get_workflow_type_map_by_type($node->type);
  412. if (!$workflow) {
  413. return;
  414. }
  415. $states = array();
  416. $state_system_names = array();
  417. foreach (workflow_get_workflow_states() as $data) {
  418. $states[$data->sid] = check_plain(t($data->state));
  419. $state_system_names[$data->sid] = check_plain($data->state);
  420. }
  421. $current = workflow_node_current_state($node);
  422. // Show current state at the top of the node display.
  423. $markup = theme('workflow_current_state', array('state' => $states[$current], 'state_system_name' => $state_system_names[$current], 'sid' => $current));
  424. $node->content['workflow_current_state'] = array(
  425. '#markup' => $markup,
  426. '#weight' => -99,
  427. );
  428. // If we are at the terminal state, then don't show the change form.
  429. $choices = workflow_field_choices($node);
  430. if (!$choices) {
  431. return;
  432. }
  433. if (count($choices) == 1) {
  434. if ($current == key($choices)) {
  435. return;
  436. }
  437. }
  438. // Show state change form at the bottom of the node display.
  439. module_load_include('inc', 'workflow', 'workflow.pages');
  440. // By including the nid in the form id.
  441. $form = drupal_get_form("workflow_tab_form_$node->nid", $node, $workflow->wid, $states, $current);
  442. $form['#weight'] = 99;
  443. $node->content['workflow'] = $form;
  444. }
  445. /**
  446. * Implements hook_field_extra_fields().
  447. */
  448. function workflow_field_extra_fields() {
  449. $extra = array();
  450. // Get all workflows by content types.
  451. $types = array_filter(workflow_get_workflow_type_map());
  452. // Add the extra fields to each content type that has a workflow.
  453. foreach ($types as $type => $wid) {
  454. $extra['node'][$type] = array(
  455. 'form' => array(
  456. 'workflow' => array(
  457. 'label' => t('Workflow'),
  458. 'description' => t('Workflow module form'),
  459. 'weight' => 99, // Default to bottom.
  460. ),
  461. ),
  462. 'display' => array(
  463. 'workflow_current_state' => array(
  464. 'label' => t('Workflow: Current State'),
  465. 'description' => t('Current workflow state'),
  466. 'weight' => -99, // Default to top.
  467. ),
  468. 'workflow' => array(
  469. 'label' => t('Workflow: State Change Form'),
  470. 'description' => t('The form for controlling workflow state changes.'),
  471. 'weight' => 99, // Default to bottom.
  472. ),
  473. ),
  474. );
  475. }
  476. return $extra;
  477. }
  478. /**
  479. * Implements hook_forms().
  480. *
  481. * Allows the workflow tab form to be repeated multiple times on a page.
  482. * See http://drupal.org/node/1970846.
  483. */
  484. function workflow_forms($form_id, $args) {
  485. $forms = array();
  486. if (strpos($form_id, 'workflow_tab_form_') !== FALSE) {
  487. $forms[$form_id] = array('callback' => 'workflow_tab_form');
  488. }
  489. return $forms;
  490. }
  491. /**
  492. * Form builder. Add form widgets for workflow change to $form.
  493. *
  494. * This builder is factored out of workflow_form_alter() because
  495. * it is also used on the Workflow tab.
  496. *
  497. * @param $form
  498. * An existing form definition array.
  499. * @param $name
  500. * The name of the workflow.
  501. * @param $current
  502. * The state ID of the current state, used as the default value.
  503. * @param $choices
  504. * An array of possible target states.
  505. */
  506. function workflow_node_form(&$form, $form_state, $title, $name, $current, $choices, $timestamp = NULL, $comment = NULL) {
  507. // Give form_alters the chance to see the parameters.
  508. $form['#wf_options'] = array(
  509. 'title' => $title,
  510. 'name' => $name,
  511. 'current' => $current,
  512. 'choices' => $choices,
  513. 'timestamp' => $timestamp,
  514. 'comment' => $comment,
  515. );
  516. if (count($choices) == 1) {
  517. // There is no need to show the single choice.
  518. // A form choice would be an array with the key of the state.
  519. $state = key($choices);
  520. $form['workflow'][$name] = array(
  521. '#type' => 'value',
  522. '#options' => array($state => $state),
  523. );
  524. }
  525. else {
  526. $form['workflow'] = array(
  527. '#type' => 'container',
  528. '#attributes' => array('class' => array('workflow-form-container')),
  529. );
  530. // Note: title needs to be sanitized before calling this function.
  531. $form['workflow'][$name] = array(
  532. '#type' => 'radios',
  533. '#title' => !empty($form['#wf']->options['name_as_title']) ? $title : '',
  534. '#options' => $choices,
  535. '#name' => $name,
  536. '#parents' => array('workflow'),
  537. '#default_value' => $current,
  538. );
  539. }
  540. // Display scheduling form only if a node is being edited and user has
  541. // permission. State change cannot be scheduled at node creation because
  542. // that leaves the node in the (creation) state.
  543. if (!(arg(0) == 'node' && arg(1) == 'add') && user_access('schedule workflow transitions')) {
  544. $scheduled = $timestamp ? 1 : 0;
  545. $timestamp = $scheduled ? $timestamp : REQUEST_TIME;
  546. $form['workflow']['workflow_scheduled'] = array(
  547. '#type' => 'radios',
  548. '#title' => t('Schedule'),
  549. '#options' => array(
  550. t('Immediately'),
  551. t('Schedule for state change'),
  552. ),
  553. '#default_value' => isset($form_state['values']['workflow_scheduled']) ? $form_state['values']['workflow_scheduled'] : $scheduled,
  554. );
  555. $form['workflow']['workflow_scheduled_date_time'] = array(
  556. '#type' => 'fieldset',
  557. '#title' => t('At'),
  558. '#prefix' => '<div style="margin-left: 1em;">',
  559. '#suffix' => '</div>',
  560. '#states' => array(
  561. 'visible' => array(':input[name="workflow_scheduled"]' => array('value' => 1)),
  562. 'invisible' => array(':input[name="workflow_scheduled"]' => array('value' => 0)),
  563. ),
  564. );
  565. $form['workflow']['workflow_scheduled_date_time']['workflow_scheduled_date'] = array(
  566. '#type' => 'date',
  567. '#default_value' => array(
  568. 'day' => isset($form_state['values']['workflow_scheduled_date']['day']) ? $form_state['values']['workflow_scheduled_date']['day'] : format_date($timestamp, 'custom', 'j'),
  569. 'month' => isset($form_state['values']['workflow_scheduled_date']['month']) ? $form_state['values']['workflow_scheduled_date']['month'] :format_date($timestamp, 'custom', 'n'),
  570. 'year' => isset($form_state['values']['workflow_scheduled_date']['year']) ? $form_state['values']['workflow_scheduled_date']['year'] : format_date($timestamp, 'custom', 'Y')
  571. ),
  572. );
  573. $hours = format_date($timestamp, 'custom', 'H:i');
  574. $form['workflow']['workflow_scheduled_date_time']['workflow_scheduled_hour'] = array(
  575. '#type' => 'textfield',
  576. '#description' => t('Please enter a time in 24 hour (eg. HH:MM) format.
  577. If no time is included, the default will be midnight on the specified date.
  578. The current time is: @time', array('@time' => $hours)),
  579. '#default_value' => $scheduled ?
  580. (isset($form_state['values']['workflow_scheduled_hour']) ?
  581. $form_state['values']['workflow_scheduled_hour'] : $hours) : '00:00',
  582. );
  583. global $user;
  584. if (variable_get('configurable_timezones', 1) && $user->uid && drupal_strlen($user->timezone)) {
  585. $timezone = $user->timezone;
  586. }
  587. else {
  588. $timezone = variable_get('date_default_timezone', 0);
  589. }
  590. $timezones = drupal_map_assoc(timezone_identifiers_list());
  591. $form['workflow']['workflow_scheduled_date_time']['workflow_scheduled_timezone'] = array(
  592. '#type' => 'select',
  593. '#options' => $timezones,
  594. '#title' => t('Time zone'),
  595. '#default_value' => array($timezone => $timezone),
  596. );
  597. }
  598. $determiner = isset($form['#tab']) ? 'comment_log_tab' : 'comment_log_node';
  599. $form['workflow']['workflow_comment'] = array(
  600. '#type' => $form['#wf']->options[$determiner] ? 'textarea': 'hidden',
  601. '#title' => t('Workflow comment'),
  602. '#description' => t('A comment to put in the workflow log.'),
  603. '#default_value' => $comment,
  604. '#rows' => 2,
  605. );
  606. }
  607. /**
  608. * Implements hook_form_alter().
  609. *
  610. * @param object &$node
  611. * @return array
  612. */
  613. function workflow_form_alter(&$form, &$form_state, $form_id) {
  614. // Ignore all forms except comment forms and node editing forms.
  615. if ((isset($form['#node']) && $form_id == 'comment_node_' . $form['#node']->type . '_form')
  616. || (isset($form['#node']->type) && isset($form['#node']) && $form['#node']->type . '_node_form' == $form_id)) {
  617. // Set node to #node if available or load from nid value.
  618. $node = isset($form['#node']) ? $form['#node'] : node_load($form['nid']['#value']);
  619. $type = $node->type;
  620. $workflow_entities = variable_get('workflow_' . $type, array());
  621. // Abort if the entity type of the form is not in the list that the user
  622. // wants to display the workflow form on.
  623. if (!in_array($form['#entity_type'], $workflow_entities)) {
  624. return;
  625. }
  626. if ($workflow = workflow_get_workflow_type_map_by_type($node->type)) {
  627. $choices = workflow_field_choices($node);
  628. $workflow = workflow_get_workflows_by_wid($workflow->wid);
  629. $states = workflow_get_workflow_states_by_wid($workflow->wid);
  630. // If this is a preview, the current state should come from
  631. // the form values, not the node, as the user may have changed
  632. // the state.
  633. $current = isset($form_state['values']['workflow']) ?
  634. $form_state['values']['workflow'] : workflow_node_current_state($node);
  635. $min = 2; // Our current status, and our new status.
  636. foreach ($states as $state) {
  637. if ($state->sid == $current) {
  638. $min = $state->sysid == WORKFLOW_CREATION ? 1 : 2;
  639. }
  640. }
  641. // Stop if user has no new target state(s) to choose.
  642. if (count($choices) < $min) {
  643. return;
  644. }
  645. $form['#wf'] = $workflow;
  646. $name = $workflow->name;
  647. // If the current node state is not one of the choices, pick first choice.
  648. // We know all states in $choices are states that user has permission to
  649. // go to because workflow_field_choices() has already checked that.
  650. if (!isset($choices[$current])) {
  651. $array = array_keys($choices);
  652. $current = $array[0];
  653. }
  654. if (sizeof($choices) > 1) {
  655. $form['workflow'] = array(
  656. '#type' => 'fieldset',
  657. '#title' => t('@name', array('@name' => $name)),
  658. '#collapsible' => TRUE,
  659. '#collapsed' => FALSE,
  660. '#weight' => 10,
  661. );
  662. }
  663. $timestamp = NULL;
  664. $comment = '';
  665. // See if scheduling information is present.
  666. if (isset($node->workflow_scheduled_timestamp) && isset($node->workflow_scheduled_sid)) {
  667. // The default value should be the upcoming sid.
  668. $current = $node->workflow_scheduled_sid;
  669. $timestamp = $node->workflow_scheduled_timestamp;
  670. $comment = $node->workflow_scheduled_comment;
  671. }
  672. if (isset($form_state['values']['workflow_comment'])) {
  673. $comment = $form_state['values']['workflow_comment'];
  674. }
  675. workflow_node_form($form, $form_state, t('Change @name state', array('@name' => $name)), $name, $current, $choices, $timestamp, $comment);
  676. }
  677. }
  678. }
  679. /**
  680. * Execute a transition (change state of a node).
  681. *
  682. * @param $node
  683. * @param $sid
  684. * Target state ID.
  685. * @param $comment
  686. * A comment for the node's workflow history.
  687. * @param $force
  688. * If set to TRUE, workflow permissions will be ignored.
  689. *
  690. * @return int
  691. * ID of new state.
  692. */
  693. function workflow_execute_transition($node, $sid, $comment = NULL, $force = FALSE) {
  694. global $user;
  695. // I think this happens because of Workflow Extensions;
  696. // it seems to be okay to ignore it.
  697. if (empty($node->nid)) {
  698. return;
  699. }
  700. $old_sid = workflow_node_current_state($node);
  701. if (!$force) {
  702. // Make sure this transition is allowed.
  703. $result = module_invoke_all('workflow', 'transition permitted', $sid, $old_sid, $node);
  704. // Did anybody veto this choice?
  705. if (in_array(FALSE, $result)) {
  706. // If vetoed, quit.
  707. return;
  708. }
  709. }
  710. // Let other modules modify the comment.
  711. $context = array(
  712. 'node' => $node,
  713. 'sid' => $sid,
  714. 'old_sid' => $old_sid,
  715. 'uid' => (isset($node->workflow_uid) ? $node->workflow_uid : $user->uid),
  716. );
  717. drupal_alter('workflow_comment', $comment, $context);
  718. if ($old_sid == $sid) {
  719. // Stop if not going to a different state.
  720. // Write comment into history though.
  721. if ($comment && empty($node->workflow_scheduled_comment)) {
  722. $node->workflow_stamp = REQUEST_TIME;
  723. workflow_update_workflow_node_stamp($node->nid, $node->workflow_stamp);
  724. $result = module_invoke_all('workflow', 'transition pre', $old_sid, $sid, $node, $force);
  725. $data = array(
  726. 'nid' => $node->nid,
  727. 'old_sid' => (isset($node->workflow) ? $node->workflow : $old_sid),
  728. 'sid' => $sid,
  729. 'uid' => (isset($node->workflow_uid) ? $node->workflow_uid : $user->uid),
  730. 'stamp' => $node->workflow_stamp,
  731. 'comment' => $comment,
  732. );
  733. workflow_insert_workflow_node_history($data);
  734. unset($node->workflow_comment);
  735. $result = module_invoke_all('workflow', 'transition post', $old_sid, $sid, $node, $force);
  736. }
  737. return;
  738. }
  739. $transition = workflow_get_workflow_transitions_by_sid_target_sid($old_sid, $sid);
  740. if (!$transition && !$force) {
  741. watchdog('workflow', 'Attempt to go to nonexistent transition (from %old to %new)', array('%old' => $old_sid, '%new' => $sid, WATCHDOG_ERROR));
  742. return;
  743. }
  744. // Make sure this transition is valid and allowed for the current user.
  745. // Check allowability of state change if user is not superuser (might be cron).
  746. if (($user->uid != 1) && !$force) {
  747. if (!workflow_transition_allowed($transition->tid, array_merge(array_keys($user->roles), array('author')))) {
  748. watchdog('workflow', 'User %user not allowed to go from state %old to %new',
  749. array('%user' => $user->name, '%old' => $old_sid, '%new' => $sid, WATCHDOG_NOTICE));
  750. return;
  751. }
  752. }
  753. // Invoke a callback indicating a transition is about to occur. Modules
  754. // may veto the transition by returning FALSE.
  755. $result = module_invoke_all('workflow', 'transition pre', $old_sid, $sid, $node, $force);
  756. // Stop if a module says so.
  757. if (in_array(FALSE, $result)) {
  758. watchdog('workflow', 'Transition vetoed by module.');
  759. return;
  760. }
  761. // If the node does not have an existing $node->workflow property, save the $old_sid there so it can be logged.
  762. if (!isset($node->workflow)) {
  763. $node->workflow = $old_sid;
  764. }
  765. // Change the state.
  766. $data = array(
  767. 'nid' => $node->nid,
  768. 'sid' => $sid,
  769. 'uid' => (isset($node->workflow_uid) ? $node->workflow_uid : $user->uid),
  770. 'stamp' => REQUEST_TIME,
  771. );
  772. // Workflow_update_workflow_node places a history comment as well.
  773. workflow_update_workflow_node($data, $old_sid, $comment);
  774. $node->workflow = $sid;
  775. // Register state change with watchdog.
  776. $type = node_type_get_name($node->type);
  777. if ($state = workflow_get_workflow_states_by_sid($sid)) {
  778. $workflow = workflow_get_workflows_by_wid($state->wid);
  779. if ($workflow->options['watchdog_log']) {
  780. watchdog('workflow', 'State of @type %node_title set to %state_name',
  781. array(
  782. '@type' => $type,
  783. '%node_title' => $node->title,
  784. '%state_name' => t($state->state)),
  785. WATCHDOG_NOTICE, l('view', 'node/' . $node->nid));
  786. }
  787. }
  788. // Notify modules that transition has occurred. Action triggers should take place in response to this callback, not the previous one.
  789. module_invoke_all('workflow', 'transition post', $old_sid, $sid, $node, $force);
  790. // Clear any references in the scheduled listing.
  791. workflow_delete_workflow_scheduled_transition_by_nid($node->nid);
  792. return $sid;
  793. }
  794. /**
  795. * Get the states current user can move to for a given node.
  796. *
  797. * @param object $node
  798. * The node to check.
  799. * @return
  800. * Array of transitions.
  801. */
  802. function workflow_field_choices($node, $force = FALSE) {
  803. global $user;
  804. $choices = FALSE;
  805. if ($workflow = workflow_get_workflow_type_map_by_type($node->type)) {
  806. $roles = array_keys($user->roles);
  807. $current_sid = workflow_node_current_state($node);
  808. // If user is node author or this is a new page, give the authorship role.
  809. if (($user->uid == $node->uid && $node->uid > 0) || (arg(0) == 'node' && arg(1) == 'add')) {
  810. $roles += array('author' => 'author');
  811. }
  812. if ($user->uid == 1 || $force) {
  813. // Superuser is special. And Force allows Rules to cause transition.
  814. $roles = 'ALL';
  815. }
  816. // Workflow_allowable_transitions() does not return the entire transition row. Would like it to, but doesn't.
  817. // Instead it returns just the allowable data as:
  818. // [tid] => 1 [state_id] => 1 [state_name] => (creation) [state_weight] => -50
  819. $transitions = workflow_allowable_transitions($current_sid, 'to', $roles);
  820. // Include current state if it is not the (creation) state.
  821. foreach ($transitions as $transition) {
  822. if ($transition->sysid != WORKFLOW_CREATION && !$force) {
  823. // Invoke a callback indicating that we are collecting state choices. Modules
  824. // may veto a choice by returning FALSE. In this case, the choice is
  825. // never presented to the user.
  826. $result = module_invoke_all('workflow', 'transition permitted', $current_sid, $transition->state_id, $node);
  827. // Did anybody veto this choice?
  828. if (!in_array(FALSE, $result)) {
  829. // If not vetoed, add to list.
  830. $choices[$transition->state_id] = check_plain(t($transition->state_name));
  831. }
  832. }
  833. }
  834. }
  835. return $choices;
  836. }
  837. /**
  838. * Get the current state of a given node.
  839. *
  840. * @param $node
  841. * The node to check.
  842. * @return
  843. * The ID of the current state.
  844. */
  845. function workflow_node_current_state($node) {
  846. $sid = FALSE;
  847. $state = FALSE;
  848. // There is no nid when creating a node.
  849. if (!empty($node->nid)) {
  850. $state = workflow_get_workflow_node_by_nid($node->nid);
  851. if ($state) {
  852. $sid = $state->sid;
  853. }
  854. }
  855. if (!$state && !empty($node->type)) {
  856. // No current state. Use creation state.
  857. $sid = workflow_get_creation_state_by_type($node->type);
  858. }
  859. return $sid;
  860. }
  861. function workflow_node_previous_state($node) {
  862. $sid = FALSE;
  863. // There is no nid when creating a node.
  864. if (!empty($node->nid)) {
  865. $sids = array();
  866. $sid = -1;
  867. $last_history = workflow_get_recent_node_history($node->nid);
  868. $sid = !$last_history ? FALSE : $last_history->old_sid;
  869. }
  870. if (!$sid && !empty($node->type)) {
  871. // No current state. Use creation state.
  872. $sid = workflow_get_creation_state_by_type($node->type);
  873. }
  874. return $sid;
  875. }
  876. /**
  877. * See if a transition is allowed for a given role.
  878. *
  879. * @param int $tid
  880. * @param mixed $role
  881. * A single role (int or string 'author') or array of roles.
  882. * @return
  883. * TRUE if the role is allowed to do the transition.
  884. */
  885. function workflow_transition_allowed($tid, $role = NULL) {
  886. $transition = workflow_get_workflow_transitions_by_tid($tid);
  887. $allowed = $transition->roles;
  888. $allowed = explode(',', $allowed);
  889. if ($role) {
  890. if (!is_array($role)) {
  891. $role = array($role);
  892. }
  893. return array_intersect($role, $allowed) == TRUE;
  894. }
  895. }
  896. /**
  897. * Return the ID of the creation state for this workflow.
  898. *
  899. * @param $wid
  900. * The ID of the workflow.
  901. */
  902. function workflow_get_creation_state_by_wid($wid) {
  903. $options = array(':sysid' => WORKFLOW_CREATION, 'wid' => $wid);
  904. $result = workflow_get_workflow_states($options);
  905. return isset($result[0]->sid) ? $result[0]->sid : 0;
  906. }
  907. /**
  908. * Return the ID of the creation state given a content type.
  909. *
  910. * @param $type
  911. * The type of the content.
  912. */
  913. function workflow_get_creation_state_by_type($type) {
  914. static $sids = array();
  915. if (!isset($sids[$type])) {
  916. $options = array(':sysid' => WORKFLOW_CREATION, ':type' => $type);
  917. $query = "SELECT s.sid "
  918. . "FROM {workflow_type_map} m "
  919. . "INNER JOIN {workflow_states} s ON s.wid = m.wid "
  920. . "WHERE m.type = :type AND s.sysid = :sysid ";
  921. $result = db_query($query, $options);
  922. $sids[$type] = $result->fetchField();
  923. }
  924. return $sids[$type];
  925. }
  926. /**
  927. * DB functions. All SQL in workflow.module should be put into its own function and placed here.
  928. * This encourages good separation of code and reuse of SQL statements. It *also* makes it easy to
  929. * make schema updates and changes without rummaging through every single inch of code looking for SQL.
  930. * Sure it's a little type A, granted. But it's useful in the long run.
  931. */
  932. /**
  933. * Functions related to table workflows.
  934. */
  935. /**
  936. * Get all workflows.
  937. */
  938. function workflow_get_workflows() {
  939. $query = "SELECT w.wid, w.name, w.tab_roles, w.options, s.sid AS creation_state "
  940. . "FROM {workflows} w "
  941. . "INNER JOIN {workflow_states} s ON s.wid = w.wid "
  942. . "WHERE s.sysid = :sysid";
  943. ;
  944. $results = db_query($query, array(':sysid' => WORKFLOW_CREATION));
  945. $workflows = $results->fetchAll();
  946. foreach ($workflows as $index => $workflow) {
  947. $workflows[$index]->options = unserialize($workflows[$index]->options);
  948. }
  949. return $workflows;
  950. }
  951. /**
  952. * Get a specific workflow, wid is a unique ID.
  953. */
  954. function workflow_get_workflows_by_wid($wid, $reset = FALSE) {
  955. static $wids = array();
  956. if (empty($wid)) {
  957. return FALSE;
  958. }
  959. if ($reset) {
  960. $wids = array();
  961. }
  962. if (!isset($wids[$wid])) {
  963. $query = "SELECT w.wid, w.name, w.tab_roles, w.options, s.sid AS creation_state "
  964. . "FROM {workflows} w "
  965. . "INNER JOIN {workflow_states} s ON s.wid = w.wid "
  966. . "WHERE w.wid = :wid AND s.sysid = :sysid";
  967. $workflow = db_query($query, array(':wid' => $wid, ':sysid' => WORKFLOW_CREATION))->fetchObject();
  968. if ($workflow) {
  969. $workflow->options = unserialize($workflow->options);
  970. }
  971. $wids[$wid] = $workflow;
  972. }
  973. return $wids[$wid];
  974. }
  975. /**
  976. * Get a specific workflow, name is a unique ID.
  977. */
  978. function workflow_get_workflows_by_name($name, $unserialize_options = FALSE) {
  979. $results = db_query('SELECT wid, name, tab_roles, options FROM {workflows} WHERE name = :name',
  980. array(':name' => $name));
  981. if ($workflow = $results->fetchObject()) {
  982. // This is only called by CRUD functions in workflow.features.inc
  983. // More than likely in prep for an import / export action.
  984. // Therefore we don't want to fiddle with the response.
  985. if ($unserialize_options) {
  986. $workflow->options = unserialize($workflow->options);
  987. }
  988. return $workflow;
  989. }
  990. return FALSE;
  991. }
  992. /**
  993. * Given a wid, delete the workflow and its stuff.
  994. *
  995. * @TODO: This should probably move to workflow_admin_ui.
  996. */
  997. function workflow_delete_workflows_by_wid($wid) {
  998. // Notify any interested modules before we delete, in case there's data needed.
  999. module_invoke_all('workflow', 'workflow delete', $wid, NULL, NULL, FALSE);
  1000. // Delete associated state (also deletes any associated transitions).
  1001. foreach (workflow_get_workflow_states_by_wid($wid) as $data) {
  1002. workflow_delete_workflow_states_by_sid($data->sid);
  1003. }
  1004. // Delete type map.
  1005. workflow_delete_workflow_type_map_by_wid($wid);
  1006. // Delete the workflow.
  1007. db_delete('workflows')->condition('wid', $wid)->execute();
  1008. }
  1009. /**
  1010. * Given information, update or insert a new workflow. Returns data by ref. (like node_save).
  1011. *
  1012. * @TODO: This should probably move to workflow_admin_ui.
  1013. */
  1014. function workflow_update_workflows(&$data, $create_creation_state = TRUE) {
  1015. $data = (object) $data;
  1016. if (isset($data->tab_roles) && is_array($data->tab_roles)) {
  1017. $data->tab_roles = implode(',', $data->tab_roles);
  1018. }
  1019. if (isset($data->wid) && count(workflow_get_workflows_by_wid($data->wid)) > 0) {
  1020. drupal_write_record('workflows', $data, 'wid');
  1021. }
  1022. else {
  1023. drupal_write_record('workflows', $data);
  1024. if ($create_creation_state) {
  1025. $state_data = array(
  1026. 'wid' => $data->wid,
  1027. 'state' => t('(creation)'),
  1028. 'sysid' => WORKFLOW_CREATION,
  1029. 'weight' => WORKFLOW_CREATION_DEFAULT_WEIGHT,
  1030. );
  1031. workflow_update_workflow_states($state_data);
  1032. // @TODO consider adding state data to return here as part of workflow data structure.
  1033. // That way we could past sructs and transitions around as a data object as a whole.
  1034. // Might make clone easier, but it might be a little hefty for our needs?
  1035. }
  1036. }
  1037. }
  1038. /**
  1039. * Functions related to table workflow_type_map.
  1040. */
  1041. /**
  1042. * Get all workflow_type_map.
  1043. */
  1044. function workflow_get_workflow_type_map() {
  1045. $results = db_query('SELECT type, wid FROM {workflow_type_map}');
  1046. return $results->fetchAllKeyed();
  1047. }
  1048. /**
  1049. * Get workflow_type_map for a type. On no record, FALSE is returned.
  1050. * Currently this is a unique result but requests have been made to allow a node to have multiple
  1051. * workflows. This is trickier than it sounds as a lot of our processing code will have to be
  1052. * tweaked to account for multiple results.
  1053. * ALERT: If a node type is *not* mapped to a workflow it will be listed as wid 0.
  1054. * Hence, we filter out the non-mapped results.
  1055. */
  1056. function workflow_get_workflow_type_map_by_type($type) {
  1057. static $map = array();
  1058. if (!isset($map[$type])) {
  1059. $results = db_query('SELECT type, wid FROM {workflow_type_map} WHERE type = :type AND wid <> 0',
  1060. array(':type' => $type));
  1061. $map[$type] = $results->fetchObject();
  1062. }
  1063. return $map[$type];
  1064. }
  1065. /**
  1066. * Given a wid, find all node types mapped to it.
  1067. */
  1068. function workflow_get_workflow_type_map_by_wid($wid) {
  1069. static $map = array();
  1070. if (!isset($map[$wid])) {
  1071. $results = db_query('SELECT type, wid FROM {workflow_type_map} WHERE wid = :wid',
  1072. array(':wid' => $wid));
  1073. $map[$wid] = $results->fetchAll();
  1074. }
  1075. return $map[$wid];
  1076. }
  1077. /**
  1078. * Delete all type maps.
  1079. * @TODO: why is this here instead of the admin_ui?
  1080. */
  1081. function workflow_delete_workflow_type_map_all() {
  1082. return db_delete('workflow_type_map')->execute();
  1083. }
  1084. /**
  1085. * Given a wid, delete the map for that workflow.
  1086. */
  1087. function workflow_delete_workflow_type_map_by_wid($wid) {
  1088. return db_delete('workflow_type_map')->condition('wid', $wid)->execute();
  1089. }
  1090. /**
  1091. * Given a type, delete the map for that workflow.
  1092. */
  1093. function workflow_delete_workflow_type_map_by_type($type) {
  1094. return db_delete('workflow_type_map')->condition('type', $type)->execute();
  1095. }
  1096. /**
  1097. * Given information, insert a new workflow_type_map. Returns data by ref. (like node_save).
  1098. * @TODO: why is this here instead of the admin_ui?
  1099. */
  1100. function workflow_insert_workflow_type_map(&$data) {
  1101. $data = (object) $data;
  1102. // Be sure we have a clean insert. There should never be more than one map for a type.
  1103. if (isset($data->type)) {
  1104. workflow_delete_workflow_type_map_by_type($data->type);
  1105. }
  1106. drupal_write_record('workflow_type_map', $data);
  1107. }
  1108. /**
  1109. * Functions related to table workflow_transitions.
  1110. */
  1111. /**
  1112. * Given a wid get the transitions.
  1113. */
  1114. function workflow_get_workflow_transitions_by_wid($wid) {
  1115. static $transitions;
  1116. if (!isset($transitions[$wid])) {
  1117. $query = 'SELECT t.tid, t.sid, t.target_sid, t.roles, s1.wid '
  1118. . 'FROM {workflow_transitions} t '
  1119. . 'INNER JOIN {workflow_states} s1 ON t.sid=s1.sid '
  1120. . 'INNER JOIN {workflow_states} s2 ON t.target_sid=s2.sid '
  1121. . 'WHERE s1.wid = :wid AND s2.wid = :wid';
  1122. $transitions[$wid] = db_query('SELECT t.*, s1.wid FROM {workflow_transitions} AS t INNER JOIN {workflow_states} AS s1 ON t.sid=s1.sid INNER JOIN {workflow_states} AS s2 ON t.target_sid=s2.sid WHERE s1.wid = :wid AND s2.wid = :wid',
  1123. array(':wid' => $wid))->fetchAll();
  1124. }
  1125. return $transitions[$wid];
  1126. }
  1127. /**
  1128. * Given a tid, get the transition. It is a unique object, only one return.
  1129. */
  1130. function workflow_get_workflow_transitions_by_tid($tid) {
  1131. static $transitions;
  1132. if (!isset($transitions[$tid])) {
  1133. $transitions[$tid] = db_query('SELECT tid, sid, target_sid, roles FROM {workflow_transitions} WHERE tid = :tid',
  1134. array(':tid' => $tid))->fetchObject();
  1135. }
  1136. return $transitions[$tid];
  1137. }
  1138. /**
  1139. * Given a sid, get the transition.
  1140. */
  1141. function workflow_get_workflow_transitions_by_sid($sid) {
  1142. static $transitions;
  1143. if (!isset($transitions[$sid])) {
  1144. $transitions[$sid] = db_query('SELECT tid, sid, target_sid, roles FROM {workflow_transitions} WHERE sid = :sid',
  1145. array(':sid' => $sid))->fetchAll();
  1146. }
  1147. return $transitions[$sid];
  1148. }
  1149. /**
  1150. * Given a target_sid, get the transition.
  1151. */
  1152. function workflow_get_workflow_transitions_by_target_sid($target_sid) {
  1153. static $transitions;
  1154. if (!isset($transitions[$target_sid])) {
  1155. $transitions[$target_sid] = db_query('SELECT tid, sid, target_sid, roles FROM {workflow_transitions} WHERE target_sid = :target_sid',
  1156. array(':target_sid' => $target_sid))->fetchAll();
  1157. }
  1158. return $transitions[$target_sid];
  1159. }
  1160. /**
  1161. * Given a sid get any transition involved.
  1162. */
  1163. function workflow_get_workflow_transitions_by_sid_involved($sid) {
  1164. $results = db_query('SELECT tid, sid, target_sid, roles FROM {workflow_transitions} WHERE sid = :sid OR target_sid = :sid', array(':sid' => $sid));
  1165. return $results->fetchAll();
  1166. }
  1167. /**
  1168. * Given a role string get any transition involved.
  1169. */
  1170. function workflow_get_workflow_transitions_by_roles($roles) {
  1171. $results = db_query('SELECT tid, sid, target_sid, roles FROM {workflow_transitions} WHERE roles LIKE :roles', array(':roles' => $roles));
  1172. return $results->fetchAll();
  1173. }
  1174. /**
  1175. * Given a sid and target_sid, get the transition. This will be unique.
  1176. */
  1177. function workflow_get_workflow_transitions_by_sid_target_sid($sid, $target_sid) {
  1178. $results = db_query('SELECT tid, sid, target_sid, roles FROM {workflow_transitions} WHERE sid = :sid AND target_sid = :target_sid', array(':sid' => $sid, ':target_sid' => $target_sid));
  1179. return $results->fetchObject();
  1180. }
  1181. /**
  1182. * Given a tid, delete the transition.
  1183. */
  1184. function workflow_delete_workflow_transitions_by_tid($tid) {
  1185. // Notify any interested modules before we delete, in case there's data needed.
  1186. module_invoke_all('workflow', 'transition delete', $tid, NULL, NULL, FALSE);
  1187. return db_delete('workflow_transitions')->condition('tid', $tid)->execute();
  1188. }
  1189. /**
  1190. * Given a sid and target_sid, get the transition. This will be unique.
  1191. */
  1192. function workflow_delete_workflow_transitions_by_roles($roles) {
  1193. // NOTE: This allows us to send notifications out.
  1194. foreach (workflow_get_workflow_transitions_by_roles($roles) as $transistion) {
  1195. workflow_delete_workflow_transitions_by_tid($transistion->tid);
  1196. }
  1197. }
  1198. /**
  1199. * Given data, insert or update a workflow_transitions.
  1200. */
  1201. function workflow_update_workflow_transitions(&$data) {
  1202. $data = (object) $data;
  1203. $transition = workflow_get_workflow_transitions_by_sid_target_sid($data->sid, $data->target_sid);
  1204. if ($transition) {
  1205. $roles = explode(',', $transition->roles);
  1206. foreach (explode(',', $data->roles) as $role) {
  1207. if (array_search($role, $roles) === FALSE) {
  1208. $roles[] = $role;
  1209. }
  1210. }
  1211. $transition->roles = implode(',', $roles);
  1212. drupal_write_record('workflow_transitions', $transition, 'tid');
  1213. }
  1214. else {
  1215. drupal_write_record('workflow_transitions', $data);
  1216. }
  1217. $data = $transition;
  1218. }
  1219. /**
  1220. * Given a tid and new roles, update them.
  1221. * @todo - this should be refactored out, and the update made a full actual update.
  1222. */
  1223. function workflow_update_workflow_transitions_roles($tid, $roles) {
  1224. return db_update('workflow_transitions')->fields(array('roles' => implode(',', $roles)))->condition('tid', $tid, '=')->execute();
  1225. }
  1226. /**
  1227. * Functions related to table workflow_states.
  1228. */
  1229. /**
  1230. * Get all active states in the system.
  1231. */
  1232. function workflow_get_workflow_states_all() {
  1233. static $states = array();
  1234. if (empty($states)) {
  1235. $query = "SELECT sid, state "
  1236. . "FROM {workflow_states} "
  1237. . "WHERE status = 1 "
  1238. . "ORDER BY sid "
  1239. ;
  1240. $states = db_query($query)->fetchAllKeyed();
  1241. }
  1242. return $states;
  1243. }
  1244. /**
  1245. * Get all states in the system by content type.
  1246. */
  1247. function workflow_get_workflow_states_by_type($type) {
  1248. $query = "SELECT ws.sid, ws.wid, ws.state, ws.weight, ws.sysid "
  1249. . "FROM {workflow_type_map} wtm "
  1250. . "INNER JOIN {workflow_states} ws ON ws.wid = wtm.wid "
  1251. . "WHERE wtm.type = :type AND ws.status = 1 "
  1252. . "ORDER BY ws.weight, ws.sid "
  1253. ;
  1254. $query_array = array(':type' => $type);
  1255. $results = db_query($query, $query_array);
  1256. return $results->fetchAll();
  1257. }
  1258. /**
  1259. * Get all states in the system, with options to filter, only where a workflow exists.
  1260. */
  1261. function workflow_get_workflow_states($options = array()) {
  1262. // Build the basic query.
  1263. $query = db_select('workflow_states', 'ws');
  1264. $query->leftJoin('workflows', 'w', 'w.wid = ws.wid');
  1265. $query->fields('ws');
  1266. $query->addField('w', 'wid');
  1267. $query->addField('w', 'name');
  1268. // Spin through the options and add conditions.
  1269. foreach ($options as $column => $value) {
  1270. $query->condition('ws.' . $column, $value);
  1271. }
  1272. // Set the sorting order.
  1273. $query->orderBy('ws.wid');
  1274. $query->orderBy('ws.weight');
  1275. // Just for grins, add a tag that might result in modifications.
  1276. $query->addTag('workflow_states');
  1277. // Give them the answer.
  1278. return $query->execute()->fetchAll();
  1279. }
  1280. function workflow_get_workflow_states_by_wid($wid, $options = array()) {
  1281. $options['wid'] = $wid;
  1282. return workflow_get_workflow_states($options);
  1283. }
  1284. function workflow_get_workflow_by_sid($sid) {
  1285. return db_query("SELECT w.wid, w.name, w.tab_roles, w.options FROM {workflow_states} s INNER JOIN {workflow} w ON w.wid=s.wid ",
  1286. array(':sid' => $sid))->fetchObject();
  1287. }
  1288. /**
  1289. * Given a sid, return a state. Sids are a unique id.
  1290. */
  1291. function workflow_get_workflow_states_by_sid($sid, $options = array()) {
  1292. static $sids = array();
  1293. if (!isset($sids[$sid])) {
  1294. $states = workflow_get_workflow_states(array('sid' => $sid));
  1295. $sids[$sid] = reset($states);
  1296. }
  1297. return $sids[$sid];
  1298. }
  1299. /**
  1300. * Given a sid, return all other states in that workflow.
  1301. */
  1302. function workflow_get_other_states_by_sid($sid) {
  1303. $query = "SELECT sid, state "
  1304. . "FROM {workflow_states} "
  1305. . "WHERE wid = (SELECT wid FROM {workflow_states} WHERE sid = :sid AND status = 1 AND sysid = 0) "
  1306. ;
  1307. return db_query($query, array(':sid' => $sid))->fetchAllKeyed();
  1308. }
  1309. /**
  1310. * Given a wid and state, return a state. Wids / states are a unique id.
  1311. */
  1312. function workflow_get_workflow_states_by_wid_state($wid, $state) {
  1313. $options = array(
  1314. 'state' => $state,
  1315. 'wid' => $wid,
  1316. );
  1317. return workflow_get_workflow_states($options);
  1318. }
  1319. /**
  1320. * Given a sid, delete the state and all associated data.
  1321. */
  1322. function workflow_delete_workflow_states_by_sid($sid, $new_sid = FALSE, $true_delete = FALSE) {
  1323. // Notify interested modules. We notify first to allow access to data before we zap it.
  1324. module_invoke_all('workflow', 'state delete', $sid, NULL, NULL, FALSE);
  1325. // Re-parent any nodes that we don't want to orphan.
  1326. if ($new_sid) {
  1327. global $user;
  1328. // A candidate for the batch API.
  1329. // @TODO: Future updates should seriously consider setting this with batch.
  1330. $node = new stdClass();
  1331. $node->workflow_stamp = REQUEST_TIME;
  1332. foreach (workflow_get_workflow_node_by_sid($sid) as $data) {
  1333. $node->nid = $data->nid;
  1334. $node->workflow = $sid;
  1335. $data = array(
  1336. 'nid' => $node->nid,
  1337. 'sid' => $new_sid,
  1338. 'uid' => $user->uid,
  1339. 'stamp' => $node->workflow_stamp,
  1340. );
  1341. workflow_update_workflow_node($data, $sid, t('Previous state deleted'));
  1342. }
  1343. }
  1344. // Find out which transitions this state is involved in.
  1345. $preexisting = array();
  1346. foreach (workflow_get_workflow_transitions_by_sid_involved($sid) as $data) {
  1347. $preexisting[$data->sid][$data->target_sid] = TRUE;
  1348. }
  1349. // Delete the transitions.
  1350. foreach ($preexisting as $from => $array) {
  1351. foreach (array_keys($array) as $target_id) {
  1352. if ($transition = workflow_get_workflow_transitions_by_sid_target_sid($from, $target_id)) {
  1353. workflow_delete_workflow_transitions_by_tid($transition->tid);
  1354. }
  1355. }
  1356. }
  1357. // Delete any lingering node to state values.
  1358. workflow_delete_workflow_node_by_sid($sid);
  1359. // Delete the state. -- We don't actually delete, just deactivate.
  1360. // This is a matter up for some debate, to delete or not to delete, since this
  1361. // causes name conflicts for states. In the meantime, we just stick with what we know.
  1362. if ($true_delete) {
  1363. db_delete('workflow_states')->condition('sid', $sid)->execute();
  1364. }
  1365. else {
  1366. db_update('workflow_states')->fields(array('status' => 0))->condition('sid', $sid, '=')->execute();
  1367. }
  1368. }
  1369. /**
  1370. * Given data, update or insert into workflow_states.
  1371. */
  1372. function workflow_update_workflow_states(&$data) {
  1373. $data = (object) $data;
  1374. if (!isset($data->sysid)) {
  1375. $data->sysid = 0;
  1376. }
  1377. if (!isset($data->status)) {
  1378. $data->status = 1;
  1379. }
  1380. if (isset($data->sid) && count(workflow_get_workflow_states_by_sid($data->sid)) > 0) {
  1381. drupal_write_record('workflow_states', $data, 'sid');
  1382. }
  1383. else {
  1384. drupal_write_record('workflow_states', $data);
  1385. }
  1386. }
  1387. /**
  1388. * Functions related to table workflow_scheduled_transition.
  1389. */
  1390. /**
  1391. * Given a node, get all scheduled transitions for it.
  1392. */
  1393. function workflow_get_workflow_scheduled_transition_by_nid($nid) {
  1394. $results = db_query('SELECT nid, old_sid, sid, uid, scheduled, comment FROM {workflow_scheduled_transition} WHERE nid = :nid ORDER BY scheduled ASC', array(':nid' => $nid));
  1395. return $results->fetchAll();
  1396. }
  1397. /**
  1398. * Given a timeframe, get all scheduled transistions.
  1399. */
  1400. function workflow_get_workflow_scheduled_transition_by_between($start = 0, $end = REQUEST_TIME) {
  1401. $results = db_query('SELECT nid, old_sid, sid, uid, scheduled, comment FROM {workflow_scheduled_transition} WHERE scheduled > :start AND scheduled < :end ORDER BY scheduled ASC', array(':start' => $start, ':end' => $end));
  1402. return $results->fetchAll();
  1403. }
  1404. /**
  1405. * Given a node, delete transitions for it.
  1406. */
  1407. function workflow_delete_workflow_scheduled_transition_by_nid($nid) {
  1408. return db_delete('workflow_scheduled_transition')->condition('nid', $nid)->execute();
  1409. }
  1410. /**
  1411. * Get allowable transitions for a given workflow state. Typical use:
  1412. *
  1413. * global $user;
  1414. * $possible = workflow_allowable_transitions($sid, 'to', $user->roles);
  1415. *
  1416. * If the state ID corresponded to the state named "Draft", $possible now
  1417. * contains the states that the current user may move to from the Draft state.
  1418. *
  1419. * @param $sid
  1420. * The ID of the state in question.
  1421. * @param $dir
  1422. * The direction of the transition: 'to' or 'from' the state denoted by $sid.
  1423. * When set to 'to' all the allowable states that may be moved to are
  1424. * returned; when set to 'from' all the allowable states that may move to the
  1425. * current state are returned.
  1426. * @param mixed $roles
  1427. * Array of ints (and possibly the string 'author') representing the user's
  1428. * roles. If the string 'ALL' is passed (instead of an array) the role
  1429. * constraint is ignored (this is the default for backwards compatibility).
  1430. *
  1431. * @return
  1432. * Associative array of states ($sid => $state_name pairs), excluding current state.
  1433. */
  1434. function workflow_allowable_transitions($sid, $dir = 'to', $roles = 'ALL') {
  1435. $transitions = array();
  1436. // Main query from transitions table.
  1437. $query = db_select('workflow_transitions', 't')
  1438. ->fields('t', array('tid'));
  1439. if ($dir == 'to') {
  1440. $query->innerJoin('workflow_states', 's', 's.sid = t.target_sid');
  1441. $query->addField('t', 'target_sid', 'state_id');
  1442. $query->condition('t.sid', $sid);
  1443. }
  1444. else {
  1445. $query->innerJoin('workflow_states', 's', 's.sid = t.sid');
  1446. $query->addField('t', 'sid', 'state_id');
  1447. $query->condition('t.target_sid', $sid);
  1448. }
  1449. $query->addField('s', 'state', 'state_name');
  1450. $query->addField('s', 'weight', 'state_weight');
  1451. $query->addField('s', 'sysid');
  1452. $query->condition('s.status', 1);
  1453. // Now let's get the current state.
  1454. $query2 = db_select('workflow_states', 's');
  1455. $query2->addField('s', 'sid', 'tid');
  1456. $query2->addField('s', 'sid', 'state_id');
  1457. $query2->addField('s', 'state', 'state_name');
  1458. $query2->addField('s', 'weight', 'state_weight');
  1459. $query2->addField('s', 'sysid');
  1460. $query2->condition('s.status', 1);
  1461. $query2->condition('s.sid', $sid);
  1462. $query2->orderBy('state_weight');
  1463. $query2->orderBy('state_id');
  1464. // Add the union of the two queries
  1465. $query->union($query2, 'UNION');
  1466. $results = $query->execute();
  1467. foreach ($results as $transition) {
  1468. if ($roles == 'ALL' // Superuser.
  1469. || $sid == $transition->state_id // Include current state for same-state transitions.
  1470. || workflow_transition_allowed($transition->tid, $roles)) {
  1471. $transitions[] = $transition;
  1472. }
  1473. }
  1474. return $transitions;
  1475. }
  1476. /**
  1477. * Insert a new scheduled transistion.
  1478. * Only one transistion at a time (for now).
  1479. */
  1480. function workflow_insert_workflow_scheduled_transition($data) {
  1481. $data = (object) $data;
  1482. workflow_delete_workflow_scheduled_transition_by_nid($data->nid);
  1483. drupal_write_record('workflow_scheduled_transition', $data);
  1484. }
  1485. /**
  1486. * Functions related to table workflow_node_history.
  1487. */
  1488. /**
  1489. * Get most recent history for a node.
  1490. */
  1491. function workflow_get_recent_node_history($nid) {
  1492. $results = db_query_range('SELECT h.hid, h.nid, h.old_sid, h.sid, h.uid, h.stamp, h.comment, '
  1493. . 's.state AS state_name '
  1494. . 'FROM {workflow_node_history} h '
  1495. . 'INNER JOIN {workflow_states} s ON s.sid = h.sid '
  1496. . 'WHERE h.nid = :nid ORDER BY h.stamp DESC', 0, 1, array(':nid' => $nid));
  1497. return $results->fetchObject();
  1498. }
  1499. /**
  1500. * Get all recorded history for a node id.
  1501. *
  1502. * Since this may return a lot of data, a limit is included to allow for only one result.
  1503. */
  1504. function workflow_get_workflow_node_history_by_nid($nid, $limit = NULL) {
  1505. if (empty($limit)) {
  1506. $limit = variable_get('workflow_states_per_page', 20);
  1507. }
  1508. $results = db_query_range('SELECT h.hid, h.nid, h.old_sid, h.sid, h.uid, h.stamp, h.comment '
  1509. . 'FROM {workflow_node_history} h '
  1510. . 'LEFT JOIN {users} u ON h.uid = u.uid '
  1511. . 'WHERE nid = :nid '
  1512. // The timestamp is only granular to the second; on a busy site, we need the id.
  1513. . 'ORDER BY h.stamp DESC, h.hid DESC ', 0, $limit,
  1514. array(':nid' => $nid));
  1515. if ($limit == 1) {
  1516. return $results->fetchObject();
  1517. }
  1518. return $results->fetchAll();
  1519. }
  1520. /**
  1521. * Given a user id, re-assign history to the new user account. Called by user_delete().
  1522. */
  1523. function workflow_update_workflow_node_history_uid($uid, $new_value) {
  1524. return db_update('workflow_node_history')->fields(array('uid' => $new_value))->condition('uid', $uid, '=')->execute();
  1525. }
  1526. /**
  1527. * Given data, insert a new history. Always insert.
  1528. */
  1529. function workflow_insert_workflow_node_history($data) {
  1530. $data = (object) $data;
  1531. if (isset($data->hid)) {
  1532. unset($data->hid);
  1533. }
  1534. // Check for no transition.
  1535. if ($data->old_sid == $data->sid) {
  1536. // Make sure we haven't already inserted history for this update.
  1537. $last_history = workflow_get_workflow_node_history_by_nid($data->nid, 1);
  1538. if (isset($last_history) && $last_history->stamp == REQUEST_TIME) {
  1539. return;
  1540. }
  1541. }
  1542. drupal_write_record('workflow_node_history', $data);
  1543. }
  1544. /**
  1545. * Functions related to table workflow_node.
  1546. */
  1547. /**
  1548. * Given a node id, find out what it's current state is. Unique (for now).
  1549. */
  1550. function workflow_get_workflow_node_by_nid($nid) {
  1551. $results = db_query('SELECT nid, sid, uid, stamp FROM {workflow_node} WHERE nid = :nid', array(':nid' => $nid));
  1552. return $results->fetchObject();
  1553. }
  1554. /**
  1555. * Given a sid, find out the nodes associated.
  1556. */
  1557. function workflow_get_workflow_node_by_sid($sid) {
  1558. $results = db_query('SELECT nid, sid, uid, stamp FROM {workflow_node} WHERE sid = :sid', array(':sid' => $sid));
  1559. return $results->fetchAll();
  1560. }
  1561. /**
  1562. * Given nid, update the new stamp. This probably can be refactored. Called by workflow_execute_transition().
  1563. * @TODO refactor into a correct insert / update.
  1564. */
  1565. function workflow_update_workflow_node_stamp($nid, $new_stamp) {
  1566. return db_update('workflow_node')->fields(array('stamp' => $new_stamp))->condition('nid', $nid, '=')->execute();
  1567. }
  1568. /**
  1569. * Given data, update the new user account. Called by user_delete().
  1570. */
  1571. function workflow_update_workflow_node_uid($uid, $new_uid) {
  1572. return db_update('workflow_node')->fields(array('uid' => $new_uid))->condition('uid', $uid, '=')->execute();
  1573. }
  1574. /**
  1575. * Given nid, delete associated workflow data.
  1576. */
  1577. function workflow_delete_workflow_node_by_nid($nid) {
  1578. return db_delete('workflow_node')->condition('nid', $nid)->execute();
  1579. }
  1580. /**
  1581. * Given sid, delete associated workflow data.
  1582. */
  1583. function workflow_delete_workflow_node_by_sid($sid) {
  1584. return db_delete('workflow_node')->condition('sid', $sid)->execute();
  1585. }
  1586. /**
  1587. * Given data, insert the node association.
  1588. */
  1589. function workflow_update_workflow_node($data, $old_sid, $comment = NULL) {
  1590. $data = (object) $data;
  1591. if (isset($data->nid) && workflow_get_workflow_node_by_nid($data->nid)) {
  1592. drupal_write_record('workflow_node', $data, 'nid');
  1593. }
  1594. else {
  1595. drupal_write_record('workflow_node', $data);
  1596. }
  1597. // Write to history for this node.
  1598. $data = array(
  1599. 'nid' => $data->nid,
  1600. 'old_sid' => $old_sid,
  1601. 'sid' => $data->sid,
  1602. 'uid' => $data->uid,
  1603. 'stamp' => $data->stamp,
  1604. 'comment' => $comment,
  1605. );
  1606. workflow_insert_workflow_node_history($data);
  1607. }
  1608. /**
  1609. * Implements hook_requirements().
  1610. * Let admins know that Workflow is in use.
  1611. */
  1612. function workflow_requirements($phase) {
  1613. $requirements = array();
  1614. switch ($phase) {
  1615. case 'runtime':
  1616. $types = db_query('SELECT wid, type FROM {workflow_type_map} WHERE wid <> 0 ORDER BY type')->fetchAllKeyed();
  1617. // If there are no types, then just bail.
  1618. if (count($types) == 0) {
  1619. return;
  1620. }
  1621. // Let's make it look nice.
  1622. if (count($types) == 1) {
  1623. $type_list = current($types);
  1624. }
  1625. else {
  1626. $last = array_pop($types);
  1627. if (count($types) > 2) {
  1628. $type_list = implode(', ', $types) . ', and ' . $last;
  1629. }
  1630. else {
  1631. $type_list = current($types) . ' and ' . $last;
  1632. }
  1633. }
  1634. $requirements['workflow'] = array(
  1635. 'title' => t('Workflow'),
  1636. 'value' => t('Workflow is active on the @types content types.', array('@types' => $type_list)),
  1637. 'severity' => REQUIREMENT_OK,
  1638. );
  1639. return $requirements;
  1640. }
  1641. }