Issue #2915383 by Sam152, amateescu, jibran: The moderation_state base field is added to all revisionable entity types even if they do not have moderation enabled

8.7.x
Nathaniel Catchpole 2018-12-10 11:14:02 +00:00
parent 3ba6bc1168
commit ba726bed0c
20 changed files with 391 additions and 81 deletions

View File

@ -21,6 +21,9 @@ use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\views\Plugin\views\filter\Broken;
use Drupal\views\ViewExecutable;
use Drupal\views\Views;
use Drupal\workflows\WorkflowInterface;
use Drupal\Core\Action\Plugin\Action\PublishAction;
use Drupal\Core\Action\Plugin\Action\UnpublishAction;
@ -64,6 +67,20 @@ function content_moderation_entity_base_field_info(EntityTypeInterface $entity_t
->entityBaseFieldInfo($entity_type);
}
/**
* Implements hook_entity_bundle_field_info().
*/
function content_moderation_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
if (isset($base_field_definitions['moderation_state'])) {
// Add the target bundle to the moderation state field. Since each bundle
// can be attached to a different moderation workflow, adding this
// information to the field definition allows the associated workflow to be
// derived where a field definition is present.
$base_field_definitions['moderation_state']->setTargetBundle($bundle);
return $base_field_definitions;
}
}
/**
* Implements hook_entity_type_alter().
*/
@ -316,6 +333,10 @@ function content_moderation_workflow_insert(WorkflowInterface $entity) {
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
// Clear field cache so extra field is added or removed.
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
// Clear the views data cache so the extra field is available in views.
if (\Drupal::moduleHandler()->moduleExists('views')) {
Views::viewsData()->clear();
}
}
/**
@ -327,4 +348,22 @@ function content_moderation_workflow_update(WorkflowInterface $entity) {
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
// Clear field cache so extra field is added or removed.
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
// Clear the views data cache so the extra field is available in views.
if (\Drupal::moduleHandler()->moduleExists('views')) {
Views::viewsData()->clear();
}
}
/**
* Implements hook_views_post_execute().
*/
function content_moderation_views_post_execute(ViewExecutable $view) {
// @todo, remove this once broken handlers in views configuration result in
// a view no longer returning results. https://www.drupal.org/node/2907954.
foreach ($view->filter as $id => $filter) {
if (strpos($id, 'moderation_state') === 0 && $filter instanceof Broken) {
$view->result = [];
break;
}
}
}

View File

@ -6,6 +6,7 @@
*/
use Drupal\Core\Config\Entity\ConfigEntityUpdater;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Site\Settings;
use Drupal\views\Entity\View;
use Drupal\workflows\Entity\Workflow;
@ -138,3 +139,34 @@ function content_moderation_post_update_set_views_filter_latest_translation_affe
$view->save();
}
}
/**
* Update the dependencies of entity displays to include associated workflow.
*/
function content_moderation_post_update_entity_display_dependencies(&$sandbox) {
/** @var \Drupal\content_moderation\ModerationInformationInterface $moderation_info */
$moderation_info = \Drupal::service('content_moderation.moderation_information');
/** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
$entity_type_manager = \Drupal::service('entity_type.manager');
\Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'entity_form_display', function (EntityFormDisplay $entity_form_display) use ($moderation_info, $entity_type_manager) {
$associated_entity_type = $entity_type_manager->getDefinition($entity_form_display->getTargetEntityTypeId());
if ($moderation_info->isModeratedEntityType($associated_entity_type)) {
$entity_form_display->calculateDependencies();
return TRUE;
}
elseif ($moderation_state_component = $entity_form_display->getComponent('moderation_state')) {
// Remove the component from the entity form display, then manually delete
// it from the hidden components list, completely purging it.
$entity_form_display->removeComponent('moderation_state');
$hidden_components = $entity_form_display->get('hidden');
unset($hidden_components['moderation_state']);
$entity_form_display->set('hidden', $hidden_components);
$entity_form_display->calculateDependencies();
return TRUE;
}
return FALSE;
});
}

View File

@ -240,7 +240,7 @@ class EntityTypeInfo implements ContainerInjectionInterface {
* @see hook_entity_base_field_info()
*/
public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
if (!$this->moderationInfo->canModerateEntitiesOfEntityType($entity_type)) {
if (!$this->moderationInfo->isModeratedEntityType($entity_type)) {
return [];
}

View File

@ -56,6 +56,14 @@ class ModerationInformation implements ModerationInformationInterface {
return $this->shouldModerateEntitiesOfBundle($entity->getEntityType(), $entity->bundle());
}
/**
* {@inheritdoc}
*/
public function isModeratedEntityType(EntityTypeInterface $entity_type) {
$bundles = $this->bundleInfo->getBundleInfo($entity_type->id());
return !empty(array_column($bundles, 'workflow'));
}
/**
* {@inheritdoc}
*/
@ -206,10 +214,17 @@ class ModerationInformation implements ModerationInformationInterface {
* {@inheritdoc}
*/
public function getWorkflowForEntity(ContentEntityInterface $entity) {
$bundles = $this->bundleInfo->getBundleInfo($entity->getEntityTypeId());
if (isset($bundles[$entity->bundle()]['workflow'])) {
return $this->entityTypeManager->getStorage('workflow')->load($bundles[$entity->bundle()]['workflow']);
};
return $this->getWorkflowForEntityTypeAndBundle($entity->getEntityTypeId(), $entity->bundle());
}
/**
* {@inheritdoc}
*/
public function getWorkflowForEntityTypeAndBundle($entity_type_id, $bundle_id) {
$bundles = $this->bundleInfo->getBundleInfo($entity_type_id);
if (isset($bundles[$bundle_id]['workflow'])) {
return $this->entityTypeManager->getStorage('workflow')->load($bundles[$bundle_id]['workflow']);
}
return NULL;
}

View File

@ -47,6 +47,17 @@ interface ModerationInformationInterface {
*/
public function shouldModerateEntitiesOfBundle(EntityTypeInterface $entity_type, $bundle);
/**
* Determines if an entity type has at least one moderated bundle.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition to check.
*
* @return bool
* TRUE if an entity type has a moderated bundle, FALSE otherwise.
*/
public function isModeratedEntityType(EntityTypeInterface $entity_type);
/**
* Loads the latest revision of a specific entity.
*
@ -163,6 +174,19 @@ interface ModerationInformationInterface {
*/
public function getWorkflowForEntity(ContentEntityInterface $entity);
/**
* Gets the workflow for the given entity type and bundle.
*
* @param string $entity_type_id
* The entity type ID.
* @param string $bundle_id
* The entity bundle ID.
*
* @return \Drupal\workflows\WorkflowInterface|null
* The associated workflow. NULL if there is no workflow.
*/
public function getWorkflowForEntityTypeAndBundle($entity_type_id, $bundle_id);
/**
* Gets unsupported features for a given entity type.
*

View File

@ -178,4 +178,15 @@ class ModerationStateWidget extends OptionsSelectWidget implements ContainerFact
return is_a($field_definition->getClass(), ModerationStateFieldItemList::class, TRUE);
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
if ($workflow = $this->moderationInformation->getWorkflowForEntityTypeAndBundle($this->fieldDefinition->getTargetEntityTypeId(), $this->fieldDefinition->getTargetBundle())) {
$dependencies[$workflow->getConfigDependencyKey()][] = $workflow->getConfigDependencyName();
}
return $dependencies;
}
}

View File

@ -52,7 +52,7 @@ class ViewsData {
$data = [];
$entity_types_with_moderation = array_filter($this->entityTypeManager->getDefinitions(), function (EntityTypeInterface $type) {
return $this->moderationInformation->canModerateEntitiesOfEntityType($type);
return $this->moderationInformation->isModeratedEntityType($type);
});
// Provides a relationship from moderated entity to its moderation state

View File

@ -0,0 +1,33 @@
<?php
// @codingStandardsIgnoreFile
/**
* @file
* Content for the update path test in #2915383.
*
* @see \Drupal\Tests\content_moderation\Functional\EntityFormDisplayDependenciesUpdateTest
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
$connection->update('config')
->fields([
'data' => 'a:11:{s:4:"uuid";s:36:"16624d7d-0800-4ed7-9861-41f7e71394a8";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:2:{s:6:"config";a:2:{i:0;s:24:"block_content.type.basic";i:1;s:36:"field.field.block_content.basic.body";}s:6:"module";a:2:{i:0;s:18:"content_moderation";i:1;s:4:"text";}}s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"e1Nu5xXAuF_QplbBUhQBPLnYWvHtDX0MkZnpuCiY8uM";}s:2:"id";s:27:"block_content.basic.default";s:16:"targetEntityType";s:13:"block_content";s:6:"bundle";s:5:"basic";s:4:"mode";s:7:"default";s:7:"content";a:3:{s:4:"body";a:5:{s:4:"type";s:26:"text_textarea_with_summary";s:6:"weight";i:-4;s:6:"region";s:7:"content";s:8:"settings";a:3:{s:4:"rows";i:9;s:12:"summary_rows";i:3;s:11:"placeholder";s:0:"";}s:20:"third_party_settings";a:0:{}}s:4:"info";a:5:{s:4:"type";s:16:"string_textfield";s:6:"weight";i:-5;s:6:"region";s:7:"content";s:8:"settings";a:2:{s:4:"size";i:60;s:11:"placeholder";s:0:"";}s:20:"third_party_settings";a:0:{}}s:16:"moderation_state";a:5:{s:4:"type";s:24:"moderation_state_default";s:6:"weight";i:100;s:8:"settings";a:0:{}s:6:"region";s:7:"content";s:20:"third_party_settings";a:0:{}}}s:6:"hidden";a:0:{}}',
])
->condition('name', 'core.entity_form_display.block_content.basic.default')
->execute();
$connection->update('config')
->fields([
'data' => 'a:11:{s:4:"uuid";s:36:"af6ca931-0ecc-46c0-8097-ffb383db6287";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:2:{s:6:"config";a:6:{i:0;s:29:"field.field.node.article.body";i:1;s:32:"field.field.node.article.comment";i:2;s:36:"field.field.node.article.field_image";i:3;s:35:"field.field.node.article.field_tags";i:4;s:21:"image.style.thumbnail";i:5;s:17:"node.type.article";}s:6:"module";a:5:{i:0;s:7:"comment";i:1;s:18:"content_moderation";i:2;s:5:"image";i:3;s:4:"path";i:4;s:4:"text";}}s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"buc38w3gxCqFnjINJhMiJvPpj9jWflKvlKDyBVMPVvw";}s:2:"id";s:20:"node.article.default";s:16:"targetEntityType";s:4:"node";s:6:"bundle";s:7:"article";s:4:"mode";s:7:"default";s:7:"content";a:12:{s:4:"body";a:5:{s:4:"type";s:26:"text_textarea_with_summary";s:6:"weight";i:1;s:6:"region";s:7:"content";s:8:"settings";a:3:{s:4:"rows";i:9;s:12:"summary_rows";i:3;s:11:"placeholder";s:0:"";}s:20:"third_party_settings";a:0:{}}s:7:"comment";a:5:{s:4:"type";s:15:"comment_default";s:6:"weight";i:20;s:6:"region";s:7:"content";s:8:"settings";a:0:{}s:20:"third_party_settings";a:0:{}}s:7:"created";a:5:{s:4:"type";s:18:"datetime_timestamp";s:6:"weight";i:10;s:6:"region";s:7:"content";s:8:"settings";a:0:{}s:20:"third_party_settings";a:0:{}}s:11:"field_image";a:5:{s:4:"type";s:11:"image_image";s:6:"weight";i:4;s:6:"region";s:7:"content";s:8:"settings";a:2:{s:18:"progress_indicator";s:8:"throbber";s:19:"preview_image_style";s:9:"thumbnail";}s:20:"third_party_settings";a:0:{}}s:10:"field_tags";a:5:{s:4:"type";s:34:"entity_reference_autocomplete_tags";s:6:"weight";i:3;s:6:"region";s:7:"content";s:8:"settings";a:3:{s:14:"match_operator";s:8:"CONTAINS";s:4:"size";i:60;s:11:"placeholder";s:0:"";}s:20:"third_party_settings";a:0:{}}s:16:"moderation_state";a:5:{s:4:"type";s:24:"moderation_state_default";s:6:"weight";i:100;s:8:"settings";a:0:{}s:6:"region";s:7:"content";s:20:"third_party_settings";a:0:{}}s:4:"path";a:5:{s:4:"type";s:4:"path";s:6:"weight";i:30;s:6:"region";s:7:"content";s:8:"settings";a:0:{}s:20:"third_party_settings";a:0:{}}s:7:"promote";a:5:{s:4:"type";s:16:"boolean_checkbox";s:8:"settings";a:1:{s:13:"display_label";b:1;}s:6:"weight";i:15;s:6:"region";s:7:"content";s:20:"third_party_settings";a:0:{}}s:6:"status";a:5:{s:4:"type";s:16:"boolean_checkbox";s:8:"settings";a:1:{s:13:"display_label";b:1;}s:6:"weight";i:120;s:6:"region";s:7:"content";s:20:"third_party_settings";a:0:{}}s:6:"sticky";a:5:{s:4:"type";s:16:"boolean_checkbox";s:8:"settings";a:1:{s:13:"display_label";b:1;}s:6:"weight";i:16;s:6:"region";s:7:"content";s:20:"third_party_settings";a:0:{}}s:5:"title";a:5:{s:4:"type";s:16:"string_textfield";s:6:"weight";i:0;s:6:"region";s:7:"content";s:8:"settings";a:2:{s:4:"size";i:60;s:11:"placeholder";s:0:"";}s:20:"third_party_settings";a:0:{}}s:3:"uid";a:5:{s:4:"type";s:29:"entity_reference_autocomplete";s:6:"weight";i:5;s:6:"region";s:7:"content";s:8:"settings";a:3:{s:14:"match_operator";s:8:"CONTAINS";s:4:"size";i:60;s:11:"placeholder";s:0:"";}s:20:"third_party_settings";a:0:{}}}s:6:"hidden";a:0:{}}',
])
->condition('name', 'core.entity_form_display.node.article.default')
->execute();
$connection->update('config')
->fields([
'data' => 'a:9:{s:4:"uuid";s:36:"08b548c7-ff59-468b-9347-7d697680d035";s:8:"langcode";s:2:"en";s:6:"status";b:1;s:12:"dependencies";a:2:{s:6:"config";a:2:{i:0;s:17:"node.type.article";i:1;s:14:"node.type.page";}s:6:"module";a:1:{i:0;s:18:"content_moderation";}}s:5:"_core";a:1:{s:19:"default_config_hash";s:43:"T_JxNjYlfoRBi7Bj1zs5Xv9xv1btuBkKp5C1tNrjMhI";}s:2:"id";s:9:"editorial";s:5:"label";s:9:"Editorial";s:4:"type";s:18:"content_moderation";s:13:"type_settings";a:3:{s:6:"states";a:3:{s:8:"archived";a:4:{s:5:"label";s:8:"Archived";s:6:"weight";i:5;s:9:"published";b:0;s:16:"default_revision";b:1;}s:5:"draft";a:4:{s:5:"label";s:5:"Draft";s:9:"published";b:0;s:16:"default_revision";b:0;s:6:"weight";i:-5;}s:9:"published";a:4:{s:5:"label";s:9:"Published";s:9:"published";b:1;s:16:"default_revision";b:1;s:6:"weight";i:0;}}s:11:"transitions";a:5:{s:7:"archive";a:4:{s:5:"label";s:7:"Archive";s:4:"from";a:1:{i:0;s:9:"published";}s:2:"to";s:8:"archived";s:6:"weight";i:2;}s:14:"archived_draft";a:4:{s:5:"label";s:16:"Restore to Draft";s:4:"from";a:1:{i:0;s:8:"archived";}s:2:"to";s:5:"draft";s:6:"weight";i:3;}s:18:"archived_published";a:4:{s:5:"label";s:7:"Restore";s:4:"from";a:1:{i:0;s:8:"archived";}s:2:"to";s:9:"published";s:6:"weight";i:4;}s:16:"create_new_draft";a:4:{s:5:"label";s:16:"Create New Draft";s:2:"to";s:5:"draft";s:6:"weight";i:0;s:4:"from";a:2:{i:0;s:5:"draft";i:1;s:9:"published";}}s:7:"publish";a:4:{s:5:"label";s:7:"Publish";s:2:"to";s:9:"published";s:6:"weight";i:1;s:4:"from";a:2:{i:0;s:5:"draft";i:1;s:9:"published";}}}s:12:"entity_types";a:1:{s:4:"node";a:2:{i:0;s:7:"article";i:1;s:4:"page";}}}}',
])
->condition('name', 'workflows.workflow.editorial')
->execute();

View File

@ -0,0 +1,61 @@
<?php
namespace Drupal\Tests\content_moderation\Functional;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\FunctionalTests\Update\UpdatePathTestBase;
/**
* Test updating the dependencies of entity form displays.
*
* @group Update
* @group legacy
*
* @see content_moderation_post_update_entity_display_dependencies()
*/
class EntityFormDisplayDependenciesUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.4.0.bare.standard.php.gz',
__DIR__ . '/../../fixtures/update/drupal-8.4.0-content_moderation_installed.php',
__DIR__ . '/../../fixtures/update/drupal-8.entity-form-display-dependencies-2915383.php',
];
}
/**
* Tests updating the dependencies of entity displays.
*/
public function testEntityDisplaysUpdated() {
$no_moderation_form_display = EntityFormDisplay::load('block_content.basic.default');
$has_moderation_form_display = EntityFormDisplay::load('node.article.default');
// Assert the moderation field and content_moderation dependency exists on
// an entity type that does not have moderation enabled, these will be
// removed.
$this->assertEquals('moderation_state_default', $no_moderation_form_display->getComponent('moderation_state')['type']);
$this->assertTrue(in_array('content_moderation', $no_moderation_form_display->getDependencies()['module']));
// Assert the editorial config dependency doesn't exist on the entity form
// with moderation, this will be added.
$this->assertFalse(in_array('workflows.workflow.editorial', $has_moderation_form_display->getDependencies()['config']));
$this->runUpdates();
$no_moderation_form_display = EntityFormDisplay::load('block_content.basic.default');
$has_moderation_form_display = EntityFormDisplay::load('node.article.default');
// The moderation_state field has been removed from the non-moderated block
// entity form display.
$this->assertEquals(NULL, $no_moderation_form_display->getComponent('moderation_state'));
$this->assertFalse(in_array('content_moderation', $no_moderation_form_display->getDependencies()['module']));
// The editorial workflow config dependency has been added to moderated
// form display.
$this->assertTrue(in_array('workflows.workflow.editorial', $has_moderation_form_display->getDependencies()['config']));
}
}

View File

@ -20,17 +20,33 @@ class ModerationStateAccessTest extends BrowserTestBase {
* {@inheritdoc}
*/
public static $modules = [
'content_moderation_test_views',
'content_moderation',
'node',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$node_type = NodeType::create([
'type' => 'test',
'label' => 'Test',
]);
$node_type->save();
$workflow = $this->createEditorialWorkflow();
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'test');
$workflow->save();
$this->container->get('module_installer')->install(['content_moderation_test_views']);
}
/**
* Test the view operation access handler with the view permission.
*/
public function testViewShowsCorrectStates() {
$node_type_id = 'test';
$this->createNodeType('Test', $node_type_id);
$permissions = [
'access content',
'view all revisions',
@ -39,7 +55,7 @@ class ModerationStateAccessTest extends BrowserTestBase {
$this->drupalLogin($editor1);
$node_1 = Node::create([
'type' => $node_type_id,
'type' => 'test',
'title' => 'Draft node',
'uid' => $editor1->id(),
]);
@ -47,7 +63,7 @@ class ModerationStateAccessTest extends BrowserTestBase {
$node_1->save();
$node_2 = Node::create([
'type' => $node_type_id,
'type' => 'test',
'title' => 'Published node',
'uid' => $editor1->id(),
]);
@ -82,29 +98,4 @@ class ModerationStateAccessTest extends BrowserTestBase {
$this->assertFalse($page->hasContent('Published'));
}
/**
* Creates a new node type.
*
* @param string $label
* The human-readable label of the type to create.
* @param string $machine_name
* The machine name of the type to create.
*
* @return \Drupal\node\Entity\NodeType
* The node type just created.
*/
protected function createNodeType($label, $machine_name) {
/** @var \Drupal\node\Entity\NodeType $node_type */
$node_type = NodeType::create([
'type' => $machine_name,
'label' => $label,
]);
$node_type->save();
$workflow = $this->createEditorialWorkflow();
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', $machine_name);
$workflow->save();
return $node_type;
}
}

View File

@ -71,7 +71,7 @@ class ModerationStateNodeTest extends ModerationStateTestBase {
if (!$node) {
$this->fail('Non-moderated test node was not saved correctly.');
}
$this->assertEqual(NULL, $node->moderation_state->value);
$this->assertFalse($node->hasField('moderation_state'));
}
/**

View File

@ -5,8 +5,8 @@ namespace Drupal\Tests\content_moderation\Functional;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
use Drupal\Tests\views\Functional\ViewTestBase;
use Drupal\views\ViewExecutable;
use Drupal\views\Views;
use Drupal\views\Entity\View;
use Drupal\views\ViewEntityInterface;
use Drupal\workflows\Entity\Workflow;
/**
@ -24,7 +24,6 @@ class ViewsModerationStateFilterTest extends ViewTestBase {
* {@inheritdoc}
*/
public static $modules = [
'content_moderation_test_views',
'node',
'content_moderation',
'workflows',
@ -47,6 +46,9 @@ class ViewsModerationStateFilterTest extends ViewTestBase {
NodeType::create([
'type' => 'example_b',
])->save();
NodeType::create([
'type' => 'example_c',
])->save();
$this->createEditorialWorkflow();
@ -56,9 +58,15 @@ class ViewsModerationStateFilterTest extends ViewTestBase {
'label' => 'New workflow',
]);
$new_workflow->getTypePlugin()->addState('bar', 'Bar');
$new_workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example_c');
$new_workflow->save();
$this->drupalLogin($this->drupalCreateUser(['administer workflows', 'administer views']));
$this->container->get('module_installer')->install(['content_moderation_test_views']);
$new_workflow->getTypePlugin()->removeEntityTypeAndBundle('node', 'example_c');
$new_workflow->save();
}
/**
@ -71,10 +79,10 @@ class ViewsModerationStateFilterTest extends ViewTestBase {
// First, check that the view doesn't have any config dependency when there
// are no states configured in the filter.
$view_id = 'test_content_moderation_state_filter_base_table';
$view = Views::getView($view_id);
$view = View::load($view_id);
$this->assertWorkflowDependencies([], $view);
$this->assertTrue($view->storage->status());
$this->assertTrue($view->status());
// Configure the Editorial workflow for a node bundle, set the filter value
// to use one of its states and check that the workflow is now a dependency
@ -87,9 +95,9 @@ class ViewsModerationStateFilterTest extends ViewTestBase {
$this->drupalPostForm("admin/structure/views/nojs/handler/$view_id/default/filter/moderation_state", $edit, 'Apply');
$this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
$view = Views::getView($view_id);
$view = $this->loadViewUnchanged($view_id);
$this->assertWorkflowDependencies(['editorial'], $view);
$this->assertTrue($view->storage->status());
$this->assertTrue($view->status());
// Create another workflow and repeat the checks above.
$this->drupalPostForm('admin/config/workflow/workflows/add', [
@ -109,35 +117,44 @@ class ViewsModerationStateFilterTest extends ViewTestBase {
$this->drupalPostForm("admin/structure/views/nojs/handler/$view_id/default/filter/moderation_state", $edit, 'Apply');
$this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
$view = Views::getView($view_id);
$view = $this->loadViewUnchanged($view_id);
$this->assertWorkflowDependencies(['editorial', 'translation'], $view);
$this->assertTrue(isset($view->storage->getDisplay('default')['display_options']['filters']['moderation_state']));
$this->assertTrue($view->storage->status());
$this->assertTrue(isset($view->getDisplay('default')['display_options']['filters']['moderation_state']));
$this->assertTrue($view->status());
// Remove the 'Translation' workflow.
$this->drupalPostForm('admin/config/workflow/workflows/manage/translation/delete', [], 'Delete');
// Check that the view has been disabled, the filter has been deleted, the
// view can be saved and there are no more config dependencies.
$view = Views::getView($view_id);
$this->assertFalse($view->storage->status());
$this->assertFalse(isset($view->storage->getDisplay('default')['display_options']['filters']['moderation_state']));
$view = $this->loadViewUnchanged($view_id);
$this->assertFalse($view->status());
$this->assertFalse(isset($view->getDisplay('default')['display_options']['filters']['moderation_state']));
$this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
$this->assertWorkflowDependencies([], $view);
}
/**
* Load a view from the database after it has been modified in a sub-request.
*
* @param string $view_id
* The view ID.
*
* @return \Drupal\views\ViewEntityInterface
* A loaded view, bypassing static caches.
*/
public function loadViewUnchanged($view_id) {
$this->container->get('cache.config')->deleteAll();
$this->container->get('config.factory')->reset();
return $this->container->get('entity_type.manager')->getStorage('view')->loadUnchanged($view_id);
}
/**
* Tests the moderation state filter when the configured workflow is changed.
*
* @dataProvider providerTestWorkflowChanges
*/
public function testWorkflowChanges($view_id, $filter_name) {
// Update the view and make the default filter not exposed anymore,
// otherwise all results will be shown when there are no more moderated
// bundles left.
$this->drupalPostForm("admin/structure/views/nojs/handler/$view_id/default/filter/moderation_state", [], 'Hide filter');
$this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
public function testWorkflowChanges($view_id) {
// First, apply the Editorial workflow to both of our content types.
$this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/type/node', [
'bundles[example_a]' => TRUE,
@ -145,6 +162,12 @@ class ViewsModerationStateFilterTest extends ViewTestBase {
], 'Save');
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
// Update the view and make the default filter not exposed anymore,
// otherwise all results will be shown when there are no more moderated
// bundles left.
$this->drupalPostForm("admin/structure/views/nojs/handler/$view_id/default/filter/moderation_state", [], 'Hide filter');
$this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
// Add a few nodes in various moderation states.
$this->createNode(['type' => 'example_a', 'moderation_state' => 'published']);
$this->createNode(['type' => 'example_b', 'moderation_state' => 'published']);
@ -158,9 +181,8 @@ class ViewsModerationStateFilterTest extends ViewTestBase {
// Check that only the archived nodes from both bundles are displayed by the
// view.
$view = Views::getView($view_id);
$this->executeView($view);
$this->assertIdenticalResultset($view, [['nid' => $archived_node_a->id()], ['nid' => $archived_node_b->id()]], ['nid' => 'nid']);
$view = $this->loadViewUnchanged($view_id);
$this->executeAndAssertIdenticalResultset($view, [['nid' => $archived_node_a->id()], ['nid' => $archived_node_b->id()]], ['nid' => 'nid']);
// Remove the Editorial workflow from one of the bundles.
$this->drupalPostForm('admin/config/workflow/workflows/manage/editorial/type/node', [
@ -169,9 +191,8 @@ class ViewsModerationStateFilterTest extends ViewTestBase {
], 'Save');
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
$view = Views::getView($view_id);
$this->executeView($view);
$this->assertIdenticalResultset($view, [['nid' => $archived_node_a->id()]], ['nid' => 'nid']);
$view = $this->loadViewUnchanged($view_id);
$this->executeAndAssertIdenticalResultset($view, [['nid' => $archived_node_a->id()]], ['nid' => 'nid']);
// Check that the view can still be edited and saved without any
// intervention.
@ -184,16 +205,31 @@ class ViewsModerationStateFilterTest extends ViewTestBase {
], 'Save');
\Drupal::service('entity_type.bundle.info')->clearCachedBundles();
$view = Views::getView($view_id);
$this->executeView($view);
// Check that the view doesn't return any result.
$this->assertEmpty($view->result);
$view = $this->loadViewUnchanged($view_id);
$this->executeAndAssertIdenticalResultset($view, [], []);
// Check that the view can not be edited without any intervention anymore
// because the user needs to fix the filter.
// Check that the view contains a broken filter, since the moderation_state
// field was removed from the entity type.
$this->drupalPostForm("admin/structure/views/view/$view_id", [], 'Save');
$this->assertSession()->pageTextContains("No valid values found on filter: $filter_name.");
$this->assertSession()->pageTextContains("Broken/missing handler");
}
/**
* Execute a view and asssert the expected results.
*
* @param \Drupal\views\ViewEntityInterface $view_entity
* A view configuration entity.
* @param array $expected
* An expected result set.
* @param array $column_map
* An associative array mapping the columns of the result set from the view
* (as keys) and the expected result set (as values).
*/
protected function executeAndAssertIdenticalResultset(ViewEntityInterface $view_entity, $expected, $column_map) {
$executable = $this->container->get('views.executable')->get($view_entity);
$this->executeView($executable);
$this->assertIdenticalResultset($executable, $expected, $column_map);
}
/**
@ -206,11 +242,9 @@ class ViewsModerationStateFilterTest extends ViewTestBase {
return [
'view on base table, filter on base table' => [
'test_content_moderation_state_filter_base_table',
'Content: Moderation state',
],
'view on base table, filter on revision table' => [
'test_content_moderation_state_filter_base_table_filter_on_revision',
'Content revision: Moderation state',
],
];
}
@ -293,10 +327,10 @@ class ViewsModerationStateFilterTest extends ViewTestBase {
*
* @param string[] $workflow_ids
* An array of workflow IDs to check.
* @param \Drupal\views\ViewExecutable $view
* An executable View object.
* @param \Drupal\views\ViewEntityInterface $view
* A view configuration object.
*/
protected function assertWorkflowDependencies(array $workflow_ids, ViewExecutable $view) {
protected function assertWorkflowDependencies(array $workflow_ids, ViewEntityInterface $view) {
$dependencies = $view->getDependencies();
$expected = [];

View File

@ -161,6 +161,7 @@ class EntityStateChangeValidationTest extends KernelTestBase {
$workflow->save();
// Validate the invalid state.
$node = Node::load($node->id());
$node->moderation_state->value = 'invalid_state';
$violations = $node->validate();
$this->assertCount(1, $violations);

View File

@ -4,7 +4,9 @@ namespace Drupal\Tests\content_moderation\Kernel;
use Drupal\content_moderation\Entity\Handler\ModerationHandler;
use Drupal\content_moderation\EntityTypeInfo;
use Drupal\entity_test\Entity\EntityTestBundle;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
/**
* @coversDefaultClass \Drupal\content_moderation\EntityTypeInfo
@ -13,6 +15,8 @@ use Drupal\KernelTests\KernelTestBase;
*/
class EntityTypeInfoTest extends KernelTestBase {
use ContentModerationTestTrait;
/**
* Modules to enable.
*
@ -43,8 +47,11 @@ class EntityTypeInfoTest extends KernelTestBase {
*/
protected function setUp() {
parent::setUp();
$this->entityTypeInfo = $this->container->get('class_resolver')->getInstanceFromDefinition(EntityTypeInfo::class);
$this->entityTypeManager = $this->container->get('entity_type.manager');
$this->installConfig(['content_moderation']);
}
/**
@ -54,6 +61,7 @@ class EntityTypeInfoTest extends KernelTestBase {
$definition = $this->entityTypeManager->getDefinition('entity_test');
$definition->setHandlerClass('moderation', ModerationHandler::class);
$this->enableModeration('entity_test', 'entity_test');
$base_fields = $this->entityTypeInfo->entityBaseFieldInfo($definition);
$this->assertFalse($base_fields['moderation_state']->isReadOnly());
@ -91,4 +99,34 @@ class EntityTypeInfoTest extends KernelTestBase {
return $tests;
}
/**
* @covers ::entityBaseFieldInfo
*/
public function testBaseFieldOnlyAddedToModeratedEntityTypes() {
$definition = $this->entityTypeManager->getDefinition('entity_test_with_bundle');
EntityTestBundle::create([
'id' => 'moderated',
])->save();
EntityTestBundle::create([
'id' => 'unmoderated',
])->save();
$base_fields = $this->entityTypeInfo->entityBaseFieldInfo($definition);
$this->assertFalse(isset($base_fields['moderation_state']));
$this->enableModeration('entity_test_with_bundle', 'moderated');
$base_fields = $this->entityTypeInfo->entityBaseFieldInfo($definition);
$this->assertTrue(isset($base_fields['moderation_state']));
}
/**
* Add moderation to an entity type and bundle.
*/
protected function enableModeration($entity_type_id, $bundle) {
$workflow = $this->createEditorialWorkflow();
$workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle);
$workflow->save();
}
}

View File

@ -39,7 +39,6 @@ class ViewsDataIntegrationTest extends ViewsKernelTestBase {
$this->installEntitySchema('user');
$this->installEntitySchema('content_moderation_state');
$this->installSchema('node', 'node_access');
$this->installConfig('content_moderation_test_views');
$this->installConfig('content_moderation');
$node_type = NodeType::create([
@ -50,6 +49,8 @@ class ViewsDataIntegrationTest extends ViewsKernelTestBase {
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'page');
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_mulrevpub', 'entity_test_mulrevpub');
$workflow->save();
$this->installConfig('content_moderation_test_views');
}
/**

View File

@ -47,7 +47,6 @@ class ViewsModerationStateFilterTest extends ViewsKernelTestBase {
$this->installEntitySchema('content_moderation_state');
$this->installEntitySchema('entity_test_no_bundle');
$this->installSchema('node', 'node_access');
$this->installConfig('content_moderation_test_views');
$this->installConfig('content_moderation');
$node_type = NodeType::create([
@ -65,6 +64,14 @@ class ViewsModerationStateFilterTest extends ViewsKernelTestBase {
]);
$node_type->save();
$workflow = $this->createEditorialWorkflow();
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
$workflow->save();
// Install the test views after moderation has been enabled on the example
// bundle, so the moderation_state field exists.
$this->installConfig('content_moderation_test_views');
ConfigurableLanguage::createFromLangcode('fr')->save();
}
@ -72,7 +79,7 @@ class ViewsModerationStateFilterTest extends ViewsKernelTestBase {
* Tests the content moderation state filter.
*/
public function testStateFilterViewsRelationship() {
$workflow = $this->createEditorialWorkflow();
$workflow = Workflow::load('editorial');
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
$workflow->getTypePlugin()->addState('translated_draft', 'Bar');
$configuration = $workflow->getTypePlugin()->getConfiguration();
@ -159,7 +166,7 @@ class ViewsModerationStateFilterTest extends ViewsKernelTestBase {
* Test the moderation filter with a non-translatable entity type.
*/
public function testNonTranslatableEntityType() {
$workflow = $this->createEditorialWorkflow();
$workflow = Workflow::load('editorial');
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_no_bundle', 'entity_test_no_bundle');
$workflow->save();
@ -181,11 +188,13 @@ class ViewsModerationStateFilterTest extends ViewsKernelTestBase {
*/
public function testStateFilterStatesList() {
// By default a view of nodes will not have states to filter.
$workflow = Workflow::load('editorial');
$workflow->getTypePlugin()->removeEntityTypeAndBundle('node', 'example');
$workflow->save();
$this->assertPluginStates([]);
// Adding a content type to the editorial workflow will enable all of the
// editorial states.
$workflow = $this->createEditorialWorkflow();
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
$workflow->save();
$this->assertPluginStates([

View File

@ -7,6 +7,7 @@ use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityType;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\content_moderation\ModerationInformation;
@ -58,10 +59,27 @@ class ModerationInformationTest extends UnitTestCase {
}
$bundle_info = $this->prophesize(EntityTypeBundleInfoInterface::class);
$bundle_info->getBundleInfo("test_entity_type")->willReturn([$bundle => $bundle_info_array]);
$bundle_info->getBundleInfo("unmoderated_test_type")->willReturn([$bundle => []]);
return $bundle_info->reveal();
}
/**
* @covers ::isModeratedEntityType
*/
public function testIsModeratedEntityType() {
$moderation_information = new ModerationInformation($this->getEntityTypeManager(), $this->setupModerationBundleInfo('test_bundle', 'workflow'));
$moderated_entity_type = $this->prophesize(EntityTypeInterface::class);
$moderated_entity_type->id()->willReturn('test_entity_type');
$unmoderated_entity_type = $this->prophesize(EntityTypeInterface::class);
$unmoderated_entity_type->id()->willReturn('unmoderated_test_type');
$this->assertTrue($moderation_information->isModeratedEntityType($moderated_entity_type->reveal()));
$this->assertFalse($moderation_information->isModeratedEntityType($unmoderated_entity_type->reveal()));
}
/**
* @dataProvider providerWorkflow
* @covers ::isModeratedEntity

View File

@ -7,6 +7,7 @@ dependencies:
- field.field.node.article.field_tags
- image.style.thumbnail
- node.type.article
- workflows.workflow.editorial
module:
- content_moderation
- image

View File

@ -4,6 +4,7 @@ dependencies:
config:
- field.field.node.page.body
- node.type.page
- workflows.workflow.editorial
module:
- content_moderation
- path

View File

@ -14,6 +14,7 @@ dependencies:
- field.field.node.recipe.field_tags
- image.style.thumbnail
- node.type.recipe
- workflows.workflow.editorial
module:
- content_moderation
- image