/** * Implements hook_field_widget_form --> WidgetInterface::formElement(). * * {@inheritdoc} * * Be careful: this widget may be shown in very different places. Test carefully!! * - On a entity add/edit page * - On a entity preview page * - On a entity view page * - On a entity 'workflow history' tab * - On a comment display, in the comment history * - On a comment form, below the comment history * @todo D8: change "array $items" to "FieldInterface $items" */ public function formElement(array $items, $delta, array $element, $langcode, array &$form, array &$form_state) { $field_name = $this->field['field_name']; $field = $this->instance; $instance = $this->instance; $entity = $this->entity; $entity_type = $this->entity_type; $entity_id = entity_id($entity_type, $entity); if (!$entity) { // If no entity given, do not show a form. E.g., on the field settings page. return $element; } // Capture settings to format the form/widget. $settings_title_as_name = !empty($this->field['settings']['widget']['name_as_title']); // The schedule cannot be shown on a Content add page. $settings_schedule = !empty($this->field['settings']['widget']['schedule']) && $entity_id; $settings_schedule_timezone = !empty($this->field['settings']['widget']['schedule_timezone']); // Show comment, when both Field and Instance allow this. $settings_comment = $this->field['settings']['widget']['comment'] ? 'textarea' : 'hidden'; $workflow = Workflow::load($this->field['settings']['wid']); $current_sid = workflow_node_current_state($entity, $entity_type, $field_name); $current_state = WorkflowState::load($current_sid); $options = array(); $options = $current_state->getOptions($entity_type, $entity); // Determine the default value. If we are in CreationState, use a fast alternative for $workflow->getFirstSid(). $default_value = $current_state->isCreationState() ? key($options) : $current_sid; // Get the scheduling info. This may change the $current_sid on the Form. $scheduled = '0'; $timestamp = REQUEST_TIME; $comment = NULL; if ($settings_schedule) { // Read scheduled information. // Technically you could have more than one scheduled, but this will only add the soonest one. foreach (WorkflowScheduledTransition::load($entity_type, $entity_id, $field_name) as $scheduled_transition) { $scheduled = '1'; $current_sid = $scheduled_transition->sid; $timestamp = $scheduled_transition->scheduled; $comment = $scheduled_transition->comment; break; } } // Fetch the form ID. This is unique for each entity, to allow multiple form per page (Views, etc.). $form_id = $form_state['build_info']['form_id']; $element_scheduled_name = 'workflow_scheduled_' . $form_id; $element_options_name = 'workflow_options_' . $form_id; $element_scheduled_name = 'workflow_scheduled'; $element_options_name = 'workflow_options'; $elt_state_name = 'workflow_scheduled_' . $form_id; $label = $workflow->label(); // Prepare a wrapper. This might be a fieldset. $element['workflow']['#type'] = 'container'; $element['workflow']['#attributes'] = array('class' => array('workflow-form-container')); // Save the current value of the node in the form, for later Workflow-module specific references. // We add prefix, since #tree == FALSE. $element['workflow']['workflow_entity'] = array('#type' => 'value', '#value' => $this->entity); $element['workflow']['workflow_entity_type'] = array('#type' => 'value', '#value' => $this->entity_type); $element['workflow']['workflow_field'] = array('#type' => 'value', '#value' => $this->field); $element['workflow']['workflow_instance'] = array('#type' => 'value', '#value' => $this->instance); // Save the form_id, so the form values can be retrieved in submit function. $element['workflow']['form_id'] = array('#type' => 'value', '#value' => $form_id); // First of all, we add the default value in the place were normal fields // have it. This is to cater for 'preview' of the entity. $element['#default_value'] = $default_value; // Decide if we show a widget or a formatter. // There is no need to a widget when the only choice is the current sid. if (!$current_state->showWidget($options)) { $element['workflow'][$element_options_name] = workflow_state_formatter($entity_type, $entity, $field, $instance); return $element; } else { $element['workflow'][$element_options_name] = array('#type' => $this->field['settings']['widget']['options'], '#title' => $settings_title_as_name ? t('Change !name state', array('!name' => $label)) : '', '#options' => $options, '#default_value' => $default_value); } // Display scheduling form, but only if entity is being edited and user has // permission. State change cannot be scheduled at entity creation because // that leaves the entity in the (creation) state. if ($settings_schedule == TRUE && user_access('schedule workflow transitions')) { global $user; if (variable_get('configurable_timezones', 1) && $user->uid && drupal_strlen($user->timezone)) { $timezone = $user->timezone; } else { $timezone = variable_get('date_default_timezone', 0); } $timezones = drupal_map_assoc(timezone_identifiers_list()); $hours = format_date($timestamp, 'custom', 'H:i', $timezone); // $element['workflow']['workflow_scheduled'] = array( $element['workflow'][$element_scheduled_name] = array('#type' => 'radios', '#title' => t('Schedule'), '#options' => array('0' => t('Immediately'), '1' => t('Schedule for state change')), '#default_value' => $scheduled, '#attributes' => array('id' => 'scheduled_' . $form_id)); $element['workflow']['workflow_scheduled_date_time'] = array('#type' => 'fieldset', '#title' => t('At'), '#attributes' => array('class' => array('container-inline')), '#prefix' => '<div style="margin-left: 1em;">', '#suffix' => '</div>', '#states' => array('visible' => array(':input[id="' . 'scheduled_' . $form_id . '"]' => array('value' => '1')))); $element['workflow']['workflow_scheduled_date_time']['workflow_scheduled_date'] = array('#type' => 'date', '#default_value' => array('day' => date('j', $timestamp), 'month' => date('n', $timestamp), 'year' => date('Y', $timestamp))); $element['workflow']['workflow_scheduled_date_time']['workflow_scheduled_hour'] = array('#type' => 'textfield', '#title' => t('Time'), '#maxlength' => 5, '#size' => 6, '#default_value' => $scheduled ? $hours : '00:00'); $element['workflow']['workflow_scheduled_date_time']['workflow_scheduled_timezone'] = array('#type' => $settings_schedule_timezone ? 'select' : 'hidden', '#title' => t('Time zone'), '#options' => $timezones, '#default_value' => array($timezone => $timezone)); $element['workflow']['workflow_scheduled_date_time']['workflow_scheduled_help'] = array('#type' => 'item', '#prefix' => '<br />', '#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: @time.', array('@time' => format_date(REQUEST_TIME, 'custom', 'H:i', $timezone)))); } $element['workflow']['workflow_comment'] = array('#type' => $settings_comment, '#title' => t('Workflow comment'), '#description' => t('A comment to put in the workflow log.'), '#default_value' => $comment, '#rows' => 2); // The 'add submit' can explicitely set by workflowfield_field_formatter_view(), // to add the submit button on the Content view page and the Workflow history tab. if (!empty($this->instance['widget']['settings']['submit_function'])) { // Add a submit button, but only on Entity View and History page. $element['workflow']['submit'] = array('#type' => 'submit', '#value' => t('Update workflow'), '#executes_submit_callback' => TRUE, '#submit' => array($this->instance['widget']['settings']['submit_function'])); } return $element; }
/** * Implements hook_field_delete() -> FieldItemInterface::delete(). */ public function delete($items) { global $user; $entity_type = $this->entity_type; $entity = $this->entity; $entity_id = entity_id($entity_type, $entity); $field_name = $this->field['field_name']; // Delete the record in {workflow_node} - not for Workflow Field. // Use a one-liner for better code analysis when grepping on old code. !$field_name ? workflow_delete_workflow_node_by_nid($entity_id) : NULL; // Add a history record in {workflow_node_history}. // @see drupal.org/node/2165349, comment by Bastlynn: // The reason for this history log upon delete is because Workflow module // has historically been used to track node states and accountability in // business environments where accountability for changes over time is // *absolutely* required. Think banking and/or particularly strict // retention policies for legal reasons. // // However, a deleted nid may be re-used under certain circumstances: // e.g., working with InnoDB or after restart the DB server. // This may cause that old history is associated with a new node. $old_sid = _workflow_get_sid_by_items($items); $new_sid = (int) WORKFLOW_DELETION; $comment = t('Entity deleted.'); $transition = new WorkflowTransition(); $transition->setValues($entity_type, $entity, $field_name, $old_sid, $new_sid, $user->uid, REQUEST_TIME, $comment); $transition->save(); // Delete all records for this node in {workflow_scheduled_transition}. foreach (WorkflowScheduledTransition::load($entity_type, $entity_id, $field_name) as $scheduled_transition) { $scheduled_transition->delete(); } }
/** * Implements hook_field_widget_form --> WidgetInterface::formElement(). * * {@inheritdoc} * * Be careful: Widget may be shown in very different places. Test carefully!! * - On a entity add/edit page * - On a entity preview page * - On a entity view page * - On a entity 'workflow history' tab * - On a comment display, in the comment history * - On a comment form, below the comment history * * @todo D8: change "array $items" to "FieldInterface $items" */ public function formElement(array $items, $delta, array $element, array &$form, array &$form_state) { global $user; // @todo #2287057: verify if formElement() really is only used for UI. If not, $user must be passed. $field = $this->field; $instance = $this->instance; $entity = $this->entity; $entity_type = $this->entity_type; $entity_id = $entity ? entity_id($entity_type, $entity) : 0; $field_name = $field['field_name']; $current_sid = FALSE; // $field['settings']['wid'] can be numeric or named. // $wid may not be specified. $wid = $field['settings']['wid']; $workflow = workflow_load_single($wid); $workflow_label = $workflow ? check_plain(t($workflow->label())) : ''; // Capture settings to format the form/widget. $settings_title_as_name = !empty($field['settings']['widget']['name_as_title']); $settings_options_type = $field['settings']['widget']['options']; // The schedule can be hidden via field settings, ... $settings_schedule = !empty($field['settings']['widget']['schedule']); if ($settings_schedule) { if (isset($form_state['step']) && $form_state['step'] == 'views_bulk_operations_config_form') { // On VBO 'modify entity values' form, leave field settings. $settings_schedule = TRUE; } else { // ... and cannot be shown on a Content add page (no $entity_id), // ...but can be shown on a VBO 'set workflow state to..'page (no entity). $settings_schedule = !($entity && !$entity_id); } } $settings_schedule_timezone = !empty($field['settings']['widget']['schedule_timezone']); // Show comment, when both Field and Instance allow this. $settings_comment = $field['settings']['widget']['comment']; $options = array(); if (!$entity) { // Sometimes, no entity is given. We encountered the following cases: // - the Field settings page, // - the VBO action form; // - the Advance Action form on admin/config/system/actions; // If so, show all options for the given workflow(s). // Set 'grouped' option. This is only valid for select list. $grouped = $settings_options_type == 'select'; $options = workflow_get_workflow_state_names($wid, $grouped, $all = FALSE); $show_widget = TRUE; $default_value = isset($items[0]['value']) ? $items[0]['value'] : '0'; } else { $force = FALSE; $current_sid = workflow_node_current_state($entity, $entity_type, $field_name); if ($current_state = workflow_state_load_single($current_sid)) { // $grouped = TRUE; // Grouped options only makes sense for multiple workflows. $options = $current_state->getOptions($entity_type, $entity, $field_name, $user, $force); $show_widget = $current_state->showWidget($entity_type, $entity, $field_name, $user, $force); // Determine the default value. If we are in CreationState, use a fast alternative for $workflow->getFirstSid(). $default_value = $current_state->isCreationState() ? key($options) : $current_sid; } else { // We are in trouble! A message is already set in workflow_node_current_state(). $show_widget = FALSE; $default_value = $current_sid; } } // Get the scheduling info. This may change the $current_sid on the Form. $scheduled = '0'; $timestamp = REQUEST_TIME; $comment = NULL; if ($settings_schedule) { // Read scheduled information. // Technically you could have more than one scheduled, but this will only add the soonest one. foreach (WorkflowScheduledTransition::load($entity_type, $entity_id, $field_name) as $scheduled_transition) { $scheduled = '1'; $default_value = $scheduled_transition->new_sid; $timestamp = $scheduled_transition->scheduled; $comment = $scheduled_transition->comment; break; } } // Fetch the form ID. This is unique for each entity, to allow multiple form per page (Views, etc.). // Make it uniquer by adding the field name, or else the scheduling of // multiple workflow_fields is not indendent. $form_id = $form_state['build_info']['form_id'] . '_' . $field_name; // Prepare a UI wrapper. This might be a fieldset. $element['workflow']['#type'] = 'container'; // 'fieldset'; $element['workflow']['#attributes'] = array('class' => array('workflow-form-container')); // Save the current value of the node in the form, for later Workflow-module specific references. // We add prefix, since #tree == FALSE. $element['workflow']['workflow_entity'] = array('#type' => 'value', '#value' => $this->entity); $element['workflow']['workflow_entity_type'] = array('#type' => 'value', '#value' => $this->entity_type); $element['workflow']['workflow_field'] = array('#type' => 'value', '#value' => $field); $element['workflow']['workflow_instance'] = array('#type' => 'value', '#value' => $instance); // Save the form_id, so the form values can be retrieved in submit function. $element['workflow']['form_id'] = array('#type' => 'value', '#value' => $form_id); // First of all, we add the default value in the place were normal fields // have it. This is to cater for 'preview' of the entity. $element['#default_value'] = $default_value; // Decide if we show a widget or a formatter. // There is no need to a widget when the only choice is the current sid. if (!$show_widget) { $element['workflow']['workflow_sid'] = workflow_state_formatter($entity_type, $entity, $field, $instance, $default_value); return $element; // <---- exit. } // The 'options' widget. May be removed later if 'Action buttons' are chosen. $element['workflow']['workflow_sid'] = array('#type' => $settings_options_type, '#title' => $settings_title_as_name ? t('Change !name state', array('!name' => $workflow_label)) : t('Target state'), '#options' => $options, '#default_value' => $default_value); // Display scheduling form, but only if entity is being edited and user has // permission. State change cannot be scheduled at entity creation because // that leaves the entity in the (creation) state. if ($settings_schedule == TRUE && user_access('schedule workflow transitions')) { if (variable_get('configurable_timezones', 1) && $user->uid && drupal_strlen($user->timezone)) { $timezone = $user->timezone; } else { $timezone = variable_get('date_default_timezone', 0); } $timezones = drupal_map_assoc(timezone_identifiers_list()); $hours = format_date($timestamp, 'custom', 'H:i', $timezone); $element['workflow']['workflow_scheduled'] = array('#type' => 'radios', '#title' => t('Schedule'), '#options' => array('0' => t('Immediately'), '1' => t('Schedule for state change')), '#default_value' => $scheduled, '#attributes' => array('id' => 'scheduled_' . $form_id)); $element['workflow']['workflow_scheduled_date_time'] = array('#type' => 'fieldset', '#title' => t('At'), '#attributes' => array('class' => array('container-inline')), '#prefix' => '<div style="margin-left: 1em;">', '#suffix' => '</div>', '#states' => array('visible' => array(':input[id="' . 'scheduled_' . $form_id . '"]' => array('value' => '1')))); $element['workflow']['workflow_scheduled_date_time']['workflow_scheduled_date'] = array('#type' => 'date', '#default_value' => array('day' => date('j', $timestamp), 'month' => date('n', $timestamp), 'year' => date('Y', $timestamp))); $element['workflow']['workflow_scheduled_date_time']['workflow_scheduled_hour'] = array('#type' => 'textfield', '#title' => t('Time'), '#maxlength' => 7, '#size' => 6, '#default_value' => $scheduled ? $hours : '00:00', '#element_validate' => array('_workflow_transition_form_element_validate_time')); $element['workflow']['workflow_scheduled_date_time']['workflow_scheduled_timezone'] = array('#type' => $settings_schedule_timezone ? 'select' : 'hidden', '#title' => t('Time zone'), '#options' => $timezones, '#default_value' => array($timezone => $timezone)); $element['workflow']['workflow_scheduled_date_time']['workflow_scheduled_help'] = array('#type' => 'item', '#prefix' => '<br />', '#description' => t('Please enter a time. If no time is included, the default will be midnight on the specified date. The current time is: @time.', array('@time' => format_date(REQUEST_TIME, 'custom', 'H:i', $timezone)))); } $element['workflow']['workflow_comment'] = array('#type' => $settings_comment == '0' ? 'hidden' : 'textarea', '#required' => $settings_comment == '2', '#title' => t('Workflow comment'), '#description' => t('A comment to put in the workflow log.'), '#default_value' => $comment, '#rows' => 2); // Finally, add Submit buttons/Action buttons. // Either a default 'Submit' button is added, or a button per permitted state. if ($settings_options_type == 'buttons') { // How do action buttons work? See also d.o. issue #2187151. // Create 'action buttons' per state option. Set $sid property on each button. // 1. Admin sets ['widget']['options']['#type'] = 'buttons'. // 2. This function formElelent() creates 'action buttons' per state option; // sets $sid property on each button. // 3. User clicks button. // 4. Callback _workflow_transition_form_validate_buttons() sets proper State. // 5. Callback _workflow_transition_form_validate_buttons() sets Submit function. // @todo: this does not work yet for the Add Comment form. // Performance: inform workflow_form_alter() to do its job. _workflow_use_action_buttons(TRUE); } $submit_functions = empty($instance['widget']['settings']['submit_function']) ? array() : array($instance['widget']['settings']['submit_function']); if ($settings_options_type == 'buttons' || $submit_functions) { $element['workflow']['actions']['#type'] = 'actions'; $element['workflow']['actions']['submit'] = array('#type' => 'submit', '#value' => t('Update workflow'), '#weight' => -5, '#attributes' => array('class' => array('form-save-default-button'))); // The 'add submit' can explicitely set by workflowfield_field_formatter_view(), // to add the submit button on the Content view page and the Workflow history tab. // Add a submit button, but only on Entity View and History page. // Add the submit function only if one provided. Set the submit_callback accordingly. if ($submit_functions) { $element['workflow']['actions']['submit']['#submit'] = $submit_functions; } else { // '#submit' Must be empty, or else the submit function is not called. // $element['workflow']['actions']['submit']['#submit'] = array(); } } else { // In some cases, no submit callback function is specified. This is // explicitly done on e.g., the node edit form, because the workflow form // is 'just a field'. // So, no Submit button is to be shown. } return $element; }
/** * Execute a transition (change state of a node). * * @param bool $force * If set to TRUE, workflow permissions will be ignored. * * @return int * New state ID. If execution failed, old state ID is returned, * * deprecated workflow_execute_transition() --> WorkflowTransition::execute(). */ public function execute($force = FALSE) { $user = $this->getUser(); $old_sid = $this->old_sid; $new_sid = $this->new_sid; // Load the entity, if not already loaded. // This also sets the (empty) $revision_id in Scheduled Transitions. $entity = $this->getEntity(); // Only after getEntity(), the following are surely set. $entity_type = $this->entity_type; $entity_id = $this->entity_id; $field_name = $this->field_name; // Make sure $force is set in the transition, too. if ($force) { $this->force($force); } // Store the transition, so it can be easily fetched later on. // Store in an array, to prepare for multiple workflow_fields per entity. // This is a.o. used in hook_entity_update to trigger 'transition post'. $entity->workflow_transitions[$field_name] = $this; // Prepare an array of arguments for error messages. $args = array('%user' => isset($user->name) ? $user->name : '', '%old' => $old_sid, '%new' => $new_sid); if (!$this->getOldState()) { drupal_set_message($message = t('You tried to set a Workflow State, but the entity is not relevant. Please contact your system administrator.'), 'error'); $message = 'Setting a non-relevant Entity from state %old to %new'; $uri = entity_uri($entity_type, $entity); watchdog('workflow', $message, $args, WATCHDOG_ERROR, l('view', $uri['path'])); return $old_sid; } // Check if the state has changed. $state_changed = $old_sid != $new_sid; // If so, check the permissions. if ($state_changed) { // State has changed. Do some checks upfront. if (!$force) { // Make sure this transition is allowed by workflow module Admin UI. $roles = array_keys($user->roles); $roles = array_merge(array(WORKFLOW_ROLE_AUTHOR_RID), $roles); if (!$this->isAllowed($roles, $user, $force)) { watchdog('workflow', 'User %user not allowed to go from state %old to %new', $args, WATCHDOG_NOTICE); // If incorrect, quit. return $old_sid; } } if (!$force) { // Make sure this transition is allowed by custom module. // @todo D8: remove, or replace by 'transition pre'. See WorkflowState::getOptions(). // @todo D8: replace all parameters that are included in $transition. $permitted = module_invoke_all('workflow', 'transition permitted', $old_sid, $new_sid, $entity, $force, $entity_type, $field_name, $this, $user); // Stop if a module says so. if (in_array(FALSE, $permitted, TRUE)) { watchdog('workflow', 'Transition vetoed by module.'); return $old_sid; } } // Make sure this transition is valid and allowed for the current user. // Invoke a callback indicating a transition is about to occur. // Modules may veto the transition by returning FALSE. // (Even if $force is TRUE, but they shouldn't do that.) $permitted = module_invoke_all('workflow', 'transition pre', $old_sid, $new_sid, $entity, $force, $entity_type, $field_name, $this); // Stop if a module says so. if (in_array(FALSE, $permitted, TRUE)) { watchdog('workflow', 'Transition vetoed by module.'); return $old_sid; } } elseif ($this->comment) { // No need to ask permission for adding comments. // Since you should not add actions to a 'transition pre' event, there is // no need to invoke the event. } else { // There is no state change, and no comment. // We may need to clean up something. } // The transition is allowed. Let other modules modify the comment. // @todo D8: remove all but last items from $context. $context = array('node' => $entity, 'sid' => $new_sid, 'old_sid' => $old_sid, 'uid' => $user->uid, 'transition' => $this); drupal_alter('workflow_comment', $this->comment, $context); // Now, change the database. // Log the new state in {workflow_node}. if (!$field_name) { if ($state_changed || $this->comment) { // If the node does not have an existing 'workflow' property, // save the $old_sid there, so it can be logged. if (!isset($entity->workflow)) { // This is a workflow_node sid. $entity->workflow = $old_sid; // This is a workflow_node sid. } // Change the state for {workflow_node}. // The equivalent for Field API is in WorkflowDefaultWidget::submit. $data = array('nid' => $entity_id, 'sid' => $new_sid, 'uid' => isset($entity->workflow_uid) ? $entity->workflow_uid : $user->uid, 'stamp' => REQUEST_TIME); workflow_update_workflow_node($data); $entity->workflow = $new_sid; // This is a workflow_node sid. } } else { // This is a Workflow Field. // Until now, adding code here (instead of in workflow_execute_transition() ) // doesn't work, creating an endless loop. /* if ($state_changed || $this->comment) { // Do a separate update to update the field (Workflow Field API) // This will call hook_field_update() and WorkflowFieldDefaultWidget::submit(). // $entity->{$field_name}[$this->language] = array(); // $entity->{$field_name}[$this->language][0]['workflow']['workflow_sid'] = $new_sid; // $entity->{$field_name}[$this->language][0]['workflow']['workflow_comment'] = $this->comment; $entity->{$field_name}[$this->language][0]['transition'] = $this; // Save the entity, but not through entity_save(), // since this will check permissions again and trigger rules. // @TODO: replace below by a workflow_field setter callback. // The transition was successfully executed, or else a message was raised. // entity_save($entity_type, $entity); // or // field_attach_update($entity_type, $entity); // Reset the entity cache after update. entity_get_controller($entity_type)->resetCache(array($entity_id)); $new_sid = workflow_node_current_state($entity, $entity_type, $field_name); } */ } $this->is_executed = TRUE; if ($state_changed || $this->comment) { // Log the transition in {workflow_node_history}. $this->save(); // Register state change with watchdog. if ($state_changed) { $workflow = $this->getWorkflow(); // Get the workflow_settings, unified for workflow_node and workflow_field. // @todo D8: move settings back to Workflow (like workflownode currently is). // @todo D8: to move settings back, grep for "workflow->options" and "field['settings']". $field = _workflow_info_field($field_name, $workflow); if (($new_state = $this->getNewState()) && !empty($field['settings']['watchdog_log'])) { $entity_type_info = entity_get_info($entity_type); $message = $this->isScheduled() ? 'Scheduled state change of @type %label to %state_name executed' : 'State of @type %label set to %state_name'; $args = array('@type' => $entity_type_info['label'], '%label' => entity_label($entity_type, $entity), '%state_name' => check_plain(t($new_state->label()))); $uri = entity_uri($entity_type, $entity); watchdog('workflow', $message, $args, WATCHDOG_NOTICE, l('view', $uri['path'])); } } // Remove any scheduled state transitions. foreach (WorkflowScheduledTransition::load($entity_type, $entity_id, $field_name) as $scheduled_transition) { /* @var $scheduled_transition WorkflowScheduledTransition */ $scheduled_transition->delete(); } // Notify modules that transition has occurred. // Action triggers should take place in response to this callback, not the 'transaction pre'. if (!$field_name) { // Now that workflow data is saved, reset stuff to avoid problems // when Rules etc want to resave the data. // Remember, this is only for nodes, and node_save() is not necessarily performed. unset($entity->workflow_comment); module_invoke_all('workflow', 'transition post', $old_sid, $new_sid, $entity, $force, $entity_type, $field_name, $this); entity_get_controller('node')->resetCache(array($entity->nid)); // from entity_load(), node_save(); } else { // module_invoke_all('workflow', 'transition post', $old_sid, $new_sid, $entity, $force, $entity_type, $field_name, $this); // We have a problem here with Rules, Trigger, etc. when invoking // 'transition post': the entity has not been saved, yet. we are still // IN the transition, not AFTER. Alternatives: // 1. Save the field here explicitly, using field_attach_save; // 2. Move the invoke to another place: hook_entity_insert(), hook_entity_update(); // 3. Rely on the entity hooks. This works for Rules, not for Trigger. // --> We choose option 2: // - First, $entity->workflow_transitions[] is set for easy re-fetching. // - Then, post_execute() is invoked via workflowfield_entity_insert(), _update(). } } return $new_sid; }
/** * {@inheritdoc} * * @param array $form * @param array $form_state * @param WorkflowTransition * The Transition to be edited, created. * * @return * The enhanced form structure. */ public function buildForm(array $form, array &$form_state) { global $user; /* @var $transition WorkflowTransition */ $transition = NULL; if (isset($form_state['WorkflowTransition'])) { // If provided, get data from WorkflowTransition. // This happens when calling entity_ui_get_form(), like in the // WorkflowTransition Comment Edit form. $transition = $form_state['WorkflowTransition']; $field_name = $transition->field_name; $workflow = $transition->getWorkflow(); $wid = $transition->wid; $entity = $this->entity = $transition->getEntity(); $entity_type = $this->entity_type = $transition->entity_type; // Figure out the $entity's bundle and id. list(, , $entity_bundle) = entity_extract_ids($entity_type, $entity); $entity_id = entity_id($entity_type, $entity); // Show the current state and the Workflow form to allow state changing. // N.B. This part is replicated in hook_node_view, workflow_tab_page, workflow_vbo, transition_edit. // @todo: support multiple workflows per entity. // For workflow_tab_page with multiple workflows, use a separate view. See [#2217291]. $field = _workflow_info_field($field_name, $workflow); $instance = $this->instance + field_info_instance($entity_type, $field_name, $entity_bundle); } else { // Get data from normal parameters. $entity = $this->entity; $entity_type = $this->entity_type; $entity_id = $entity ? entity_id($entity_type, $entity) : 0; $field = $this->field; $field_name = $field['field_name']; $instance = $this->instance; // $field['settings']['wid'] can be numeric or named. // $wid may not be specified. $wid = $field['settings']['wid']; $workflow = workflow_load_single($wid); } $force = FALSE; // Get values. // Current sid and default value may differ in a scheduled transition. // Set 'grouped' option. Only valid for select list and undefined/multiple workflows. $settings_options_type = $field['settings']['widget']['options']; $grouped = $settings_options_type == 'select'; if ($transition) { // If a Transition is passed as parameter, use this. if ($transition->isExecuted()) { // We are editing an existing/executed/not-scheduled transition. // Only the comments may be changed! // Fetch the old state for the formatter on top of form. $current_state = $transition->getOldState(); $current_sid = $current_state->sid; // The states may not be changed anymore. $new_state = $transition->getNewState(); $options = array($new_state->sid => $new_state->label()); // We need the widget to edit the comment. $show_widget = TRUE; } else { $current_state = $transition->getOldState(); $current_sid = $current_state->sid; $options = $current_state->getOptions($entity_type, $entity, $field_name, $user, $force); $show_widget = $current_state->showWidget($entity_type, $entity, $field_name, $user, $force); } $default_value = $transition->new_sid; } elseif (!$entity) { // Sometimes, no entity is given. We encountered the following cases: // - the Field settings page, // - the VBO action form; // - the Advance Action form on admin/config/system/actions; // If so, show all options for the given workflow(s). $options = workflow_get_workflow_state_names($wid, $grouped, $all = FALSE); $show_widget = TRUE; $default_value = $current_sid = isset($items[0]['value']) ? $items[0]['value'] : '0'; } else { $current_sid = workflow_node_current_state($entity, $entity_type, $field_name); if ($current_state = workflow_state_load_single($current_sid)) { /* @var $current_state WorkflowTransition */ $options = $current_state->getOptions($entity_type, $entity, $field_name, $user, $force); $show_widget = $current_state->showWidget($entity_type, $entity, $field_name, $user, $force); $default_value = !$current_state->isCreationState() ? $current_sid : $workflow->getFirstSid($entity_type, $entity, $field_name, $user, FALSE); } else { // We are in trouble! A message is already set in workflow_node_current_state(). $options = array(); $show_widget = FALSE; $default_value = $current_sid; } // Get the scheduling info. This may change the $default_value on the Form. // Read scheduled information, only if an entity exists. // Technically you could have more than one scheduled, but this will only add the soonest one. foreach (WorkflowScheduledTransition::load($entity_type, $entity_id, $field_name, 1) as $transition) { $default_value = $transition->new_sid; break; } } // Prepare a new transition, if still not provided. if (!$transition) { $transition = new WorkflowTransition(array('old_sid' => $default_value, 'stamp' => REQUEST_TIME)); } // Fetch the form ID. This is unique for each entity, to allow multiple form per page (Views, etc.). // Make it uniquer by adding the field name, or else the scheduling of // multiple workflow_fields is not independent of each other. // IF we are truly on a Transition form (so, not a Node Form with widget) // then change the form id, too. $form_id = $this->getFormId(); if (!isset($form_state['build_info']['base_form_id'])) { // Strange: on node form, the base_form_id is node_form, // but on term form, it is not set. // In both cases, it is OK. } else { if ($form_state['build_info']['base_form_id'] == 'workflow_transition_wrapper_form') { $form_state['build_info']['base_form_id'] = 'workflow_transition_form'; } if ($form_state['build_info']['base_form_id'] == 'workflow_transition_form') { $form_state['build_info']['form_id'] = $form_id; } } $workflow_label = $workflow ? check_plain(t($workflow->label())) : ''; // Change settings locally. if (!$field_name) { // This is a Workflow Node workflow. Set widget options as in v7.x-1.2 if ($form_state['build_info']['base_form_id'] == 'node_form') { $field['settings']['widget']['comment'] = isset($workflow->options['comment_log_node']) ? $workflow->options['comment_log_node'] : 1; // vs. ['comment_log_tab']; $field['settings']['widget']['current_status'] = TRUE; } else { $field['settings']['widget']['comment'] = isset($workflow->options['comment_log_tab']) ? $workflow->options['comment_log_tab'] : 1; // vs. ['comment_log_node']; $field['settings']['widget']['current_status'] = TRUE; } } // Capture settings to format the form/widget. $settings_title_as_name = !empty($field['settings']['widget']['name_as_title']); $settings_fieldset = isset($field['settings']['widget']['fieldset']) ? $field['settings']['widget']['fieldset'] : 0; $settings_options_type = $field['settings']['widget']['options']; // The scheduling info can be hidden via field settings, ... // You may not schedule an existing Transition. // You must have the correct permission. $settings_schedule = !empty($field['settings']['widget']['schedule']) && !$transition->isExecuted() && user_access('schedule workflow transitions'); if ($settings_schedule) { if (isset($form_state['step']) && $form_state['step'] == 'views_bulk_operations_config_form') { // On VBO 'modify entity values' form, leave field settings. $settings_schedule = TRUE; } else { // ... and cannot be shown on a Content add page (no $entity_id), // ...but can be shown on a VBO 'set workflow state to..'page (no entity). $settings_schedule = !($entity && !$entity_id); } } $settings_schedule_timezone = !empty($field['settings']['widget']['schedule_timezone']); // Show comment, when both Field and Instance allow this. $settings_comment = $field['settings']['widget']['comment']; // Save the current value of the node in the form, for later Workflow-module specific references. // We add prefix, since #tree == FALSE. $element['workflow']['workflow_entity'] = array('#type' => 'value', '#value' => $this->entity); $element['workflow']['workflow_entity_type'] = array('#type' => 'value', '#value' => $this->entity_type); $element['workflow']['workflow_field'] = array('#type' => 'value', '#value' => $field); $element['workflow']['workflow_instance'] = array('#type' => 'value', '#value' => $instance); // Save the form_id, so the form values can be retrieved in submit function. $element['workflow']['form_id'] = array('#type' => 'value', '#value' => $form_id); // Save the hid, when editing an existing transition. $element['workflow']['workflow_hid'] = array('#type' => 'hidden', '#value' => $transition->hid); // Add the default value in the place where normal fields // have it. This is to cater for 'preview' of the entity. $element['#default_value'] = $default_value; // Decide if we show a widget or a formatter. // There is no need for a widget when the only option is the current sid. // Show state formatter before the rest of the form, // when transition is scheduled or widget is hidden. if (!$show_widget || $transition->isScheduled() || $transition->isExecuted()) { $form['workflow_current_state'] = workflow_state_formatter($entity_type, $entity, $field, $instance, $current_sid); // Set a proper weight, which works for Workflow Options in select list AND action buttons. $form['workflow_current_state']['#weight'] = -0.005; } // Add class following node-form pattern (both on form and container). $workflow_type_id = $workflow ? $workflow->getName() : 'none'; // No workflow on New Action form. $element['workflow']['#attributes']['class'][] = 'workflow-transition-container'; $element['workflow']['#attributes']['class'][] = 'workflow-transition-' . $workflow_type_id . '-container'; // Add class for D7-backwards compatibility (only on container). $element['workflow']['#attributes']['class'][] = 'workflow-form-container'; if (!$show_widget) { // Show no widget. $element['workflow']['workflow_sid']['#type'] = 'value'; $element['workflow']['workflow_sid']['#value'] = $default_value; $element['workflow']['workflow_sid']['#options'] = $options; // In case action buttons need them. $form += $element; return $form; // <---- exit. } else { // Prepare a UI wrapper. This might be a fieldset or a container. if ($settings_fieldset == 0) { // Use 'container'. $element['workflow'] += array('#type' => 'container'); } else { $element['workflow'] += array('#type' => 'fieldset', '#title' => t($workflow_label), '#collapsible' => TRUE, '#collapsed' => $settings_fieldset == 1 ? FALSE : TRUE); } // The 'options' widget. May be removed later if 'Action buttons' are chosen. // The help text is not available for container. Let's add it to the // State box. $help_text = isset($instance['description']) ? $instance['description'] : ''; $element['workflow']['workflow_sid'] = array('#type' => $settings_options_type, '#title' => $settings_title_as_name ? t('Change !name state', array('!name' => $workflow_label)) : t('Target state'), '#access' => TRUE, '#options' => $options, '#default_value' => $default_value, '#description' => $help_text); } // Display scheduling form, but only if entity is being edited and user has // permission. State change cannot be scheduled at entity creation because // that leaves the entity in the (creation) state. if ($settings_schedule == TRUE) { if (variable_get('configurable_timezones', 1) && $user->uid && drupal_strlen($user->timezone)) { $timezone = $user->timezone; } else { $timezone = variable_get('date_default_timezone', 0); } $timezones = drupal_map_assoc(timezone_identifiers_list()); $timestamp = $transition->getTimestamp(); $hours = !$transition->isScheduled() ? '00:00' : format_date($timestamp, 'custom', 'H:i', $timezone); // Add a container, so checkbox and time stay together in extra fields. $element['workflow']['workflow_scheduling'] = array('#type' => 'container', '#tree' => TRUE); $element['workflow']['workflow_scheduling']['scheduled'] = array('#type' => 'radios', '#title' => t('Schedule'), '#options' => array('0' => t('Immediately'), '1' => t('Schedule for state change')), '#default_value' => $transition->isScheduled() ? '1' : '0', '#attributes' => array('class' => array(drupal_html_class('scheduled_' . $form_id)))); $element['workflow']['workflow_scheduling']['date_time'] = array('#type' => 'fieldset', '#title' => t('At'), '#attributes' => array('class' => array('container-inline')), '#prefix' => '<div style="margin-left: 1em;">', '#suffix' => '</div>', '#states' => array('visible' => array('input.' . drupal_html_class('scheduled_' . $form_id) => array('value' => '1')))); $element['workflow']['workflow_scheduling']['date_time']['workflow_scheduled_date'] = array('#type' => 'date', '#default_value' => array('day' => date('j', $timestamp), 'month' => date('n', $timestamp), 'year' => date('Y', $timestamp))); $element['workflow']['workflow_scheduling']['date_time']['workflow_scheduled_hour'] = array('#type' => 'textfield', '#title' => t('Time'), '#maxlength' => 7, '#size' => 6, '#default_value' => $hours, '#element_validate' => array('_workflow_transition_form_element_validate_time')); $element['workflow']['workflow_scheduling']['date_time']['workflow_scheduled_timezone'] = array('#type' => $settings_schedule_timezone ? 'select' : 'hidden', '#title' => t('Time zone'), '#options' => $timezones, '#default_value' => array($timezone => $timezone)); $element['workflow']['workflow_scheduling']['date_time']['workflow_scheduled_help'] = array('#type' => 'item', '#prefix' => '<br />', '#description' => t('Please enter a time. If no time is included, the default will be midnight on the specified date. The current time is: @time.', array('@time' => format_date(REQUEST_TIME, 'custom', 'H:i', $timezone)))); } $element['workflow']['workflow_comment'] = array('#type' => 'textarea', '#required' => $settings_comment == '2', '#access' => $settings_comment != '0', '#title' => t('Workflow comment'), '#description' => t('A comment to put in the workflow log.'), '#default_value' => $transition->comment, '#rows' => 2); // Add the fields and extra_fields from the WorkflowTransition. // Because we have a 'workflow' wrapper, it doesn't work flawlessly. field_attach_form('WorkflowTransition', $transition, $element['workflow'], $form_state); // Undo the following elements from field_attach_from. They mess up $this->getTransition(). // - '#parents' corrupts the Defaultwidget. unset($element['workflow']['#parents']); // - '#pre_render' adds the exra_fields from workflow_field_extra_fields(). // That doesn't work, since 'workflow' is not of #type 'form', but // 'container' or 'fieldset', and must be executed separately,. $element['workflow']['#pre_render'] = array_diff($element['workflow']['#pre_render'], array('_field_extra_fields_pre_render')); // Add extra fields. $rescue_value = $element['workflow']['#type']; $element['workflow']['#type'] = 'form'; $element['workflow'] = _field_extra_fields_pre_render($element['workflow']); $element['workflow']['#type'] = $rescue_value; // Finally, add Submit buttons/Action buttons. // Either a default 'Submit' button is added, or a button per permitted state. if ($settings_options_type == 'buttons') { // How do action buttons work? See also d.o. issue #2187151. // Create 'action buttons' per state option. Set $sid property on each button. // 1. Admin sets ['widget']['options']['#type'] = 'buttons'. // 2. This function formElement() creates 'action buttons' per state option; // sets $sid property on each button. // 3. User clicks button. // 4. Callback _workflow_transition_form_validate_buttons() sets proper State. // 5. Callback _workflow_transition_form_validate_buttons() sets Submit function. // @todo: this does not work yet for the Add Comment form. // Performance: inform workflow_form_alter() to do its job. _workflow_use_action_buttons(TRUE); // Hide the options box. It will be replaced by action buttons. $element['workflow']['workflow_sid']['#type'] = 'select'; $element['workflow']['workflow_sid']['#access'] = FALSE; } if ($form_state['build_info']['base_form_id'] == 'workflow_transition_form') { // Add action buttons on WorkflowTransitionForm (history tab, formatter) // but not on Entity form, and not if action_buttons is selected. // you can explicitly NOT add a submit button, e.g., on VBO page. if ($instance['widget']['settings']['submit_function'] !== '') { // @todo D8: put buttons outside of 'workflow' element, in the standard location. $element['workflow']['actions']['#type'] = 'actions'; $element['workflow']['actions']['submit'] = array('#type' => 'submit', '#value' => t('Update workflow'), '#weight' => -5, '#attributes' => array('class' => array('form-save-default-button'))); // The 'add submit' can explicitly set by workflowfield_field_formatter_view(), // to add the submit button on the Content view page and the Workflow history tab. // Add a submit button, but only on Entity View and History page. // Add the submit function only if one provided. Set the submit_callback accordingly. if (!empty($instance['widget']['settings']['submit_function'])) { $element['workflow']['actions']['submit']['#submit'] = array($instance['widget']['settings']['submit_function']); } else { // '#submit' Must be empty, or else the submit function is not called. // $element['workflow']['actions']['submit']['#submit'] = array(); } } } /* $submit_functions = empty($instance['widget']['settings']['submit_function']) ? array() : array($instance['widget']['settings']['submit_function']); if ($settings_options_type == 'buttons' || $submit_functions) { } else { // In some cases, no submit callback function is specified. This is // explicitly done on e.g., the node edit form, because the workflow form // is 'just a field'. // So, no Submit button is to be shown. } */ $form += $element; // Add class following node-form pattern (both on form and container). $workflow_type_id = $workflow ? $workflow->getName() : 'none'; // No workflow on New Action form. $form['#attributes']['class'][] = 'workflow-transition-form'; $form['#attributes']['class'][] = 'workflow-transition-' . $workflow_type_id . '-form'; return $form; }
/** * Execute a transition (change state of a node). * @deprecated: workflow_execute_transition() --> WorkflowTransition::execute(). * * @param bool $force * If set to TRUE, workflow permissions will be ignored. * * @return int * new state ID. If execution failed, old state ID is returned, */ public function execute($force = FALSE) { global $user; $old_sid = $this->old_sid; $new_sid = $this->new_sid; $entity_type = $this->entity_type; $entity_id = $this->entity_id; $entity = $this->getEntity(); // Entity may not be loaded, yet. $field_name = $this->field_name; if ($old_sid == $new_sid) { // Stop if not going to a different state. // Write comment into history though. if ($this->comment) { $this->stamp = REQUEST_TIME; if (!$field_name) { // @todo D8: remove; this is only for Node API. $entity->workflow_stamp = REQUEST_TIME; workflow_update_workflow_node_stamp($entity_id, $this->stamp); } $result = module_invoke_all('workflow', 'transition pre', $old_sid, $new_sid, $entity, $force, $entity_type, $field_name); $this->save(); if (!$field_name) { // @todo D8: remove; this is only for Node API. unset($entity->workflow_comment); // @todo D8: remove; this line is only for Node API. } $result = module_invoke_all('workflow', 'transition post', $old_sid, $new_sid, $entity, $force, $entity_type, $field_name); } // Clear any references in the scheduled listing. foreach (WorkflowScheduledTransition::load($entity_type, $entity_id, $field_name) as $scheduled_transition) { $scheduled_transition->delete(); } return $new_sid; } if (!$force) { // Make sure this transition is allowed. $result = module_invoke_all('workflow', 'transition permitted', $old_sid, $new_sid, $entity, $force, $entity_type, $field_name); // Did anybody veto this choice? if (in_array(FALSE, $result)) { // If vetoed, quit. return $old_sid; } } // Let other modules modify the comment. // @todo D8: remove all but last items from $context. $context = array('node' => $entity, 'sid' => $new_sid, 'old_sid' => $old_sid, 'uid' => $this->uid, 'transition' => $this); drupal_alter('workflow_comment', $this->comment, $context); $args = array('%user' => $user->name, '%old' => $old_sid, '%new' => $new_sid); $transition = workflow_get_workflow_transitions_by_sid_target_sid($old_sid, $new_sid); if (!$transition && !$force) { watchdog('workflow', 'Attempt to go to nonexistent transition (from %old to %new)', $args, WATCHDOG_ERROR); return $old_sid; } // Make sure this transition is valid and allowed for the current user. // Check allow-ability of state change if user is not superuser (might be cron). if ($user->uid != 1 && !$force) { if (!workflow_transition_allowed($transition->tid, array_merge(array_keys($user->roles), array('author')))) { watchdog('workflow', 'User %user not allowed to go from state %old to %new', $args, WATCHDOG_NOTICE); return $old_sid; } } // Invoke a callback indicating a transition is about to occur. // Modules may veto the transition by returning FALSE. $result = module_invoke_all('workflow', 'transition pre', $old_sid, $new_sid, $entity, $force, $entity_type, $field_name); // Stop if a module says so. if (in_array(FALSE, $result)) { watchdog('workflow', 'Transition vetoed by module.'); return $old_sid; } // Log the new state in {workflow_node_history}. // This is only valid for Node API. if (!$field_name) { // If the node does not have an existing 'workflow' property, save the $old_sid there, so it can be logged. if (!isset($entity->workflow)) { $entity->workflow = $old_sid; } // Change the state for {workflow_node}. // The equivalent for Field API is in WorkflowDefaultWidget::submit. $data = array('nid' => $entity_id, 'sid' => $new_sid, 'uid' => isset($entity->workflow_uid) ? $entity->workflow_uid : $user->uid, 'stamp' => REQUEST_TIME); workflow_update_workflow_node($data); $entity->workflow = $new_sid; } // Log the transition in {workflow_node_history}. $this->is_executed = TRUE; $this->save(); // Register state change with watchdog. if ($state = WorkflowState::load($new_sid)) { $workflow = $state->getWorkflow(); if (!empty($workflow->options['watchdog_log'])) { $entity_type_info = entity_get_info($entity_type); $message = $this->isScheduled() ? 'Scheduled state change of @type %label to %state_name executed' : 'State of @type %label set to %state_name'; $args = array('@type' => $entity_type_info['label'], '%label' => entity_label($entity_type, $entity), '%state_name' => $state->label()); $uri = entity_uri($entity_type, $entity); watchdog('workflow', $message, $args, WATCHDOG_NOTICE, l('view', $uri['path'])); } } // Notify modules that transition has occurred. // Action triggers should take place in response to this callback, not the previous one. module_invoke_all('workflow', 'transition post', $old_sid, $new_sid, $entity, $force, $entity_type, $field_name); // Clear any references in the scheduled listing. foreach (WorkflowScheduledTransition::load($entity_type, $entity_id, $field_name) as $scheduled_transition) { $scheduled_transition->delete(); } return $new_sid; }