diff --git a/core/modules/content_moderation/content_moderation.info.yml b/core/modules/content_moderation/content_moderation.info.yml index 12f6059..6d92b64 100644 --- a/core/modules/content_moderation/content_moderation.info.yml +++ b/core/modules/content_moderation/content_moderation.info.yml @@ -5,6 +5,3 @@ version: VERSION core: 8.x package: Core (Experimental) configure: content_moderation.overview -dependencies: - - views - - options diff --git a/core/modules/content_moderation/content_moderation.libraries.yml b/core/modules/content_moderation/content_moderation.libraries.yml index 31dd1c4..6caaaf3 100644 --- a/core/modules/content_moderation/content_moderation.libraries.yml +++ b/core/modules/content_moderation/content_moderation.libraries.yml @@ -1,5 +1,5 @@ entity-moderation-form: - version: 1.x + version: VERSION css: layout: css/entity-moderation-form.css: {} diff --git a/core/modules/content_moderation/content_moderation.module b/core/modules/content_moderation/content_moderation.module index 938dc3c..3a7582d 100644 --- a/core/modules/content_moderation/content_moderation.module +++ b/core/modules/content_moderation/content_moderation.module @@ -65,9 +65,10 @@ function content_moderation_entity_base_field_info(EntityTypeInterface $entity_t */ function content_moderation_module_implements_alter(&$implementations, $hook) { if ($hook === 'entity_view_alter') { - // Find the quickedit implementation and move content after it. + // Move the content_moderation implementation to the end of the list. + $group = $implementations['content_moderation']; unset($implementations['content_moderation']); - $implementations['content_moderation'] = FALSE; + $implementations['content_moderation'] = $group; } } @@ -114,24 +115,11 @@ function _content_moderation_create_entity_operations() { \Drupal::service('content_moderation.moderation_information'), \Drupal::service('entity_type.manager'), \Drupal::service('form_builder'), - \Drupal::service('event_dispatcher'), \Drupal::service('content_moderation.revision_tracker') ); } /** - * Implements hook_entity_storage_load(). - */ -function content_moderation_entity_storage_load(array $entities, $entity_type_id) { - // Work around the fact that this hook might be called when the container is - // not fully initialized after the module has been enabled. - // @todo Remove this check after https://www.drupal.org/node/2753733 is fixed. - if (\Drupal::hasService('content_moderation.moderation_information')) { - _content_moderation_create_entity_operations()->entityStorageLoad($entities, $entity_type_id); - } -} - -/** * Implements hook_entity_presave(). */ function content_moderation_entity_presave(EntityInterface $entity) { @@ -207,23 +195,33 @@ function content_moderation_entity_view(array &$build, EntityInterface $entity, * the appropriate permission. This permission is therefore effectively * mandatory for any user that wants to moderate things. */ -function content_moderation_node_access(NodeInterface $entity, $operation, AccountInterface $account) { +function content_moderation_node_access(NodeInterface $node, $operation, AccountInterface $account) { /** @var \Drupal\content_moderation\ModerationInformationInterface $modinfo */ $moderation_info = Drupal::service('content_moderation.moderation_information'); - if ($operation == 'view') { - return (!$entity->isPublished()) + $access_result = NULL; + if ($operation === 'view') { + $access_result = (!$node->isPublished()) ? AccessResult::allowedIfHasPermission($account, 'view any unpublished content') : AccessResult::neutral(); + + $access_result->addCacheableDependency($node); } - elseif ($operation == 'update' && $moderation_info->isModeratableEntity($entity) && $entity->moderation_information && $entity->moderation_information->target_id) { + elseif ($operation === 'update' && $moderation_info->isModeratableEntity($node) && $node->moderation_state && $node->moderation_state->target_id) { /** @var \Drupal\content_moderation\StateTransitionValidation $transition_validation */ $transition_validation = \Drupal::service('content_moderation.state_transition_validation'); - return $transition_validation->getValidTransitionTargets($entity, $account) - ? AccessResult::neutral() - : AccessResult::forbidden(); + $valid_transition_targets = $transition_validation->getValidTransitionTargets($node, $account); + $access_result = $valid_transition_targets ? AccessResult::neutral() : AccessResult::forbidden(); + + $access_result->addCacheableDependency($node); + $access_result->addCacheableDependency($account); + foreach ($valid_transition_targets as $valid_transition_target) { + $access_result->addCacheableDependency($valid_transition_target); + } } + + return $access_result; } /** @@ -249,27 +247,3 @@ function content_moderation_action_info_alter(&$definitions) { $definitions['node_unpublish_action']['class'] = ModerationOptOutUnpublishNode::class; } } - -/** - * Implements hook_views_data_alter(). - * - * @todo Use \Drupal\workbench_moderation\ViewsData - */ -function content_moderation_views_data_alter(array &$data) { - - /** @var \Drupal\content_moderation\ModerationInformationInterface $mod_info */ - $mod_info = \Drupal::service('content_moderation.moderation_information'); - - /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $etm */ - $etm = \Drupal::service('entity_type.manager'); - - $revisionable_types = $mod_info->selectRevisionableEntities($etm->getDefinitions()); - - foreach ($revisionable_types as $type) { - $data[$type->getRevisionTable()]['latest_revision'] = [ - 'title' => t('Is Latest Revision'), - 'help' => t('Restrict the view to only revisions that are the latest revision of their entity.'), - 'filter' => ['id' => 'latest_revision'], - ]; - } -} diff --git a/core/modules/content_moderation/content_moderation.routing.yml b/core/modules/content_moderation/content_moderation.routing.yml index ea71032..cc7a5ff 100644 --- a/core/modules/content_moderation/content_moderation.routing.yml +++ b/core/modules/content_moderation/content_moderation.routing.yml @@ -14,8 +14,6 @@ entity.moderation_state.collection: _title: 'Moderation states' requirements: _permission: 'administer moderation states' - options: - _admin_route: TRUE entity.moderation_state.add_form: path: '/admin/config/workflow/moderation/states/add' @@ -24,8 +22,6 @@ entity.moderation_state.add_form: _title: 'Add Moderation state' requirements: _permission: 'administer moderation states' - options: - _admin_route: TRUE entity.moderation_state.edit_form: path: '/admin/config/workflow/moderation/states/{moderation_state}' @@ -34,8 +30,6 @@ entity.moderation_state.edit_form: _title: 'Edit Moderation state' requirements: _permission: 'administer moderation states' - options: - _admin_route: TRUE entity.moderation_state.delete_form: path: '/admin/config/workflow/moderation/states/{moderation_state}/delete' @@ -44,8 +38,6 @@ entity.moderation_state.delete_form: _title: 'Delete Moderation state' requirements: _permission: 'administer moderation states' - options: - _admin_route: TRUE # ModerationStateTransition routing definition entity.moderation_state_transition.collection: @@ -55,8 +47,6 @@ entity.moderation_state_transition.collection: _title: 'Moderation state transitions' requirements: _permission: 'administer moderation state transitions' - options: - _admin_route: TRUE entity.moderation_state_transition.add_form: path: '/admin/config/workflow/moderation/transitions/add' @@ -65,8 +55,6 @@ entity.moderation_state_transition.add_form: _title: 'Add Moderation state transition' requirements: _permission: 'administer moderation state transitions' - options: - _admin_route: TRUE entity.moderation_state_transition.edit_form: path: '/admin/config/workflow/moderation/transitions/{moderation_state_transition}' @@ -75,8 +63,6 @@ entity.moderation_state_transition.edit_form: _title: 'Edit Moderation state transition' requirements: _permission: 'administer moderation state transitions' - options: - _admin_route: TRUE entity.moderation_state_transition.delete_form: path: '/admin/config/workflow/moderation/transitions/{moderation_state_transition}/delete' @@ -85,5 +71,3 @@ entity.moderation_state_transition.delete_form: _title: 'Delete Moderation state transition' requirements: _permission: 'administer moderation state transitions' - options: - _admin_route: TRUE diff --git a/core/modules/content_moderation/content_moderation.views.inc b/core/modules/content_moderation/content_moderation.views.inc index 64f767a..742c398 100644 --- a/core/modules/content_moderation/content_moderation.views.inc +++ b/core/modules/content_moderation/content_moderation.views.inc @@ -13,9 +13,19 @@ * Implements hook_views_data(). */ function content_moderation_views_data() { - $views_data = new ViewsData( + return _content_moderation_views_data_object()->getViewsData(); +} + +/** + * Implements hook_views_data_alter(). + */ +function content_moderation_views_data_alter(array &$data) { + _content_moderation_views_data_object()->alterViewsData($data); +} + +function _content_moderation_views_data_object() { + return new ViewsData( \Drupal::service('entity_type.manager'), \Drupal::service('content_moderation.moderation_information') ); - return $views_data->getViewsData(); } diff --git a/core/modules/content_moderation/src/Access/LatestRevisionCheck.php b/core/modules/content_moderation/src/Access/LatestRevisionCheck.php index c5747f3..528d195 100644 --- a/core/modules/content_moderation/src/Access/LatestRevisionCheck.php +++ b/core/modules/content_moderation/src/Access/LatestRevisionCheck.php @@ -9,9 +9,14 @@ use Drupal\content_moderation\ModerationInformationInterface; use Symfony\Component\Routing\Route; +/** + * Access check for the entity moderation tab. + */ class LatestRevisionCheck implements AccessInterface { /** + * The moderation information service. + * * @var \Drupal\content_moderation\ModerationInformationInterface */ protected $moderationInfo; @@ -32,20 +37,18 @@ public function __construct(ModerationInformationInterface $moderation_informati * This checker assumes the presence of an '_entity_access' requirement key * in the same form as used by EntityAccessCheck. * - * @see \Drupal\Core\Entity\EntityAccessCheck - * * @param \Symfony\Component\Routing\Route $route * The route to check against. * @param \Drupal\Core\Routing\RouteMatchInterface $route_match - * The parametrized route + * The parametrized route. * * @return \Drupal\Core\Access\AccessResultInterface * The access result. + * + * @see \Drupal\Core\Entity\EntityAccessCheck */ public function access(Route $route, RouteMatchInterface $route_match) { - - // This tab should not show up period unless there's a reason to show it. - // @todo Do we need any extra cache tags here? + // This tab should not show up unless there's a reason to show it. $entity = $this->loadEntity($route, $route_match); return $this->moderationInfo->hasForwardRevision($entity) ? AccessResult::allowed()->addCacheableDependency($entity) @@ -58,7 +61,7 @@ public function access(Route $route, RouteMatchInterface $route_match) { * @param \Symfony\Component\Routing\Route $route * The route to check against. * @param \Drupal\Core\Routing\RouteMatchInterface $route_match - * The parametrized route + * The parametrized route. * * @return \Drupal\Core\Entity\ContentEntityInterface * returns the Entity in question. diff --git a/core/modules/content_moderation/src/ContentModerationStateInterface.php b/core/modules/content_moderation/src/ContentModerationStateInterface.php index e69de29..5b7ee2e 100644 --- a/core/modules/content_moderation/src/ContentModerationStateInterface.php +++ b/core/modules/content_moderation/src/ContentModerationStateInterface.php @@ -0,0 +1,16 @@ +setLabel(t('User')) + ->setDescription(t('The username of the entity creator.')) + ->setSetting('target_type', 'user') + ->setDefaultValueCallback('Drupal\content_moderation\Entity\ContentModerationState::getCurrentUserId') + ->setTranslatable(TRUE) + ->setRevisionable(TRUE); + + $fields['moderation_state'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Moderation state')) + ->setDescription(t('The moderation state of the referenced content.')) + ->setSetting('target_type', 'moderation_state') + ->setRequired(TRUE) + ->setTranslatable(TRUE) + ->setRevisionable(TRUE) + ->addConstraint('ModerationState', []); + + $fields['content_entity_type_id'] = BaseFieldDefinition::create('string') + ->setLabel(t('Content entity type ID')) + ->setDescription(t('The ID of the content entity type this moderation state is for.')) + ->setRequired(TRUE) + ->setRevisionable(TRUE); + + $fields['content_entity_id'] = BaseFieldDefinition::create('integer') + ->setLabel(t('Content entity ID')) + ->setDescription(t('The ID of the content entity this moderation state is for.')) + ->setRequired(TRUE) + ->setRevisionable(TRUE); + + // @todo Add constraint that enforces unique content_entity_type_id / content_entity_id + // @todo Index on content_entity_type_id, content_entity_id and content_entity_revision_id + + $fields['content_entity_revision_id'] = BaseFieldDefinition::create('integer') + ->setLabel(t('Content entity revision ID')) + ->setDescription(t('The revision ID of the content entity this moderation state is for.')) + ->setRequired(TRUE) + ->setRevisionable(TRUE); + + return $fields; + } + + /** + * {@inheritdoc} + */ + public function getOwner() { + return $this->get('uid')->entity; + } + + /** + * {@inheritdoc} + */ + public function getOwnerId() { + return $this->getEntityKey('uid'); + } + + /** + * {@inheritdoc} + */ + public function setOwnerId($uid) { + $this->set('uid', $uid); + return $this; + } + + /** + * {@inheritdoc} + */ + public function setOwner(UserInterface $account) { + $this->set('uid', $account->id()); + return $this; + } + + /** + * Creates or updates the moderation state of an entity. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The content entity to moderate. + * @param string $moderation_state_id + * (optional) The ID of the state to give the entity. + */ + public static function updateOrCreateFromEntity(EntityInterface $entity, $moderation_state_id = NULL) { + $moderation_state = $moderation_state_id ?: $entity->moderation_state->target_id; + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + if (!$moderation_state) { + $moderation_state = \Drupal::service('content_moderation.moderation_information') + ->loadBundleEntity($entity->getEntityType()->getBundleEntityType(), $entity->bundle()) + ->getThirdPartySetting('content_moderation', 'default_moderation_state'); + } + + // @todo what if $entity->moderation_state->target_id is null at this point? + + $entity_type_id = $entity->getEntityTypeId(); + $entity_id = $entity->id(); + $entity_revision_id = $entity->getRevisionId(); + $entity_langcode = $entity->language()->getId(); + + // @todo maybe just try and get it from the computed field? + $entities = \Drupal::entityTypeManager() + ->getStorage('content_moderation_state') + ->loadByProperties([ + 'content_entity_type_id' => $entity_type_id, + 'content_entity_id' => $entity_id, + ]); + + /** @var \Drupal\content_moderation\ContentModerationStateInterface $content_moderation_state */ + $content_moderation_state = reset($entities); + if (!($content_moderation_state instanceof ContentModerationStateInterface)) { + $content_moderation_state = ContentModerationState::create([ + 'content_entity_type_id' => $entity_type_id, + 'content_entity_id' => $entity_id, + ]); + } + else { + // Create a new revision. + $content_moderation_state->setNewRevision(TRUE); + } + + // Sync translations. + if (!$content_moderation_state->hasTranslation($entity_langcode)) { + $content_moderation_state->addTranslation($entity_langcode); + } + if ($content_moderation_state->language()->getId() !== $entity_langcode) { + $content_moderation_state = $content_moderation_state->getTranslation($entity_langcode); + } + + // Create the ContentModerationState entity for the inserted entity. + $content_moderation_state->set('content_entity_revision_id', $entity_revision_id); + $content_moderation_state->set('moderation_state', $moderation_state); + $content_moderation_state->save(); + } + + /** + * Default value callback for the 'uid' base field definition. + * + * @see \Drupal\content_moderation\Entity\ContentModerationState::baseFieldDefinitions() + * + * @return array + * An array of default values. + */ + public static function getCurrentUserId() { + return array(\Drupal::currentUser()->id()); + } + +} diff --git a/core/modules/content_moderation/src/Entity/Handler/BlockContentModerationHandler.php b/core/modules/content_moderation/src/Entity/Handler/BlockContentModerationHandler.php index 152a8a3..b88b415 100644 --- a/core/modules/content_moderation/src/Entity/Handler/BlockContentModerationHandler.php +++ b/core/modules/content_moderation/src/Entity/Handler/BlockContentModerationHandler.php @@ -4,7 +4,6 @@ use Drupal\Core\Form\FormStateInterface; - /** * Customizations for block content entities. */ diff --git a/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php b/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php index 7bce45d..f2835e4 100644 --- a/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php +++ b/core/modules/content_moderation/src/Entity/Handler/ModerationHandler.php @@ -2,7 +2,6 @@ namespace Drupal\content_moderation\Entity\Handler; - use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityHandlerInterface; @@ -21,7 +20,7 @@ class ModerationHandler implements ModerationHandlerInterface, EntityHandlerInte use StringTranslationTrait; /** - * @inheritDoc + * {@inheritdoc} */ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { return new static(); @@ -41,13 +40,12 @@ public function onPresave(ContentEntityInterface $entity, $default_revision, $pu * {@inheritdoc} */ public function onBundleModerationConfigurationFormSubmit(ConfigEntityInterface $bundle) { - // The Revisions portion of Entity API is not uniformly applied or consistent. - // Until that's fixed in core, we'll make a best-attempt to apply it to - // the common entity patterns so as to avoid every entity type needing to - // implement this method, although some will still need to do so for now. - - // This is the API that should be universal, but isn't yet. See NodeType - // for an example. + // The Revisions portion of Entity API is not uniformly applied or + // consistent. Until that's fixed in core, we'll make a best-attempt to + // apply it to the common entity patterns so as to avoid every entity type + // needing to implement this method, although some will still need to do so + // for now. This is the API that should be universal, but isn't yet. + // @see \Drupal\node\Entity\NodeType if (method_exists($bundle, 'setNewRevision')) { $bundle->setNewRevision(TRUE); } @@ -60,25 +58,19 @@ public function onBundleModerationConfigurationFormSubmit(ConfigEntityInterface $bundle->set('revision', TRUE); } - $bundle->save(); - - return; } /** * {@inheritdoc} */ public function enforceRevisionsEntityFormAlter(array &$form, FormStateInterface $form_state, $form_id) { - return; } - /** * {@inheritdoc} */ public function enforceRevisionsBundleFormAlter(array &$form, FormStateInterface $form_state, $form_id) { - return; } } diff --git a/core/modules/content_moderation/src/Entity/Handler/ModerationHandlerInterface.php b/core/modules/content_moderation/src/Entity/Handler/ModerationHandlerInterface.php index 0946e6c..e897cf4 100644 --- a/core/modules/content_moderation/src/Entity/Handler/ModerationHandlerInterface.php +++ b/core/modules/content_moderation/src/Entity/Handler/ModerationHandlerInterface.php @@ -2,7 +2,6 @@ namespace Drupal\content_moderation\Entity\Handler; - use Drupal\Core\Config\Entity\ConfigEntityInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Form\FormStateInterface; @@ -17,6 +16,8 @@ interface ModerationHandlerInterface { /** + * Operates on moderatable content entities preSave(). + * * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity to modify. * @param bool $default_revision @@ -36,9 +37,8 @@ public function onPresave(ContentEntityInterface $entity, $default_revision, $pu * The most common use case is to force revisions on for this bundle if * moderation is enabled. That, sadly, does not have a common API in core. * - * @param \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $bundle + * @param \Drupal\Core\Config\Entity\ConfigEntityInterface $bundle * The bundle definition that is being saved. - * @return mixed */ public function onBundleModerationConfigurationFormSubmit(ConfigEntityInterface $bundle); diff --git a/core/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php b/core/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php index 7f81ff1..83de187 100644 --- a/core/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php +++ b/core/modules/content_moderation/src/Entity/Handler/NodeModerationHandler.php @@ -14,7 +14,7 @@ class NodeModerationHandler extends ModerationHandler { * {@inheritdoc} */ public function onPresave(ContentEntityInterface $entity, $default_revision, $published_state) { - if ($this->shouldModerate($entity)) { + if ($this->shouldModerate($entity, $published_state)) { parent::onPresave($entity, $default_revision, $published_state); // Only nodes have a concept of published. /** @var \Drupal\node\NodeInterface $entity */ @@ -49,15 +49,18 @@ public function enforceRevisionsBundleFormAlter(array &$form, FormStateInterface * Check if an entity's default revision and/or state needs adjusting. * * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity to check. + * @param bool $published_state + * Whether the state being transitioned to is a published state or not. * * @return bool * TRUE when either the default revision or the state needs to be updated. */ - protected function shouldModerate(ContentEntityInterface $entity) { + protected function shouldModerate(ContentEntityInterface $entity, $published_state) { + // @todo clarify the first condition. // First condition is needed so you can add a translation. - // Second condition is needed when you want to publish a translation. - // Third condition is needed when you want to create a new draft for a published translation. - return $entity->isDefaultTranslation() || $entity->moderation_state->entity->isPublishedState() || $entity->isPublished(); + // Second condition checks to see if the published status has changed. + return $entity->isDefaultTranslation() || $entity->isPublished() !== $published_state; } } diff --git a/core/modules/content_moderation/src/Entity/ModerationState.php b/core/modules/content_moderation/src/Entity/ModerationState.php index e6268ad..291a74a 100644 --- a/core/modules/content_moderation/src/Entity/ModerationState.php +++ b/core/modules/content_moderation/src/Entity/ModerationState.php @@ -34,6 +34,7 @@ * ) */ class ModerationState extends ConfigEntityBase implements ModerationStateInterface { + /** * The Moderation state ID. * diff --git a/core/modules/content_moderation/src/Entity/ModerationStateTransition.php b/core/modules/content_moderation/src/Entity/ModerationStateTransition.php index 41863f0..99dbf93 100644 --- a/core/modules/content_moderation/src/Entity/ModerationStateTransition.php +++ b/core/modules/content_moderation/src/Entity/ModerationStateTransition.php @@ -35,6 +35,7 @@ * ) */ class ModerationStateTransition extends ConfigEntityBase implements ModerationStateTransitionInterface { + /** * The Moderation state transition ID. * diff --git a/core/modules/content_moderation/src/EntityOperations.php b/core/modules/content_moderation/src/EntityOperations.php index e0e1519..a934b0e 100644 --- a/core/modules/content_moderation/src/EntityOperations.php +++ b/core/modules/content_moderation/src/EntityOperations.php @@ -2,16 +2,15 @@ namespace Drupal\content_moderation; +use Drupal\content_moderation\Entity\ContentModerationState; +use Drupal\content_moderation\Entity\ModerationState; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\TypedData\TranslatableInterface; -use Drupal\content_moderation\Event\ContentModerationEvents; -use Drupal\content_moderation\Event\ContentModerationTransitionEvent; use Drupal\content_moderation\Form\EntityModerationForm; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * Defines a class for reacting to entity events. @@ -33,13 +32,6 @@ class EntityOperations { protected $entityTypeManager; /** - * The event dispatcher. - * - * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface - */ - protected $eventDispatcher; - - /** * The Form Builder service. * * @var \Drupal\Core\Form\FormBuilderInterface @@ -62,54 +54,17 @@ class EntityOperations { * Entity type manager service. * @param \Drupal\Core\Form\FormBuilderInterface $form_builder * The form builder. - * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher - * The event dispatcher. * @param \Drupal\content_moderation\RevisionTrackerInterface $tracker * The revision tracker. */ - public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, EventDispatcherInterface $event_dispatcher, RevisionTrackerInterface $tracker) { + public function __construct(ModerationInformationInterface $moderation_info, EntityTypeManagerInterface $entity_type_manager, FormBuilderInterface $form_builder, RevisionTrackerInterface $tracker) { $this->moderationInfo = $moderation_info; $this->entityTypeManager = $entity_type_manager; $this->formBuilder = $form_builder; - $this->eventDispatcher = $event_dispatcher; $this->tracker = $tracker; } /** - * Hook bridge. - * - * @see hook_entity_storage_load() - * - * @param EntityInterface[] $entities - * An array of entity objects that have just been loaded. - * @param string $entity_type_id - * The type of entity being loaded, such as "node" or "user". - */ - public function entityStorageLoad(array $entities, $entity_type_id) { - - // Ensure that all moderatable entities always have a moderation_state field - // with data, in all translations. That avoids us needing to have a thousand - // NULL checks elsewhere in the code. - - // Quickly exclude any non-moderatable entities. - $to_check = array_filter($entities, [$this->moderationInfo, 'isModeratableEntity']); - if (!$to_check) { - return; - } - - // @todo make this more functional, less iterative. - // https://www.drupal.org/node/2755099 - foreach ($to_check as $entity) { - foreach ($entity->getTranslationLanguages() as $language) { - $translation = $entity->getTranslation($language->getId()); - if ($translation->moderation_state->target_id == NULL) { - $translation->moderation_state->target_id = $this->getDefaultLoadStateId($translation); - } - } - } - } - - /** * Determines the default moderation state on load for an entity. * * This method is only applicable when an entity is loaded that has @@ -145,73 +100,67 @@ public function entityPresave(EntityInterface $entity) { if (!$this->moderationInfo->isModeratableEntity($entity)) { return; } - if ($entity->moderation_state->entity) { - $published_state = $entity->moderation_state->entity->isPublishedState(); + if ($entity->moderation_state->target_id) { + $moderation_state = ModerationState::load($entity->moderation_state->target_id); + $published_state = $moderation_state->isPublishedState(); // This entity is default if it is new, the default revision, or the // default revision is not published. $update_default_revision = $entity->isNew() - || $entity->moderation_state->entity->isDefaultRevisionState() + || $moderation_state->isDefaultRevisionState() || !$this->isDefaultRevisionPublished($entity); // Fire per-entity-type logic for handling the save process. $this->entityTypeManager->getHandler($entity->getEntityTypeId(), 'moderation')->onPresave($entity, $update_default_revision, $published_state); - - // There's currently a bug in core where $entity->original always points - // to the default revision, for now work around this by loading the latest - // revision. - $latest_revision = $this->moderationInfo->getLatestRevision($entity->getEntityTypeId(), $entity->id()); - $state_before = !empty($latest_revision) ? $latest_revision->moderation_state->target_id : NULL; - // @todo: Revert to this simpler version when https://www.drupal.org/node/2700747 is fixed. - // $state_before = isset($entity->original) ? $entity->original->moderation_state->target_id : NULL; - - $state_after = $entity->moderation_state->target_id; - - // Allow other modules to respond to the transition. Note that this - // does not provide any mechanism to cancel the transition, since - // Entity API doesn't allow hook_entity_presave to short-circuit a save. - $event = new ContentModerationTransitionEvent($entity, $state_before, $state_after); - - $this->eventDispatcher->dispatch(ContentModerationEvents::STATE_TRANSITION, $event); } } /** * Hook bridge. * - * @see hook_entity_insert() - * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity that was just saved. + * + * @see hook_entity_insert() */ public function entityInsert(EntityInterface $entity) { if (!$this->moderationInfo->isModeratableEntity($entity)) { return; } - - /** ContentEntityInterface $entity */ - - // Update our own record keeping. - $this->tracker->setLatestRevision($entity->getEntityTypeId(), $entity->id(), $entity->language()->getId(), $entity->getRevisionId()); + ContentModerationState::updateOrCreateFromEntity($entity); + $this->setLatestRevision($entity); } /** * Hook bridge. * - * @see hook_entity_update() - * * @param \Drupal\Core\Entity\EntityInterface $entity * The entity that was just saved. + * + * @see hook_entity_update() */ public function entityUpdate(EntityInterface $entity) { if (!$this->moderationInfo->isModeratableEntity($entity)) { return; } + ContentModerationState::updateOrCreateFromEntity($entity); + $this->setLatestRevision($entity); + } - /** ContentEntityInterface $entity */ - - // Update our own record keeping. - $this->tracker->setLatestRevision($entity->getEntityTypeId(), $entity->id(), $entity->language()->getId(), $entity->getRevisionId()); + /** + * Set the latest revision. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The content entity to create content_moderation_state entity for. + */ + protected function setLatestRevision(EntityInterface $entity) { + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $this->tracker->setLatestRevision( + $entity->getEntityTypeId(), + $entity->id(), + $entity->language()->getId(), + $entity->getRevisionId() + ); } /** @@ -223,14 +172,13 @@ public function entityUpdate(EntityInterface $entity) { * @see EntityFieldManagerInterface::getExtraFields() */ public function entityView(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) { - if (!$this->moderationInfo->isModeratableEntity($entity)) { return; } if (!$this->moderationInfo->isLatestRevision($entity)) { return; } - /** @var ContentEntityInterface $entity */ + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ if ($entity->isDefaultRevision()) { return; } diff --git a/core/modules/content_moderation/src/EntityTypeInfo.php b/core/modules/content_moderation/src/EntityTypeInfo.php index 7528dbb..95a1be4 100644 --- a/core/modules/content_moderation/src/EntityTypeInfo.php +++ b/core/modules/content_moderation/src/EntityTypeInfo.php @@ -2,6 +2,7 @@ namespace Drupal\content_moderation; +use Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList; use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; use Drupal\Core\Entity\ContentEntityTypeInterface; use Drupal\Core\Entity\EntityInterface; @@ -37,12 +38,15 @@ class EntityTypeInfo { protected $moderationInfo; /** + * The entity type manager. + * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** * A keyed array of custom moderation handlers for given entity types. + * * Any entity not specified will use a common default. * * @var array @@ -119,7 +123,7 @@ protected function addModerationToEntity(ContentEntityTypeInterface $type) { } /** - * Modifies an entity type definition to include moderation configuration support. + * Configures moderation configuration support on a entity type definition. * * That "configuration support" includes a configuration form, a hypermedia * link, and a route provider to tie it all together. There's also a @@ -199,16 +203,16 @@ public function entityOperation(EntityInterface $entity) { * - edit: (optional) String containing markup (normally a link) used as the * element's 'edit' operation in the administration interface. Only for * 'form' context. - * - delete: (optional) String containing markup (normally a link) used as the - * element's 'delete' operation in the administration interface. Only for - * 'form' context. + * - delete: (optional) String containing markup (normally a link) used as + * the element's 'delete' operation in the administration interface. Only + * for 'form' context. */ public function entityExtraFieldInfo() { $return = []; foreach ($this->getModeratedBundles() as $bundle) { $return[$bundle['entity']][$bundle['bundle']]['display']['content_moderation_control'] = [ 'label' => $this->t('Moderation control'), - 'description' => $this->t('Status listing and form for the entity\'s moderation state.'), + 'description' => $this->t("Status listing and form for the entity's moderation state."), 'weight' => -20, 'visible' => TRUE, ]; @@ -225,7 +229,8 @@ public function entityExtraFieldInfo() { * * @return \Generator * A generator, yielding a 2 element associative array: - * - entity: The machine name of an entity, such as "node" or "block_content". + * - entity: The machine name of an entity type, such as "node" or + * "block_content". * - bundle: The machine name of a bundle, such as "page" or "article". */ protected function getModeratedBundles() { @@ -254,21 +259,17 @@ protected function getModeratedBundles() { * New fields added by moderation state. */ public function entityBaseFieldInfo(EntityTypeInterface $entity_type) { - if (!$this->moderationInfo->isModeratableEntityType($entity_type)) { return []; } $fields = []; - // @todo write a test for this. $fields['moderation_state'] = BaseFieldDefinition::create('entity_reference') ->setLabel(t('Moderation state')) ->setDescription(t('The moderation state of this piece of content.')) + ->setComputed(TRUE) + ->setClass(ModerationStateFieldItemList::class) ->setSetting('target_type', 'moderation_state') - ->setTargetEntityTypeId($entity_type->id()) - ->setRevisionable(TRUE) - ->setTranslatable(TRUE) - // @todo write a test for this. ->setDisplayOptions('view', [ 'label' => 'hidden', 'type' => 'hidden', @@ -283,12 +284,14 @@ public function entityBaseFieldInfo(EntityTypeInterface $entity_type) { ]) ->addConstraint('ModerationState', []) ->setDisplayConfigurable('form', FALSE) - ->setDisplayConfigurable('view', FALSE); + ->setDisplayConfigurable('view', FALSE) + ->setTranslatable(TRUE); + return $fields; } /** - * Force moderatable bundles to have a moderation_state field. + * Adds the ModerationState constraint to bundles that are moderatable. * * @param \Drupal\Core\Field\FieldDefinitionInterface[] $fields * The array of bundle field definitions. @@ -330,7 +333,7 @@ public function bundleFormAlter(array &$form, FormStateInterface $form_state, $f $this->entityTypeManager->getHandler($entity->getEntityTypeId(), 'moderation')->enforceRevisionsEntityFormAlter($form, $form_state, $form_id); - // Submit handler to redirect to the + // Submit handler to redirect to the latest version, if available. $form['actions']['submit']['#submit'][] = [EntityTypeInfo::class, 'bundleFormRedirect']; } } diff --git a/core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php b/core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php index f024942..5498ff8 100644 --- a/core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php +++ b/core/modules/content_moderation/src/Form/BundleModerationConfigurationForm.php @@ -2,8 +2,9 @@ namespace Drupal\content_moderation\Form; -use Drupal\Core\Config\Entity\ConfigEntityInterface; +use Drupal\Core\Config\Entity\ThirdPartySettingsInterface; use Drupal\Core\Entity\EntityForm; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\content_moderation\Entity\ModerationState; @@ -143,16 +144,22 @@ public function form(array $form, FormStateInterface $form_state) { * * @todo This should be folded into the form method. * - * @param $entity_type - * @param \Drupal\Core\Config\Entity\ConfigEntityInterface $bundle + * @param string $entity_type_id + * The entity type identifier. + * @param \Drupal\Core\Entity\EntityInterface $bundle + * The bundle entity updated with the submitted values. * @param array $form + * The complete form array. * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. */ - public function formBuilderCallback($entity_type, ConfigEntityInterface $bundle, &$form, FormStateInterface $form_state) { + public function formBuilderCallback($entity_type_id, EntityInterface $bundle, &$form, FormStateInterface $form_state) { // @todo write a test for this. - $bundle->setThirdPartySetting('content_moderation', 'enabled', $form_state->getValue('enable_moderation_state')); - $bundle->setThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys(array_filter($form_state->getValue('allowed_moderation_states_published') + $form_state->getValue('allowed_moderation_states_unpublished')))); - $bundle->setThirdPartySetting('content_moderation', 'default_moderation_state', $form_state->getValue('default_moderation_state')); + if ($bundle instanceof ThirdPartySettingsInterface) { + $bundle->setThirdPartySetting('content_moderation', 'enabled', $form_state->getValue('enable_moderation_state')); + $bundle->setThirdPartySetting('content_moderation', 'allowed_moderation_states', array_keys(array_filter($form_state->getValue('allowed_moderation_states_published') + $form_state->getValue('allowed_moderation_states_unpublished')))); + $bundle->setThirdPartySetting('content_moderation', 'default_moderation_state', $form_state->getValue('default_moderation_state')); + } } /** @@ -172,9 +179,8 @@ public function validateForm(array &$form, FormStateInterface $form_state) { * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { - - // If moderation is enabled, revisions MUST be enabled as well. - // Otherwise we can't have forward revisions. + // If moderation is enabled, revisions MUST be enabled as well. Otherwise we + // can't have forward revisions. if ($form_state->getValue('enable_moderation_state')) { /* @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $bundle */ $bundle = $form_state->getFormObject()->getEntity(); @@ -182,7 +188,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $this->entityTypeManager->getHandler($bundle->getEntityType()->getBundleOf(), 'moderation')->onBundleModerationConfigurationFormSubmit($bundle); } - parent::submitForm( $form, $form_state); + parent::submitForm($form, $form_state); drupal_set_message($this->t('Your settings have been saved.')); } diff --git a/core/modules/content_moderation/src/Form/EntityModerationForm.php b/core/modules/content_moderation/src/Form/EntityModerationForm.php index 734b56a..39baec0 100644 --- a/core/modules/content_moderation/src/Form/EntityModerationForm.php +++ b/core/modules/content_moderation/src/Form/EntityModerationForm.php @@ -11,23 +11,42 @@ use Drupal\content_moderation\StateTransitionValidation; use Symfony\Component\DependencyInjection\ContainerInterface; +/** + * The EntityModerationForm provides a simple UI for changing moderation state. + */ class EntityModerationForm extends FormBase { /** + * The moderation information service. + * * @var \Drupal\content_moderation\ModerationInformationInterface */ protected $moderationInfo; /** + * The moderation state transition validation service. + * * @var \Drupal\content_moderation\StateTransitionValidation */ protected $validation; /** + * The entity type manager. + * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; + /** + * EntityModerationForm constructor. + * + * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info + * The moderation information service. + * @param \Drupal\content_moderation\StateTransitionValidation $validation + * The moderation state transition validation service. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ public function __construct(ModerationInformationInterface $moderation_info, StateTransitionValidation $validation, EntityTypeManagerInterface $entity_type_manager) { $this->moderationInfo = $moderation_info; $this->validation = $validation; @@ -35,7 +54,7 @@ public function __construct(ModerationInformationInterface $moderation_info, Sta } /** - * @inheritDoc + * {@inheritdoc} */ public static function create(ContainerInterface $container) { return new static( @@ -46,14 +65,14 @@ public static function create(ContainerInterface $container) { } /** - * @inheritDoc + * {@inheritdoc} */ public function getFormId() { return 'content_moderation_entity_moderation_form'; } /** - * @inheritDoc + * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state, ContentEntityInterface $entity = NULL) { /** @var \Drupal\content_moderation\Entity\ModerationState $current_state */ @@ -110,15 +129,17 @@ public function buildForm(array $form, FormStateInterface $form_state, ContentEn } /** - * @inheritDoc + * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state) { /** @var ContentEntityInterface $entity */ $entity = $form_state->get('entity'); $new_state = $form_state->getValue('new_state'); - $entity->moderation_state->target_id = $new_state; + // @todo should we just just be updating the content moderation state + // entity? That would prevent setting the revision log. + $entity->moderation_state->target_id = $new_state; $entity->revision_log = $form_state->getValue('revision_log'); $entity->save(); diff --git a/core/modules/content_moderation/src/Form/ModerationStateDeleteForm.php b/core/modules/content_moderation/src/Form/ModerationStateDeleteForm.php index 1bbec59..43e2b36 100644 --- a/core/modules/content_moderation/src/Form/ModerationStateDeleteForm.php +++ b/core/modules/content_moderation/src/Form/ModerationStateDeleteForm.php @@ -10,6 +10,7 @@ * Builds the form to delete Moderation state entities. */ class ModerationStateDeleteForm extends EntityConfirmFormBase { + /** * {@inheritdoc} */ @@ -37,13 +38,10 @@ public function getConfirmText() { public function submitForm(array &$form, FormStateInterface $form_state) { $this->entity->delete(); - drupal_set_message( - $this->t('Moderation state %label deleted.', - [ - '%label' => $this->entity->label() - ] - ) - ); + drupal_set_message($this->t( + 'Moderation state %label deleted.', + ['%label' => $this->entity->label()] + )); $form_state->setRedirectUrl($this->getCancelUrl()); } diff --git a/core/modules/content_moderation/src/Form/ModerationStateTransitionDeleteForm.php b/core/modules/content_moderation/src/Form/ModerationStateTransitionDeleteForm.php index 5b49cd0..f153f1f 100644 --- a/core/modules/content_moderation/src/Form/ModerationStateTransitionDeleteForm.php +++ b/core/modules/content_moderation/src/Form/ModerationStateTransitionDeleteForm.php @@ -10,6 +10,7 @@ * Builds the form to delete Moderation state transition entities. */ class ModerationStateTransitionDeleteForm extends EntityConfirmFormBase { + /** * {@inheritdoc} */ @@ -37,13 +38,10 @@ public function getConfirmText() { public function submitForm(array &$form, FormStateInterface $form_state) { $this->entity->delete(); - drupal_set_message( - $this->t('Moderation transition %label deleted.', - [ - '%label' => $this->entity->label() - ] - ) - ); + drupal_set_message($this->t( + 'Moderation transition %label deleted.', + ['%label' => $this->entity->label()] + )); $form_state->setRedirectUrl($this->getCancelUrl()); } diff --git a/core/modules/content_moderation/src/Form/ModerationStateTransitionForm.php b/core/modules/content_moderation/src/Form/ModerationStateTransitionForm.php index 084f794..d7444ef 100644 --- a/core/modules/content_moderation/src/Form/ModerationStateTransitionForm.php +++ b/core/modules/content_moderation/src/Form/ModerationStateTransitionForm.php @@ -16,11 +16,15 @@ class ModerationStateTransitionForm extends EntityForm { /** + * The entity type manager. + * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** + * The entity query factory. + * * @var \Drupal\Core\Entity\Query\QueryFactory */ protected $queryFactory; @@ -29,6 +33,9 @@ class ModerationStateTransitionForm extends EntityForm { * Constructs a new ModerationStateTransitionForm. * * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Entity\Query\QueryFactory $query_factory + * The entity query factory. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, QueryFactory $query_factory) { $this->entityTypeManager = $entity_type_manager; diff --git a/core/modules/content_moderation/src/ModerationInformationInterface.php b/core/modules/content_moderation/src/ModerationInformationInterface.php index afecbc3..b203792 100644 --- a/core/modules/content_moderation/src/ModerationInformationInterface.php +++ b/core/modules/content_moderation/src/ModerationInformationInterface.php @@ -21,6 +21,7 @@ * The bundle ID. * * @return \Drupal\Core\Config\Entity\ConfigEntityInterface|null + * The bundle entity. */ public function loadBundleEntity($bundle_entity_type_id, $bundle_id); @@ -60,7 +61,7 @@ public function isModeratableEntityType(EntityTypeInterface $entity_type); public function isModeratableBundle(EntityTypeInterface $entity_type, $bundle); /** - * Filters an entity list to just bundle definitions for revisionable entities. + * Filters entity lists to just bundle definitions for revisionable entities. * * @param EntityTypeInterface[] $entity_types * The master entity type list filter. @@ -71,9 +72,10 @@ public function isModeratableBundle(EntityTypeInterface $entity_type, $bundle); public function selectRevisionableEntityTypes(array $entity_types); /** - * Filters an entity list to just the definitions for moderatable entities. + * Filters entity lists to just the definitions for moderatable entities. * - * An entity type is moderatable only if it is both revisionable and bundable. + * An entity type is moderatable only if it is both revisionable and + * bundleable. * * @param EntityTypeInterface[] $entity_types * The master entity type list filter. @@ -173,8 +175,8 @@ public function getDefaultRevisionId($entity_type_id, $entity_id); /** * Determines if an entity is a latest revision. * - * @param \Drupal\Core\Entity\EntityInterface $entity - * A revisionable Content entity. + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * A revisionable content entity. * * @return bool * TRUE if the specified object is the latest revision of its entity, @@ -199,9 +201,9 @@ public function hasForwardRevision(ContentEntityInterface $entity); * A "live" entity revision is one whose latest revision is also the default, * and whose moderation state, if any, is a published state. * - * * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity to check. + * * @return bool * TRUE if the specified entity is a live revision, FALSE otherwise. */ diff --git a/core/modules/content_moderation/src/ModerationStateInterface.php b/core/modules/content_moderation/src/ModerationStateInterface.php index 38c7569..99f664f 100644 --- a/core/modules/content_moderation/src/ModerationStateInterface.php +++ b/core/modules/content_moderation/src/ModerationStateInterface.php @@ -10,16 +10,15 @@ interface ModerationStateInterface extends ConfigEntityInterface { /** - * Determines if this state represents a published node. + * Determines if content updated to this state should be published. * * @return bool - * TRUE if this state deems the node published. + * TRUE if content updated to this state should be published. */ public function isPublishedState(); /** - * Determines if a revision should be made the default revision upon transition to - * this state. + * Determines if content updated to this state should be the default revision. * * @return bool * TRUE if content in this state should be the default revision. diff --git a/core/modules/content_moderation/src/ModerationStateListBuilder.php b/core/modules/content_moderation/src/ModerationStateListBuilder.php index 4ebaff1..c3dc933 100644 --- a/core/modules/content_moderation/src/ModerationStateListBuilder.php +++ b/core/modules/content_moderation/src/ModerationStateListBuilder.php @@ -9,6 +9,7 @@ * Provides a listing of Moderation state entities. */ class ModerationStateListBuilder extends ConfigEntityListBuilder { + /** * {@inheritdoc} */ @@ -24,7 +25,7 @@ public function buildHeader() { public function buildRow(EntityInterface $entity) { $row['label'] = $entity->label(); $row['id'] = $entity->id(); - // You probably want a few more properties here... + // You probably want a few more properties here. return $row + parent::buildRow($entity); } diff --git a/core/modules/content_moderation/src/ModerationStateTransitionListBuilder.php b/core/modules/content_moderation/src/ModerationStateTransitionListBuilder.php index 8cade6f..5544530 100644 --- a/core/modules/content_moderation/src/ModerationStateTransitionListBuilder.php +++ b/core/modules/content_moderation/src/ModerationStateTransitionListBuilder.php @@ -21,7 +21,7 @@ class ModerationStateTransitionListBuilder extends DraggableListBuilder { protected $stateStorage; /** - * @inheritDoc + * {@inheritdoc} */ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { return new static( @@ -37,7 +37,7 @@ public static function createInstance(ContainerInterface $container, EntityTypeI * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type * Entity Type. * @param \Drupal\Core\Entity\EntityStorageInterface $transition_storage - * Moderation state transition entity storage + * Moderation state transition entity storage. * @param \Drupal\Core\Entity\EntityStorageInterface $state_storage * Moderation state entity storage. */ @@ -82,7 +82,7 @@ public function buildRow(EntityInterface $entity) { * {@inheritdoc} */ public function render() { - $build = parent::render(); // TODO: Change the autogenerated stub + $build = parent::render(); $build['item'] = [ '#type' => 'item', diff --git a/core/modules/content_moderation/src/ParamConverter/EntityRevisionConverter.php b/core/modules/content_moderation/src/ParamConverter/EntityRevisionConverter.php index 0fbe46b..ee7b2d5 100644 --- a/core/modules/content_moderation/src/ParamConverter/EntityRevisionConverter.php +++ b/core/modules/content_moderation/src/ParamConverter/EntityRevisionConverter.php @@ -3,11 +3,11 @@ namespace Drupal\content_moderation\ParamConverter; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\ParamConverter\EntityConverter; use Drupal\Core\TypedData\TranslatableInterface; use Drupal\content_moderation\ModerationInformationInterface; use Symfony\Component\Routing\Route; -use Drupal\Core\Entity\EntityTypeManagerInterface; /** * Defines a class for making sure the edit-route loads the current draft. @@ -24,16 +24,16 @@ class EntityRevisionConverter extends EntityConverter { /** * EntityRevisionConverter constructor. * - * @todo: If the parent class is ever cleaned up to use EntityTypeManager - * instead of Entity manager, this method will also need to be adjusted. - * - * @param \Drupal\Core\Entity\EntityManagerInterface $entity_type_manager + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager, needed by the parent class. * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info * The moderation info utility service. + * + * @todo: If the parent class is ever cleaned up to use EntityTypeManager + * instead of Entity manager, this method will also need to be adjusted. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, ModerationInformationInterface $moderation_info) { - parent::__construct($entity_type_manager); + public function __construct(EntityManagerInterface $entity_manager, ModerationInformationInterface $moderation_info) { + parent::__construct($entity_manager); $this->moderationInformation = $moderation_info; } @@ -70,7 +70,7 @@ protected function hasForwardRevisionFlag(array $definition) { * Returns TRUE if the route is the edit form of an entity, FALSE otherwise. */ protected function isEditFormPage(Route $route) { - if ($default = $route->getDefault('_entity_form') ) { + if ($default = $route->getDefault('_entity_form')) { // If no operation is provided, use 'default'. $default .= '.default'; list($entity_type_id, $operation) = explode('.', $default); diff --git a/core/modules/content_moderation/src/Permissions.php b/core/modules/content_moderation/src/Permissions.php index 90f509c..f23ccc7 100644 --- a/core/modules/content_moderation/src/Permissions.php +++ b/core/modules/content_moderation/src/Permissions.php @@ -2,7 +2,6 @@ namespace Drupal\content_moderation; -use Drupal\Core\Routing\UrlGeneratorTrait; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\content_moderation\Entity\ModerationState; use Drupal\content_moderation\Entity\ModerationStateTransition; @@ -13,7 +12,6 @@ class Permissions { use StringTranslationTrait; - use UrlGeneratorTrait; /** * Returns an array of transition permissions. diff --git a/core/modules/content_moderation/src/Plugin/Action/ModerationOptOutPublishNode.php b/core/modules/content_moderation/src/Plugin/Action/ModerationOptOutPublishNode.php index 3975492..a85bac6 100644 --- a/core/modules/content_moderation/src/Plugin/Action/ModerationOptOutPublishNode.php +++ b/core/modules/content_moderation/src/Plugin/Action/ModerationOptOutPublishNode.php @@ -18,24 +18,36 @@ class ModerationOptOutPublishNode extends PublishNode implements ContainerFactor /** * Moderation information service. - * + * * @var \Drupal\content_moderation\ModerationInformationInterface */ protected $moderationInfo; - public function __construct(array $configuration, $plugin_id, $plugin_definition, ModerationInformationInterface $mod_info) { + /** + * ModerationOptOutPublishNode constructor. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info + * The moderation information service. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, ModerationInformationInterface $moderation_info) { parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->moderationInfo = $mod_info; + $this->moderationInfo = $moderation_info; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, $plugin_id, $plugin_definition, - $container->get('content_moderation.moderation_information') - ); + return new static( + $configuration, $plugin_id, $plugin_definition, + $container->get('content_moderation.moderation_information') + ); } /** diff --git a/core/modules/content_moderation/src/Plugin/Action/ModerationOptOutUnpublishNode.php b/core/modules/content_moderation/src/Plugin/Action/ModerationOptOutUnpublishNode.php index f006595..b0fbd87 100644 --- a/core/modules/content_moderation/src/Plugin/Action/ModerationOptOutUnpublishNode.php +++ b/core/modules/content_moderation/src/Plugin/Action/ModerationOptOutUnpublishNode.php @@ -18,24 +18,36 @@ class ModerationOptOutUnpublishNode extends UnpublishNode implements ContainerFa /** * Moderation information service. - * + * * @var \Drupal\content_moderation\ModerationInformationInterface */ protected $moderationInfo; - public function __construct(array $configuration, $plugin_id, $plugin_definition, ModerationInformationInterface $mod_info) { + /** + * ModerationOptOutUnpublishNode constructor. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_info + * The moderation information service. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, ModerationInformationInterface $moderation_info) { parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->moderationInfo = $mod_info; + $this->moderationInfo = $moderation_info; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, $plugin_id, $plugin_definition, - $container->get('content_moderation.moderation_information') - ); + return new static( + $configuration, $plugin_id, $plugin_definition, + $container->get('content_moderation.moderation_information') + ); } /** diff --git a/core/modules/content_moderation/src/Plugin/Derivative/DynamicLocalTasks.php b/core/modules/content_moderation/src/Plugin/Derivative/DynamicLocalTasks.php index eab8f94..64d5ed9 100644 --- a/core/modules/content_moderation/src/Plugin/Derivative/DynamicLocalTasks.php +++ b/core/modules/content_moderation/src/Plugin/Derivative/DynamicLocalTasks.php @@ -3,8 +3,7 @@ namespace Drupal\content_moderation\Plugin\Derivative; use Drupal\Component\Plugin\Derivative\DeriverBase; -use Drupal\Core\Config\Entity\ConfigEntityTypeInterface; -use Drupal\Core\Entity\ContentEntityTypeInterface; +use Drupal\content_moderation\ModerationInformationInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; @@ -20,7 +19,7 @@ class DynamicLocalTasks extends DeriverBase implements ContainerDeriverInterface use StringTranslationTrait; /** - * The base plugin ID + * The base plugin ID. * * @var string */ @@ -34,6 +33,13 @@ class DynamicLocalTasks extends DeriverBase implements ContainerDeriverInterface protected $entityTypeManager; /** + * The moderation information service. + * + * @var \Drupal\content_moderation\ModerationInformationInterface + */ + protected $moderationInfo; + + /** * Creates an FieldUiLocalTask object. * * @param string $base_plugin_id @@ -43,10 +49,11 @@ class DynamicLocalTasks extends DeriverBase implements ContainerDeriverInterface * @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation * The translation manager. */ - public function __construct($base_plugin_id, EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation) { + public function __construct($base_plugin_id, EntityTypeManagerInterface $entity_type_manager, TranslationInterface $string_translation, ModerationInformationInterface $moderation_information) { $this->entityTypeManager = $entity_type_manager; $this->stringTranslation = $string_translation; $this->basePluginId = $base_plugin_id; + $this->moderationInfo = $moderation_information; } /** @@ -56,7 +63,8 @@ public static function create(ContainerInterface $container, $base_plugin_id) { return new static( $base_plugin_id, $container->get('entity_type.manager'), - $container->get('string_translation') + $container->get('string_translation'), + $container->get('content_moderation.moderation_information') ); } @@ -68,12 +76,12 @@ public function getDerivativeDefinitions($base_plugin_definition) { foreach ($this->moderatableEntityTypeDefinitions() as $entity_type_id => $entity_type) { $this->derivatives["$entity_type_id.moderation_tab"] = [ - 'route_name' => "entity.$entity_type_id.moderation", - 'title' => $this->t('Manage moderation'), - // @todo - are we sure they all have an edit_form? - 'base_route' => "entity.$entity_type_id.edit_form", - 'weight' => 30, - ] + $base_plugin_definition; + 'route_name' => "entity.$entity_type_id.moderation", + 'title' => $this->t('Manage moderation'), + // @todo - are we sure they all have an edit_form? + 'base_route' => "entity.$entity_type_id.edit_form", + 'weight' => 30, + ] + $base_plugin_definition; } $latest_version_entities = array_filter($this->moderatableEntityDefinitions(), function (EntityTypeInterface $type) { @@ -82,11 +90,11 @@ public function getDerivativeDefinitions($base_plugin_definition) { foreach ($latest_version_entities as $entity_type_id => $entity_type) { $this->derivatives["$entity_type_id.latest_version_tab"] = [ - 'route_name' => "entity.$entity_type_id.latest_version", - 'title' => $this->t('Latest version'), - 'base_route' => "entity.$entity_type_id.canonical", - 'weight' => 1, - ] + $base_plugin_definition; + 'route_name' => "entity.$entity_type_id.latest_version", + 'title' => $this->t('Latest version'), + 'base_route' => "entity.$entity_type_id.canonical", + 'weight' => 1, + ] + $base_plugin_definition; } return $this->derivatives; @@ -99,27 +107,17 @@ public function getDerivativeDefinitions($base_plugin_definition) { * An array of just those entities we care about. */ protected function moderatableEntityDefinitions() { - return array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $type) { - return ($type instanceof ContentEntityTypeInterface) - && $type->getBundleEntityType() - && $type->isRevisionable(); - }); + return $this->moderationInfo->selectRevisionableEntities($this->entityTypeManager->getDefinitions()); } /** - * Returns an iterable of the config entities representing moderatable content. + * Returns entity types that represent bundles that can be moderated. * * @return EntityTypeInterface[] - * An array of just those entity types we care about. + * An array of entity types that represent bundles that can be moderated. */ protected function moderatableEntityTypeDefinitions() { - $entity_types = $this->entityTypeManager->getDefinitions(); - - return array_filter($entity_types, function (EntityTypeInterface $type) use ($entity_types) { - return ($type instanceof ConfigEntityTypeInterface) - && ($bundle_of = $type->get('bundle_of')) - && $entity_types[$bundle_of]->isRevisionable(); - }); + return $this->moderationInfo->selectRevisionableEntityTypes($this->entityTypeManager->getDefinitions()); } } diff --git a/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php b/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php index d5de21d..f2ab789 100644 --- a/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php +++ b/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php @@ -2,7 +2,6 @@ namespace Drupal\content_moderation\Plugin\Field\FieldWidget; -use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -52,6 +51,8 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact protected $moderationStateStorage; /** + * Moderation information service. + * * @var \Drupal\content_moderation\ModerationInformation */ protected $moderationInformation; @@ -64,11 +65,15 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact protected $entityTypeManager; /** + * Moderation state transition storage. + * * @var \Drupal\Core\Entity\EntityStorageInterface */ protected $moderationStateTransitionStorage; /** + * Moderation state transition validation service. + * * @var \Drupal\content_moderation\StateTransitionValidation */ protected $validator; @@ -140,7 +145,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen return $element + ['#access' => FALSE]; } - $default = $items->get($delta)->target_id ?: $bundle_entity->getThirdPartySetting('content_moderation', 'default_moderation_state', FALSE); + $default = $items->get($delta)->value ?: $bundle_entity->getThirdPartySetting('content_moderation', 'default_moderation_state', FALSE); /** @var \Drupal\content_moderation\ModerationStateInterface $default_state */ $default_state = $this->entityTypeManager->getStorage('moderation_state')->load($default); if (!$default || !$default_state) { @@ -162,7 +167,9 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#options' => $target_states, '#default_value' => $default, '#published' => $default ? $default_state->isPublishedState() : FALSE, + '#key_column' => $this->column, ]; + $element['#element_validate'][] = array(get_class($this), 'validateElement'); // Use the dropbutton. $element['#process'][] = [get_called_class(), 'processActions']; @@ -193,9 +200,9 @@ public static function updateStatus($entity_type_id, ContentEntityInterface $ent */ public static function processActions($element, FormStateInterface $form_state, array &$form) { - // We'll steal most of the button configuration from the default submit button. - // However, NodeForm also hides that button for admins (as it adds its own, - // too), so we have to restore it. + // We'll steal most of the button configuration from the default submit + // button. However, NodeForm also hides that button for admins (as it adds + // its own, too), so we have to restore it. $default_button = $form['actions']['submit']; $default_button['#access'] = TRUE; @@ -216,7 +223,6 @@ public static function processActions($element, FormStateInterface $form_state, ? t('Save and @transition (this translation)', ['@transition' => $label]) : t('Save and @transition', ['@transition' => $label]); - $form['actions']['moderation_state_' . $id] = $button + $default_button; } @@ -240,27 +246,4 @@ public static function isApplicable(FieldDefinitionInterface $field_definition) return parent::isApplicable($field_definition) && $field_definition->getName() === 'moderation_state'; } - /** - * {@inheritdoc} - */ - public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) { - $field_name = $this->fieldDefinition->getName(); - - // Extract the values from $form_state->getValues(). - $path = array_merge($form['#parents'], array($field_name)); - $key_exists = NULL; - // Convert the field value into expected array format. - $values = $form_state->getValues(); - $value = NestedArray::getValue($values, $path, $key_exists); - if (empty($value)) { - parent::extractFormValues($items, $form, $form_state); - return; - } - if (!isset($value[0]['target_id'])) { - NestedArray::setValue($values, $path, [['target_id' => reset($value)]]); - $form_state->setValues($values); - } - parent::extractFormValues($items, $form, $form_state); - } - } diff --git a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php index e69de29..644a76b 100644 --- a/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php +++ b/core/modules/content_moderation/src/Plugin/Field/ModerationStateFieldItemList.php @@ -0,0 +1,82 @@ +getEntity(); + + if ($entity->id() && $entity->getRevisionId()) { + $revisions = \Drupal::service('entity.query')->get('content_moderation_state') + ->condition('content_entity_type_id', $entity->getEntityTypeId()) + ->condition('content_entity_id', $entity->id()) + ->condition('content_entity_revision_id', $entity->getRevisionId()) + ->allRevisions() + ->sort('revision_id', 'DESC') + ->execute(); + + if ($revision_to_load = key($revisions)) { + /** @var \Drupal\content_moderation\ContentModerationStateInterface $content_moderation_state */ + $content_moderation_state = \Drupal::entityTypeManager() + ->getStorage('content_moderation_state') + ->loadRevision($revision_to_load); + + // Return the correct translation. + $langcode = $entity->language()->getId(); + if (!$content_moderation_state->hasTranslation($langcode)) { + $content_moderation_state->addTranslation($langcode); + } + if ($content_moderation_state->language()->getId() !== $langcode) { + $content_moderation_state = $content_moderation_state->getTranslation($langcode); + } + + return $content_moderation_state->get('moderation_state')->entity; + } + } + // It is possible that the bundle does not exist at this point. For example, + // the node type form creates a fake Node entity to get default values. + // @see \Drupal\node\NodeTypeForm::form() + $bundle_entity = \Drupal::service('content_moderation.moderation_information') + ->loadBundleEntity($entity->getEntityType()->getBundleEntityType(), $entity->bundle()); + if ($bundle_entity && ($default = $bundle_entity->getThirdPartySetting('content_moderation', 'default_moderation_state'))) { + return ModerationState::load($default); + } + } + + /** + * {@inheritdoc} + */ + public function get($index) { + if ($index !== 0) { + throw new \InvalidArgumentException('An entity can not have multiple moderation states at the same time.'); + } + // Compute the value of the moderation state. + if (!isset($this->list[$index]) || $this->list[$index]->isEmpty()) { + $moderation_state = $this->getModerationState(); + // Do not store NULL values in the static cache. + if ($moderation_state) { + $this->list[$index] = $this->createItem($index, ['entity' => $moderation_state]); + } + } + + return isset($this->list[$index]) ? $this->list[$index] : NULL; + } + +} diff --git a/core/modules/content_moderation/src/Plugin/Menu/EditTab.php b/core/modules/content_moderation/src/Plugin/Menu/EditTab.php index 0c466b4..1a630c2 100644 --- a/core/modules/content_moderation/src/Plugin/Menu/EditTab.php +++ b/core/modules/content_moderation/src/Plugin/Menu/EditTab.php @@ -11,7 +11,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** - * Defines a class for making the edit tab use 'Edit draft' or 'New draft' + * Defines a class for making the edit tab use 'Edit draft' or 'New draft'. */ class EditTab extends LocalTaskDefault implements ContainerFactoryPluginInterface { diff --git a/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraint.php b/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraint.php index e69de29..c2c373f 100644 --- a/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraint.php +++ b/core/modules/content_moderation/src/Plugin/Validation/Constraint/ModerationStateConstraint.php @@ -0,0 +1,19 @@ +validation = $validation; + $this->entityTypeManager = $entity_type_manager; + $this->moderationInformation = $moderation_information; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('content_moderation.state_transition_validation'), + $container->get('content_moderation.moderation_information') + ); + } + + /** + * {@inheritdoc} + */ + public function validate($value, Constraint $constraint) { + /** @var \Drupal\Core\Entity\EntityInterface $entity */ + $entity = $value->getEntity(); + + // Ignore entities that are not subject to moderation anyway. + if (!$this->moderationInformation->isModeratableEntity($entity)) { + return; + } + + // Ignore entities that are being created for the first time. + if ($entity->isNew()) { + return; + } + + // Ignore entities that are being moderated for the first time, such as + // when they existed before moderation was enabled for this entity type. + if ($this->isFirstTimeModeration($entity)) { + return; + } + + $original_entity = $this->moderationInformation->getLatestRevision($entity->getEntityTypeId(), $entity->id()); + if (!$entity->isDefaultTranslation() && $original_entity->hasTranslation($entity->language()->getId())) { + $original_entity = $original_entity->getTranslation($entity->language()->getId()); + } + + if ($entity->moderation_state->target_id) { + $new_state_id = $entity->moderation_state->target_id; + } + else { + $new_state_id = $default = $this->moderationInformation + ->loadBundleEntity($entity->getEntityType()->getBundleEntityType(), $entity->bundle()) + ->getThirdPartySetting('content_moderation', 'default_moderation_state'); + } + if ($new_state_id) { + $new_state = ModerationStateEntity::load($new_state_id); + } + // @todo - what if $new_state_id references something that does not exist or + // is null. + if (!$this->validation->isTransitionAllowed($original_entity->moderation_state->entity, $new_state)) { + $this->context->addViolation($constraint->message, ['%from' => $original_entity->moderation_state->entity->label(), '%to' => $new_state->label()]); + } + } + + /** + * Determines if this entity is being moderated for the first time. + * + * If the previous version of the entity has no moderation state, we assume + * that means it predates the presence of moderation states. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity being moderated. + * + * @return bool + * TRUE if this is the entity's first time being moderated, FALSE otherwise. + */ + protected function isFirstTimeModeration(EntityInterface $entity) { + $original_entity = $this->moderationInformation->getLatestRevision($entity->getEntityTypeId(), $entity->id()); + + $original_id = $original_entity->moderation_state->target_id; + + return !($entity->moderation_state->target_id && $original_entity && $original_id); + } + +} diff --git a/core/modules/content_moderation/src/Plugin/views/filter/LatestRevision.php b/core/modules/content_moderation/src/Plugin/views/filter/LatestRevision.php index f6d3814..6440019 100644 --- a/core/modules/content_moderation/src/Plugin/views/filter/LatestRevision.php +++ b/core/modules/content_moderation/src/Plugin/views/filter/LatestRevision.php @@ -44,8 +44,11 @@ class LatestRevision extends FilterPluginBase implements ContainerFactoryPluginI * Constructs a new LatestRevision. * * @param array $configuration + * A configuration array containing information about the plugin instance. * @param string $plugin_id + * The plugin_id for the plugin instance. * @param mixed $plugin_definition + * The plugin implementation definition. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * Entity Type Manager Service. * @param \Drupal\views\Plugin\ViewsHandlerManager $join_handler @@ -75,17 +78,21 @@ public static function create(ContainerInterface $container, array $configuratio /** * {@inheritdoc} */ - public function adminSummary() { } + public function adminSummary() { + } /** * {@inheritdoc} */ - protected function operatorForm(&$form, FormStateInterface $form_state) { } + protected function operatorForm(&$form, FormStateInterface $form_state) { + } /** * {@inheritdoc} */ - public function canExpose() { return FALSE; } + public function canExpose() { + return FALSE; + } /** * {@inheritdoc} diff --git a/core/modules/content_moderation/src/RevisionTracker.php b/core/modules/content_moderation/src/RevisionTracker.php index 010128c..2011237 100644 --- a/core/modules/content_moderation/src/RevisionTracker.php +++ b/core/modules/content_moderation/src/RevisionTracker.php @@ -1,9 +1,7 @@ recordLatestRevision($entity_type, $entity_id, $langcode, $revision_id); + $this->recordLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id); } catch (DatabaseExceptionWrapper $e) { $this->ensureTableExists(); - $this->recordLatestRevision($entity_type, $entity_id, $langcode, $revision_id); + $this->recordLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id); } return $this; @@ -58,24 +56,24 @@ public function setLatestRevision($entity_type, $entity_id, $langcode, $revision /** * Records the latest revision of a given entity. * - * @param $entity_type + * @param string $entity_type_id * The machine name of the type of entity. - * @param $entity_id + * @param string $entity_id * The Entity ID in question. - * @param $langcode + * @param string $langcode * The langcode of the revision we're saving. Each language has its own * effective tree of entity revisions, so in different languages * different revisions will be "latest". - * @param $revision_id + * @param int $revision_id * The revision ID that is now the latest revision. * * @return int * One of the valid returns from a merge query's execute method. */ - protected function recordLatestRevision($entity_type, $entity_id, $langcode, $revision_id) { + protected function recordLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id) { return $this->connection->merge($this->tableName) ->keys([ - 'entity_type' => $entity_type, + 'entity_type' => $entity_type_id, 'entity_id' => $entity_id, 'langcode' => $langcode, ]) diff --git a/core/modules/content_moderation/src/RevisionTrackerInterface.php b/core/modules/content_moderation/src/RevisionTrackerInterface.php index e848b5b..2b7cf95 100644 --- a/core/modules/content_moderation/src/RevisionTrackerInterface.php +++ b/core/modules/content_moderation/src/RevisionTrackerInterface.php @@ -10,19 +10,19 @@ /** * Sets the latest revision of a given entity. * - * @param $entity_type + * @param string $entity_type_id * The machine name of the type of entity. - * @param $entity_id + * @param string $entity_id * The Entity ID in question. - * @param $langcode + * @param string $langcode * The langcode of the revision we're saving. Each language has its own * effective tree of entity revisions, so in different languages * different revisions will be "latest". - * @param $revision_id + * @param int $revision_id * The revision ID that is now the latest revision. * * @return static */ - public function setLatestRevision($entity_type, $entity_id, $langcode, $revision_id); + public function setLatestRevision($entity_type_id, $entity_id, $langcode, $revision_id); } diff --git a/core/modules/content_moderation/src/Routing/EntityModerationRouteProvider.php b/core/modules/content_moderation/src/Routing/EntityModerationRouteProvider.php index e78e5ad..f953d80 100644 --- a/core/modules/content_moderation/src/Routing/EntityModerationRouteProvider.php +++ b/core/modules/content_moderation/src/Routing/EntityModerationRouteProvider.php @@ -12,8 +12,9 @@ use Symfony\Component\Routing\RouteCollection; /** - * Provides the following routes: + * Dynamic route provider for the Content moderation module. * + * Provides the following routes: * - The latest version tab, showing the latest revision of an entity, not the * default one. */ @@ -86,7 +87,7 @@ protected function getLatestVersionRoute(EntityTypeInterface $entity_type) { ->setOption('parameters', [ $entity_type_id => [ 'type' => 'entity:' . $entity_type_id, - 'load_forward_revision' => 1 + 'load_forward_revision' => 1, ], ]); diff --git a/core/modules/content_moderation/src/Routing/EntityTypeModerationRouteProvider.php b/core/modules/content_moderation/src/Routing/EntityTypeModerationRouteProvider.php index c3c7e57..c722a67 100644 --- a/core/modules/content_moderation/src/Routing/EntityTypeModerationRouteProvider.php +++ b/core/modules/content_moderation/src/Routing/EntityTypeModerationRouteProvider.php @@ -41,12 +41,13 @@ protected function getModerationFormRoute(EntityTypeInterface $entity_type) { $route = new Route($entity_type->getLinkTemplate('moderation-form')); + // @todo Come up with a new permission. $route ->setDefaults([ '_entity_form' => "{$entity_type_id}.moderation", '_title' => 'Moderation', ]) - ->setRequirement('_permission', 'administer moderation states') // @todo Come up with a new permission. + ->setRequirement('_permission', 'administer moderation states') ->setOption('parameters', [ $entity_type_id => ['type' => 'entity:' . $entity_type_id], ]); diff --git a/core/modules/content_moderation/src/StateTransitionValidation.php b/core/modules/content_moderation/src/StateTransitionValidation.php index af5f752..b5d0e58 100644 --- a/core/modules/content_moderation/src/StateTransitionValidation.php +++ b/core/modules/content_moderation/src/StateTransitionValidation.php @@ -14,11 +14,15 @@ class StateTransitionValidation implements StateTransitionValidationInterface { /** + * Entity type manager. + * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ protected $entityTypeManager; /** + * Entity query factory. + * * @var \Drupal\Core\Entity\Query\QueryFactory */ protected $queryFactory; @@ -90,20 +94,18 @@ public function getValidTransitionTargets(ContentEntityInterface $entity, Accoun $states_for_bundle = $bundle->getThirdPartySetting('content_moderation', 'allowed_moderation_states', []); - /** @var \Drupal\content_moderation\Entity\ModerationState $state */ - $state = $entity->moderation_state->entity; - $current_state_id = $state->id(); + /** @var \Drupal\content_moderation\Entity\ModerationState $current_state */ + $current_state = $entity->moderation_state->entity; $all_transitions = $this->getPossibleTransitions(); - $destinations = $all_transitions[$current_state_id]; + $destination_ids = $all_transitions[$current_state->id()]; - $destinations = array_intersect($states_for_bundle, $destinations); + $destination_ids = array_intersect($states_for_bundle, $destination_ids); + $destinations = $this->entityTypeManager->getStorage('moderation_state')->loadMultiple($destination_ids); - $permitted_destinations = array_filter($destinations, function($state_name) use ($current_state_id, $user) { - return $this->userMayTransition($current_state_id, $state_name, $user); + return array_filter($destinations, function(ModerationStateInterface $destination_state) use ($current_state, $user) { + return $this->userMayTransition($current_state, $destination_state, $user); }); - - return $this->entityTypeManager->getStorage('moderation_state')->loadMultiple($permitted_destinations); } /** @@ -131,7 +133,7 @@ public function getValidTransitions(ContentEntityInterface $entity, AccountInter } /** - * Returns a list of transitions from a given state. + * Returns a list of possible transitions from a given state. * * This list is based only on those transitions that exist, not what * transitions are legal in a given context. @@ -140,6 +142,7 @@ public function getValidTransitions(ContentEntityInterface $entity, AccountInter * The machine name of the state from which we are transitioning. * * @return ModerationStateTransition[] + * A list of possible transitions from a given state. */ protected function getTransitionsFrom($state_name) { $result = $this->transitionStateQuery() @@ -153,7 +156,7 @@ protected function getTransitionsFrom($state_name) { /** * {@inheritdoc} */ - public function userMayTransition($from, $to, AccountInterface $user) { + public function userMayTransition(ModerationStateInterface $from, ModerationStateInterface $to, AccountInterface $user) { if ($transition = $this->getTransitionFromStates($from, $to)) { return $user->hasPermission('use ' . $transition->id() . ' transition'); } @@ -163,18 +166,18 @@ public function userMayTransition($from, $to, AccountInterface $user) { /** * Returns the transition object that transitions from one state to another. * - * @param string $from - * The name of the "from" state. - * @param string $to - * The name of the "to" state. + * @param \Drupal\content_moderation\ModerationStateInterface $from + * The origin state. + * @param \Drupal\content_moderation\ModerationStateInterface $to + * The destination state. * * @return ModerationStateTransition|null - * A transition object, or NULL if there is no such transition in the system. + * A transition object, or NULL if there is no such transition. */ - protected function getTransitionFromStates($from, $to) { + protected function getTransitionFromStates(ModerationStateInterface $from, ModerationStateInterface $to) { $from = $this->transitionStateQuery() - ->condition('stateFrom', $from) - ->condition('stateTo', $to) + ->condition('stateFrom', $from->id()) + ->condition('stateTo', $to->id()) ->execute(); $transitions = $this->transitionStorage()->loadMultiple($from); @@ -188,18 +191,19 @@ protected function getTransitionFromStates($from, $to) { /** * {@inheritdoc} */ - public function isTransitionAllowed($from, $to) { + public function isTransitionAllowed(ModerationStateInterface $from, ModerationStateInterface $to) { $allowed_transitions = $this->calculatePossibleTransitions(); - if (isset($allowed_transitions[$from])) { - return in_array($to, $allowed_transitions[$from], TRUE); + if (isset($allowed_transitions[$from->id()])) { + return in_array($to->id(), $allowed_transitions[$from->id()], TRUE); } return FALSE; } /** + * Returns a transition state entity query. * * @return \Drupal\Core\Entity\Query\QueryInterface - * A transition state query. + * A transition state entity query. */ protected function transitionStateQuery() { return $this->queryFactory->get('moderation_state_transition', 'AND'); @@ -209,6 +213,7 @@ protected function transitionStateQuery() { * Returns the transition entity storage service. * * @return \Drupal\Core\Entity\EntityStorageInterface + * The transition state entity storage. */ protected function transitionStorage() { return $this->entityTypeManager->getStorage('moderation_state_transition'); @@ -218,6 +223,7 @@ protected function transitionStorage() { * Returns the state entity storage service. * * @return \Drupal\Core\Entity\EntityStorageInterface + * The moderation state entity storage. */ protected function stateStorage() { return $this->entityTypeManager->getStorage('moderation_state'); @@ -232,11 +238,10 @@ protected function stateStorage() { * The bundle ID. * * @return \Drupal\Core\Config\Entity\ConfigEntityInterface|null + * The specific bundle entity. */ protected function loadBundleEntity($bundle_entity_type_id, $bundle_id) { - if ($bundle_entity_type_id) { - return $this->entityTypeManager->getStorage($bundle_entity_type_id)->load($bundle_id); - } + return $this->entityTypeManager->getStorage($bundle_entity_type_id)->load($bundle_id); } } diff --git a/core/modules/content_moderation/src/StateTransitionValidationInterface.php b/core/modules/content_moderation/src/StateTransitionValidationInterface.php index 016eaf8..5ef0dd1 100644 --- a/core/modules/content_moderation/src/StateTransitionValidationInterface.php +++ b/core/modules/content_moderation/src/StateTransitionValidationInterface.php @@ -33,6 +33,7 @@ public function getValidTransitionTargets(ContentEntityInterface $entity, Accoun * The account that wants to perform a transition. * * @return \Drupal\content_moderation\Entity\ModerationStateTransition[] + * The list of transitions that are legal for this user on this entity. */ public function getValidTransitions(ContentEntityInterface $entity, AccountInterface $user); @@ -42,29 +43,29 @@ public function getValidTransitions(ContentEntityInterface $entity, AccountInter * This method will also return FALSE if there is no transition between the * specified states at all. * - * @param string $from - * The origin state machine name. - * @param string $to - * The destination state machine name. + * @param \Drupal\content_moderation\ModerationStateInterface $from + * The origin state. + * @param \Drupal\content_moderation\ModerationStateInterface $to + * The destination state. * @param \Drupal\Core\Session\AccountInterface $user * The user to validate. * * @return bool * TRUE if the given user may transition between those two states. */ - public function userMayTransition($from, $to, AccountInterface $user); + public function userMayTransition(ModerationStateInterface $from, ModerationStateInterface $to, AccountInterface $user); /** * Determines a transition allowed. * - * @param string $from - * The from state. - * @param string $to - * The to state. + * @param \Drupal\content_moderation\ModerationStateInterface $from + * The origin state. + * @param \Drupal\content_moderation\ModerationStateInterface $to + * The destination state. * * @return bool * Is the transition allowed. */ - public function isTransitionAllowed($from, $to); + public function isTransitionAllowed(ModerationStateInterface $from, ModerationStateInterface $to); } diff --git a/core/modules/content_moderation/src/Tests/ModerationFormTest.php b/core/modules/content_moderation/src/Tests/ModerationFormTest.php index fcb1b79..e412859 100644 --- a/core/modules/content_moderation/src/Tests/ModerationFormTest.php +++ b/core/modules/content_moderation/src/Tests/ModerationFormTest.php @@ -15,10 +15,10 @@ class ModerationFormTest extends ModerationStateTestBase { protected function setUp() { parent::setUp(); $this->drupalLogin($this->adminUser); - $this->createContentTypeFromUI('Moderated content', 'moderated_content', TRUE, [ + $this->createContentTypeFromUi('Moderated content', 'moderated_content', TRUE, [ 'draft', 'needs_review', - 'published' + 'published', ], 'draft'); $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content'); } diff --git a/core/modules/content_moderation/src/Tests/ModerationLocaleTest.php b/core/modules/content_moderation/src/Tests/ModerationLocaleTest.php index 5a77fc5..2af08be 100644 --- a/core/modules/content_moderation/src/Tests/ModerationLocaleTest.php +++ b/core/modules/content_moderation/src/Tests/ModerationLocaleTest.php @@ -14,17 +14,27 @@ class ModerationLocaleTest extends ModerationStateTestBase { * * @var array */ - public static $modules = ['node', 'content_moderation', 'locale', 'content_translation']; + public static $modules = [ + 'node', + 'content_moderation', + 'locale', + 'content_translation', + ]; /** - * Test that an article can be translated and its translations can be - * moderated separately as core does. + * Tests article translations can be moderated separately. */ public function testTranslateModeratedContent() { $this->drupalLogin($this->rootUser); // Enable moderation on Article node type. - $this->createContentTypeFromUI('Article', 'article', TRUE, ['draft', 'published', 'archived'], 'draft'); + $this->createContentTypeFromUi( + 'Article', + 'article', + TRUE, + ['draft', 'published', 'archived'], + 'draft' + ); // Add French language. $edit = [ @@ -41,6 +51,10 @@ public function testTranslateModeratedContent() { ]; $this->drupalPostForm(NULL, $edit, t('Save configuration')); + // Adding languages requires a container rebuild in the test running + // environment so that multilingual services are used. + $this->rebuildContainer(); + // Create a published article in English. $edit = [ 'title[0][value]' => 'Published English node', @@ -61,8 +75,6 @@ public function testTranslateModeratedContent() { // Please try again later." // If the translation has got lost. $this->assertText(t('Article French node Draft has been updated.')); - $english_node = $this->drupalGetNodeByTitle('Published English node', TRUE); - $french_node = $english_node->getTranslation('fr'); // Create an article in English. $edit = [ @@ -82,7 +94,6 @@ public function testTranslateModeratedContent() { $this->drupalPostForm(NULL, $edit, t('Save and Create New Draft (this translation)')); $this->assertText(t('Article French node has been updated.')); $english_node = $this->drupalGetNodeByTitle('English node', TRUE); - $french_node = $english_node->getTranslation('fr'); // Publish the English article and check that the translation stays // unpublished. @@ -90,6 +101,8 @@ public function testTranslateModeratedContent() { $this->assertText(t('Article English node has been updated.')); $english_node = $this->drupalGetNodeByTitle('English node', TRUE); $french_node = $english_node->getTranslation('fr'); + $this->assertEqual('French node', $french_node->label()); + $this->assertEqual($english_node->moderation_state->target_id, 'published'); $this->assertTrue($english_node->isPublished()); $this->assertEqual($french_node->moderation_state->target_id, 'draft'); @@ -116,7 +129,7 @@ public function testTranslateModeratedContent() { $french_node = $english_node->getTranslation('fr'); // Publish the translation and check that the source language version stays - // unpublished + // unpublished. $this->drupalPostForm('fr/node/' . $english_node->id() . '/edit', [], t('Save and Publish (this translation)')); $this->assertText(t('Article Translated node has been updated.')); $english_node = $this->drupalGetNodeByTitle('Another node', TRUE); @@ -154,6 +167,8 @@ public function testTranslateModeratedContent() { // Publish the English article before testing the archive transition. $this->drupalPostForm('node/' . $english_node->id() . '/edit', [], t('Save and Publish (this translation)')); $this->assertText(t('Article Another node has been updated.')); + $english_node = $this->drupalGetNodeByTitle('Another node', TRUE); + $this->assertEqual($english_node->moderation_state->target_id, 'published'); // Archive the node and its translation. $this->drupalPostForm('node/' . $english_node->id() . '/edit', [], t('Save and Archive (this translation)')); diff --git a/core/modules/content_moderation/src/Tests/ModerationStateBlockTest.php b/core/modules/content_moderation/src/Tests/ModerationStateBlockTest.php index a99c706..001d6b5 100644 --- a/core/modules/content_moderation/src/Tests/ModerationStateBlockTest.php +++ b/core/modules/content_moderation/src/Tests/ModerationStateBlockTest.php @@ -51,7 +51,8 @@ protected function setUp() { public function testCustomBlockModeration() { $this->drupalLogin($this->rootUser); - // Enable moderation for custom blocks at admin/structure/block/block-content/manage/basic/moderation. + // Enable moderation for custom blocks at + // admin/structure/block/block-content/manage/basic/moderation. $edit = [ 'enable_moderation_state' => TRUE, 'allowed_moderation_states_unpublished[draft]' => TRUE, diff --git a/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php b/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php index 8f4f1c5..7e14125 100644 --- a/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php +++ b/core/modules/content_moderation/src/Tests/ModerationStateNodeTest.php @@ -18,11 +18,13 @@ class ModerationStateNodeTest extends ModerationStateTestBase { protected function setUp() { parent::setUp(); $this->drupalLogin($this->adminUser); - $this->createContentTypeFromUI('Moderated content', 'moderated_content', TRUE, [ - 'draft', - 'needs_review', - 'published' - ], 'draft'); + $this->createContentTypeFromUi( + 'Moderated content', + 'moderated_content', + TRUE, + ['draft', 'needs_review', 'published'], + 'draft' + ); $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content'); } diff --git a/core/modules/content_moderation/src/Tests/ModerationStateNodeTypeTest.php b/core/modules/content_moderation/src/Tests/ModerationStateNodeTypeTest.php index e1c3038..debb32c 100644 --- a/core/modules/content_moderation/src/Tests/ModerationStateNodeTypeTest.php +++ b/core/modules/content_moderation/src/Tests/ModerationStateNodeTypeTest.php @@ -15,7 +15,7 @@ class ModerationStateNodeTypeTest extends ModerationStateTestBase { */ public function testNotModerated() { $this->drupalLogin($this->adminUser); - $this->createContentTypeFromUI('Not moderated', 'not_moderated'); + $this->createContentTypeFromUi('Not moderated', 'not_moderated'); $this->assertText('The content type Not moderated has been added.'); $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'not_moderated'); $this->drupalGet('node/add/not_moderated'); @@ -29,14 +29,10 @@ public function testNotModerated() { /** * Tests enabling moderation on an existing node-type, with content. */ - /** - * A node type without moderation state enabled. - */ public function testEnablingOnExistingContent() { - // Create a node type that is not moderated. $this->drupalLogin($this->adminUser); - $this->createContentTypeFromUI('Not moderated', 'not_moderated'); + $this->createContentTypeFromUi('Not moderated', 'not_moderated'); $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'not_moderated'); // Create content. @@ -47,12 +43,15 @@ public function testEnablingOnExistingContent() { $this->assertText('Not moderated Test has been created.'); // Now enable moderation state. - $this->enableModerationThroughUI('not_moderated', ['draft', 'needs_review', 'published'], 'draft'); + $this->enableModerationThroughUi( + 'not_moderated', + ['draft', 'needs_review', 'published'], + 'draft' + ); // And make sure it works. - $nodes = \Drupal::entityTypeManager()->getStorage('node')->loadByProperties([ - 'title' => 'Test' - ]); + $nodes = \Drupal::entityTypeManager()->getStorage('node') + ->loadByProperties(['title' => 'Test']); if (empty($nodes)) { $this->fail('Could not load node with title Test'); return; diff --git a/core/modules/content_moderation/src/Tests/ModerationStateTestBase.php b/core/modules/content_moderation/src/Tests/ModerationStateTestBase.php index 970c2bb..1ff974a 100644 --- a/core/modules/content_moderation/src/Tests/ModerationStateTestBase.php +++ b/core/modules/content_moderation/src/Tests/ModerationStateTestBase.php @@ -18,7 +18,7 @@ protected $profile = 'testing'; /** - * Admin user + * Admin user. * * @var \Drupal\Core\Session\AccountInterface */ @@ -34,7 +34,9 @@ 'administer moderation state transitions', 'use draft_draft transition', 'use draft_needs_review transition', + 'use draft_published transition', 'use published_draft transition', + 'use published_archived transition', 'use needs_review_published transition', 'access administration pages', 'administer content types', @@ -54,9 +56,6 @@ 'block', 'block_content', 'node', - 'views', - 'options', - 'user', ]; /** @@ -78,13 +77,13 @@ protected function setUp() { * @param string $content_type_id * Machine name. * @param bool $moderated - * TRUE if should be moderated + * TRUE if should be moderated. * @param string[] $allowed_states - * Array of allowed state IDs + * Array of allowed state IDs. * @param string $default_state * Default state. */ - protected function createContentTypeFromUI($content_type_name, $content_type_id, $moderated = FALSE, array $allowed_states = [], $default_state = NULL) { + protected function createContentTypeFromUi($content_type_name, $content_type_id, $moderated = FALSE, array $allowed_states = [], $default_state = NULL) { $this->drupalGet('admin/structure/types'); $this->clickLink('Add content type'); $edit = [ @@ -94,7 +93,7 @@ protected function createContentTypeFromUI($content_type_name, $content_type_id, $this->drupalPostForm(NULL, $edit, t('Save content type')); if ($moderated) { - $this->enableModerationThroughUI($content_type_id, $allowed_states, $default_state); + $this->enableModerationThroughUi($content_type_id, $allowed_states, $default_state); } } @@ -108,7 +107,7 @@ protected function createContentTypeFromUI($content_type_name, $content_type_id, * @param string $default_state * Default state. */ - protected function enableModerationThroughUI($content_type_id, array $allowed_states, $default_state) { + protected function enableModerationThroughUi($content_type_id, array $allowed_states, $default_state) { $this->drupalGet('admin/structure/types/manage/' . $content_type_id . '/moderation'); $this->assertFieldByName('enable_moderation_state'); $this->assertNoFieldChecked('edit-enable-moderation-state'); @@ -116,9 +115,9 @@ protected function enableModerationThroughUI($content_type_id, array $allowed_st $edit['enable_moderation_state'] = 1; /** @var ModerationState $state */ - foreach (ModerationState::loadMultiple() as $id => $state) { + foreach (ModerationState::loadMultiple() as $state) { $key = $state->isPublishedState() ? 'allowed_moderation_states_published[' . $state->id() . ']' : 'allowed_moderation_states_unpublished[' . $state->id() . ']'; - $edit[$key] = (int)in_array($id, $allowed_states); + $edit[$key] = in_array($state->id(), $allowed_states, TRUE) ? $state->id() : FALSE; } $edit['default_moderation_state'] = $default_state; @@ -126,7 +125,6 @@ protected function enableModerationThroughUI($content_type_id, array $allowed_st $this->drupalPostForm(NULL, $edit, t('Save')); } - /** * Grants given user permission to create content of given type. * diff --git a/core/modules/content_moderation/src/Tests/NodeAccessTest.php b/core/modules/content_moderation/src/Tests/NodeAccessTest.php index 69aa21e..8f84af5 100644 --- a/core/modules/content_moderation/src/Tests/NodeAccessTest.php +++ b/core/modules/content_moderation/src/Tests/NodeAccessTest.php @@ -15,11 +15,13 @@ class NodeAccessTest extends ModerationStateTestBase { protected function setUp() { parent::setUp(); $this->drupalLogin($this->adminUser); - $this->createContentTypeFromUI('Moderated content', 'moderated_content', TRUE, [ - 'draft', - 'needs_review', - 'published' - ], 'draft'); + $this->createContentTypeFromUi( + 'Moderated content', + 'moderated_content', + TRUE, + ['draft', 'needs_review', 'published'], + 'draft' + ); $this->grantUserPermissionToCreateContentOfType($this->adminUser, 'moderated_content'); } diff --git a/core/modules/content_moderation/src/ViewsData.php b/core/modules/content_moderation/src/ViewsData.php index 8225860..cad1187 100644 --- a/core/modules/content_moderation/src/ViewsData.php +++ b/core/modules/content_moderation/src/ViewsData.php @@ -49,7 +49,7 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Mod public function getViewsData() { $data = []; - $data['content_revision_tracker']['table']['group'] = $this->t('Content moderation'); + $data['content_revision_tracker']['table']['group'] = $this->t('Content moderation (tracker)'); $data['content_revision_tracker']['entity_type'] = [ 'title' => $this->t('Entity type'), @@ -173,10 +173,78 @@ public function getViewsData() { } } + // Provides a relationship from moderated entity to its moderation state + // entity. + $content_moderation_state_entity_type = \Drupal::entityTypeManager()->getDefinition('content_moderation_state'); + $content_moderation_state_entity_base_table = $content_moderation_state_entity_type->getDataTable() ?: $content_moderation_state_entity_type->getBaseTable(); + $content_moderation_state_entity_revision_base_table = $content_moderation_state_entity_type->getRevisionDataTable() ?: $content_moderation_state_entity_type->getRevisionTable(); + foreach ($this->moderationInformation->selectRevisionableEntities($this->entityTypeManager->getDefinitions()) as $entity_type_id => $entity_type) { + $table = $entity_type->getDataTable() ?: $entity_type->getBaseTable(); + + $data[$table]['moderation_state'] = [ + 'title' => t('Moderation state'), + 'relationship' => [ + 'id' => 'standard', + 'label' => $this->t('@label moderation state', ['@label' => $entity_type->getLabel()]), + 'base' => $content_moderation_state_entity_base_table, + 'base field' => 'content_entity_id', + 'relationship field' => $entity_type->getKey('id'), + 'join_extra' => [ + [ + 'field' => 'content_entity_type_id', + 'value' => $entity_type_id, + ], + [ + 'field' => 'content_entity_revision_id', + 'left_field' => $entity_type->getKey('revision'), + ], + ], + ], + ]; + + $revision_table = $entity_type->getRevisionDataTable() ?: $entity_type->getRevisionTable(); + $data[$revision_table]['moderation_state'] = [ + 'title' => t('Moderation state'), + 'relationship' => [ + 'id' => 'standard', + 'label' => $this->t('@label moderation state', ['@label' => $entity_type->getLabel()]), + 'base' => $content_moderation_state_entity_revision_base_table, + 'base field' => 'content_entity_revision_id', + 'relationship field' => $entity_type->getKey('revision'), + 'join_extra' => [ + [ + 'field' => 'content_entity_type_id', + 'value' => $entity_type_id, + ], + ], + ], + ]; + } + return $data; } /** + * Alters the table and field information from hook_views_data(). + * + * @param array $data + * An array of all information about Views tables and fields, collected from + * hook_views_data(), passed by reference. + * + * @see hook_views_data() + */ + public function alterViewsData(array &$data) { + $revisionable_types = $this->moderationInformation->selectRevisionableEntities($this->entityTypeManager->getDefinitions()); + foreach ($revisionable_types as $type) { + $data[$type->getRevisionTable()]['latest_revision'] = [ + 'title' => t('Is Latest Revision'), + 'help' => t('Restrict the view to only revisions that are the latest revision of their entity.'), + 'filter' => ['id' => 'latest_revision'], + ]; + } + } + + /** * Gets the table of an entity type to be used as revision table in views. * * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.latest.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.latest.yml index 4be38e0..46a64ab 100644 --- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.latest.yml +++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.latest.yml @@ -4,8 +4,8 @@ dependencies: config: - system.menu.main module: - - user - content_moderation + - user id: latest label: Latest module: views @@ -253,9 +253,9 @@ display: field_api_classes: false moderation_state: id: moderation_state - table: node_field_revision + table: content_moderation_state_field_revision field: moderation_state - relationship: none + relationship: moderation_state group_type: group admin_label: '' label: 'Moderation state' @@ -313,7 +313,7 @@ display: multi_type: separator separator: ', ' field_api_classes: false - entity_type: node + entity_type: content_moderation_state entity_field: moderation_state plugin_id: field filters: @@ -359,7 +359,17 @@ display: header: { } footer: { } empty: { } - relationships: { } + relationships: + moderation_state: + id: moderation_state + table: node_field_revision + field: moderation_state + relationship: none + group_type: group + admin_label: 'Content moderation state' + required: false + entity_type: node + plugin_id: standard arguments: { } display_extenders: { } cache_metadata: diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_base_table_test.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_base_table_test.yml index e69de29..6f95251 100644 --- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_base_table_test.yml +++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_base_table_test.yml @@ -0,0 +1,406 @@ +langcode: en +status: true +dependencies: + module: + - content_moderation + - node + - user +id: test_content_moderation_base_table_test +label: test_content_moderation_base_table_test +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + nid: + id: nid + table: node_field_data + field: nid + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: number_integer + settings: + thousand_separator: '' + prefix_suffix: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: nid + plugin_id: field + moderation_state: + id: moderation_state + table: content_moderation_state_field_data + field: moderation_state + relationship: moderation_state + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: target_id + type: entity_reference_label + settings: + link: false + group_column: target_id + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: content_moderation_state + entity_field: moderation_state + plugin_id: field + moderation_state_1: + id: moderation_state_1 + table: content_moderation_state_field_revision + field: moderation_state + relationship: moderation_state + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: target_id + type: entity_reference_label + settings: + link: false + group_column: target_id + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: content_moderation_state + entity_field: moderation_state + plugin_id: field + moderation_state_2: + id: moderation_state_2 + table: content_moderation_state_field_revision + field: moderation_state + relationship: moderation_state_1 + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: target_id + type: entity_reference_entity_id + settings: { } + group_column: target_id + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: content_moderation_state + entity_field: moderation_state + plugin_id: field + filters: { } + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + vid: + id: vid + table: node_field_data + field: vid + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + entity_type: node + entity_field: vid + plugin_id: standard + header: { } + footer: { } + empty: { } + relationships: + moderation_state: + id: moderation_state + table: node_field_data + field: moderation_state + relationship: none + group_type: group + admin_label: 'Content moderation state' + required: false + entity_type: node + plugin_id: standard + moderation_state_1: + id: moderation_state_1 + table: node_field_revision + field: moderation_state + relationship: none + group_type: group + admin_label: 'Content moderation state (revision)' + required: false + entity_type: node + plugin_id: standard + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml index 139ffa7..7673394 100644 --- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml +++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_latest_revision.yml @@ -4,8 +4,6 @@ dependencies: module: - node - user -_core: - default_config_hash: 5PoZglK-ZXEh1fnzVSvRWZ7QvLPqkT-gwz-8DJT1cKY id: test_content_moderation_latest_revision label: test_content_moderation_latest_revision module: views @@ -261,9 +259,9 @@ display: plugin_id: field moderation_state: id: moderation_state - table: node_field_data + table: content_moderation_state_field_revision field: moderation_state - relationship: none + relationship: moderation_state group_type: group admin_label: '' label: '' @@ -308,9 +306,8 @@ display: empty_zero: false hide_alter_empty: true click_sort_column: target_id - type: entity_reference_label - settings: - link: true + type: entity_reference_entity_id + settings: { } group_column: target_id group_columns: { } group_rows: true @@ -321,14 +318,14 @@ display: multi_type: separator separator: ', ' field_api_classes: false - entity_type: node + entity_type: content_moderation_state entity_field: moderation_state plugin_id: field - moderation_state_revision: - id: moderation_state_revision - table: node_field_revision + moderation_state_1: + id: moderation_state_1 + table: content_moderation_state_field_revision field: moderation_state - relationship: latest_revision__node + relationship: moderation_state_1 group_type: group admin_label: '' label: '' @@ -373,9 +370,8 @@ display: empty_zero: false hide_alter_empty: true click_sort_column: target_id - type: entity_reference_label - settings: - link: true + type: entity_reference_entity_id + settings: { } group_column: target_id group_columns: { } group_rows: true @@ -386,7 +382,7 @@ display: multi_type: separator separator: ', ' field_api_classes: false - entity_type: node + entity_type: content_moderation_state entity_field: moderation_state plugin_id: field filters: { } @@ -418,6 +414,26 @@ display: admin_label: 'Content latest revision' required: false plugin_id: standard + moderation_state_1: + id: moderation_state_1 + table: node_field_revision + field: moderation_state + relationship: latest_revision__node + group_type: group + admin_label: 'Content moderation state (latest revision)' + required: false + entity_type: node + plugin_id: standard + moderation_state: + id: moderation_state + table: node_field_revision + field: moderation_state + relationship: none + group_type: group + admin_label: 'Content moderation state' + required: false + entity_type: node + plugin_id: standard arguments: { } display_extenders: { } rendering_language: '***LANGUAGE_entity_default***' diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_revision_test.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_revision_test.yml index e69de29..2362098 100644 --- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_revision_test.yml +++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/config/install/views.view.test_content_moderation_revision_test.yml @@ -0,0 +1,315 @@ +langcode: en +status: true +dependencies: + module: + - user +id: test_content_moderation_revision_test +label: test_content_moderation_revision_test +module: views +description: '' +tag: '' +base_table: node_field_revision +base_field: vid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'view all revisions' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + vid: + id: vid + table: node_field_revision + field: vid + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: number_integer + settings: + thousand_separator: '' + prefix_suffix: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: node + entity_field: vid + plugin_id: field + moderation_state: + id: moderation_state + table: content_moderation_state_field_revision + field: moderation_state + relationship: moderation_state + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: target_id + type: entity_reference_entity_id + settings: { } + group_column: target_id + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: content_moderation_state + entity_field: moderation_state + plugin_id: field + revision_id: + id: revision_id + table: content_moderation_state_field_revision + field: revision_id + relationship: moderation_state + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: number_integer + settings: + thousand_separator: '' + prefix_suffix: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: content_moderation_state + entity_field: revision_id + plugin_id: field + filters: { } + sorts: + vid: + id: vid + table: node_field_revision + field: vid + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + entity_type: node + entity_field: vid + plugin_id: standard + header: { } + footer: { } + empty: { } + relationships: + moderation_state: + id: moderation_state + table: node_field_revision + field: moderation_state + relationship: none + group_type: group + admin_label: 'Content moderation state' + required: false + entity_type: node + plugin_id: standard + arguments: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.info.yml b/core/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.info.yml index 4364b33..44b68d4 100644 --- a/core/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.info.yml +++ b/core/modules/content_moderation/tests/modules/content_moderation_test_views/content_moderation_test_views.info.yml @@ -6,3 +6,5 @@ version: VERSION core: 8.x dependencies: - content_moderation + - node + - views \ No newline at end of file diff --git a/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php b/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php index 5d7e92e..77ae046 100644 --- a/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php +++ b/core/modules/content_moderation/tests/src/Functional/LatestRevisionViewsFilterTest.php @@ -16,7 +16,10 @@ class LatestRevisionViewsFilterTest extends BrowserTestBase { /** * {@inheritdoc} */ - public static $modules = ['content_moderation_test_views', 'content_moderation', 'node', 'views', 'options', 'user', 'system']; + public static $modules = [ + 'content_moderation_test_views', + 'content_moderation', + ]; /** * Tests view shows the correct node IDs. @@ -42,12 +45,10 @@ public function testViewShowsCorrectNids() { $node_0->save(); // Now enable moderation for subsequent nodes. - $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); $node_type->save(); // Make a node that is only ever in Draft. - /** @var Node $node_1 */ $node_1 = Node::create([ 'type' => 'test', @@ -58,7 +59,6 @@ public function testViewShowsCorrectNids() { $node_1->save(); // Make a node that is in Draft, then Published. - /** @var Node $node_2 */ $node_2 = Node::create([ 'type' => 'test', @@ -73,7 +73,6 @@ public function testViewShowsCorrectNids() { $node_2->save(); // Make a node that is in Draft, then Published, then Draft. - /** @var Node $node_3 */ $node_3 = Node::create([ 'type' => 'test', @@ -91,9 +90,7 @@ public function testViewShowsCorrectNids() { $node_3->moderation_state->target_id = 'draft'; $node_3->save(); - // Now show the View, and confirm that only the correct titles are showing. - $this->drupalGet('/latest'); $page = $this->getSession()->getPage(); $this->assertEquals(200, $this->getSession()->getStatusCode()); diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php index fee1ce4..9951c83 100644 --- a/core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php +++ b/core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php @@ -19,11 +19,6 @@ class ModerationStateAccessTest extends BrowserTestBase { public static $modules = [ 'content_moderation_test_views', 'content_moderation', - 'node', - 'views', - 'options', - 'user', - 'system', ]; /** diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationSchemaTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationSchemaTest.php index 07135b4..8b382c1 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationSchemaTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationSchemaTest.php @@ -21,7 +21,13 @@ class ContentModerationSchemaTest extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['content_moderation', 'node', 'user', 'block_content', 'system']; + public static $modules = [ + 'content_moderation', + 'node', + 'user', + 'block_content', + 'system', + ]; /** * Tests content moderation default schema. diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php index e69de29..1535f9a 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateTest.php @@ -0,0 +1,194 @@ +installSchema('node', 'node_access'); + $this->installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installEntitySchema('content_moderation_state'); + $this->installConfig('content_moderation'); + } + + /** + * Tests basic monolingual content moderation through the API. + */ + public function testBasicModeration() { + $node_type = NodeType::create([ + 'type' => 'example', + ]); + $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); + $node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft', 'needs_review', 'published']); + $node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft'); + $node_type->save(); + $node = Node::create([ + 'type' => 'example', + 'title' => 'Test title', + ]); + $node->save(); + $node = $this->reloadNode($node); + $this->assertEquals('draft', $node->moderation_state->entity->id()); + + $node->moderation_state->target_id = 'needs_review'; + $node->save(); + + $node = $this->reloadNode($node); + $this->assertEquals('needs_review', $node->moderation_state->entity->id()); + + $published = ModerationState::load('published'); + $node->moderation_state->entity = $published; + $node->save(); + + $node = $this->reloadNode($node); + $this->assertEquals('published', $node->moderation_state->entity->id()); + + // Change the state without saving the node. + $content_moderation_state = ContentModerationState::load(1); + $content_moderation_state->set('moderation_state', 'draft'); + $content_moderation_state->setNewRevision(TRUE); + $content_moderation_state->save(); + + $node = $this->reloadNode($node); + $this->assertEquals('draft', $node->moderation_state->entity->id()); + + $node->moderation_state->target_id = 'needs_review'; + $node->save(); + + $node = $this->reloadNode($node); + $this->assertEquals('needs_review', $node->moderation_state->entity->id()); + } + + /** + * Tests basic multilingual content moderation through the API. + */ + public function testMultilingualModeration() { + // Enable French. + ConfigurableLanguage::createFromLangcode('fr')->save(); + $node_type = NodeType::create([ + 'type' => 'example', + ]); + $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); + $node_type->setThirdPartySetting('content_moderation', 'allowed_moderation_states', ['draft', 'needs_review', 'published']); + $node_type->setThirdPartySetting('content_moderation', 'default_moderation_state', 'draft'); + $node_type->save(); + $english_node = Node::create([ + 'type' => 'example', + 'title' => 'Test title', + ]); + $english_node + ->setPublished(FALSE) + ->save(); + $this->assertEquals('draft', $english_node->moderation_state->entity->id()); + $this->assertFalse($english_node->isPublished()); + + // Create a French translation. + $french_node = $english_node->addTranslation('fr', ['title' => 'French title']); + $french_node->setPublished(FALSE); + $french_node->save(); + $this->assertEquals('draft', $french_node->moderation_state->entity->id()); + $this->assertFalse($french_node->isPublished()); + + // Move English node to needs review. + $english_node = $this->reloadNode($english_node); + $english_node->moderation_state->target_id = 'needs_review'; + $english_node->save(); + $this->assertEquals('needs_review', $english_node->moderation_state->entity->id()); + + // French node should still be in draft. + $french_node = $this->reloadNode($english_node)->getTranslation('fr'); + $this->assertEquals('draft', $french_node->moderation_state->entity->id()); + + // Publish the French node. + $french_node->moderation_state->target_id = 'published'; + $french_node->save(); + $this->assertTrue($french_node->isPublished()); + $this->assertEquals('published', $french_node->moderation_state->entity->id()); + $this->assertTrue($french_node->isPublished()); + $english_node = $this->reloadNode($french_node)->getTranslation('en'); + $this->assertEquals('needs_review', $english_node->moderation_state->entity->id()); + + // Publish the English node. + $english_node->moderation_state->target_id = 'published'; + $english_node->save(); + $this->assertTrue($english_node->isPublished()); + + // Move the French node back to draft. + $french_node = $this->reloadNode($english_node)->getTranslation('fr'); + $this->assertTrue($french_node->isPublished()); + $french_node->moderation_state->target_id = 'draft'; + $french_node->save(); + $this->assertFalse($french_node->isPublished()); + $this->assertTrue($french_node->getTranslation('en')->isPublished()); + + // Republish the French node. + $french_node->moderation_state->target_id = 'published'; + $french_node->save(); + + // Change the state without saving the node. + $english_node = $this->reloadNode($french_node); + ContentModerationState::updateOrCreateFromEntity($english_node, 'draft'); + + // $node = Node::load($node->id()); + $this->assertEquals('draft', $english_node->moderation_state->entity->id()); + $french_node = $this->reloadNode($english_node)->getTranslation('fr'); + $this->assertEquals('published', $french_node->moderation_state->entity->id()); + + // This should unpublish the French node. + ContentModerationState::updateOrCreateFromEntity($french_node, 'needs_review'); + + $english_node = $this->reloadNode($french_node); + $this->assertEquals('draft', $english_node->moderation_state->entity->id()); + $french_node = $this->reloadNode($english_node)->getTranslation('fr'); + $this->assertEquals('needs_review', $french_node->moderation_state->entity->id()); + // @todo Switching the moderation state to an unpublished state should + // update the entity, but currently doesn't. + // $this->assertFalse($french_node->isPublished()); + } + + /** + * Reloads the node after clearing the static cache. + * + * @param \Drupal\node\NodeInterface $node + * The node to reload. + * + * @return \Drupal\node\NodeInterface + * The reloaded node. + */ + protected function reloadNode(NodeInterface $node) { + \Drupal::entityTypeManager()->getStorage('node')->resetCache([$node->id()]); + return Node::load($node->id()); + } + +} diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php index 60def39..99d8f0e 100644 --- a/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/EntityOperationsTest.php @@ -9,9 +9,8 @@ use Drupal\node\Entity\NodeType; /** - * Class EntityOperationsTest - * * @coversDefaultClass \Drupal\content_moderation\EntityOperations + * * @group content_moderation */ class EntityOperationsTest extends KernelTestBase { @@ -19,7 +18,12 @@ class EntityOperationsTest extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['content_moderation', 'node', 'views', 'options', 'user', 'system']; + public static $modules = [ + 'content_moderation', + 'node', + 'user', + 'system', + ]; /** * {@inheritdoc} @@ -29,6 +33,7 @@ protected function setUp() { $this->installEntitySchema('node'); $this->installSchema('node', 'node_access'); $this->installEntitySchema('user'); + $this->installEntitySchema('content_moderation_state'); $this->installConfig('content_moderation'); $this->createNodeType(); diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityRevisionConverterTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityRevisionConverterTest.php index d32595b..89c84f9 100644 --- a/core/modules/content_moderation/tests/src/Kernel/EntityRevisionConverterTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/EntityRevisionConverterTest.php @@ -13,7 +13,13 @@ */ class EntityRevisionConverterTest extends KernelTestBase { - public static $modules = ['user', 'entity_test', 'system', 'content_moderation', 'node']; + public static $modules = [ + 'user', + 'entity_test', + 'system', + 'content_moderation', + 'node', + ]; /** * {@inheritdoc} @@ -24,12 +30,16 @@ protected function setUp() { $this->installEntitySchema('entity_test'); $this->installEntitySchema('node'); $this->installEntitySchema('user'); + $this->installEntitySchema('content_moderation_state'); $this->installSchema('system', 'router'); $this->installSchema('system', 'sequences'); $this->installSchema('node', 'node_access'); \Drupal::service('router.builder')->rebuild(); } + /** + * @covers ::convert + */ public function testConvertNonRevisionableEntityType() { $entity_test = EntityTest::create([ 'name' => 'test', @@ -45,6 +55,9 @@ public function testConvertNonRevisionableEntityType() { $this->assertEquals($entity_test->getRevisionId(), $result['entity_test']->getRevisionId()); } + /** + * @covers ::convert + */ public function testConvertWithRevisionableEntityType() { $node_type = NodeType::create([ 'type' => 'article', @@ -55,7 +68,7 @@ public function testConvertWithRevisionableEntityType() { $revision_ids = []; $node = Node::create([ 'title' => 'test', - 'type' => 'article' + 'type' => 'article', ]); $node->save(); diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php index a93571a..8b55fe9 100644 --- a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php @@ -8,7 +8,7 @@ use Drupal\node\Entity\NodeType; /** - * @coversDefaultClass \Drupal\content_moderation\Plugin\Validation\Constraint\ModerationStateValidator + * @coversDefaultClass \Drupal\content_moderation\Plugin\Validation\Constraint\ModerationStateConstraintValidator * @group content_moderation */ class EntityStateChangeValidationTest extends KernelTestBase { @@ -16,7 +16,14 @@ class EntityStateChangeValidationTest extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['node', 'content_moderation', 'user', 'system', 'language', 'content_translation']; + public static $modules = [ + 'node', + 'content_moderation', + 'user', + 'system', + 'language', + 'content_translation', + ]; /** * {@inheritdoc} @@ -27,6 +34,7 @@ protected function setUp() { $this->installSchema('node', 'node_access'); $this->installEntitySchema('node'); $this->installEntitySchema('user'); + $this->installEntitySchema('content_moderation_state'); $this->installConfig('content_moderation'); } @@ -39,16 +47,20 @@ public function testValidTransition() { $node_type = NodeType::create([ 'type' => 'example', ]); + $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); $node_type->save(); $node = Node::create([ 'type' => 'example', 'title' => 'Test title', - 'moderation_state' => 'draft', ]); + $node->moderation_state->target_id = 'draft'; $node->save(); $node->moderation_state->target_id = 'needs_review'; $this->assertCount(0, $node->validate()); + $node->save(); + + $this->assertEquals('needs_review', $node->moderation_state->entity->id()); } /** @@ -65,8 +77,8 @@ public function testInvalidTransition() { $node = Node::create([ 'type' => 'example', 'title' => 'Test title', - 'moderation_state' => 'draft', ]); + $node->moderation_state->target_id = 'draft'; $node->save(); $node->moderation_state->target_id = 'archived'; @@ -77,7 +89,7 @@ public function testInvalidTransition() { } /** - * Verifies that content without prior moderation information can be moderated. + * Tests that content without prior moderation information can be moderated. */ public function testLegacyContent() { $node_type = NodeType::create([ @@ -114,10 +126,10 @@ public function testLegacyContent() { } /** - * Verifies that content without prior moderation information can be translated. + * Tests that content without prior moderation information can be translated. */ public function testLegacyMultilingualContent() { - // Enable French + // Enable French. ConfigurableLanguage::createFromLangcode('fr')->save(); $node_type = NodeType::create([ diff --git a/core/modules/content_moderation/tests/src/Kernel/ModerationStateEntityTest.php b/core/modules/content_moderation/tests/src/Kernel/ModerationStateEntityTest.php index c9d639c..f312cde 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ModerationStateEntityTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ModerationStateEntityTest.php @@ -6,8 +6,6 @@ use Drupal\content_moderation\Entity\ModerationState; /** - * Class ModerationStateEntityTest - * * @coversDefaultClass \Drupal\content_moderation\Entity\ModerationState * * @group content_moderation diff --git a/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php b/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php index b25a323..193920fc 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php @@ -31,22 +31,29 @@ protected function setUp($import_test_views = TRUE) { $this->installEntitySchema('node'); $this->installEntitySchema('user'); + $this->installEntitySchema('content_moderation_state'); $this->installSchema('node', 'node_access'); $this->installConfig('content_moderation_test_views'); - } + $this->installConfig('content_moderation'); - public function testViewsData() { $node_type = NodeType::create([ 'type' => 'page', ]); $node_type->setThirdPartySetting('content_moderation', 'enabled', TRUE); $node_type->save(); + } + /** + * Tests content_moderation_views_data(). + * + * @see content_moderation_views_data() + */ + public function testViewsData() { $node = Node::create([ 'type' => 'page', 'title' => 'Test title first revision', - 'moderation_state' => 'published', ]); + $node->moderation_state->target_id = 'published'; $node->save(); $revision = clone $node; @@ -68,11 +75,82 @@ public function testViewsData() { 'nid' => $node->id(), 'revision_id' => $revision->getRevisionId(), 'title' => $revision->label(), - 'moderation_state_revision' => 'draft', + 'moderation_state_1' => 'draft', 'moderation_state' => 'published', ], ]; - $this->assertIdenticalResultset($view, $expected_result, ['nid' => 'nid', 'content_revision_tracker_revision_id' => 'revision_id', 'moderation_state_revision' => 'moderation_state_revision', 'moderation_state' => 'moderation_state']); + $this->assertIdenticalResultset($view, $expected_result, ['nid' => 'nid', 'content_revision_tracker_revision_id' => 'revision_id', 'moderation_state' => 'moderation_state', 'moderation_state_1' => 'moderation_state_1']); + } + + /** + * Tests the join from the revision data table to the moderation state table. + */ + public function testContentModerationStateRevisionJoin() { + $node = Node::create([ + 'type' => 'page', + 'title' => 'Test title first revision', + ]); + $node->moderation_state->target_id = 'published'; + $node->save(); + + $revision = clone $node; + $revision->setNewRevision(TRUE); + $revision->isDefaultRevision(FALSE); + $revision->title->value = 'Test title second revision'; + $revision->moderation_state->target_id = 'draft'; + $revision->save(); + + $view = Views::getView('test_content_moderation_revision_test'); + $view->execute(); + + $expected_result = [ + [ + 'revision_id' => $node->getRevisionId(), + 'moderation_state' => 'published', + ], + [ + 'revision_id' => $revision->getRevisionId(), + 'moderation_state' => 'draft', + ], + ]; + $this->assertIdenticalResultset($view, $expected_result, ['revision_id' => 'revision_id', 'moderation_state' => 'moderation_state']); + } + + /** + * Tests the join from the data table to the moderation state table. + */ + public function testContentModerationStateBaseJoin() { + $node = Node::create([ + 'type' => 'page', + 'title' => 'Test title first revision', + ]); + $node->moderation_state->target_id = 'published'; + $node->save(); + + $revision = clone $node; + $revision->setNewRevision(TRUE); + $revision->isDefaultRevision(FALSE); + $revision->title->value = 'Test title second revision'; + $revision->moderation_state->target_id = 'draft'; + $revision->save(); + + $view = Views::getView('test_content_moderation_base_table_test'); + $view->execute(); + + $expected_result = [ + [ + 'nid' => $node->id(), + // @todo I would have expected that the content_moderation_state default + // revision is the same one as in the node, but it isn't + // joins from the base table to the default revision of the content_moderation. + 'moderation_state' => 'draft', + // joins from the revision table to the default revision of the content_moderation. + 'moderation_state_1' => 'draft', + // joins from the revision table to the revision of the content_moderation. + 'moderation_state_2' => 'published', + ], + ]; + $this->assertIdenticalResultset($view, $expected_result, ['nid' => 'nid', 'moderation_state' => 'moderation_state', 'moderation_state_1' => 'moderation_state_1', 'moderation_state_2' => 'moderation_state_2']); } } diff --git a/core/modules/content_moderation/tests/src/Unit/ContentPreprocessTest.php b/core/modules/content_moderation/tests/src/Unit/ContentPreprocessTest.php index 32f8fc3..d026dd5 100644 --- a/core/modules/content_moderation/tests/src/Unit/ContentPreprocessTest.php +++ b/core/modules/content_moderation/tests/src/Unit/ContentPreprocessTest.php @@ -7,9 +7,8 @@ use Drupal\node\Entity\Node; /** - * Class ContentPreprocessTest. - * * @coversDefaultClass \Drupal\content_moderation\ContentPreprocess + * * @group content_moderation */ class ContentPreprocessTest extends \PHPUnit_Framework_TestCase { @@ -24,6 +23,9 @@ public function testIsLatestVersionPage($route_name, $route_nid, $check_nid, $re $this->assertEquals($result, $content_preprocess->isLatestVersionPage($node), $message); } + /** + * Data provider for self::testIsLatestVersionPage(). + */ public function routeNodeProvider() { return [ ['entity.node.cannonical', 1, 1, FALSE, 'Not on the latest version tab route.'], @@ -35,14 +37,17 @@ public function routeNodeProvider() { /** * Mock the current route matching object. * - * @param string $route + * @param string $route_name + * The route to mock. * @param int $nid + * The node ID for mocking. * - * @return CurrentRouteMatch + * @return \Drupal\Core\Routing\CurrentRouteMatch + * The mocked current route match object. */ - protected function setupCurrentRouteMatch($routeName, $nid) { + protected function setupCurrentRouteMatch($route_name, $nid) { $route_match = $this->prophesize(CurrentRouteMatch::class); - $route_match->getRouteName()->willReturn($routeName); + $route_match->getRouteName()->willReturn($route_name); $route_match->getParameter('node')->willReturn($this->setupNode($nid)); return $route_match->reveal(); @@ -52,7 +57,10 @@ protected function setupCurrentRouteMatch($routeName, $nid) { * Mock a node object. * * @param int $nid - * @return Node + * The node ID to mock. + * + * @return \Drupal\node\Entity\Node + * The mocked node. */ protected function setupNode($nid) { $node = $this->prophesize(Node::class); diff --git a/core/modules/content_moderation/tests/src/Unit/LatestRevisionCheckTest.php b/core/modules/content_moderation/tests/src/Unit/LatestRevisionCheckTest.php index 21373cc..1f8838b 100644 --- a/core/modules/content_moderation/tests/src/Unit/LatestRevisionCheckTest.php +++ b/core/modules/content_moderation/tests/src/Unit/LatestRevisionCheckTest.php @@ -20,8 +20,6 @@ class LatestRevisionCheckTest extends \PHPUnit_Framework_TestCase { /** * Test the access check of the LatestRevisionCheck service. * - * @dataProvider accessSituationProvider - * * @param string $entity_class * The class of the entity to mock. * @param string $entity_type @@ -31,6 +29,8 @@ class LatestRevisionCheckTest extends \PHPUnit_Framework_TestCase { * @param string $result_class * The AccessResult class that should result. One of AccessResultAllowed, * AccessResultForbidden, AccessResultNeutral. + * + * @dataProvider accessSituationProvider */ public function testLatestAccessPermissions($entity_class, $entity_type, $has_forward, $result_class) { @@ -62,8 +62,6 @@ public function testLatestAccessPermissions($entity_class, $entity_type, $has_fo /** * Data provider for testLastAccessPermissions(). - * - * @return array */ public function accessSituationProvider() { return [ diff --git a/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php b/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php index f913a36..6833cdb 100644 --- a/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php +++ b/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php @@ -22,6 +22,7 @@ class ModerationInformationTest extends \PHPUnit_Framework_TestCase { * Builds a mock user. * * @return AccountInterface + * The mocked user. */ protected function getUser() { return $this->prophesize(AccountInterface::class)->reveal(); @@ -31,8 +32,10 @@ protected function getUser() { * Returns a mock Entity Type Manager. * * @param \Drupal\Core\Entity\EntityStorageInterface $entity_bundle_storage + * Entity bundle storage. * * @return EntityTypeManagerInterface + * The mocked entity type manager. */ protected function getEntityTypeManager(EntityStorageInterface $entity_bundle_storage) { $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); @@ -40,6 +43,15 @@ protected function getEntityTypeManager(EntityStorageInterface $entity_bundle_st return $entity_type_manager->reveal(); } + /** + * Sets up content moderation and entity manager mocking. + * + * @param bool $status + * TRUE if content_moderation should be enabled, FALSE if not. + * + * @return \Drupal\Core\Entity\EntityTypeManagerInterface + * The mocked entity type manager. + */ public function setupModerationEntityManager($status) { $bundle = $this->prophesize(ConfigEntityInterface::class); $bundle->getThirdPartySetting('content_moderation', 'enabled', FALSE)->willReturn($status); @@ -123,6 +135,9 @@ public function testIsModeratedEntityForm($status) { $this->assertEquals($status, $moderation_information->isModeratedEntityForm($form->reveal())); } + /** + * @covers ::isModeratedEntityForm + */ public function testIsModeratedEntityFormWithNonContentEntityForm() { $form = $this->prophesize(EntityFormInterface::class); $moderation_information = new ModerationInformation($this->setupModerationEntityManager(TRUE), $this->getUser()); @@ -130,6 +145,9 @@ public function testIsModeratedEntityFormWithNonContentEntityForm() { $this->assertFalse($moderation_information->isModeratedEntityForm($form->reveal())); } + /** + * Data provider for several tests. + */ public function providerBoolean() { return [ [FALSE], diff --git a/core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php b/core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php index 366db21..b057478 100644 --- a/core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php +++ b/core/modules/content_moderation/tests/src/Unit/StateTransitionValidationTest.php @@ -21,6 +21,7 @@ class StateTransitionValidationTest extends \PHPUnit_Framework_TestCase { * Builds a mock storage object for Transitions. * * @return EntityStorageInterface + * The mocked storage object for Transitions. */ protected function setupTransitionStorage() { $entity_storage = $this->prophesize(EntityStorageInterface::class); @@ -46,6 +47,7 @@ protected function setupTransitionStorage() { * Builds an array of mocked Transition objects. * * @return ModerationStateTransitionInterface[] + * An array of mocked Transition objects. */ protected function setupTransitionEntityList() { $transition = $this->prophesize(ModerationStateTransitionInterface::class); @@ -97,6 +99,7 @@ protected function setupTransitionEntityList() { * Builds a mock storage object for States. * * @return EntityStorageInterface + * The mocked storage object for States. */ protected function setupStateStorage() { $entity_storage = $this->prophesize(EntityStorageInterface::class); @@ -116,14 +119,32 @@ protected function setupStateStorage() { $states['needs_review'] = $state->reveal(); $state = $this->prophesize(ModerationStateInterface::class); + $state->id()->willReturn('staging'); + $state->label()->willReturn('Staging'); + $state->isPublishedState()->willReturn(FALSE); + $state->isDefaultRevisionState()->willReturn(FALSE); + $states['staging'] = $state->reveal(); + + $state = $this->prophesize(ModerationStateInterface::class); $state->id()->willReturn('published'); $state->label()->willReturn('Published'); $state->isPublishedState()->willReturn(TRUE); $state->isDefaultRevisionState()->willReturn(TRUE); $states['published'] = $state->reveal(); + $state = $this->prophesize(ModerationStateInterface::class); + $state->id()->willReturn('archived'); + $state->label()->willReturn('Archived'); + $state->isPublishedState()->willReturn(TRUE); + $state->isDefaultRevisionState()->willReturn(TRUE); + $states['archived'] = $state->reveal(); + $entity_storage->loadMultiple()->willReturn($states); + foreach ($states as $id => $state) { + $entity_storage->load($id)->willReturn($state); + } + return $entity_storage->reveal(); } @@ -131,10 +152,11 @@ protected function setupStateStorage() { * Builds a mocked Entity Type Manager. * * @return EntityTypeManagerInterface + * The mocked Entity Type Manager. */ - protected function setupEntityTypeManager() { + protected function setupEntityTypeManager(EntityStorageInterface $storage) { $entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); - $entityTypeManager->getStorage('moderation_state')->willReturn($this->setupStateStorage()); + $entityTypeManager->getStorage('moderation_state')->willReturn($storage); $entityTypeManager->getStorage('moderation_state_transition')->willReturn($this->setupTransitionStorage()); return $entityTypeManager->reveal(); @@ -144,6 +166,7 @@ protected function setupEntityTypeManager() { * Builds a mocked query factory that does nothing. * * @return QueryFactory + * The mocked query factory that does nothing. */ protected function setupQueryFactory() { $factory = $this->prophesize(QueryFactory::class); @@ -154,40 +177,62 @@ protected function setupQueryFactory() { /** * @covers ::isTransitionAllowed * @covers ::calculatePossibleTransitions + * + * @dataProvider providerIsTransitionAllowedWithValidTransition */ - public function testIsTransitionAllowedWithValidTransition() { - $state_transition_validation = new StateTransitionValidation($this->setupEntityTypeManager(), $this->setupQueryFactory()); - - $this->assertTrue($state_transition_validation->isTransitionAllowed('draft', 'draft')); - $this->assertTrue($state_transition_validation->isTransitionAllowed('draft', 'needs_review')); - $this->assertTrue($state_transition_validation->isTransitionAllowed('needs_review', 'needs_review')); - $this->assertTrue($state_transition_validation->isTransitionAllowed('needs_review', 'staging')); - $this->assertTrue($state_transition_validation->isTransitionAllowed('staging', 'published')); - $this->assertTrue($state_transition_validation->isTransitionAllowed('needs_review', 'draft')); + public function testIsTransitionAllowedWithValidTransition($from_id, $to_id) { + $storage = $this->setupStateStorage(); + $state_transition_validation = new StateTransitionValidation($this->setupEntityTypeManager($storage), $this->setupQueryFactory()); + $this->assertTrue($state_transition_validation->isTransitionAllowed($storage->load($from_id), $storage->load($to_id))); + } + + /** + * Data provider for self::testIsTransitionAllowedWithValidTransition(). + */ + public function providerIsTransitionAllowedWithValidTransition() { + return [ + ['draft', 'draft'], + ['draft', 'needs_review'], + ['needs_review', 'needs_review'], + ['needs_review', 'staging'], + ['staging', 'published'], + ['needs_review', 'draft'], + ]; } /** * @covers ::isTransitionAllowed * @covers ::calculatePossibleTransitions + * + * @dataProvider providerIsTransitionAllowedWithInValidTransition */ - public function testIsTransitionAllowedWithInValidTransition() { - $state_transition_validation = new StateTransitionValidation($this->setupEntityTypeManager(), $this->setupQueryFactory()); - - $this->assertFalse($state_transition_validation->isTransitionAllowed('published', 'needs_review')); - $this->assertFalse($state_transition_validation->isTransitionAllowed('published', 'staging')); - $this->assertFalse($state_transition_validation->isTransitionAllowed('staging', 'needs_review')); - $this->assertFalse($state_transition_validation->isTransitionAllowed('staging', 'staging')); - $this->assertFalse($state_transition_validation->isTransitionAllowed('needs_review', 'published')); - $this->assertFalse($state_transition_validation->isTransitionAllowed('published', 'archived')); - $this->assertFalse($state_transition_validation->isTransitionAllowed('archived', 'published')); + public function testIsTransitionAllowedWithInValidTransition($from_id, $to_id) { + $storage = $this->setupStateStorage(); + $state_transition_validation = new StateTransitionValidation($this->setupEntityTypeManager($storage), $this->setupQueryFactory()); + $this->assertFalse($state_transition_validation->isTransitionAllowed($storage->load($from_id), $storage->load($to_id))); + } + + /** + * Data provider for self::testIsTransitionAllowedWithInValidTransition(). + */ + public function providerIsTransitionAllowedWithInValidTransition() { + return [ + ['published', 'needs_review'], + ['published', 'staging'], + ['staging', 'needs_review'], + ['staging', 'staging'], + ['needs_review', 'published'], + ['published', 'archived'], + ['archived', 'published'], + ]; } /** * Verifies user-aware transition validation. * - * @param string $from + * @param string $from_id * The state to transition from. - * @param string $to + * @param string $to_id * The state to transition to. * @param string $permission * The permission to give the user, or not. @@ -198,22 +243,21 @@ public function testIsTransitionAllowedWithInValidTransition() { * * @dataProvider userTransitionsProvider */ - public function testUserSensitiveValidTransitions($from, $to, $permission, $allowed, $result) { + public function testUserSensitiveValidTransitions($from_id, $to_id, $permission, $allowed, $result) { $user = $this->prophesize(AccountInterface::class); // The one listed permission will be returned as instructed; Any others are // always denied. $user->hasPermission($permission)->willReturn($allowed); $user->hasPermission(Argument::type('string'))->willReturn(FALSE); - $validator = new Validator($this->setupEntityTypeManager(), $this->setupQueryFactory()); + $storage = $this->setupStateStorage(); + $validator = new Validator($this->setupEntityTypeManager($storage), $this->setupQueryFactory()); - $this->assertEquals($result, $validator->userMayTransition($from, $to, $user->reveal())); + $this->assertEquals($result, $validator->userMayTransition($storage->load($from_id), $storage->load($to_id), $user->reveal())); } /** * Data provider for the user transition test. - * - * @return array */ public function userTransitionsProvider() { // The user has the right permission, so let it through. @@ -240,11 +284,12 @@ public function userTransitionsProvider() { * method that uses it. */ class Validator extends StateTransitionValidation { + /** - * @inheritDoc + * {@inheritdoc} */ - protected function getTransitionFromStates($from, $to) { - if ($from == 'draft' && $to == 'draft') { + protected function getTransitionFromStates(ModerationStateInterface $from, ModerationStateInterface $to) { + if ($from->id() === 'draft' && $to->id() === 'draft') { return $this->transitionStorage()->loadMultiple(['draft__draft'])[0]; } }