diff --git a/core/modules/content_moderation/content_moderation.module b/core/modules/content_moderation/content_moderation.module index 347cbaf8aa5..fb1d3b66207 100644 --- a/core/modules/content_moderation/content_moderation.module +++ b/core/modules/content_moderation/content_moderation.module @@ -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; + } + } } diff --git a/core/modules/content_moderation/content_moderation.post_update.php b/core/modules/content_moderation/content_moderation.post_update.php index 52a6ea7eead..3405fc28fe4 100644 --- a/core/modules/content_moderation/content_moderation.post_update.php +++ b/core/modules/content_moderation/content_moderation.post_update.php @@ -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; + }); +} diff --git a/core/modules/content_moderation/src/EntityTypeInfo.php b/core/modules/content_moderation/src/EntityTypeInfo.php index f54943d427d..30a0a51ca45 100644 --- a/core/modules/content_moderation/src/EntityTypeInfo.php +++ b/core/modules/content_moderation/src/EntityTypeInfo.php @@ -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 []; } diff --git a/core/modules/content_moderation/src/ModerationInformation.php b/core/modules/content_moderation/src/ModerationInformation.php index 286104ffd78..abc93f403f0 100644 --- a/core/modules/content_moderation/src/ModerationInformation.php +++ b/core/modules/content_moderation/src/ModerationInformation.php @@ -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; } diff --git a/core/modules/content_moderation/src/ModerationInformationInterface.php b/core/modules/content_moderation/src/ModerationInformationInterface.php index c2368e9ff0b..c9522a632a9 100644 --- a/core/modules/content_moderation/src/ModerationInformationInterface.php +++ b/core/modules/content_moderation/src/ModerationInformationInterface.php @@ -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. * 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 0d4c43a3334..005001b5064 100644 --- a/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php +++ b/core/modules/content_moderation/src/Plugin/Field/FieldWidget/ModerationStateWidget.php @@ -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; + } + } diff --git a/core/modules/content_moderation/src/ViewsData.php b/core/modules/content_moderation/src/ViewsData.php index 40be3031ab4..da7e582aac5 100644 --- a/core/modules/content_moderation/src/ViewsData.php +++ b/core/modules/content_moderation/src/ViewsData.php @@ -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 diff --git a/core/modules/content_moderation/tests/fixtures/update/drupal-8.entity-form-display-dependencies-2915383.php b/core/modules/content_moderation/tests/fixtures/update/drupal-8.entity-form-display-dependencies-2915383.php new file mode 100644 index 00000000000..3305e37e078 --- /dev/null +++ b/core/modules/content_moderation/tests/fixtures/update/drupal-8.entity-form-display-dependencies-2915383.php @@ -0,0 +1,33 @@ +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(); diff --git a/core/modules/content_moderation/tests/src/Functional/EntityFormDisplayDependenciesUpdateTest.php b/core/modules/content_moderation/tests/src/Functional/EntityFormDisplayDependenciesUpdateTest.php new file mode 100644 index 00000000000..50013b3ed0f --- /dev/null +++ b/core/modules/content_moderation/tests/src/Functional/EntityFormDisplayDependenciesUpdateTest.php @@ -0,0 +1,61 @@ +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'])); + } + +} diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php index 86269193e3f..09d5c8db64f 100644 --- a/core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php +++ b/core/modules/content_moderation/tests/src/Functional/ModerationStateAccessTest.php @@ -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; - } - } diff --git a/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php b/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php index 5fd168d0bb5..92b7219be02 100644 --- a/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php +++ b/core/modules/content_moderation/tests/src/Functional/ModerationStateNodeTest.php @@ -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')); } /** diff --git a/core/modules/content_moderation/tests/src/Functional/ViewsModerationStateFilterTest.php b/core/modules/content_moderation/tests/src/Functional/ViewsModerationStateFilterTest.php index 12f391aea6d..e7b1d35b05a 100644 --- a/core/modules/content_moderation/tests/src/Functional/ViewsModerationStateFilterTest.php +++ b/core/modules/content_moderation/tests/src/Functional/ViewsModerationStateFilterTest.php @@ -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 = []; diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php index df90bf63c88..7b8d8dd2653 100644 --- a/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/EntityStateChangeValidationTest.php @@ -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); diff --git a/core/modules/content_moderation/tests/src/Kernel/EntityTypeInfoTest.php b/core/modules/content_moderation/tests/src/Kernel/EntityTypeInfoTest.php index 09990fe796a..d103ca150b4 100644 --- a/core/modules/content_moderation/tests/src/Kernel/EntityTypeInfoTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/EntityTypeInfoTest.php @@ -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(); + } + } diff --git a/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php b/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php index 0f89125bb6a..7dfb92e4692 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsDataIntegrationTest.php @@ -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'); } /** diff --git a/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php b/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php index 32323cc13e0..58d1ab0edca 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ViewsModerationStateFilterTest.php @@ -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([ diff --git a/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php b/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php index a2c3dfc5185..dead3c88eeb 100644 --- a/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php +++ b/core/modules/content_moderation/tests/src/Unit/ModerationInformationTest.php @@ -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 diff --git a/core/profiles/demo_umami/config/install/core.entity_form_display.node.article.default.yml b/core/profiles/demo_umami/config/install/core.entity_form_display.node.article.default.yml index 50db6035b74..bc287024274 100644 --- a/core/profiles/demo_umami/config/install/core.entity_form_display.node.article.default.yml +++ b/core/profiles/demo_umami/config/install/core.entity_form_display.node.article.default.yml @@ -7,6 +7,7 @@ dependencies: - field.field.node.article.field_tags - image.style.thumbnail - node.type.article + - workflows.workflow.editorial module: - content_moderation - image diff --git a/core/profiles/demo_umami/config/install/core.entity_form_display.node.page.default.yml b/core/profiles/demo_umami/config/install/core.entity_form_display.node.page.default.yml index 9a896e5a337..76b8b9fe0b4 100644 --- a/core/profiles/demo_umami/config/install/core.entity_form_display.node.page.default.yml +++ b/core/profiles/demo_umami/config/install/core.entity_form_display.node.page.default.yml @@ -4,6 +4,7 @@ dependencies: config: - field.field.node.page.body - node.type.page + - workflows.workflow.editorial module: - content_moderation - path diff --git a/core/profiles/demo_umami/config/install/core.entity_form_display.node.recipe.default.yml b/core/profiles/demo_umami/config/install/core.entity_form_display.node.recipe.default.yml index 5c3431958d5..f1281c62833 100644 --- a/core/profiles/demo_umami/config/install/core.entity_form_display.node.recipe.default.yml +++ b/core/profiles/demo_umami/config/install/core.entity_form_display.node.recipe.default.yml @@ -14,6 +14,7 @@ dependencies: - field.field.node.recipe.field_tags - image.style.thumbnail - node.type.recipe + - workflows.workflow.editorial module: - content_moderation - image