updated workflow module to last dev

This commit is contained in:
2017-02-07 11:09:53 +01:00
parent 363a992240
commit a44ba1e90b
119 changed files with 17242 additions and 5677 deletions
@@ -0,0 +1,811 @@
<?php
/**
* @file
* Contains workflow\includes\Entity\Workflow.
* Contains workflow\includes\Entity\WorkflowController.
*/
// Include file to avoid drush upgrade errors.
include_once('WorkflowInterface.php');
class Workflow extends Entity implements WorkflowInterface {
public $wid = 0;
public $name = '';
public $tab_roles = array();
public $options = array();
protected $creation_sid = 0;
// Attached States.
public $states = NULL;
public $transitions = NULL;
/**
* CRUD functions.
*/
// public function __construct(array $values = array(), $entityType = NULL) {
// return parent::__construct($values, $entityType);
// }
public function __clone() {
// Clone the arrays of States and Transitions.
foreach ($this->states as &$state) {
$state = clone $state;
}
foreach ($this->transitions as &$transition) {
$transition = clone $transition;
}
}
/**
* Given information, update or insert a new workflow.
*
* This also handles importing, rebuilding, reverting from Features,
* as defined in workflow.features.inc.
*
* When changing this function, test with the following situations:
* - maintain Workflow in Admin UI;
* - clone Workflow in Admin UI;
* - create/revert/rebuild Workflow with Features; @see workflow.features.inc
* - save Workflow programmatically;
*/
public function save($create_creation_state = TRUE) {
// Are we saving a new Workflow?
$is_new = !empty($this->is_new);
// Are we rebuilding, reverting a new Workflow? @see workflow.features.inc
$is_rebuild = !empty($this->is_rebuild) || !empty($this->is_reverted);
if ($is_rebuild) {
$this->is_rebuild = TRUE;
$this->preRebuild();
}
$return = parent::save();
// On either clone or rebuild from features.
if ($is_new || $is_rebuild) {
$this->rebuildInternals();
if ($is_rebuild) {
// The above may have marked us overridden!
$this->status = ENTITY_IN_CODE;
parent::save();
}
}
// Make sure a Creation state exists.
if ($is_new) {
$state = $this->getCreationState();
}
workflow_reset_cache($this->wid);
return $return;
}
/**
* Rebuild things that get saved with this entity.
*/
protected function preRebuild() {
// Remap roles. They can come from another system with shifted role IDs.
// See also https://drupal.org/node/1702626 .
$this->rebuildRoles($this->tab_roles);
// After update.php or import feature, label might be empty. @todo: remove in D8.
if (empty($this->label)) {
$this->label = $this->name;
}
}
/**
* Rebuild internals that get saved separately.
*/
protected function rebuildInternals() {
// Insert the type_map when building from Features.
if (isset($this->typeMap)) {
foreach ($this->typeMap as $node_type) {
workflow_insert_workflow_type_map($node_type, $this->wid);
}
}
// Index the existing states and transitions by name.
$db_name_map = WorkflowState::getStates($this->wid, TRUE); // sid -> state.
$db_states = array(); // name -> state.
foreach ($db_name_map as $state) {
$db_states[$state->getName()] = $state;
}
$db_transitions = array();
foreach (entity_load('WorkflowConfigTransition') as $transition) {
if ($transition->wid == $this->wid) {
$start_name = $db_name_map[$transition->sid]->getName();
$end_name = $db_name_map[$transition->target_sid]->getName();
$name = WorkflowConfigTransition::machineName($start_name, $end_name);
$db_transitions[$name] = $transition;
}
}
// Update/create states.
$states = isset($this->states) ? $this->states : array();
$saved_states = array(); // Saved states: key -> sid.
$saved_state_names = array();
foreach ($states as $key => $data) {
$data = (array)$data;
$name = $data['name'];
if (isset($db_states[$name])) {
$state = $db_states[$name];
}
else {
$state = $this->createState($name, FALSE);
}
$state->wid = $this->wid;
$state->state = $data['state'];
$state->weight = $data['weight'];
$state->sysid = $data['sysid'];
if (!$data['status']) {
$this->rebuildStateInactive($state);
}
$state->status = $data['status'];
$state->save();
unset($db_states[$name]);
$saved_states[$key] = $state;
$saved_state_names[$state->sid] = $key;
}
// Update/create transitions.
$transitions = isset($this->transitions) ? $this->transitions : array();
foreach ($transitions as $name => $data) {
$data = (array)$data;
if (is_numeric($name)) {
$start_state = $saved_states[$saved_state_names[$data['sid']]];
$end_state = $saved_states[$saved_state_names[$data['target_sid']]];
$name = WorkflowConfigTransition::machineName($start_state->getName(),
$end_state->getName());
}
else {
$start_state = $saved_states[$data['start_state']];
$end_state = $saved_states[$data['end_state']];
}
if (isset($db_transitions[$name])) {
$transition = $db_transitions[$name];
}
else {
$transition = $this->createTransition($start_state->sid,
$end_state->sid);
}
$transition->wid = $this->wid;
$transition->sid = $start_state->sid;
$transition->target_sid = $end_state->sid;
$transition->label = $data['label'];
$transition->roles = $data['roles'];
$this->rebuildRoles($transition->roles);
$transition->save();
unset($db_transitions[$name]);
}
// Any states/transitions left in $db_states/transitions need deletion.
foreach ($db_states as $state) {
$this->rebuildStateInactive($state);
$state->delete();
}
foreach ($db_transitions as $transition) {
$transition->delete();
}
// Clear the caches, and set $this->states and $this->transitions.
$this->states = $this->transitions = NULL;
$this->getStates(TRUE, TRUE);
$this->getTransitions(FALSE, array(), TRUE);
}
/**
* Handle a state becoming inactive during a rebuild.
*/
protected function rebuildStateInactive($state) {
if (!$state->isActive()) {
return;
}
// TODO: What should we do in this case? Is this safe?
$state->deactivate(NULL);
}
/**
* Given a wid, delete the workflow and its data.
*
* @deprecated: workflow_delete_workflows_by_wid() --> Workflow::delete().
*/
public function delete() {
$wid = $this->wid;
// Notify any interested modules before we delete the workflow.
// E.g., Workflow Node deletes the {workflow_type_map} record.
module_invoke_all('workflow', 'workflow delete', $wid, NULL, NULL, FALSE);
// Delete associated state (also deletes any associated transitions).
foreach ($this->getStates($all = TRUE) as $state) {
$state->deactivate(0);
$state->delete();
}
// Delete the workflow.
db_delete('workflows')->condition('wid', $wid)->execute();
}
/**
* Validate the workflow. Generate a message if not correct.
*
* This function is used on the settings page of:
* - Workflow node: workflow_admin_ui_type_map_form()
* - Workflow field: WorkflowItem->settingsForm()
*
* @return bool
* $is_valid
*/
public function isValid() {
$is_valid = TRUE;
// Don't allow workflows with no states. There should always be a creation state.
$states = $this->getStates($all = FALSE);
if (count($states) < 1) {
// That's all, so let's remind them to create some states.
$message = t('Workflow %workflow has no states defined, so it cannot be assigned to content yet.',
array('%workflow' => $this->getName()));
drupal_set_message($message, 'warning');
// Skip allowing this workflow.
$is_valid = FALSE;
}
// Also check for transitions, at least out of the creation state. Use 'ALL' role.
$transitions = $this->getTransitionsBySid($this->getCreationSid(), $roles = 'ALL');
if (count($transitions) < 1) {
// That's all, so let's remind them to create some transitions.
$message = t('Workflow %workflow has no transitions defined, so it cannot be assigned to content yet.',
array('%workflow' => $this->getName()));
drupal_set_message($message, 'warning');
// Skip allowing this workflow.
$is_valid = FALSE;
}
// If the Workflow is mapped to a node type, check if workflow->options is set.
if ($this->getTypeMap() && !count($this->options)) {
// That's all, so let's remind them to create some transitions.
$message = t('Please maintain Workflow %workflow on its <a href="@url">settings</a> page.',
array(
'%workflow' => $this->getName(),
'@url' => url('admin/config/workflow/workflow/manage/' . $this->wid),
)
);
drupal_set_message($message, 'warning');
// Skip allowing this workflow.
// $is_valid = FALSE;
}
return $is_valid;
}
/**
* Returns if the Workflow may be deleted.
*
* @return bool $is_deletable
* TRUE if a Workflow may safely be deleted.
*/
public function isDeletable() {
$is_deletable = FALSE;
// May not be deleted if a TypeMap exists.
if ($this->getTypeMap()) {
return $is_deletable;
}
// May not be deleted if assigned to a Field.
foreach (_workflow_info_fields() as $field) {
if ($field['settings']['wid'] == $this->wid) {
return $is_deletable;
}
}
// May not be deleted if a State is assigned to a state.
foreach ($this->getStates(TRUE) as $state) {
if ($state->count()) {
return $is_deletable;
}
}
$is_deletable = TRUE;
return $is_deletable;
}
/**
* Property functions.
*/
/**
* Returns the workflow id.
*
* @return int
* $wid
*/
public function getWorkflowId() {
return $this->wid;
}
/**
* Create a new state for this workflow.
*
* @param string $name
* The untranslated human readable label of the state.
* @param bool $save
* Indicator if the new state must be saved. Normally, the new State is
* saved directly in the database. This is because you can use States only
* with Transitions, and they rely on State IDs which are generated
* magically when saving the State. But you may need a temporary state.
*
* @return WorkflowState
*/
public function createState($name, $save = TRUE) {
$wid = $this->wid;
$state = workflow_state_load_by_name($name, $wid);
if (!$state) {
$state = entity_create('WorkflowState', array('name' => $name, 'state' => $name, 'wid' => $wid));
if ($save) {
$state->save();
}
}
$state->setWorkflow($this);
// Maintain the new object in the workflow.
$this->states[$state->sid] = $state;
return $state;
}
/**
* Gets the initial state for a newly created entity.
*/
public function getCreationState() {
$sid = $this->getCreationSid();
return ($sid) ? $this->getState($sid) : $this->createState(WORKFLOW_CREATION_STATE_NAME);
}
/**
* Gets the ID of the initial state for a newly created entity.
*/
public function getCreationSid() {
if (!$this->creation_sid) {
foreach ($this->getStates($all = TRUE) as $state) {
if ($state->isCreationState()) {
$this->creation_sid = $state->sid;
}
}
}
return $this->creation_sid;
}
/**
* Gets the first valid state ID, after the creation state.
*
* Uses WorkflowState::getOptions(), because this does a access check.
* The first State ID is user-dependent!
*/
public function getFirstSid($entity_type, $entity, $field_name, $user, $force) {
$creation_state = $this->getCreationState();
$options = $creation_state->getOptions($entity_type, $entity, $field_name, $user, $force);
if ($options) {
$keys = array_keys($options);
$sid = $keys[0];
}
else {
// This should never happen, but it did during testing.
drupal_set_message(t('There are no workflow states available. Please notify your site administrator.'), 'error');
$sid = 0;
}
return $sid;
}
/**
* Returns the next state for the current state.
*
* @param string $entity_type
* The type of the entity at hand.
* @param object $entity
* The entity at hand. May be NULL (E.g., on a Field settings page).
* @param $field_name
* @param $user
* @param bool $force
*
* @return int $sid
* A state ID.
*/
public function getNextSid($entity_type, $entity, $field_name, $user, $force = FALSE) {
$new_sid = workflow_node_current_state($entity, $entity_type, $field_name);
if ($new_sid && $new_state = workflow_state_load_single($new_sid)) {
/* @var $current_state WorkflowState */
$options = $new_state->getOptions($entity_type, $entity, $field_name, $user, $force);
// Loop over every option. To find the next one.
$flag = $new_state->isCreationState();
foreach ($options as $sid => $name) {
if ($flag) {
$new_sid = $sid;
break;
}
if ($sid == $new_state->sid) {
$flag = TRUE;
}
}
}
return $new_sid;
}
/**
* Gets all states for a given workflow.
*
* @param mixed $all
* Indicates to which states to return.
* - TRUE = all, including Creation and Inactive;
* - FALSE = only Active states, not Creation;
* - 'CREATION' = only Active states, including Creation.
*
* @return array
* An array of WorkflowState objects.
*/
public function getStates($all = FALSE, $reset = FALSE) {
if ($this->states === NULL || $reset) {
$this->states = $this->wid ? WorkflowState::getStates($this->wid, $reset) : array();
}
// Do not unset, but add to array - you'll remove global objects otherwise.
$states = array();
foreach ($this->states as $state) {
if ($all === TRUE) {
$states[$state->sid] = $state;
}
elseif (($all === FALSE) && ($state->isActive() && !$state->isCreationState())) {
$states[$state->sid] = $state;
}
elseif (($all == 'CREATION') && ($state->isActive() || $state->isCreationState())) {
$states[$state->sid] = $state;
}
}
return $states;
}
/**
* Gets a state for a given workflow.
*
* @param mixed $key
* A state ID or state Name.
*
* @return WorkflowState
* A WorkflowState object.
*/
public function getState($key) {
if (is_numeric($key)) {
return workflow_state_load_single($key, $this->wid);
}
else {
return workflow_state_load_by_name($key, $this->wid);
}
}
/**
* Creates a Transition for this workflow.
*/
public function createTransition($sid, $target_sid, $values = array()) {
$workflow = $this;
if (is_numeric($sid) && is_numeric($target_sid)) {
$values['sid'] = $sid;
$values['target_sid'] = $target_sid;
}
else {
$state = $workflow->getState($sid);
$target_state = $workflow->getState($target_sid);
$values['sid'] = $state->sid;
$values['target_sid'] = $target_state->sid;
}
// First check if this transition already exists.
if ($transitions = entity_load('WorkflowConfigTransition', FALSE, $values)) {
$transition = reset($transitions);
}
else {
$values['wid'] = $workflow->wid;
$transition = entity_create('WorkflowConfigTransition', $values);
$transition->save();
}
$transition->setWorkflow($this);
// Maintain the new object in the workflow.
$this->transitions[$transition->tid] = $transition;
return $transition;
}
/**
* Sorts all Transitions for this workflow, according to State weight.
*
* This is only needed for the Admin UI.
*/
public function sortTransitions() {
// Sort the transitions on state weight.
usort($this->transitions, '_workflow_transitions_sort_by_weight');
}
/**
* Loads all allowed ConfigTransitions for this workflow.
*
* @param mixed $tids
* Array of Transitions IDs. If FALSE, show all transitions.
* @param array $conditions
* $conditions['sid'] : if provided, a 'from' State ID.
* $conditions['target_sid'] : if provided, a 'to' state ID.
* $conditions['roles'] : if provided, an array of roles, or 'ALL'.
* @param bool $reset
* Indicator to reset the cache.
*
* @return array
* An array of keyed transitions.
*/
public function getTransitions($tids = FALSE, array $conditions = array(), $reset = FALSE) {
$config_transitions = array();
// Get valid + creation states.
$states = $this->getStates('CREATION');
// Get filters on 'from' states, 'to' states, roles.
$sid = isset($conditions['sid']) ? $conditions['sid'] : FALSE;
$target_sid = isset($conditions['target_sid']) ? $conditions['target_sid'] : FALSE;
$roles = isset($conditions['roles']) ? $conditions['roles'] : 'ALL';
// Cache all transitions in the workflow.
// We may have 0 transitions....
if ($this->transitions === NULL) {
$this->transitions = array();
// Get all transitions. (Even from other workflows. :-( )
$config_transitions = entity_load('WorkflowConfigTransition', $tids, array(), $reset);
foreach ($config_transitions as &$config_transition) {
if (isset($states[$config_transition->sid])) {
$config_transition->setWorkflow($this);
$this->transitions[$config_transition->tid] = $config_transition;
}
}
$this->sortTransitions();
}
$config_transitions = array();
foreach ($this->transitions as &$config_transition) {
if (!isset($states[$config_transition->sid])) {
// Not a valid transition for this workflow.
}
elseif ($sid && $sid != $config_transition->sid) {
// Not the requested 'from' state.
}
elseif ($target_sid && $target_sid != $config_transition->target_sid) {
// Not the requested 'to' state.
}
elseif ($roles == 'ALL' || $config_transition->isAllowed($roles)) {
// Transition is allowed, permitted. Add to list.
$config_transition->setWorkflow($this);
$config_transitions[$config_transition->tid] = $config_transition;
}
else {
// Transition is otherwise not allowed.
}
}
return $config_transitions;
}
public function getTransitionsByTid($tid, $roles = '', $reset = FALSE) {
$conditions = array(
'roles' => $roles,
);
return $this->getTransitions(array($tid), $conditions, $reset);
}
public function getTransitionsBySid($sid, $roles = '', $reset = FALSE) {
$conditions = array(
'sid' => $sid,
'roles' => $roles,
);
return $this->getTransitions(FALSE, $conditions, $reset);
}
public function getTransitionsByTargetSid($target_sid, $roles = '', $reset = FALSE) {
$conditions = array(
'target_sid' => $target_sid,
'roles' => $roles,
);
return $this->getTransitions(FALSE, $conditions, $reset);
}
/**
* Get a specific transition. Therefore, use $roles = 'ALL'.
*/
public function getTransitionsBySidTargetSid($sid, $target_sid, $roles = 'ALL', $reset = FALSE) {
$conditions = array(
'sid' => $sid,
'target_sid' => $target_sid,
'roles' => $roles,
);
return $this->getTransitions(FALSE, $conditions, $reset);
}
/**
* Gets a the type map for a given workflow.
*
* @param int $sid
* A state ID.
*
* @return array
* An array of typemaps.
*/
public function getTypeMap() {
$result = array();
$type_maps = module_exists('workflownode') ? workflow_get_workflow_type_map_by_wid($this->wid) : array();
foreach ($type_maps as $map) {
$result[] = $map->type;
}
return $result;
}
/**
* Gets a setting from the state object.
*/
public function getSetting($key, array $field = array()) {
switch ($key) {
case 'watchdog_log':
if (isset($this->options['watchdog_log'])) {
// This is set via Node API.
return $this->options['watchdog_log'];
}
elseif ($field) {
if (isset($field['settings']['watchdog_log'])) {
// This is set via Field API.
return $field['settings']['watchdog_log'];
}
}
drupal_set_message('Setting Workflow::getSetting(' . $key . ') does not exist', 'error');
break;
default:
drupal_set_message('Setting Workflow::getSetting(' . $key . ') does not exist', 'error');
}
}
/**
* Mimics Entity API functions.
*/
public function getName() {
return $this->name;
}
protected function defaultLabel() {
return isset($this->label) ? $this->label : '';
}
protected function defaultUri() {
return array('path' => 'admin/config/workflow/workflow/manage/' . $this->wid);
}
protected function rebuildRoles(array &$roles) {
$role_map = isset($this->system_roles) ? $this->system_roles : array();
if (!$role_map) {
return;
}
// See also https://drupal.org/node/1702626 .
$new_roles = array();
foreach ($roles as $key => $rid) {
if ($rid == WORKFLOW_ROLE_AUTHOR_RID) {
$new_roles[$rid] = $rid;
}
else {
if ($role = user_role_load_by_name($role_map[$rid])) {
$new_roles[$role->rid] = (int)($role->rid);
}
}
}
$roles = $new_roles;
}
}
/**
* Helper function to sort the transitions.
*
* @param WorkflowConfigTransition $a
* @param WorkflowConfigTransition $b
*
* @return int
*/
function _workflow_transitions_sort_by_weight($a, $b) {
// First sort on From-State.
$old_state_a = $a->getOldState();
$old_state_b = $b->getOldState();
if ($old_state_a->weight < $old_state_b->weight) return -1;
if ($old_state_a->weight > $old_state_b->weight) return +1;
// Then sort on To-State.
$new_state_a = $a->getNewState();
$new_state_b = $b->getNewState();
if ($new_state_a->weight < $new_state_b->weight) return -1;
if ($new_state_a->weight > $new_state_b->weight) return +1;
return 0;
}
/**
* Implements a controller class for Workflow.
*/
class WorkflowController extends EntityAPIControllerExportable {
// public function create(array $values = array()) { return parent::create($values); }
// public function load($ids = array(), $conditions = array()) { }
public function delete($ids, DatabaseTransaction $transaction = NULL) {
// @todo: replace WorkflowController::delete() with parent.
// @todo: throw error if not workflow->isDeletable().
foreach ($ids as $wid) {
if ($workflow = workflow_load($wid)) {
$workflow->delete();
}
}
$this->resetCache();
}
/**
* Overrides DrupalDefaultEntityController::cacheGet().
*
* Override default function, due to Core issue #1572466.
*/
protected function cacheGet($ids, $conditions = array()) {
// Load any available entities from the internal cache.
if ($ids === FALSE && !$conditions) {
return $this->entityCache;
}
return parent::cacheGet($ids, $conditions);
}
/**
* Overrides DrupalDefaultEntityController::cacheSet().
*/
/*
// protected function cacheSet($entities) { }
// return parent::cacheSet($entities);
// }
*/
/**
* Overrides DrupalDefaultEntityController::resetCache().
*
* Called by workflow_reset_cache, to
* Reset the Workflow when States, Transitions have been changed.
*/
// public function resetCache(array $ids = NULL) {
// parent::resetCache($ids);
// }
/**
* Overrides DrupalDefaultEntityController::attachLoad().
*/
protected function attachLoad(&$queried_entities, $revision_id = FALSE) {
foreach ($queried_entities as $entity) {
// Load the states, so they are already present on the next (cached) load.
$entity->states = $entity->getStates($all = TRUE);
$entity->transitions = $entity->getTransitions(FALSE);
$entity->typeMap = $entity->getTypeMap();
}
parent::attachLoad($queried_entities, $revision_id);
}
}
@@ -0,0 +1,192 @@
<?php
/**
* @file
* Contains workflow\includes\Entity\WorkflowConfigTransition.
* Contains workflow\includes\Entity\WorkflowConfigTransitionController.
*/
/**
* Implements a configurated Transition.
*/
class WorkflowConfigTransition extends Entity {
// Transition data.
public $tid = 0;
// public $old_sid = 0;
// public $new_sid = 0;
public $sid = 0; // @todo D8: remove $sid, use $new_sid. (requires conversion of Views displays.)
public $target_sid = 0;
public $roles = array();
// Extra fields.
public $wid = 0;
// The following must explicitely defined, and not be public, to avoid errors when exporting with json_encode().
protected $workflow = NULL;
/**
* Entity class functions.
*/
/*
// Implementing clone needs a list of tid-less transitions, and a conversion
// of sids for both States and ConfigTransitions.
// public function __clone() {}
*/
public function __construct(array $values = array(), $entityType = NULL) {
// Please be aware that $entity_type and $entityType are different things!
return parent::__construct($values, $entityType = 'WorkflowConfigTransition');
}
/**
* Permanently deletes the entity.
*/
public function delete() {
// Notify any interested modules before we delete, in case there's data needed.
// @todo D8: this can be replaced by a hook_entity_delete(?)
module_invoke_all('workflow', 'transition delete', $this->tid, NULL, NULL, FALSE);
return parent::delete();
}
protected function defaultLabel() {
return $this->label;
}
protected function defaultUri() {
return array('path' => 'admin/config/workflow/workflow/manage/' . $this->wid . '/transitions/');
}
/**
* Property functions.
*/
/**
* Returns the Workflow object of this State.
*
* @param Workflow $workflow
* An optional workflow object. Can be used as a setter.
*
* @return Workflow
* Workflow object.
*/
public function setWorkflow($workflow) {
$this->wid = $workflow->wid;
$this->workflow = $workflow;
}
public function getWorkflow() {
if (isset($this->workflow)) {
return $this->workflow;
}
return workflow_load_single($this->wid);
}
public function getOldState() {
return workflow_state_load_single($this->sid);
}
public function getNewState() {
return workflow_state_load_single($this->target_sid);
}
/**
* Verifies if the given transition is allowed.
*
* - In settings;
* - In permissions;
* - By permission hooks, implemented by other modules.
*
* @return bool
* TRUE if OK, else FALSE.
*/
public function isAllowed($user_roles) {
if ($user_roles == 'ALL') {
// Superuser.
return TRUE;
}
elseif ($user_roles) {
return array_intersect($user_roles, $this->roles) == TRUE;
}
return TRUE;
}
/**
* Generate a machine name for a transition.
*/
public static function machineName($start_name, $end_name) {
$new_name = sprintf("%s_to_%s", $start_name, $end_name);
// Special case: replace parens in creation state transition names.
$new_name = str_replace("(creation)", "_creation", $new_name);
return $new_name;
}
public function save() {
parent::save();
// Ensure Workflow is marked overridden.
$workflow = $this->getWorkflow();
if ($workflow->status == ENTITY_IN_CODE) {
$workflow->status = ENTITY_OVERRIDDEN;
$workflow->save();
}
}
}
/**
* Implements a controller class for WorkflowConfigTransition.
*
* The 'true' controller class is 'Workflow'.
*/
class WorkflowConfigTransitionController extends EntityAPIController {
/**
* Overrides DrupalDefaultEntityController::cacheGet().
*
* Override default function, due to core issue #1572466.
*/
protected function cacheGet($ids, $conditions = array()) {
// Load any available entities from the internal cache.
if ($ids === FALSE && !$conditions) {
return $this->entityCache;
}
return parent::cacheGet($ids, $conditions);
}
public function save($entity, DatabaseTransaction $transaction = NULL) {
$workflow = $entity->getWorkflow();
// To avoid double posting, check if this transition already exist.
if (empty($entity->tid)) {
if ($workflow) {
$config_transitions = $workflow->getTransitionsBySidTargetSid($entity->sid, $entity->target_sid);
$config_transition = reset($config_transitions);
if ($config_transition) {
$entity->tid = $config_transition->tid;
}
}
}
// Create the machine_name. This can be used to rebuild/revert the Feature in a target system.
if (empty($entity->name)) {
$entity->name = $entity->sid . '_' . $entity->target_sid;
}
$return = parent::save($entity, $transaction);
if ($return) {
// Save in current workflow for the remainder of this page request.
// Keep in sync with Workflow::getTransitions() !
$workflow = $entity->getWorkflow();
if ($workflow) {
$workflow->transitions[$entity->tid] = $entity;
// $workflow->sortTransitions();
}
}
// Reset the cache for the affected workflow, to force reload upon next page_load.
workflow_reset_cache($entity->wid);
return $return;
}
}
@@ -0,0 +1,180 @@
<?php
/**
* @file
* Contains Drupal\workflow\Entity\WorkflowInterface.
*/
// D8: namespace Drupal\workflow\Entity;
// D8: use Drupal\Core\Config\Entity\ConfigEntityBase;
// D8: use Drupal\Core\Session\AccountInterface;
/**
* Defines a common interface for Workflow*Transition* objects.
*
* @see \Drupal\workflow\Entity\WorkflowConfigTransition
* @see \Drupal\workflow\Entity\WorkflowTransition
* @see \Drupal\workflow\Entity\WorkflowScheduledTransition
*/
interface WorkflowInterface {
/**
* Retrieves the entity manager service.
*
* @return \Drupal\workflow\Entity\WorkflowManagerInterface
* The entity manager service.
*/
//D8: public static function workflowManager();
/**
* Returns the workflow id.
*
* @return string
* $wid
*/
public function getWorkflowId();
/**
* Validate the workflow. Generate a message if not correct.
*
* This function is used on the settings page of:
* - Workflow field: WorkflowItem->settingsForm()
*
* @return bool
* $is_valid
*/
public function isValid();
/**
* Returns if the Workflow may be deleted.
*
* @return bool $is_deletable
* TRUE if a Workflow may safely be deleted.
*/
public function isDeletable();
/**
* Create a new state for this workflow.
*
* @param string $name
* The untranslated human readable label of the state.
* @param bool $save
* Indicator if the new state must be saved. Normally, the new State is
* saved directly in the database. This is because you can use States only
* with Transitions, and they rely on State IDs which are generated
* magically when saving the State. But you may need a temporary state.
* @return \Drupal\workflow\Entity\WorkflowState
* The new state.
*/
public function createState($sid, $save = TRUE);
/**
* Gets the initial state for a newly created entity.
*/
public function getCreationState();
/**
* Gets the ID of the initial state for a newly created entity.
*/
public function getCreationSid();
/**
* Gets the first valid state ID, after the creation state.
*
* Uses WorkflowState::getOptions(), because this does an access check.
* The first State ID is user-dependent!
*/
public function getFirstSid($entity_type, $entity, $field_name, $user, $force);
/**
* Returns the next state for the current state.
* Is used in VBO Bulk actions.
*
* @param string $entity_type
* The type of the entity at hand.
* @param object $entity
* The entity at hand. May be NULL (E.g., on a Field settings page).
* @param $field_name
* @param $user
* @param bool $force
*
* @return array
* An array of sid=>label pairs.
* If $this->sid is set, returns the allowed transitions from this state.
* If $this->sid is 0 or FALSE, then labels of ALL states of the State's
* Workflow are returned.
*
*/
public function getNextSid($entity_type, $entity, $field_name, $user, $force = FALSE);
/**
* Gets all states for a given workflow.
*
* @param mixed $all
* Indicates to which states to return.
* - TRUE = all, including Creation and Inactive;
* - FALSE = only Active states, not Creation;
* - 'CREATION' = only Active states, including Creation.
*
* @return WorkflowState[]
* An array of WorkflowState objects.
*/
public function getStates($all = FALSE, $reset = FALSE);
/**
* Gets a state for a given workflow.
*
* @param mixed $key
* A state ID or state Name.
*
* @return WorkflowState
* A WorkflowState object.
*/
public function getState($sid);
/**
* Creates a Transition for this workflow.
*
* @param string $from_sid
* @param string $to_sid
* @param array $values
*
* @return mixed|null|static
*/
public function createTransition($from_sid, $to_sid, $values = array());
/**
* Sorts all Transitions for this workflow, according to State weight.
*
* This is only needed for the Admin UI.
*/
public function sortTransitions();
/**
* Loads all allowed ConfigTransitions for this workflow.
*
* @param array|NULL $ids
* Array of Transitions IDs. If NULL, show all transitions.
* @param array $conditions
* $conditions['from_sid'] : if provided, a 'from' State ID.
* $conditions['to_sid'] : if provided, a 'to' state ID.
*
* @return \Drupal\workflow\Entity\WorkflowConfigTransition[]
*/
public function getTransitions($tids = FALSE, array $conditions = array(), $reset = FALSE);
public function getTransitionsByTid($tid);
/**
*
* Get a specific transition.
*
* @param string $from_sid
* @param string $to_sid
*
* @return WorkflowConfigTransition[]
*/
//D8: public function getTransitionsByStateId($from_sid, $to_sid);
}
@@ -0,0 +1,165 @@
<?php
/**
* @file
* Contains workflow\includes\Entity\WorkflowScheduledTransition.
*/
/**
* Implements a scheduled transition, as shown on Workflow form.
*/
class WorkflowScheduledTransition extends WorkflowTransition {
// Scheduled timestamp of state change.
public $scheduled;
/**
* Constructor.
*/
public function __construct(array $values = array(), $entityType = 'WorkflowScheduledTransition') {
// Please be aware that $entity_type and $entityType are different things!
parent::__construct($values, $entityType);
$this->is_scheduled = TRUE;
$this->is_executed = FALSE;
}
public function setValues($entity_type, $entity, $field_name, $old_sid, $new_sid, $uid = NULL, $scheduled = REQUEST_TIME, $comment = '') {
// A scheduled transition does not have a timestamp, yet.
$stamp = 0;
parent::setValues($entity_type, $entity, $field_name, $old_sid, $new_sid, $uid, $stamp, $comment);
// Set the scheduled timestamp of state change.
$this->scheduled = $scheduled;
}
/**
* Given a node, get all scheduled transitions for it.
*
* @param string $entity_type
* @param int $entity_id
* @param string $field_name
* Optional.
*
* @return array
* An array of WorkflowScheduledTransitions.
*
* deprecated: workflow_get_workflow_scheduled_transition_by_nid() --> WorkflowScheduledTransition::load()
*/
public static function load($entity_type, $entity_id, $field_name = '', $limit = NULL) {
if (!$entity_id) {
return array();
}
$query = db_select('workflow_scheduled_transition', 'wst');
$query->fields('wst');
$query->condition('entity_type', $entity_type, '=');
$query->condition('nid', $entity_id, '=');
if ($field_name !== NULL) {
$query->condition('field_name', $field_name, '=');
}
$query->orderBy('scheduled', 'ASC');
$query->addTag('workflow_scheduled_transition');
if ($limit) {
$query->range(0, $limit);
}
$result = $query->execute()->fetchAll(PDO::FETCH_CLASS, 'WorkflowScheduledTransition');
return $result;
}
/**
* Given a timeframe, get all scheduled transitions.
*
* deprecated: workflow_get_workflow_scheduled_transition_by_between() --> WorkflowScheduledTransition::loadBetween()
*/
public static function loadBetween($start = 0, $end = 0) {
$query = db_select('workflow_scheduled_transition', 'wst');
$query->fields('wst');
$query->orderBy('scheduled', 'ASC');
$query->addTag('workflow_scheduled_transition');
if ($start) {
$query->condition('scheduled', $start, '>');
}
if ($end) {
$query->condition('scheduled', $end, '<');
}
$result = $query->execute()->fetchAll(PDO::FETCH_CLASS, 'WorkflowScheduledTransition');
return $result;
}
/**
* Save a scheduled transition. If the transition is executed, save in history.
*/
public function save() {
// If executed, save in history.
if ($this->is_executed) {
// Be careful, we are not a WorkflowScheduleTransition anymore!
$this->entityType = 'WorkflowTransition';
$this->setUp();
return parent::save(); // <--- exit !!
}
// Since we do not have an entity_id here, we cannot use entity_delete.
// @todo: Add an 'entity id' to WorkflowScheduledTransition entity class.
// $result = parent::save();
// Avoid duplicate entries.
$clone = clone $this;
$clone->delete();
// Save (insert or update) a record to the database based upon the schema.
drupal_write_record('workflow_scheduled_transition', $this);
// Create user message.
if ($state = $this->getNewState()) {
$entity_type = $this->entity_type;
$entity = $this->getEntity();
$message = '%entity_title scheduled for state change to %state_name on %scheduled_date';
$args = array(
'@entity_type' => $entity_type,
'%entity_title' => entity_label($entity_type, $entity),
'%state_name' => entity_label('WorkflowState', $state),
'%scheduled_date' => format_date($this->scheduled),
);
$uri = entity_uri($entity_type, $entity);
watchdog('workflow', $message, $args, WATCHDOG_NOTICE, l('view', $uri['path'] . '/workflow'));
drupal_set_message(t($message, $args));
}
}
/**
* Given a node, delete transitions for it.
*
* deprecated: workflow_delete_workflow_scheduled_transition_by_nid() --> WorkflowScheduledTransition::delete()
*/
public function delete() {
// Support translated Workflow Field workflows by including the language.
db_delete($this->entityInfo['base table'])
->condition('entity_type', $this->entity_type)
->condition('nid', $this->entity_id)
->condition('field_name', $this->field_name)
->condition('language', $this->language)
->execute();
}
/**
* Property functions.
*/
/**
* If a scheduled transition has no comment, a default comment is added before executing it.
*/
public function addDefaultComment() {
$this->comment = t('Scheduled by user @uid.', array('@uid' => $this->uid));
}
/**
* {@inheritdoc}
*/
public function getTimestamp() {
return $this->scheduled;
}
}
@@ -0,0 +1,607 @@
<?php
/**
* @file
* Contains workflow\includes\Entity\WorkflowState.
* Contains workflow\includes\Entity\WorkflowStateController.
*/
/**
* Class WorkflowState
*/
class WorkflowState extends Entity {
// Since workflows do not change, it is implemented as a singleton.
protected static $states = array();
public $sid = 0;
public $wid = 0;
public $weight = 0;
public $sysid = 0;
public $state = ''; // @todo D8: remove $state, use $label/$name. (requires conversion of Views displays.)
public $status = 1;
/**
* CRUD functions.
*/
/**
* Constructor.
*
* @param array $values
* @param string $entityType
*/
public function __construct(array $values = array(), $entityType = 'WorkflowState') {
// Please be aware that $entity_type and $entityType are different things!
// Keep official name and external name equal. Both are required.
// @todo: still needed? test import, manual creation, programmatic creation, etc.
if (!isset($values['state']) && isset($values['name'])) {
$values['state'] = $values['name'];
}
// Set default values for '(creation)' state.
if (!empty($values['is_new']) && $values['name'] == WORKFLOW_CREATION_STATE_NAME) {
$values['sysid'] = WORKFLOW_CREATION;
$values['weight'] = WORKFLOW_CREATION_DEFAULT_WEIGHT;
$values['name'] = '(creation)'; // machine_name;
}
parent::__construct($values, $entityType);
if (empty($values)) {
// Automatic constructor when casting an array or object.
// Add pre-existing states to cache (not new/temp ones).
if (!isset(self::$states[$this->sid])) {
self::$states[$this->sid] = $this;
}
}
}
/*
// Implementing clone needs a list of tid-less transitions, and a conversion
// of sids for both States and ConfigTransitions.
// public function __clone() {}
*/
/**
* Alternative constructor, loading objects from table {workflow_states}.
*
* @param int $sid
* The requested State ID
* @param int $wid
* An optional Workflow ID, to check if the requested State is valid for the Workflow.
*
* @return WorkflowState|NULL|FALSE $state
* WorkflowState if state is successfully loaded,
* NULL if not loaded,
* FALSE if state does not belong to requested Workflow.
*/
public static function load($sid, $wid = 0) {
$states = self::getStates();
$state = isset($states[$sid]) ? $states[$sid] : NULL;
if ($wid && $state && ($wid != $state->wid)) {
return FALSE;
}
return $state;
}
/**
* Get all states in the system, with options to filter, only where a workflow exists.
*
* @param $wid
* The requested Workflow ID.
* @param bool $reset
* An option to refresh all caches.
*
* @return array $states
* An array of cached states.
*
* D7.x-2.x: deprecated workflow_get_workflow_states --> workflow_state_load_multiple
* D7.x-2.x: deprecated workflow_get_workflow_states_all --> workflow_state_load_multiple
* D7.x-2.x: deprecated workflow_get_other_states_by_sid --> workflow_state_load_multiple
*/
public static function getStates($wid = 0, $reset = FALSE) {
if ($reset) {
self::$states = array();
}
if (empty(self::$states)) {
// Build the query, and get ALL states.
// Note: self::states[] is populated in respective constructors.
$query = db_select('workflow_states', 'ws');
$query->fields('ws');
$query->orderBy('ws.weight');
$query->orderBy('ws.wid');
// Just for grins, add a tag that might result in modifications.
$query->addTag('workflow_states');
// @see #2285983 for using SQLite.
// $query->execute()->fetchAll(PDO::FETCH_CLASS, 'WorkflowState');
/* @var $tmp DatabaseStatementBase */
$statement = $query->execute();
$statement->setFetchMode(PDO::FETCH_CLASS,'WorkflowState');
foreach ($statement->fetchAll() as $state) {
self::$states[$state->sid] = $state;
}
}
if (!$wid) {
// All states are requested and cached: return them.
return self::$states;
}
else {
// All states of only 1 Workflow is requested: return this one.
$result = array();
foreach (self::$states as $state) {
if ($state->wid == $wid) {
$result[$state->sid] = $state;
}
}
return $result;
}
}
/**
* Get all states in the system, with options to filter, only where a workflow exists.
*
* May return more then one State, since a name is not (yet) an UUID.
*
* @param $name
* @param int $wid
*
* @return WorkflowState
*/
public static function loadByName($name, $wid = 0) {
/* @var $state WorkflowState */
foreach ($states = self::getStates($wid) as $state) {
if ($name == $state->getName()) {
return $state;
}
}
return NULL;
}
/**
* Deactivate a Workflow State, moving existing nodes to a given State.
*
* @param int $new_sid
* The state ID, to which all affected entities must be moved.
*
* D7.x-2.x: deprecated workflow_delete_workflow_states_by_sid() --> WorkflowState->deactivate() + delete()
*/
public function deactivate($new_sid) {
$current_sid = $this->sid;
$force = TRUE;
// Notify interested modules. We notify first to allow access to data before we zap it.
// E.g., Node API implements this.
// - re-parents any nodes that we don't want to orphan, whilst deactivating a State.
// - delete any lingering node to state values.
module_invoke_all('workflow', 'state delete', $current_sid, $new_sid, NULL, $force);
// Re-parent any nodes that we don't want to orphan, whilst deactivating a State.
if ($new_sid) {
// A candidate for the batch API.
// @TODO: Future updates should seriously consider setting this with batch.
global $user; // We can use global, since deactivate() is a UI-only function.
$comment = t('Previous state deleted');
// Re-assign workflow_node nodes.
foreach (workflow_get_workflow_node_by_sid($current_sid) as $workflow_node) {
// @todo: add Field support in 'state delete', by using workflow_node_history or reading current field.
$entity_type = 'node';
$entity = entity_load_single('node', $workflow_node->nid);
$field_name = '';
$transition = new WorkflowTransition();
$transition->setValues($entity_type, $entity, $field_name, $current_sid, $new_sid, $user->uid, REQUEST_TIME, $comment);
$transition->force($force);
// Execute Transition, invoke 'pre' and 'post' events, save new state in workflow_node, save also in workflow_node_history.
// For Workflow Node, only {workflow_node} and {workflow_node_history} are updated. For Field, also the Entity itself.
$new_sid = workflow_execute_transition($entity_type, $entity, $field_name, $transition, $force);
}
// Re-assign workflow_field_entities.
foreach(_workflow_info_fields() as $field_name => $field_info) {
$query = new EntityFieldQuery();
$query->fieldCondition($field_name, 'value', $current_sid, '=');
$result = $query->execute();
foreach ($result as $entity_type => $entities) {
if ($entity_type == 'comment') {
// Do not reset comments.
continue;
}
foreach ($entities as $entity_id => $entity) {
$entity = entity_load_single($entity_type, $entity_id);
/* @var $transition WorkflowTransition */
$transition = new WorkflowTransition();
$transition->setValues($entity_type, $entity, $field_name, $current_sid, $new_sid, $user->uid, REQUEST_TIME, $comment, TRUE);
$transition->force($force);
// Execute Transition, invoke 'pre' and 'post' events, save new state in Field-table, save also in workflow_transition_history.
// For Workflow Node, only {workflow_node} and {workflow_transition_history} are updated. For Field, also the Entity itself.
$new_sid = workflow_execute_transition($entity_type, $entity, $field_name, $transition, $force);
}
}
}
}
// Delete any lingering node to state values.
workflow_delete_workflow_node_by_sid($current_sid);
// Delete the config transitions this state is involved in.
$workflow = workflow_load_single($this->wid);
/* @var $transition WorkflowTransition */
foreach ($workflow->getTransitionsBySid($current_sid, 'ALL') as $transition) {
$transition->delete();
}
foreach ($workflow->getTransitionsByTargetSid($current_sid, 'ALL') as $transition) {
$transition->delete();
}
// Delete the state. -- We don't actually delete, just deactivate.
// This is a matter up for some debate, to delete or not to delete, since this
// causes name conflicts for states. In the meantime, we just stick with what we know.
// If you really want to delete the states, use workflow_cleanup module, or delete().
$this->status = FALSE;
$this->save();
// Clear the cache.
self::getStates(0, TRUE);
}
/**
* Property functions.
*/
/**
* Returns the Workflow object of this State.
*
* @return Workflow
* Workflow object.
*/
public function getWorkflow() {
if (isset($this->workflow)) {
return $this->workflow;
}
return workflow_load_single($this->wid);
}
public function setWorkflow($workflow) {
$this->wid = $workflow->wid;
$this->workflow = $workflow;
}
/**
* Returns the Workflow object of this State.
*
* @return bool
* TRUE if state is active, else FALSE.
*/
public function isActive() {
return (bool) $this->status;
}
public function isCreationState() {
return $this->sysid == WORKFLOW_CREATION;
}
/**
* Determines if the Workflow Form must be shown.
*
* If not, a formatter must be shown, since there are no valid options.
*
* @param $entity_type
* @param $entity
* @param $field_name
* @param $user
* @param $force
*
* @return bool $show_widget
* TRUE = a form (a.k.a. widget) must be shown; FALSE = no form, a formatter must be shown instead.
*/
public function showWidget($entity_type, $entity, $field_name, $user, $force) {
$options = $this->getOptions($entity_type, $entity, $field_name, $user, $force);
$count = count($options);
// The easiest case first: more then one option: always show form.
if ($count > 1) {
return TRUE;
}
// #2226451: Even in Creation state, we must have 2 visible states to show the widget.
// // Only when in creation phase, one option is sufficient,
// // since the '(creation)' option is not included in $options.
// // When in creation state,
// if ($this->isCreationState()) {
// return TRUE;
// }
return FALSE;
}
/**
* Returns the allowed transitions for the current state.
*
* @param string $entity_type
* The type of the entity at hand.
* @param object $entity
* The entity at hand. May be NULL (E.g., on a Field settings page).
* @param string $field_name
* @param null $user
* @param bool $force
*
* @return array
* An array of tid=>transition pairs with allowed transitions for State.
*/
public function getTransitions($entity_type = '', $entity = NULL, $field_name = '', $user = NULL, $force = FALSE) {
$transitions = array();
$current_sid = $this->sid;
$current_state = $this;
if (!$workflow = $this->getWorkflow()) {
// No workflow, no options ;-)
return $transitions;
}
// Get the role IDs of the user, to get the proper permissions.
$roles = $user ? array_keys($user->roles) : array();
// Some entities (e.g., taxonomy_term) do not have a uid.
$entity_uid = isset($entity->uid) ? $entity->uid : 0;
// Fetch entity_id from entity for _newness_ check
$entity_id = ($entity) ? entity_id($entity_type, $entity) : '';
if ($force || ($user && $user->uid == 1)) {
// Superuser is special. And $force allows Rules to cause transition.
$roles = 'ALL';
}
elseif ($entity && (!empty($entity->is_new) || empty($entity_id))) {
// Add 'author' role to user, if this is a new entity.
// - $entity can be NULL (E.g., on a Field settings page).
// - on display of new entity, $entity_id and $is_new are not set.
// - on submit of new entity, $entity_id and $is_new are both set.
$roles = array_merge(array(WORKFLOW_ROLE_AUTHOR_RID), $roles);
}
elseif (($entity_uid > 0) && ($user->uid > 0) && ($entity_uid == $user->uid)) {
// Add 'author' role to user, if user is author of this entity.
// - Some entities (e.g, taxonomy_term) do not have a uid.
// - If 'anonymous' is the author, don't allow access to History Tab,
// since anyone can access it, and it will be published in Search engines.
$roles = array_merge(array(WORKFLOW_ROLE_AUTHOR_RID), $roles);
}
// Set up an array with states - they are already properly sorted.
// Unfortunately, the config_transitions are not sorted.
// Also, $transitions does not contain the 'stay on current state' transition.
// The allowed objects will be replaced with names.
$transitions = $workflow->getTransitionsBySid($current_sid, $roles);
// Let custom code add/remove/alter the available transitions.
// Using the new drupal_alter.
// Modules may veto a choice by removing a transition from the list.
$context = array(
'entity_type' => $entity_type,
'entity' => $entity,
'field_name' => $field_name,
'force' => $force,
'workflow' => $workflow,
'state' => $current_state,
'user' => $user,
'user_roles' => $roles, // @todo: can be removed in D8, since $user is in.
);
// @todo D8: rename to 'workflow_permitted_transitions'.
drupal_alter('workflow_permitted_state_transitions', $transitions, $context);
// Let custom code change the options, using old_style hook.
// @todo D8: delete below foreach/hook for better performance and flexibility.
// Above drupal_alter() calls hook_workflow_permitted_state_transitions_alter() only once.
foreach ($transitions as $transition) {
$new_sid = $transition->target_sid;
$permitted = array();
// We now have a list of config_transitions. Check each against the Entity.
// Invoke a callback indicating that we are collecting state choices.
// Modules may veto a choice by returning FALSE.
// In this case, the choice is never presented to the user.
if ($roles != 'ALL') {
$permitted = module_invoke_all('workflow', 'transition permitted', $current_sid, $new_sid, $entity, $force, $entity_type, $field_name, $transition, $user);
}
// If vetoed by a module, remove from list.
if (in_array(FALSE, $permitted, TRUE)) {
unset($transitions[$transition->tid]);
}
}
return $transitions;
}
/**
* Returns the allowed values for the current state.
*
* @param string $entity_type
* The type of the entity at hand.
* @param object $entity
* The entity at hand. May be NULL (E.g., on a Field settings page).
* @param $field_name
* @param $user
* @param bool $force
*
* @return array
* An array of sid=>label pairs.
* If $this->sid is set, returns the allowed transitions from this state.
* If $this->sid is 0 or FALSE, then labels of ALL states of the State's
* Workflow are returned.
*
* D7.x-2.x: deprecated workflow_field_choices() --> WorkflowState->getOptions()
*/
public function getOptions($entity_type, $entity, $field_name, $user, $force = FALSE) {
// Define an Entity-specific cache per page load.
static $cache = array();
$options = array();
$entity_id = ($entity) ? entity_id($entity_type, $entity) : '';
$current_sid = $this->sid;
// Get options from page cache, using a non-empty index (just to be sure).
$entity_index = (!$entity) ? 'x' : $entity_id;
if (isset($cache[$entity_type][$entity_index][$force][$current_sid])) {
$options = $cache[$entity_type][$entity_index][$force][$current_sid];
return $options;
}
$workflow = $this->getWorkflow();
if (!$workflow) {
// No workflow, no options ;-)
}
elseif (!$current_sid) {
// If no State ID is given, we return all states.
// We cannot use getTransitions, since there are no ConfigTransitions
// from State with ID 0, and we do not want to repeat States.
foreach ($workflow->getStates() as $state) {
$options[$state->value()] = $state->label(); // Translation is done later.
}
}
else {
/* @var $transition WorkflowTransition */
$transitions = $this->getTransitions($entity_type, $entity, $field_name, $user, $force);
foreach ($transitions as $transition) {
// Get the label of the transition, and if empty of the target state.
// Beware: the target state may not exist, since it can be invented
// by custom code in the above drupal_alter() hook.
if (!$label = $transition->label()) {
$target_state = $transition->getNewState();
$label = $target_state ? $target_state->label() : '';
}
$new_sid = $transition->target_sid;
$options[$new_sid] = $label; // Translation is done later.
}
// Include current state for same-state transitions, except when $sid = 0.
// Caveat: this unnecessary since 7.x-2.3 (where stay-on-state transitions are saved, too.)
// but only if the transitions have been saved at least one time.
if ($current_sid && ($current_sid != $workflow->getCreationSid())) {
if (!isset($options[$current_sid])) {
$options[$current_sid] = $this->label(); // Translation is done later.
}
}
// Properly fix the labels.
// Translate, convert '&', make secure.
foreach($options as $key => $label) {
$options[$key] = html_entity_decode(check_plain(t($label)));
}
// Save to entity-specific cache.
$cache[$entity_type][$entity_index][$force][$current_sid] = $options;
}
return $options;
}
/**
* Returns the number of entities with this state.
*
* @return int
* Counted number.
*
* @todo: add $options to select on entity type, etc.
*/
public function count() {
$sid = $this->sid;
// Get the numbers for Workflow Node.
$result = db_select('workflow_node', 'wn')
->fields('wn')
->condition('sid', $sid, '=')
->execute();
$count = count($result->fetchAll()); // @see #2285983 for using SQLite.
// Get the numbers for Workflow Field.
$fields = _workflow_info_fields();
foreach ($fields as $field_name => $field_map) {
if ($field_map['type'] == 'workflow') {
$query = new EntityFieldQuery();
$query
->fieldCondition($field_name, 'value', $sid, '=')
// ->entityCondition('bundle', 'article')
// ->addMetaData('account', user_load(1)) // Run the query as user 1.
->count(); // We only need the count.
$result = $query->execute();
$count += $result;
}
}
return $count;
}
/**
* Mimics Entity API functions.
*/
protected function defaultLabel() {
return $this->state;
}
public function getName() {
return isset($this->name) ? $this->name : '';
}
public function setName($name) {
return $this->name = $name;
}
public function value() {
return $this->sid;
}
public function save() {
parent::save();
// Ensure Workflow is marked overridden.
$workflow = $this->getWorkflow();
if ($workflow->status == ENTITY_IN_CODE) {
$workflow->status = ENTITY_OVERRIDDEN;
$workflow->save();
}
}
}
class WorkflowStateController extends EntityAPIController {
public function save($entity, DatabaseTransaction $transaction = NULL) {
// Create the machine_name.
if (empty($entity->name)) {
if ($label = $entity->state) {
$entity->name = str_replace(' ', '_', strtolower($label));
}
else {
$entity->name = 'state_' . $entity->sid;
}
}
$return = parent::save($entity, $transaction);
if ($return) {
$workflow = $entity->getWorkflow();
// Maintain the new object in the workflow.
$workflow->states[$entity->sid] = $entity;
}
// Reset the cache for the affected workflow.
workflow_reset_cache($entity->wid);
return $return;
}
public function delete($ids, DatabaseTransaction $transaction = NULL) {
// @todo: replace with parent.
foreach ($ids as $id) {
if ($state = workflow_state_load($id)) {
$wid = $state->wid;
db_delete('workflow_states')
->condition('sid', $state->sid)
->execute();
// Reset the cache for the affected workflow.
workflow_reset_cache($wid);
}
}
}
}
@@ -0,0 +1,737 @@
<?php
/**
* @file
* Contains workflow\includes\Entity\WorkflowTransition.
* Contains workflow\includes\Entity\WorkflowTransitionController.
*
* Implements (scheduled/executed) state transitions on entities.
*/
/**
* Implements an actual Transition.
*
* If a transition is executed, the new state is saved in the Field or {workflow_node}.
* If a transition is saved, it is saved in table {workflow_history_node}
*/
class WorkflowTransition extends Entity {
// Field data.
public $entity_type;
public $field_name = '';
public $language = LANGUAGE_NONE;
public $delta = 0;
// Entity data.
public $revision_id;
public $entity_id; // Use WorkflowTransition->getEntity() to fetch this.
public $nid; // @todo D8: remove $nid, use $entity_id. (requires conversion of Views displays.)
// Transition data.
// public $hid = 0;
public $wid = 0;
public $old_sid = 0;
public $new_sid = 0;
public $sid = 0; // @todo D8: remove $sid, use $new_sid. (requires conversion of Views displays.)
public $uid = 0; // Use WorkflowTransition->getUser() to fetch this.
public $stamp;
public $comment = '';
// Cached data, from $this->entity_id and $this->uid.
protected $entity = NULL; // Use WorkflowTransition->getEntity() to fetch this.
protected $user = NULL; // Use WorkflowTransition->getUser() to fetch this.
// Extra data.
protected $is_scheduled = NULL;
protected $is_executed = NULL;
protected $force = NULL;
/**
* Entity class functions.
*/
/**
* Creates a new entity.
*
* @param array $values
* The initial values.
* @param string $entityType
* The entity type of this Entity subclass.
*
* @see entity_create()
*
* No arguments passed, when loading from DB.
* All arguments must be passed, when creating an object programmatically.
* One argument $entity may be passed, only to directly call delete() afterwards.
*/
public function __construct(array $values = array(), $entityType = 'WorkflowTransition') {
// Please be aware that $entity_type and $entityType are different things!
parent::__construct($values, $entityType);
$this->hid = isset($this->hid) ? $this->hid : 0;
// This transition is not scheduled
$this->is_scheduled = FALSE;
// This transition is not executed, if it has no hid, yet, upon load.
$this->is_executed = ($this->hid > 0);
// Fill the 'new' fields correctly. @todo D8: rename these fields in db table.
$this->entity_id = $this->nid;
$this->new_sid = $this->sid;
// Initialize wid, if not set.
if ($this->old_sid && !$this->wid) {
$this->getWorkflow();
}
}
/**
* Helper function for __construct. Used for all children of WorkflowTransition (aka WorkflowScheduledTransition)
*
* @param $entity_type
* @param $entity
* @param $field_name
* @param $old_sid
* @param $new_sid
* @param null $uid
* @param int $stamp
* @param string $comment
*/
public function setValues($entity_type, $entity, $field_name, $old_sid, $new_sid, $uid = NULL, $stamp = REQUEST_TIME, $comment = '') {
// Normally, the values are passed in an array, and set in parent::__construct, but we do it ourselves.
// (But there is no objection to do it there.)
global $user;
$this->entity_type = (!$entity_type) ? $this->entity_type : $entity_type;
$this->field_name = (!$field_name) ? $this->field_name : $field_name;
$uid = ($uid === NULL) ? $user->uid : $uid;
// If constructor is called with new() and arguments.
// Load the supplied entity.
if ($entity && !$entity_type) {
// Not all parameters are passed programmatically.
drupal_set_message(t('Wrong call to new Workflow*Transition()'), 'error');
}
elseif ($entity) {
$this->setEntity($entity_type, $entity);
}
if (!$entity && !$old_sid && !$new_sid) {
// If constructor is called without arguments, e.g., loading from db.
}
elseif ($entity && $old_sid) {
// Caveat: upon entity_delete, $new_sid is '0'.
// If constructor is called with new() and arguments.
$this->old_sid = $old_sid;
$this->sid = $new_sid;
$this->uid = $uid;
$this->stamp = $stamp;
$this->comment = $comment;
// Set language. Multi-language is not supported for Workflow Node.
$this->language = _workflow_metadata_workflow_get_properties($entity, array(), 'langcode', $entity_type, $field_name);
}
elseif (!$old_sid) {
// Not all parameters are passed programmatically.
drupal_set_message(
t('Wrong call to constructor Workflow*Transition(@old_sid to @new_sid)', array('@old_sid' => $old_sid, '@new_sid' => $new_sid)),
'error');
}
// Fill the 'new' fields correctly. @todo D8: rename these fields in db table.
$this->entity_id = $this->nid;
$this->new_sid = $this->sid;
// Initialize wid, if not set.
if ($this->old_sid && !$this->wid) {
$this->getWorkflow();
}
}
/**
* {@inheritdoc}
*/
protected function defaultLabel() {
// @todo; Should return title of WorkflowConfigTransition. Make it a superclass??
return t('Workflow transition !hid', array('!hid' =>3));
}
// protected function defaultUri() {
// return array('path' => 'workflow_transition/' . $this->hid);
// }
/**
* CRUD functions.
*/
/**
* Given a node, get all transitions for it.
*
* Since this may return a lot of data, a limit is included to allow for only one result.
*
* @param string $entity_type
* @param array $entity_ids
* @param string $field_name
* Optional. Can be NULL, if you want to load any field.
* @param null $limit
* @param string $langcode
*
* @return array
* An array of WorkflowTransitions.
*/
public static function loadMultiple($entity_type, array $entity_ids, $field_name = '', $limit = NULL, $langcode = '') {
$query = db_select('workflow_node_history', 'h');
$query->condition('h.entity_type', $entity_type);
if ($entity_ids) {
$query->condition('h.nid', $entity_ids);
}
if ($field_name !== NULL) {
// If we do not know/care for the field_name, fetch all history.
// E.g., in workflow.tokens.
$query->condition('h.field_name', $field_name);
}
// Add selection on language.
// Workflow Node: only has 'und'.
// Workflow Field: untranslated field have 'und'.
// Workflow Field: translated fields may be specified.
if ($langcode) {
$query->condition('h.language', $langcode);
}
$query->fields('h');
// The timestamp is only granular to the second; on a busy site, we need the id.
// $query->orderBy('h.stamp', 'DESC');
$query->orderBy('h.hid', 'DESC');
if ($limit) {
$query->range(0, $limit);
}
$result = $query->execute()->fetchAll(PDO::FETCH_CLASS, 'WorkflowTransition');
return $result;
}
/**
* Property functions.
*/
/**
* Verifies if the given transition is allowed.
*
* - In settings;
* - In permissions;
* - By permission hooks, implemented by other modules.
*
* @param $roles
* @param $user
* @param $force
*
* @return bool TRUE if OK, else FALSE.
* TRUE if OK, else FALSE.
*
* Having both $roles AND $user seems redundant, but $roles have been
* tampered with, even though they belong to the $user.
* @see WorkflowConfigTransition::isAllowed()
*/
protected function isAllowed($roles, $user, $force) {
if ($force || ($user->uid == 1)) {
return TRUE;
}
// Check allow-ability of state change if user is not superuser (might be cron).
// Get the WorkflowConfigTransition.
// @todo: some day, WorkflowConfigTransition can be a parent of WorkflowTransition.
$workflow = $this->getWorkflow();
$config_transitions = $workflow->getTransitionsBySidTargetSid($this->old_sid, $this->new_sid);
$config_transition = reset($config_transitions);
if (!$config_transition || !$config_transition->isAllowed($roles)) {
$t_args = array(
'%old_sid' => $this->old_sid,
'%new_sid' => $this->new_sid,
);
watchdog('workflow', 'Attempt to go to nonexistent transition (from %old_sid to %new_sid)', $t_args, WATCHDOG_ERROR);
return FALSE;
}
return TRUE;
}
/**
* 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);
}
$force = $this->isForced();
// 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.
}
if ($state_changed || $this->comment) {
// 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'.
// Only add the Transition once! or you will encounter endless loops in
// hook_entity_update() in workflow_actions_entity_update et all.
if (!isset($entity->workflow_transitions[$field_name])) {
$entity->workflow_transitions[$field_name] = &$this;
}
// 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.
// Update 10-dec-2016: the following line, added above, may have resolved that.
// if (!isset($entity->workflow_transitions[$field_name]))
/*
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;
}
/**
* Invokes 'transition post'.
*
* Add the possibility to invoke the hook from elsewhere.
*/
public function post_execute($force = FALSE) {
$old_sid = $this->old_sid;
$new_sid = $this->new_sid;
$entity = $this->getEntity(); // Entity may not be loaded, yet.
$entity_type = $this->entity_type;
// $entity_id = $this->entity_id;
$field_name = $this->field_name;
$state_changed = ($old_sid != $new_sid);
if ($state_changed || $this->comment) {
module_invoke_all('workflow', 'transition post', $old_sid, $new_sid, $entity, $force, $entity_type, $field_name, $this);
}
}
/**
* Get the Transitions $workflow.
*
* @return Workflow|NULL
* The workflow for this Transition.
*/
public function getWorkflow() {
$workflow = NULL;
if (!$this->wid) {
$state = workflow_state_load_single($this->new_sid ? $this->new_sid : $this->old_sid);
$this->wid = (int) $state->wid;
}
if ($this->wid) {
$workflow = workflow_load($this->wid);
}
return $workflow;
}
/**
* Get the Transitions $entity.
*
* @return object
* The entity, that is added to the Transition.
*/
public function getEntity() {
if (empty($this->entity) && $this->entity_type) {
$entity_type = $this->entity_type;
$entity_id = $this->entity_id;
$entity = entity_load_single($entity_type, $entity_id);
// Set the entity cache.
$this->entity = $entity;
// Make sure the vid of Entity and Transition are equal.
// Especially for Scheduled Transition, that do not have this set, yet,
// or may have an outdated revision ID.
$info = entity_get_info($entity_type);
$revision_key = $info['entity keys']['revision'];
$this->revision_id = (isset($entity->{$revision_key})) ? $entity->{$revision_key} : NULL;
}
return $this->entity;
}
/**
* Set the Transitions $entity.
*
* @param string $entity_type
* The entity type of the entity.
* @param mixed $entity
* The Entity ID or the Entity object, to add to the Transition.
*
* @return object $entity
* The Entity, that is added to the Transition.
*/
public function setEntity($entity_type, $entity) {
if (!is_object($entity)) {
$entity_id = $entity;
// Use node API or Entity API to load the object first.
$entity = entity_load_single($entity_type, $entity_id);
}
$this->entity = $entity;
$this->entity_type = $entity_type;
list($this->entity_id, $this->revision_id,) = entity_extract_ids($entity_type, $entity);
// For backwards compatibility, set nid.
$this->nid = $this->entity_id;
return $this->entity;
}
public function getUser() {
if (!isset($this->user) || ($this->user->uid != $this->uid)) {
$this->user = user_load($this->uid);
}
return $this->user;
}
/**
* {@inheritdoc}
*/
public function getFieldName() {
return $this->field_name;
}
/**
* Functions, common to the WorkflowTransitions.
*/
public function getOldState() {
return workflow_state_load_single($this->old_sid);
}
public function getNewState() {
return workflow_state_load_single($this->new_sid);
}
/**
* {@inheritdoc}
*/
public function getComment() {
return $this->comment;
}
/**
* Returns the time on which the transitions was or will be executed.
*
* @return mixed
*/
public function getTimestamp() {
return $this->stamp;
}
/**
* {@inheritdoc}
*/
public function getTimestampFormatted() {
$timestamp = $this->stamp;
return format_date($timestamp);;
}
/**
* {@inheritdoc}
*/
public function setTimestamp($value) {
$this->stamp = $value;
return $this;
}
/**
* Returns if this is a Scheduled Transition.
*/
public function isScheduled() {
return $this->is_scheduled;
}
public function schedule($schedule = TRUE) {
return $this->is_scheduled = $schedule;
}
public function isExecuted() {
return $this->is_executed;
}
/**
* A transition may be forced skipping checks.
*/
public function isForced() {
return (bool) $this->force;
}
public function force($force = TRUE) {
return $this->force = $force;
}
/**
* Helper debugging function to easily show the contents fo a transition.
*/
public function dpm($function = '') {
$transition = $this;
$entity = $transition->getEntity();
$entity_type = $transition->entity_type;
list($entity_id, , $entity_bundle) = ($entity) ? entity_extract_ids($entity_type, $entity) : array('', '', '');
$time = $transition->getTimestampFormatted();
// Do this extensive $user_name lines, for some troubles with Action.
$user = $transition->getUser();
$user_name = ($user) ? $user->name : 'unknown username';
$t_string = get_class($this) . ' ' . (isset($this->hid) ? $this->hid : '') . ' ' . ($function ? ("in function '$function'") : '');
$output[] = 'Entity = ' . ((!$entity) ? 'NULL' : ($entity_type . '/' . $entity_bundle . '/' . $entity_id));
$output[] = 'Field = ' . $transition->getFieldName();
$output[] = 'From/To = ' . $transition->old_sid . ' > ' . $transition->new_sid . ' @ ' . $time;
$output[] = 'Comment = ' . $user_name . ' says: ' . $transition->getComment();
$output[] = 'Forced = ' . ($transition->isForced() ? 'yes' : 'no');
if (function_exists('dpm')) { dpm($output, $t_string); }
}
}
/**
* Implements a controller class for WorkflowTransition.
*
* The 'true' controller class is 'Workflow'.
*/
class WorkflowTransitionController extends EntityAPIController {
/**
* Overrides DrupalDefaultEntityController::cacheGet().
*
* Override default function, due to core issue #1572466.
*/
protected function cacheGet($ids, $conditions = array()) {
// Load any available entities from the internal cache.
if ($ids === FALSE && !$conditions) {
return $this->entityCache;
}
return parent::cacheGet($ids, $conditions);
}
/**
* Insert (no update) a transition.
*
* deprecated workflow_insert_workflow_node_history() --> WorkflowTransition::save()
*/
public function save($entity, DatabaseTransaction $transaction = NULL) {
// Check for no transition.
if ($entity->old_sid == $entity->new_sid) {
if (!$entity->comment) {
// Write comment into history though.
return;
}
}
if (empty($entity->hid)) {
// Insert the transition. Make sure it hasn't already been inserted.
$last_history = workflow_transition_load_single($entity->entity_type, $entity->entity_id, $entity->field_name, $entity->language);
if ($last_history &&
$last_history->stamp == REQUEST_TIME &&
$last_history->new_sid == $entity->new_sid) {
return;
}
else {
unset($entity->hid);
$entity->stamp = isset($entity->stamp) ? $entity->stamp : REQUEST_TIME;
return parent::save($entity, $transaction);
}
}
else {
// Update the transition.
return parent::save($entity, $transaction);
}
}
}
@@ -0,0 +1,15 @@
<?php
/**
* @file
* Contains a stub class.
* The functionality is removed but apparently caches aren't refreshed
* properly during upgrade.
* @see https://www.drupal.org/node/2620530
*/
/**
*
*/
class WorkflowTransitionController {
}
@@ -0,0 +1,64 @@
<?php
/**
* @file
* Contains workflow\includes\Field\WorkflowD7Base.
*/
/*
* A Retrofit/Stub class, that contains the most basic functions of the D8 WidgetBase class.
* It serves as a superclass to containe the $field and $instance array structures for the Field Type and the Widget.
* @todo D8: Remove this class.
*/
abstract class WorkflowD7Base {
// Properties for Field and Widget.
protected $field = array();
protected $instance = array();
// Properties for Field.
protected $entity = NULL;
protected $entity_type = '';
/**
* Constructor, stub for D8 WidgetBase.
*/
public function __construct(array $field, array $instance, $entity_type = '', $entity = NULL) {
if (!empty($entity) && !is_object($entity)) {
throw new Exception('Entity should be an object.');
}
// Properties for Widget and Field.
$this->field = $field;
$this->instance = $instance;
// Properties for FieldItem.
$this->entity = $entity;
$this->entity_type = $entity_type;
}
public function getField() {
return $this->field;
}
public function getInstance() {
return $this->instance;
}
public function delete() {
}
protected function getSettings() {
$settings = isset($this->instance['widget']['settings']) ? $this->instance['widget']['settings'] : array();
$field_info = self::settings();
return $settings += $field_info['workflow']['settings'];
}
protected function getSetting($key) {
if (isset($this->instance['widget']['settings'][$key])) {
return $this->instance['widget']['settings'][$key];
}
else {
$field_info = $this->settings();
return $field_info['workflow']['settings'][$key];
}
}
}
@@ -0,0 +1,107 @@
<?php
/**
* @file
* Contains workflow\includes\Field\WorkflowDefaultWidget.
*/
/**
* Plugin implementation of the 'workflow_default' widget.
*/
class WorkflowDefaultWidget extends WorkflowD7Base { // D8: extends WidgetBase {
/**
* Returns the settings.
*
* @todo d8: Replace by the 'annotations' in D8 (See comments above this class).
*/
public static function settings() {
return array(
'workflow_default' => array(
'label' => t('Workflow'),
'field types' => array('workflow'),
'settings' => array(
'name_as_title' => 1,
'fieldset' => 0,
'comment' => 1,
),
),
);
}
/**
* Implements hook_field_widget_settings_form() --> WidgetInterface::settingsForm().
*
* {@inheritdoc}
*
* The Widget Instance has no settings. To have a uniform UX, all settings are done on the Field level.
*/
public function settingsForm(array $form, array &$form_state, $has_data) {
$element = array();
return $element;
}
/**
* 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) {
$field = $this->field;
$instance = $this->instance;
$entity = $this->entity;
$entity_type = $this->entity_type;
// Add the element. Do not use drupal_get_form, or you will have a form in a form.
workflow_transition_form($form, $form_state, $field, $instance, $entity_type, $entity);
}
/**
* Implements workflow_transition() -> WorkflowDefaultWidget::submit().
*
* Overrides submit(array $form, array &$form_state).
* Contains 2 extra parameters for D7
*
* @param array $form
* @param array $form_state
* @param array $items
* The value of the field.
* @param bool $force
* TRUE if all access must be overridden, e.g., for Rules.
*
* @return int
* If update succeeded, the new State Id. Else, the old Id is returned.
*
* This is called from function _workflowfield_form_submit($form, &$form_state)
* It is a replacement of function workflow_transition($node, $new_sid, $force, $field)
* It performs the following actions;
* - save a scheduled action
* - update history
* - restore the normal $items for the field.
* @todo: remove update of {node_form} table. (separate task, because it has features, too)
*/
public function submit(array $form, array &$form_state, array &$items, $force = FALSE) {
return workflow_transition_form_submit($form, $form_state);
}
/**
* Implements hook_field_widget_error --> WidgetInterface::errorElement().
*/
// public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, array &$form_state) {
// }
// public function settingsSummary() {
// }
// public function massageFormValues(array $values, array $form, array &$form_state) {
// }
}
@@ -0,0 +1,329 @@
<?php
/**
* @file
* Contains workflow\includes\Field\WorkflowItem.
* @see https://drupal.org/node/2064123 for 'Field Type Plugin' change record D7->D8.
*/
/**
* Plugin implementation of the 'workflow' field type.
*
* @FieldType(
* id = "workflow",
* label = @Translation("Workflow"),
* description = @Translation("This field stores Workflow values for a certain Workflow type from a list of allowed 'value => label' pairs, i.e. 'Publishing': 1 => unpublished, 2 => draft, 3 => published."),
* default_widget = "options_select",
* default_formatter = "list_formatter",
* property_type' = WORKFLOWFIELD_PROPERTY_TYPE,
* )
*/
class WorkflowItem extends WorkflowD7Base {// D8: extends ConfigFieldItemBase implements PrepareCacheInterface {
/**
* Function, that gets replaced by the 'annotations' in D8. (@see comments above this class)
*/
public static function getInfo() {
return array(
'workflow' => array(
'label' => t('Workflow'),
'description' => t("This field stores Workflow values for a certain Workflow type from a list of allowed 'value => label' pairs, i.e. 'Publishing': 1 => unpublished, 2 => draft, 3 => published."),
'settings' => array(
'allowed_values_function' => 'workflowfield_allowed_values', // For the list.module formatter
// 'allowed_values_function' => 'WorkflowItem::getAllowedValues', // For the list.module formatter.
'wid' => '',
// 'history' => 1,
// 'schedule' => 0,
// 'comment' => 0,
'widget' => array(
'options' => 'select',
'name_as_title' => 1,
'fieldset' => 0,
'hide' => 0,
'schedule' => 1,
'schedule_timezone' => 1,
'comment' => 1,
),
'watchdog_log' => 1,
'history' => array(
'history_tab_show' => 1,
'roles' => array(),
),
),
'instance_settings' => array(),
'default_widget' => 'workflow',
'default_formatter' => 'list_default',
// Properties are introduced in Entity API and used for Rules integration.
'property_type' => WORKFLOWFIELD_PROPERTY_TYPE,
'property_callbacks' => array('workflowfield_property_info_callback'),
),
);
}
/**
* Implements hook_field_settings_form() -> ConfigFieldItemInterface::settingsForm().
*
* @param array $form
* @param array $form_state
* @param $has_data
*
* @return array $element
* The newly constructed element.
*/
public function settingsForm(array $form, array &$form_state, $has_data) {
$field_info = self::getInfo();
$settings = $this->field['settings'];
$settings += $field_info['workflow']['settings'];
$settings['widget'] += $field_info['workflow']['settings']['widget'];
// Create list of all Workflow types. Include an initial empty value.
// Validate each workflow, and generate a message if not complete.
/* @var $workflow Workflow */
$workflows = array();
$workflows[''] = t('- Select a value -');
foreach ($workflows += workflow_get_workflow_names() as $wid => $label) {
$workflow = workflow_load_single($wid);
if ($wid && !$workflow->isValid()) {
unset($workflows[$wid]);
}
}
// Set message, if no 'validated' workflows exist.
if (count($workflows) == 1) {
drupal_set_message(
t('You must create at least one workflow before content can be
assigned to a workflow.')
);
}
// The allowed_values_functions is used in the formatter from list.module.
$element['allowed_values_function'] = array(
'#type' => 'value',
'#value' => $settings['allowed_values_function'], // = 'workflowfield_allowed_values',
);
// $field['settings']['wid'] can be numeric or named, or empty.
$wid = isset($settings['wid']) ? $settings['wid'] : '';
// Let the user choose between the available workflow types.
$element['wid'] = array(
'#type' => 'select',
'#title' => t('Workflow type'),
'#options' => $workflows,
'#default_value' => $wid,
'#required' => TRUE,
'#disabled' => $has_data,
'#description' => t('Choose the Workflow type. Maintain workflows !url.', array('!url' => l(t('here'), 'admin/config/workflow/workflow'))),
);
// Inform the user of possible states.
// If no Workflow type is selected yet, do not show anything.
if ($wid) {
// Get a string representation to show all options.
$allowed_values = workflow_state_load_multiple($wid);
$allowed_values_string = $this->_allowed_values_string($wid);
$element['allowed_values_string'] = array(
'#type' => 'textarea',
'#title' => t('Allowed values for the selected Workflow type'),
'#default_value' => $allowed_values_string,
'#rows' => count($allowed_values),
'#access' => TRUE, // User can see the data,
'#disabled' => TRUE, // .. but cannot change them.
);
}
$element['widget'] = array(
'#type' => 'fieldset',
'#title' => t('Workflow widget'),
'#description' => t('Set some global properties of the widgets for this
workflow. Some can be altered per widget instance.'
),
);
$fieldset_options = array(0 => t('No fieldset'), 1 => t('Collapsible fieldset'), 2 => t('Collapsed fieldset'));
$element['widget']['fieldset'] = array(
'#type' => 'select',
'#options' => $fieldset_options,
'#title' => t('Show the form in a fieldset?'),
'#default_value' => $settings['widget']['fieldset'],
'#description' => t("The Widget can be wrapped in a visible fieldset. You'd
do this when you use the widget on a Node Edit page."
),
);
$element['widget']['options'] = array(
'#type' => 'select',
'#title' => t('How to show the available states'),
'#required' => FALSE,
'#default_value' => $settings['widget']['options'],
// '#multiple' => TRUE / FALSE,
'#options' => array(
// These options are taken from options.module
'select' => 'Select list',
'radios' => 'Radio buttons',
// This option does not work properly on Comment Add form.
'buttons' => 'Action buttons',
),
'#description' => t("The Widget shows all available states. Decide which
is the best way to show them. ('Action buttons' do not work on Comment form.)"
),
);
$element['widget']['hide'] = array(
'#type' => 'checkbox',
'#attributes' => array('class' => array('container-inline')),
'#title' => t('Hide the widget on Entity form.'),
'#default_value' => $settings['widget']['hide'],
'#description' => t(
'Using Workflow Field, the widget is always shown when editing an
Entity. Set this checkbox in case you only want to change the status
on the Workflow History tab or on the Node View. (This checkbox is
only needed because Drupal core does not have a "hidden" widget.)'
),
);
$element['widget']['name_as_title'] = array(
'#type' => 'checkbox',
'#attributes' => array('class' => array('container-inline')),
'#title' => t('Use the workflow name as the title of the workflow form'),
'#default_value' => $settings['widget']['name_as_title'],
'#description' => t(
'The workflow section of the editing form is in its own fieldset.
Checking the box will add the workflow name as the title of workflow
section of the editing form.'
),
);
$element['widget']['schedule'] = array(
'#type' => 'checkbox',
'#title' => t('Allow scheduling of workflow transitions.'),
'#required' => FALSE,
'#default_value' => $settings['widget']['schedule'],
'#description' => t(
'Workflow transitions may be scheduled to a moment in the future.
Soon after the desired moment, the transition is executed by Cron.
This may be hidden by settings in widgets, formatters or permissions.'
),
);
$element['widget']['schedule_timezone'] = array(
'#type' => 'checkbox',
'#title' => t('Show a timezone when scheduling a transition.'),
'#required' => FALSE,
'#default_value' => $settings['widget']['schedule_timezone'],
);
$element['widget']['comment'] = array(
'#type' => 'select',
'#title' => t('Allow adding a comment to workflow transitions'),
'#required' => FALSE,
'#options' => array(
// Use 0/1/2 to stay compatible with previous checkbox.
0 => t('hidden'),
1 => t('optional'),
2 => t('required'),
),
'#default_value' => $settings['widget']['comment'],
'#description' => t('On the Workflow form, a Comment form can be included
so that the person making the state change can record reasons for doing
so. The comment is then included in the node\'s workflow history. This
may be altered by settings in widgets, formatters or permissions.'
),
);
$element['watchdog_log'] = array(
'#type' => 'checkbox',
'#attributes' => array('class' => array('container-inline')),
'#title' => t('Log informational watchdog messages when a transition is
executed (a state value is changed)'),
'#default_value' => $settings['watchdog_log'],
'#description' => t('Optionally log transition state changes to watchdog.'),
);
$element['history'] = array(
'#type' => 'fieldset',
'#title' => t('Workflow history'),
'#collapsible' => TRUE,
'#collapsed' => FALSE,
);
$element['history']['history_tab_show'] = array(
'#type' => 'checkbox',
'#title' => t('Use the workflow history, and show it on a separate tab.'),
'#required' => FALSE,
'#default_value' => $settings['history']['history_tab_show'],
'#description' => t("Every state change is recorded in table
{workflow_node_history}. If checked and user has proper permission, a
tab 'Workflow' is shown on the entity view page, which gives access to
the History of the workflow. If you have multiple workflows per bundle,
better disable this feature, and use, clone & adapt the Views display
'Workflow history per Entity'."),
);
$element['history']['roles'] = array(
'#type' => 'checkboxes',
'#options' => workflow_get_roles(),
'#title' => t('Workflow history permissions'),
'#default_value' => $settings['history']['roles'],
'#description' => t('Select any roles that should have access to the workflow tab on nodes that have a workflow.'),
);
return $element;
}
/**
* Implements hook_field_insert() -> FieldItemInterface::insert().
*/
public function insert() {
return $this->update();
}
/**
* Helper functions for the Field Settings page.
*
* Generates a string representation of an array of 'allowed values'.
* This is a copy from list.module's list_allowed_values_string().
* The string format is suitable for edition in a textarea.
*
* @param int $wid
* The Workflow Id.
*
* @return string
* The string representation of the $values array:
* - Values are separated by a carriage return.
* - Each value is in the format "value|label" or "value".
*/
protected function _allowed_values_string($wid = 0) {
$lines = array();
$states = workflow_state_load_multiple($wid);
$previous_wid = -1;
/* @var $state WorkflowState */
foreach ($states as $state) {
// Only show enabled states.
if ($state->isActive()) {
// Show a Workflow name between Workflows, if more then 1 in the list.
if (($wid == 0) && ($previous_wid <> $state->wid)) {
$previous_wid = $state->wid;
$lines[] = $state->name . "'s states: ";
}
$label = check_plain(t($state->label()));
$states[$state->sid] = $label;
$lines[] = $state->sid . ' | ' . $label;
}
}
return implode("\n", $lines);
}
/**
* Helper function for list.module formatter.
*
* Callback function for the list module formatter.
*
* @see list_allowed_values
* "The strings are not safe for output. Keys and values of the array should
* "be sanitized through field_filter_xss() before being displayed.
*
* @return array
* The array of allowed values. Keys of the array are the raw stored values
* (number or text), values of the array are the display labels.
* It contains all possible values, beause the result is cached,
* and used for all nodes on a page.
*/
public function getAllowedValues() {
// Get all state names, including inactive states.
$options = workflow_get_workflow_state_names(0, $grouped = FALSE, $all = TRUE);
return $options;
}
}
@@ -0,0 +1,752 @@
<?php
/**
* @file
* Contains \workflow\Form\WorkflowTransitionForm.
*/
/**
* Provides a Transition Form to be used in the Workflow Widget.
*/
class WorkflowTransitionForm { // extends FormBase {
/**
* The Workflow Transition storage.
*/
protected $field;
protected $instance;
protected $entity;
/**
* Constructs a WorkflowTransitionForm object.
* @param array $field
* @param array $instance
* @param $entity_type
* @param $entity
*/
public function __construct(array $field, array $instance, $entity_type, $entity) {
$this->field = $field; // TODO : needed?
$this->instance = $instance; // TODO : needed?
$this->entity = $entity; // TODO : needed?
$this->entity_type = $entity_type; // TODO : needed?
}
/**
* {@inheritdoc}
*/
public function getFormId() {
$field = $this->field;
// No entity may be set on VBO form.
$entity_id = ($this->entity) ? entity_id($this->entity_type, $this->entity) : '';
// The field is not set when editing a stand alone Transition.
$field_id = isset($field['id']) ? $field['id'] : '';
return implode('_', array('workflow_transition_form', $this->entity_type, $entity_id, $field_id));
}
/**
* {@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,
// '#name' => $workflow_label,
// '#parents' => array('workflow'),
'#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(
// 'id' => 'scheduled_' . $form_id,
'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[id="' . 'scheduled_' . $form_id . '"]' => array('value' => '1')),
'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', // Align with action buttons.
'#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;
}
// Some forms (Term) do not have 'base_form_id' set.
if (isset($form_state['build_info']['base_form_id']) && $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',
// '#access' => TRUE,
'#value' => t('Update workflow'),
'#weight' => -5,
// '#submit' => array( isset($instance['widget']['settings']['submit_function']) ? $instance['widget']['settings']['submit_function'] : NULL),
// '#executes_submit_callback' => TRUE,
'#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;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, array $form_state) {
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, array &$form_state, array &$items) {
// $items is a D7 parameter.
// @todo: clean this code up. It is the result of gluing code together.
global $user; // @todo #2287057: verify if submit() really is only used for UI. If not, $user must be passed.
$entity = $this->entity;
$entity_type = $this->entity_type;
$field = $this->field;
$field_name = $field['field_name'];
// Retrieve the data from the form.
if (isset($form_state['values']['workflow_field'])) {
// If $entity filled: We are on a Entity View page or Workflow History Tab page.
// If $entity empty: We are on an Advanced Action page.
// $field = $form_state['values']['workflow_field'];
// $instance = $form_state['values']['workflow_instance'];
// $entity_type = $form_state['values']['workflow_entity_type'];
// $entity = $form_state['values']['workflow_entity'];
// $field_name = $field['field_name'];
}
elseif (isset($form_state['triggering_element'])) {
// We are on an Entity/Node/Comment Form page (add/edit).
$field_name = $form_state['triggering_element']['#workflow_field_name'];
}
else {
// We are on an Entity/Comment Form page (add/edit).
}
// Determine if the transition is forced.
// This can be set by a 'workflow_vbo action' in an additional form element.
$force = isset($form_state['input']['workflow_force']) ? $form_state['input']['workflow_force'] : FALSE;
// Set language. Multi-language is not supported for Workflow Node.
$langcode = _workflow_metadata_workflow_get_properties($entity, array(), 'langcode', $entity_type, $field_name);
if (!$entity) {
// E.g., on VBO form.
}
elseif ($field_name) {
// Save the entity, but only if we were not in edit mode.
// Perhaps there is a better way, but for now we use 'changed' property.
// Also test for 'is_new'. When Migrating content, the 'changed' property may be set externally.
// Caveat: Some entities do not have 'changed' property set.
if ((!empty($entity->is_new)) || (isset($entity->changed) && $entity->changed == REQUEST_TIME)) {
// N.B. ONLY for Nodes!
// We are in add/edit mode. No need to save the entity explicitly.
// // Add the $form_state to the $items, so we can do a getTransition() later on.
// $items[0]['workflow'] = $form_state['input'];
// // Create a Transition. The Widget knows if it is scheduled.
// $widget = new WorkflowDefaultWidget($field, $instance, $entity_type, $entity);
// $new_sid = $widget->submit($form, $form_state, $items, $force);
}
elseif (isset($form_state['input'])) {
// Save $entity, but only if sid has changed.
// Use field_attach_update for this? Save always?
$entity->{$field_name}[$langcode][0]['workflow'] = $form_state['input'];
// @todo & totest: Save ony the field, not the complete entity.
// workflow_entity_field_save($entity_type, $entity, $field_name, $langcode, FALSE);
entity_save($entity_type, $entity);
return; // <-- exit!
}
elseif ($entity_type == 'node') {
// N.B. ONLY for Nodes!
// We are saving a node from a comment.
$entity->{$field_name}[$langcode] = $items;
// @todo & totest: Save ony the field, not the complete entity.
// workflow_entity_field_save($entity_type, $entity, $field_name, $langcode, FALSE);
entity_save($entity_type, $entity);
return; // <-- exit!
}
else {
// We are saving a non-node from an entity form.
$entity->{$field_name}[$langcode] = $items;
}
}
else {
// For a Node API form, only contrib fields need to be filled.
// No updating of the node itself.
// (Unless we need to record the timestamp.)
// Add the $form_state to the $items, so we can do a getTransition() later on.
$items[0]['workflow'] = $form_state['input'];
// // Create a Transition. The Widget knows if it is scheduled.
// $widget = new WorkflowDefaultWidget($field, $instance, $entity_type, $entity);
// $new_sid = $widget->submit($form, $form_state, $items, $force);
}
// Extract the data from $items, depending on the type of widget.
// @todo D8: use MassageFormValues($values, $form, $form_state).
$old_sid = workflow_node_previous_state($entity, $entity_type, $field_name);
if (!$old_sid) {
// At this moment, $old_sid should have a value. If the content does not
// have a state yet, old_sid contains '(creation)' state. But if the
// content is not associated to a workflow, old_sid is now 0. This may
// happen in workflow_vbo, if you assign a state to non-relevant nodes.
$entity_id = entity_id($entity_type, $entity);
drupal_set_message(t('Error: content !id has no workflow attached. The data is not saved.', array('!id' => $entity_id)), 'error');
// The new state is still the previous state.
$new_sid = $old_sid;
return $new_sid;
}
// Now, save/execute the transition.
$transition = $this->getTransition($old_sid, $items, $field_name, $user, $form, $form_state);
// Try to execute the transition. Return $old_sid when error.
if (!$transition) {
// This should only happen when testing/developing.
drupal_set_message(t('Error: the transition from %old_sid to %new_sid could not be generated.'), 'error');
// The current value is still the previous state.
$new_sid = $old_sid;
}
elseif ($transition->isScheduled() || $transition->isExecuted()) {
// A scheduled or executed transition must only be saved to the database.
// The entity is not changed.
$force = $force || $transition->isForced();
$transition->save();
// The current value is still the previous state.
$new_sid = $old_sid;
}
elseif (!$transition->isScheduled()) {
// Now the data is captured in the Transition, and before calling the
// Execution, restore the default values for Workflow Field.
// For instance, workflow_rules evaluates this.
if ($field_name) {
// $items = array();
// $items[0]['value'] = $old_sid;
// $entity->{$field_name}[$transition->language] = $items;
}
// It's an immediate change. Do the transition.
// - validate option; add hook to let other modules change comment.
// - add to history; add to watchdog
// Return the new State ID. (Execution may fail and return the old Sid.)
$force = $force || $transition->isForced();
$new_sid = $transition->execute($force);
}
// The entity is still to be saved, so set to a 'normal' value.
if ($field_name) {
$items = array();
$items[0]['value'] = $new_sid;
$entity->{$field_name}[$transition->language] = $items;
}
return $new_sid;
}
/**
* Extract WorkflowTransition or WorkflowScheduledTransition from the form.
*
* This merely extracts the transition from the form/widget. No validation.
*
* @param $old_sid
* @param array $items
* @param $field_name
* @param \stdClass $user
*
* @return \WorkflowScheduledTransition|\WorkflowTransition|null
*/
public function getTransition($old_sid, array $items, $field_name, stdClass $user, array &$form = array(), array &$form_state = array()) {
$entity_type = $this->entity_type;
$entity = $this->entity;
// $entity_id = entity_id($entity_type, $entity);
$field_name = !empty($this->field) ? $this->field['field_name'] : '';
if (isset($items[0]['transition'])) {
// a complete transition was already passed on.
$transition = $items[0]['transition'];
}
else {
// Get the new Transition properties. First the new State ID.
if (isset($items[0]['workflow']['workflow_sid'])) {
// We have shown a workflow form.
$new_sid = $items[0]['workflow']['workflow_sid'];
}
elseif (isset($items[0]['value'])) {
// We have shown a core options widget (radios, select).
$new_sid = $items[0]['value'];
}
else {
// This may happen if only 1 option is left, and a formatter is shown.
$state = workflow_state_load_single($old_sid);
if (!$state->isCreationState()) {
$new_sid = $old_sid;
}
else {
// This only happens on workflows, when only one transition from
// '(creation)' to another state is allowed.
/* @var $workflow Workflow */
$workflow = $state->getWorkflow();
$new_sid = $workflow->getFirstSid($this->entity_type, $this->entity, $field_name, $user, FALSE);
}
}
// If an existing Transition has been edited, $hid is set.
$hid = isset($items[0]['workflow']['workflow_hid']) ? $items[0]['workflow']['workflow_hid'] : '';
// Get the comment.
$comment = isset($items[0]['workflow']['workflow_comment']) ? $items[0]['workflow']['workflow_comment'] : '';
// Remember, the workflow_scheduled element is not set on 'add' page.
$scheduled = !empty($items[0]['workflow']['workflow_scheduling']['scheduled']);
if ($hid) {
// We are editing an existing transition. Only comment may be changed.
$transition = workflow_transition_load($hid);
$transition->comment = $comment;
}
elseif (!$scheduled) {
$transition = new WorkflowTransition();
$transition->setValues($entity_type, $entity, $field_name, $old_sid, $new_sid, $user->uid, REQUEST_TIME, $comment);
}
else {
// Schedule the time to change the state.
// If Field Form is used, use plain values;
// If Node Form is used, use fieldset 'date_time'.
$schedule = isset($items[0]['workflow']['workflow_scheduling']['date_time']) ? $items[0]['workflow']['workflow_scheduling']['date_time'] : $items[0]['workflow'];
if (!isset($schedule['workflow_scheduled_hour'])) {
$schedule['workflow_scheduled_hour'] = '00:00';
}
$scheduled_date_time
= $schedule['workflow_scheduled_date']['year']
. substr('0' . $schedule['workflow_scheduled_date']['month'], -2, 2)
. substr('0' . $schedule['workflow_scheduled_date']['day'], -2, 2)
. ' '
. $schedule['workflow_scheduled_hour']
. ' '
. $schedule['workflow_scheduled_timezone'];
if ($timestamp = strtotime($scheduled_date_time)) {
$transition = new WorkflowScheduledTransition();
$transition->setValues($entity_type, $entity, $field_name, $old_sid, $new_sid, $user->uid, $timestamp, $comment);
}
else {
$transition = NULL;
}
}
}
return $transition;
}
}
@@ -0,0 +1,84 @@
<?php
/**
* @file
* Contains workflow\lib\entity\WorkflowUnitTest.
*/
/**
* Tests for the Workflow classes.
*/
class WorkflowExampleTestCase extends DrupalWebTestCase {
protected $workflow;
public static function getInfo() {
return array(
'name' => 'Workflow',
'description' => 'Ensure that the Workflow API works as expected.',
'group' => 'Workflow',
);
}
public function setUp() {
parent::setUp('workflow'); // Enable any modules required for the test.
}
/**
* Creates a simpletest_example node using the node form.
*/
public function testWorkflow() {
// $workflows = workflow_load_multiple();
// dpm($workflows, "These are the current Workflows in the system.");
$wid = 1;
$workflow = workflow_load_single($wid);
$this->assertEqual($workflow->wid, $wid, t('The wid of the Workflow should be the same as we decided.'));
$workflow = new Workflow($wid);
$this->assertEqual($workflow->wid, $wid, t('The wid of the Workflow should be the same as we decided.'));
// $creation_state = $workflow->getCreationState();
// $this->assertEqual($creation_state, 1, t('The creation_state of wid 1 has value 1.'));
}
/*
function testWorkflowState($sid = 2, $wid = 1) {
$this->testId = '2';
$wf_state = $workflow->createState('State 2');
dpm($wf_state, 'This is state ' . $sid . ' of workflow ' . $wid);
$workflow = $wf_state->getWorkflow();
dpm($workflow, 'This is the workflow of sid ' . $sid);
return;
$this->machine->fire_event('goto2');
$this->assertEqual($this->machine->get_current_state(), 'step2', t('Current state should change when a valid event is fired.'));
$this->machine->fire_event('goto2');
$this->assertEqual($this->machine->get_current_state(), 'step2', t('Event should not execute if current state is not valid for the specified event.'));
$this->machine->fire_event('reset');
$this->assertEqual($this->machine->get_current_state(), 'step1', t('Event should allow transitions from multiple origins.'));
$current = $this->machine->get_current_state();
$this->machine->fire_event('dont_do_it');
$this->assertEqual($current, $this->machine->get_current_state(), t('State should not change when guard function returns FALSE.'));
$this->machine->fire_event('reset');
$this->machine->reset_logs();
$this->machine->fire_event('goto2_with_logs');
$this->assertEqual($this->machine->logs[0], 'guard', t('The guard condition should be the first callback executed.'));
$this->assertEqual($this->machine->logs[1], 'before_transition', t('The before_transition callback should be the second callback executed.'));
$this->assertEqual($this->machine->logs[2], 'on_exit', t('The on_exit callback should be the third callback executed.'));
$this->assertEqual($this->machine->logs[3], 'on_enter', t('The on_enter callback should be the fourth callback executed.'));
$this->assertEqual($this->machine->logs[4], 'after_transition', t('The after_transition callback should be the fifth callback executed.'));
$this->machine->fire_event('reset');
$events = $this->machine->get_available_events();
$this->assertTrue(in_array('goto2', $events), t('The machine should return a list of available events.'));
$this->assertTrue(in_array('goto3', $events), t('The machine should return a list of available events.'));
}
*/
}