Issue #3037136 by amateescu, Sam152, blazey, Berdir: Make Workspaces and Content Moderation work together
parent
c6c2ae2ec1
commit
4362606d71
|
@ -5,21 +5,6 @@
|
|||
* Install, update and uninstall functions for the Content Moderation module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_requirements().
|
||||
*/
|
||||
function content_moderation_requirements($phase) {
|
||||
$requirements = [];
|
||||
if ($phase === 'install' && \Drupal::moduleHandler()->moduleExists('workspaces')) {
|
||||
$requirements['workspaces_incompatibility'] = [
|
||||
'severity' => REQUIREMENT_ERROR,
|
||||
'description' => t('Content Moderation can not be installed when Workspaces is also installed.'),
|
||||
];
|
||||
}
|
||||
|
||||
return $requirements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the 'content_revision_tracker' table.
|
||||
*/
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* Contains content_moderation.module.
|
||||
*/
|
||||
|
||||
use Drupal\content_moderation\ContentModerationState;
|
||||
use Drupal\content_moderation\EntityOperations;
|
||||
use Drupal\content_moderation\EntityTypeInfo;
|
||||
use Drupal\content_moderation\ContentPreprocess;
|
||||
|
@ -29,6 +30,7 @@ use Drupal\workflows\WorkflowInterface;
|
|||
use Drupal\Core\Action\Plugin\Action\PublishAction;
|
||||
use Drupal\Core\Action\Plugin\Action\UnpublishAction;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
use Drupal\workspaces\WorkspaceInterface;
|
||||
use Drupal\views\Entity\View;
|
||||
|
||||
/**
|
||||
|
@ -273,6 +275,65 @@ function content_moderation_entity_field_access($operation, FieldDefinitionInter
|
|||
return AccessResult::neutral();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_ENTITY_TYPE_access() for the 'workspace' entity type.
|
||||
*
|
||||
* Prevents a workspace to be published if there are any pending revisions in a
|
||||
* moderation state that doesn't create default revisions.
|
||||
*/
|
||||
function content_moderation_workspace_access(WorkspaceInterface $workspace, $operation, AccountInterface $account) {
|
||||
if ($operation !== 'publish') {
|
||||
return AccessResult::neutral();
|
||||
}
|
||||
|
||||
/** @var \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association */
|
||||
$workspace_association = \Drupal::service('workspaces.association');
|
||||
$entity_type_manager = \Drupal::entityTypeManager();
|
||||
|
||||
$tracked_revisions = $workspace_association->getTrackedEntities($workspace->id());
|
||||
// Extract all the second-level keys (revision IDs) of the two-dimensional
|
||||
// array.
|
||||
$tracked_revision_ids = array_reduce(array_map('array_keys', $tracked_revisions), 'array_merge', []);
|
||||
|
||||
// Gather a list of moderation states that don't create a default revision.
|
||||
$workflow_non_default_states = [];
|
||||
foreach ($entity_type_manager->getStorage('workflow')->loadByProperties(['type' => 'content_moderation']) as $workflow) {
|
||||
/** @var \Drupal\content_moderation\Plugin\WorkflowType\ContentModerationInterface $workflow_type */
|
||||
$workflow_type = $workflow->getTypePlugin();
|
||||
// Find all workflows which are moderating entity types of the same type to
|
||||
// those that are tracked by the workspace.
|
||||
if ($entity_type_ids = array_intersect($workflow_type->getEntityTypes(), array_keys($tracked_revisions))) {
|
||||
$workflow_non_default_states[$workflow->id()] = array_filter(array_map(function (ContentModerationState $state) {
|
||||
return !$state->isDefaultRevisionState() ? $state->id() : NULL;
|
||||
}, $workflow_type->getStates()));
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any revisions that are about to be published are in a non-default
|
||||
// revision moderation state.
|
||||
$query = $entity_type_manager->getStorage('content_moderation_state')->getQuery()
|
||||
->allRevisions()
|
||||
->accessCheck(FALSE);
|
||||
$query->condition('content_entity_revision_id', $tracked_revision_ids, 'IN');
|
||||
|
||||
$workflow_condition_group = $query->orConditionGroup();
|
||||
foreach ($workflow_non_default_states as $workflow_id => $non_default_states) {
|
||||
$group = $query->andConditionGroup()
|
||||
->condition('workflow', $workflow_id, '=')
|
||||
->condition('moderation_state', $non_default_states, 'IN');
|
||||
|
||||
$workflow_condition_group->condition($group);
|
||||
}
|
||||
$query->condition($workflow_condition_group);
|
||||
|
||||
if ($count = $query->count()->execute()) {
|
||||
$message = \Drupal::translation()->formatPlural($count, 'The @label workspace can not be published because it contains 1 item in an unpublished moderation state.', 'The @label workspace can not be published because it contains @count items in an unpublished moderation state.', [
|
||||
'@label' => $workspace->label(),
|
||||
]);
|
||||
return AccessResult::forbidden((string) $message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_theme().
|
||||
*/
|
||||
|
|
|
@ -130,6 +130,9 @@ class EntityModerationForm extends FormBase {
|
|||
$form['#theme'] = ['entity_moderation_form'];
|
||||
$form['#attached']['library'][] = 'content_moderation/content_moderation';
|
||||
|
||||
// Moderating an entity is allowed in a workspace.
|
||||
$form_state->set('workspace_safe', TRUE);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Functional;
|
||||
|
||||
use Drupal\Tests\workspaces\Functional\WorkspaceTestUtilities;
|
||||
use Drupal\workspaces\Entity\Workspace;
|
||||
|
||||
/**
|
||||
* Tests Workspaces together with Content Moderation.
|
||||
*
|
||||
* @group content_moderation
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspaceContentModerationIntegrationTest extends ModerationStateTestBase {
|
||||
|
||||
use WorkspaceTestUtilities;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = ['node', 'workspaces'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->drupalLogin($this->rootUser);
|
||||
|
||||
// Enable moderation on Article node type.
|
||||
$this->createContentTypeFromUi('Article', 'article', TRUE);
|
||||
|
||||
$this->setupWorkspaceSwitcherBlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests moderating nodes in a workspace.
|
||||
*/
|
||||
public function testModerationInWorkspace() {
|
||||
$stage = Workspace::load('stage');
|
||||
$this->switchToWorkspace($stage);
|
||||
|
||||
// Create two nodes, a published and a draft one.
|
||||
$this->drupalPostForm('node/add/article', [
|
||||
'title[0][value]' => 'First article - published',
|
||||
'moderation_state[0][state]' => 'published',
|
||||
], 'Save');
|
||||
$this->drupalPostForm('node/add/article', [
|
||||
'title[0][value]' => 'Second article - draft',
|
||||
'moderation_state[0][state]' => 'draft',
|
||||
], 'Save');
|
||||
|
||||
$first_article = $this->drupalGetNodeByTitle('First article - published', TRUE);
|
||||
$this->assertEquals('published', $first_article->moderation_state->value);
|
||||
|
||||
$second_article = $this->drupalGetNodeByTitle('Second article - draft', TRUE);
|
||||
$this->assertEquals('draft', $second_article->moderation_state->value);
|
||||
|
||||
// Check that neither of them are visible in Live.
|
||||
$this->switchToLive();
|
||||
$this->drupalGet('<front>');
|
||||
$this->assertNoText('First article');
|
||||
$this->assertNoText('Second article');
|
||||
|
||||
// Switch back to Stage.
|
||||
$this->switchToWorkspace($stage);
|
||||
|
||||
// Take the first node through various moderation states.
|
||||
$this->drupalGet('/node/1/edit');
|
||||
$this->assertEquals('Current state Published', $this->cssSelect('#edit-moderation-state-0-current')[0]->getText());
|
||||
|
||||
$this->drupalPostForm(NULL, [
|
||||
'title[0][value]' => 'First article - draft',
|
||||
'moderation_state[0][state]' => 'draft',
|
||||
], 'Save');
|
||||
|
||||
$this->drupalGet('/node/1');
|
||||
$this->assertText('First article - draft');
|
||||
|
||||
$this->drupalGet('/node/1/edit');
|
||||
$this->assertEquals('Current state Draft', $this->cssSelect('#edit-moderation-state-0-current')[0]->getText());
|
||||
|
||||
$this->drupalPostForm(NULL, [
|
||||
'title[0][value]' => 'First article - published',
|
||||
'moderation_state[0][state]' => 'published',
|
||||
], 'Save');
|
||||
|
||||
$this->drupalPostForm('/node/1/edit', [
|
||||
'title[0][value]' => 'First article - archived',
|
||||
'moderation_state[0][state]' => 'archived',
|
||||
], 'Save');
|
||||
|
||||
$this->drupalGet('/node/1');
|
||||
$this->assertText('First article - archived');
|
||||
|
||||
// Get the second node to a default revision state and publish the
|
||||
// workspace.
|
||||
$this->drupalPostForm('/node/2/edit', [
|
||||
'title[0][value]' => 'Second article - published',
|
||||
'moderation_state[0][state]' => 'published',
|
||||
], 'Save');
|
||||
|
||||
$stage->publish();
|
||||
|
||||
// The admin user can see unpublished nodes.
|
||||
$this->drupalGet('/node/1');
|
||||
$this->assertText('First article - archived');
|
||||
|
||||
$this->drupalGet('/node/2');
|
||||
$this->assertText('Second article - published');
|
||||
}
|
||||
|
||||
}
|
|
@ -7,12 +7,11 @@ use Drupal\Core\Entity\EntityInterface;
|
|||
use Drupal\Core\Entity\EntityPublishedInterface;
|
||||
use Drupal\Core\Entity\EntityStorageException;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\entity_test\Entity\EntityTestRev;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
|
||||
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
|
||||
use Drupal\Tests\system\Functional\Entity\Traits\EntityDefinitionTestTrait;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
|
@ -25,11 +24,12 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
|
||||
use ContentModerationTestTrait;
|
||||
use EntityDefinitionTestTrait;
|
||||
use ContentTypeCreationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
protected static $modules = [
|
||||
'entity_test',
|
||||
'node',
|
||||
'block',
|
||||
|
@ -39,6 +39,7 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
'image',
|
||||
'file',
|
||||
'field',
|
||||
'filter',
|
||||
'content_moderation',
|
||||
'user',
|
||||
'system',
|
||||
|
@ -53,6 +54,13 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The ID of the revisionable entity type used in the tests.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $revEntityTypeId = 'entity_test_rev';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -62,7 +70,7 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
$this->installSchema('node', 'node_access');
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
$this->installEntitySchema('entity_test_rev');
|
||||
$this->installEntitySchema($this->revEntityTypeId);
|
||||
$this->installEntitySchema('entity_test_no_bundle');
|
||||
$this->installEntitySchema('entity_test_mulrevpub');
|
||||
$this->installEntitySchema('block_content');
|
||||
|
@ -71,7 +79,10 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
$this->installEntitySchema('content_moderation_state');
|
||||
$this->installConfig('content_moderation');
|
||||
$this->installSchema('file', 'file_usage');
|
||||
$this->installConfig(['field', 'system', 'image', 'file', 'media']);
|
||||
$this->installConfig(['field', 'file', 'filter', 'image', 'media', 'node', 'system']);
|
||||
|
||||
// Add the French language.
|
||||
ConfigurableLanguage::createFromLangcode('fr')->save();
|
||||
|
||||
$this->entityTypeManager = $this->container->get('entity_type.manager');
|
||||
}
|
||||
|
@ -82,11 +93,7 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
* @dataProvider basicModerationTestCases
|
||||
*/
|
||||
public function testBasicModeration($entity_type_id) {
|
||||
$entity = $this->createEntity($entity_type_id);
|
||||
if ($entity instanceof EntityPublishedInterface) {
|
||||
$entity->setUnpublished();
|
||||
}
|
||||
$entity->save();
|
||||
$entity = $this->createEntity($entity_type_id, 'draft');
|
||||
$entity = $this->reloadEntity($entity);
|
||||
$this->assertEquals('draft', $entity->moderation_state->value);
|
||||
|
||||
|
@ -108,12 +115,8 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
$this->assertFalse($entity->isPublished());
|
||||
}
|
||||
|
||||
// Get the default revision.
|
||||
$entity = $this->reloadEntity($entity);
|
||||
if ($entity instanceof EntityPublishedInterface) {
|
||||
$this->assertTrue((bool) $entity->isPublished());
|
||||
}
|
||||
$this->assertEquals(2, $entity->getRevisionId());
|
||||
// Check the default revision.
|
||||
$this->assertDefaultRevision($entity, 2);
|
||||
|
||||
$entity->moderation_state->value = 'published';
|
||||
$entity->save();
|
||||
|
@ -121,12 +124,8 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
$entity = $this->reloadEntity($entity, 4);
|
||||
$this->assertEquals('published', $entity->moderation_state->value);
|
||||
|
||||
// Get the default revision.
|
||||
$entity = $this->reloadEntity($entity);
|
||||
if ($entity instanceof EntityPublishedInterface) {
|
||||
$this->assertTrue((bool) $entity->isPublished());
|
||||
}
|
||||
$this->assertEquals(4, $entity->getRevisionId());
|
||||
// Check the default revision.
|
||||
$this->assertDefaultRevision($entity, 4);
|
||||
|
||||
// Update the node to archived which will then be the default revision.
|
||||
$entity->moderation_state->value = 'archived';
|
||||
|
@ -139,12 +138,8 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
$previous_revision->setNewRevision(TRUE);
|
||||
$previous_revision->save();
|
||||
|
||||
// Get the default revision.
|
||||
$entity = $this->reloadEntity($entity);
|
||||
$this->assertEquals('published', $entity->moderation_state->value);
|
||||
if ($entity instanceof EntityPublishedInterface) {
|
||||
$this->assertTrue($entity->isPublished());
|
||||
}
|
||||
// Check the default revision.
|
||||
$this->assertDefaultRevision($entity, 6);
|
||||
|
||||
// Set an invalid moderation state.
|
||||
$this->expectException(EntityStorageException::class);
|
||||
|
@ -186,7 +181,6 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
public function testContentModerationStateDataRemoval($entity_type_id) {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $this->createEntity($entity_type_id);
|
||||
$entity->save();
|
||||
$entity = $this->reloadEntity($entity);
|
||||
$entity->delete();
|
||||
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
|
||||
|
@ -201,20 +195,30 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
public function testContentModerationStateRevisionDataRemoval($entity_type_id) {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $this->createEntity($entity_type_id);
|
||||
$entity->save();
|
||||
$revision = clone $entity;
|
||||
$revision->isDefaultRevision(FALSE);
|
||||
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($revision);
|
||||
$this->assertTrue($content_moderation_state);
|
||||
$revision_1 = clone $entity;
|
||||
$this->assertNotNull(ContentModerationState::loadFromModeratedEntity($revision_1));
|
||||
|
||||
// Create a second revision.
|
||||
$entity = $this->reloadEntity($entity);
|
||||
$entity->setNewRevision(TRUE);
|
||||
$entity->save();
|
||||
$revision_2 = clone $entity;
|
||||
|
||||
// Create a third revision.
|
||||
$entity = $this->reloadEntity($entity);
|
||||
$entity->setNewRevision(TRUE);
|
||||
$entity->save();
|
||||
$revision_3 = clone $entity;
|
||||
|
||||
// Delete the second revision and check that its content moderation state is
|
||||
// removed as well, while the content moderation states for revisions 1 and
|
||||
// 3 are kept in place.
|
||||
$entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
|
||||
$entity_storage->deleteRevision($revision->getRevisionId());
|
||||
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($revision);
|
||||
$this->assertFalse($content_moderation_state);
|
||||
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($entity);
|
||||
$this->assertTrue($content_moderation_state);
|
||||
$entity_storage->deleteRevision($revision_2->getRevisionId());
|
||||
|
||||
$this->assertNotNull(ContentModerationState::loadFromModeratedEntity($revision_1));
|
||||
$this->assertNull(ContentModerationState::loadFromModeratedEntity($revision_2));
|
||||
$this->assertNotNull(ContentModerationState::loadFromModeratedEntity($revision_3));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -223,9 +227,7 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
* @dataProvider basicModerationTestCases
|
||||
*/
|
||||
public function testContentModerationStatePendingRevisionDataRemoval($entity_type_id) {
|
||||
$entity = $this->createEntity($entity_type_id);
|
||||
$entity->moderation_state = 'published';
|
||||
$entity->save();
|
||||
$entity = $this->createEntity($entity_type_id, 'published');
|
||||
$entity->setNewRevision(TRUE);
|
||||
$entity->moderation_state = 'draft';
|
||||
$entity->save();
|
||||
|
@ -246,13 +248,11 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
public function testExistingContentModerationStateDataRemoval() {
|
||||
$storage = $this->entityTypeManager->getStorage('entity_test_mulrevpub');
|
||||
|
||||
$entity = $storage->create([]);
|
||||
$entity->save();
|
||||
$entity = $this->createEntity('entity_test_mulrevpub', 'published', FALSE);
|
||||
$original_revision_id = $entity->getRevisionId();
|
||||
|
||||
$workflow = $this->createEditorialWorkflow();
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle($entity->getEntityTypeId(), $entity->bundle());
|
||||
$workflow->save();
|
||||
$this->addEntityTypeAndBundleToWorkflow($workflow, $entity->getEntityTypeId(), $entity->bundle());
|
||||
|
||||
$entity = $this->reloadEntity($entity);
|
||||
$entity->moderation_state = 'draft';
|
||||
|
@ -274,12 +274,9 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
// Test content moderation state translation deletion.
|
||||
if ($this->entityTypeManager->getDefinition($entity_type_id)->isTranslatable()) {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity = $this->createEntity($entity_type_id);
|
||||
$langcode = 'it';
|
||||
ConfigurableLanguage::createFromLangcode($langcode)
|
||||
->save();
|
||||
$entity->save();
|
||||
$translation = $entity->addTranslation($langcode, ['title' => 'Titolo test']);
|
||||
$entity = $this->createEntity($entity_type_id, 'published');
|
||||
$langcode = 'fr';
|
||||
$translation = $entity->addTranslation($langcode, ['title' => 'French title test']);
|
||||
// Make sure we add values for all of the required fields.
|
||||
if ($entity_type_id == 'block_content') {
|
||||
$translation->info = $this->randomString();
|
||||
|
@ -298,16 +295,12 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
* Tests basic multilingual content moderation through the API.
|
||||
*/
|
||||
public function testMultilingualModeration() {
|
||||
// Enable French.
|
||||
ConfigurableLanguage::createFromLangcode('fr')->save();
|
||||
$node_type = NodeType::create([
|
||||
$this->createContentType([
|
||||
'type' => 'example',
|
||||
]);
|
||||
$node_type->save();
|
||||
|
||||
$workflow = $this->createEditorialWorkflow();
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
|
||||
$workflow->save();
|
||||
$this->addEntityTypeAndBundleToWorkflow($workflow, 'node', 'example');
|
||||
|
||||
$english_node = Node::create([
|
||||
'type' => 'example',
|
||||
|
@ -377,7 +370,7 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
$this->assertTrue($french_node->isPublished());
|
||||
|
||||
// Change the EN state without saving the node.
|
||||
$content_moderation_state = ContentModerationState::load(1);
|
||||
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($english_node);
|
||||
$content_moderation_state->set('moderation_state', 'draft');
|
||||
$content_moderation_state->setNewRevision(TRUE);
|
||||
// Revision 8 (en, fr).
|
||||
|
@ -389,7 +382,7 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
$this->assertEquals('published', $french_node->moderation_state->value);
|
||||
|
||||
// This should unpublish the French node.
|
||||
$content_moderation_state = ContentModerationState::load(1);
|
||||
$content_moderation_state = ContentModerationState::loadFromModeratedEntity($english_node);
|
||||
$content_moderation_state = $content_moderation_state->getTranslation('fr');
|
||||
$content_moderation_state->set('moderation_state', 'draft');
|
||||
$content_moderation_state->setNewRevision(TRUE);
|
||||
|
@ -404,23 +397,20 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
// entity.
|
||||
$this->assertFalse($french_node->isPublished());
|
||||
|
||||
// Get the default english node.
|
||||
$english_node = $this->reloadEntity($english_node);
|
||||
$this->assertTrue($english_node->isPublished());
|
||||
$this->assertEquals(7, $english_node->getRevisionId());
|
||||
// Check that revision 7 is still the default one for the node.
|
||||
$this->assertDefaultRevision($english_node, 7);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests moderation when the moderation_state field has a config override.
|
||||
*/
|
||||
public function testModerationWithFieldConfigOverride() {
|
||||
NodeType::create([
|
||||
$this->createContentType([
|
||||
'type' => 'test_type',
|
||||
])->save();
|
||||
]);
|
||||
|
||||
$workflow = $this->createEditorialWorkflow();
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'test_type');
|
||||
$workflow->save();
|
||||
$this->addEntityTypeAndBundleToWorkflow($workflow, 'node', 'test_type');
|
||||
|
||||
$fields = $this->container->get('entity_field.manager')->getFieldDefinitions('node', 'test_type');
|
||||
$field_config = $fields['moderation_state']->getConfig('test_type');
|
||||
|
@ -448,11 +438,11 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
*/
|
||||
public function testModerationWithSpecialLanguages($original_language, $updated_language) {
|
||||
$workflow = $this->createEditorialWorkflow();
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
|
||||
$workflow->save();
|
||||
$this->addEntityTypeAndBundleToWorkflow($workflow, $this->revEntityTypeId, $this->revEntityTypeId);
|
||||
|
||||
// Create a test entity.
|
||||
$entity = EntityTestRev::create([
|
||||
$storage = $this->entityTypeManager->getStorage($this->revEntityTypeId);
|
||||
$entity = $storage->create([
|
||||
'langcode' => $original_language,
|
||||
]);
|
||||
$entity->save();
|
||||
|
@ -462,7 +452,7 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
$entity->langcode = $updated_language;
|
||||
$entity->save();
|
||||
|
||||
$this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
|
||||
$this->assertEquals('published', $storage->load($entity->id())->moderation_state->value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -489,13 +479,11 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
* Test changing the language of content without adding a translation.
|
||||
*/
|
||||
public function testChangingContentLangcode() {
|
||||
ConfigurableLanguage::createFromLangcode('fr')->save();
|
||||
NodeType::create([
|
||||
$this->createContentType([
|
||||
'type' => 'test_type',
|
||||
])->save();
|
||||
]);
|
||||
$workflow = $this->createEditorialWorkflow();
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'test_type');
|
||||
$workflow->save();
|
||||
$this->addEntityTypeAndBundleToWorkflow($workflow, 'node', 'test_type');
|
||||
|
||||
$entity = Node::create([
|
||||
'title' => 'Test node',
|
||||
|
@ -525,22 +513,22 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
*/
|
||||
public function testNonTranslatableEntityTypeModeration() {
|
||||
$workflow = $this->createEditorialWorkflow();
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
|
||||
$workflow->save();
|
||||
$this->addEntityTypeAndBundleToWorkflow($workflow, $this->revEntityTypeId, $this->revEntityTypeId);
|
||||
|
||||
// Check that the tested entity type is not translatable.
|
||||
$entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_rev');
|
||||
$entity_type = $this->entityTypeManager->getDefinition($this->revEntityTypeId);
|
||||
$this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
|
||||
|
||||
// Create a test entity.
|
||||
$entity = EntityTestRev::create();
|
||||
$storage = $this->entityTypeManager->getStorage($this->revEntityTypeId);
|
||||
$entity = $storage->create();
|
||||
$entity->save();
|
||||
$this->assertEquals('draft', $entity->moderation_state->value);
|
||||
|
||||
$entity->moderation_state->value = 'published';
|
||||
$entity->save();
|
||||
|
||||
$this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
|
||||
$this->assertEquals('published', $storage->load($entity->id())->moderation_state->value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -549,51 +537,49 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
*/
|
||||
public function testNonLangcodeEntityTypeModeration() {
|
||||
// Unset the langcode entity key for 'entity_test_rev'.
|
||||
$entity_type = clone \Drupal::entityTypeManager()->getDefinition('entity_test_rev');
|
||||
$entity_type = clone $this->entityTypeManager->getDefinition($this->revEntityTypeId);
|
||||
$keys = $entity_type->getKeys();
|
||||
unset($keys['langcode']);
|
||||
$entity_type->set('entity_keys', $keys);
|
||||
\Drupal::state()->set('entity_test_rev.entity_type', $entity_type);
|
||||
\Drupal::state()->set($this->revEntityTypeId . '.entity_type', $entity_type);
|
||||
|
||||
// Update the entity type in order to remove the 'langcode' field.
|
||||
\Drupal::entityDefinitionUpdateManager()->updateFieldableEntityType($entity_type, \Drupal::service('entity_field.manager')->getFieldStorageDefinitions($entity_type->id()));
|
||||
|
||||
$workflow = $this->createEditorialWorkflow();
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
|
||||
$workflow->save();
|
||||
$this->addEntityTypeAndBundleToWorkflow($workflow, $this->revEntityTypeId, $this->revEntityTypeId);
|
||||
|
||||
// Check that the tested entity type is not translatable and does not have a
|
||||
// 'langcode' entity key.
|
||||
$entity_type = \Drupal::entityTypeManager()->getDefinition('entity_test_rev');
|
||||
$entity_type = $this->entityTypeManager->getDefinition($this->revEntityTypeId);
|
||||
$this->assertFalse($entity_type->isTranslatable(), 'The test entity type is not translatable.');
|
||||
$this->assertFalse($entity_type->getKey('langcode'), "The test entity type does not have a 'langcode' entity key.");
|
||||
|
||||
// Create a test entity.
|
||||
$entity = EntityTestRev::create();
|
||||
$storage = $this->entityTypeManager->getStorage($this->revEntityTypeId);
|
||||
$entity = $storage->create();
|
||||
$entity->save();
|
||||
$this->assertEquals('draft', $entity->moderation_state->value);
|
||||
|
||||
$entity->moderation_state->value = 'published';
|
||||
$entity->save();
|
||||
|
||||
$this->assertEquals('published', EntityTestRev::load($entity->id())->moderation_state->value);
|
||||
$this->assertEquals('published', $storage->load($entity->id())->moderation_state->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the dependencies of the workflow when using content moderation.
|
||||
*/
|
||||
public function testWorkflowDependencies() {
|
||||
$node_type = NodeType::create([
|
||||
$node_type = $this->createContentType([
|
||||
'type' => 'example',
|
||||
]);
|
||||
$node_type->save();
|
||||
|
||||
$workflow = $this->createEditorialWorkflow();
|
||||
// Test both a config and non-config based bundle and entity type.
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_rev', 'entity_test_rev');
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle('entity_test_no_bundle', 'entity_test_no_bundle');
|
||||
$workflow->save();
|
||||
$this->addEntityTypeAndBundleToWorkflow($workflow, 'node', 'example');
|
||||
$this->addEntityTypeAndBundleToWorkflow($workflow, 'entity_test_rev', 'entity_test_rev');
|
||||
$this->addEntityTypeAndBundleToWorkflow($workflow, 'entity_test_no_bundle', 'entity_test_no_bundle');
|
||||
|
||||
$this->assertEquals([
|
||||
'module' => [
|
||||
|
@ -670,35 +656,24 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
public function testRevisionDefaultState($entity_type_id) {
|
||||
// Check that the revision default state of the moderated entity and the
|
||||
// content moderation state entity always match.
|
||||
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
|
||||
$storage = $this->entityTypeManager->getStorage($entity_type_id);
|
||||
/** @var \Drupal\Core\Entity\ContentEntityStorageInterface $cms_storage */
|
||||
$cms_storage = $this->entityTypeManager->getStorage('content_moderation_state');
|
||||
|
||||
$entity = $this->createEntity($entity_type_id);
|
||||
$entity->get('moderation_state')->value = 'published';
|
||||
$storage->save($entity);
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */
|
||||
$cms_entity = $cms_storage->loadUnchanged(1);
|
||||
$this->assertEquals($entity->getLoadedRevisionId(), $cms_entity->get('content_entity_revision_id')->value);
|
||||
$entity = $this->createEntity($entity_type_id, 'published');
|
||||
$cms_entity = ContentModerationState::loadFromModeratedEntity($entity);
|
||||
$this->assertEquals($entity->isDefaultRevision(), $cms_entity->isDefaultRevision());
|
||||
|
||||
$entity->get('moderation_state')->value = 'published';
|
||||
$storage->save($entity);
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */
|
||||
$cms_entity = $cms_storage->loadUnchanged(1);
|
||||
$this->assertEquals($entity->getLoadedRevisionId(), $cms_entity->get('content_entity_revision_id')->value);
|
||||
$entity->save();
|
||||
$cms_entity = ContentModerationState::loadFromModeratedEntity($entity);
|
||||
$this->assertEquals($entity->isDefaultRevision(), $cms_entity->isDefaultRevision());
|
||||
|
||||
$entity->get('moderation_state')->value = 'draft';
|
||||
$storage->save($entity);
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */
|
||||
$cms_entity = $cms_storage->loadUnchanged(1);
|
||||
$this->assertEquals($entity->getLoadedRevisionId() - 1, $cms_entity->get('content_entity_revision_id')->value);
|
||||
$entity->save();
|
||||
$cms_entity = ContentModerationState::loadFromModeratedEntity($entity);
|
||||
$this->assertEquals($entity->isDefaultRevision(), $cms_entity->isDefaultRevision());
|
||||
|
||||
$entity->get('moderation_state')->value = 'published';
|
||||
$storage->save($entity);
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $cms_entity */
|
||||
$cms_entity = $cms_storage->loadUnchanged(1);
|
||||
$this->assertEquals($entity->getLoadedRevisionId(), $cms_entity->get('content_entity_revision_id')->value);
|
||||
$entity->save();
|
||||
$cms_entity = ContentModerationState::loadFromModeratedEntity($entity);
|
||||
$this->assertEquals($entity->isDefaultRevision(), $cms_entity->isDefaultRevision());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -719,11 +694,17 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
*
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID.
|
||||
* @param string $moderation_state
|
||||
* (optional) The initial moderation state of the newly created entity.
|
||||
* Defaults to 'published'.
|
||||
* @param bool $create_workflow
|
||||
* (optional) Whether to create an editorial workflow and configure it for
|
||||
* the given entity type. Defaults to TRUE.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\ContentEntityInterface
|
||||
* The created entity.
|
||||
*/
|
||||
protected function createEntity($entity_type_id) {
|
||||
protected function createEntity($entity_type_id, $moderation_state = 'published', $create_workflow = TRUE) {
|
||||
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
|
||||
|
||||
$bundle_id = $entity_type_id;
|
||||
|
@ -751,20 +732,25 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
}
|
||||
}
|
||||
|
||||
$workflow = $this->createEditorialWorkflow();
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle_id);
|
||||
$workflow->save();
|
||||
if ($create_workflow) {
|
||||
$workflow = $this->createEditorialWorkflow();
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle_id);
|
||||
$workflow->save();
|
||||
}
|
||||
|
||||
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
|
||||
$entity_storage = $this->entityTypeManager->getStorage($entity_type_id);
|
||||
$entity = $entity_storage->create([
|
||||
$entity_type->getKey('label') => 'Test title',
|
||||
$entity_type->getKey('bundle') => $bundle_id,
|
||||
'moderation_state' => $moderation_state,
|
||||
]);
|
||||
// Make sure we add values for all of the required fields.
|
||||
if ($entity_type_id == 'block_content') {
|
||||
$entity->info = $this->randomString();
|
||||
}
|
||||
|
||||
$entity->save();
|
||||
return $entity;
|
||||
}
|
||||
|
||||
|
@ -789,4 +775,25 @@ class ContentModerationStateTest extends KernelTestBase {
|
|||
return $storage->load($entity->id());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the default revision ID and publishing status for an entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* An entity object.
|
||||
* @param int $revision_id
|
||||
* The expected revision ID.
|
||||
* @param bool|null $published
|
||||
* (optional) Whether to check if the entity is published or not. Defaults
|
||||
* to TRUE.
|
||||
*/
|
||||
protected function assertDefaultRevision(EntityInterface $entity, $revision_id, $published = TRUE) {
|
||||
// Get the default revision.
|
||||
$entity = $this->reloadEntity($entity);
|
||||
$this->assertEquals($revision_id, $entity->getRevisionId());
|
||||
|
||||
if ($published !== NULL && $entity instanceof EntityPublishedInterface) {
|
||||
$this->assertSame($published, $entity->isPublished());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,259 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\Tests\content_moderation\Traits\ContentModerationTestTrait;
|
||||
use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
|
||||
use Drupal\Tests\user\Traits\UserCreationTrait;
|
||||
use Drupal\Tests\workspaces\Kernel\WorkspaceTestTrait;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
use Drupal\workflows\WorkflowInterface;
|
||||
use Drupal\workspaces\WorkspaceAccessException;
|
||||
|
||||
/**
|
||||
* Tests that Workspaces and Content Moderation work together properly.
|
||||
*
|
||||
* @group content_moderation
|
||||
* @group workspaces
|
||||
*/
|
||||
class WorkspacesContentModerationStateTest extends ContentModerationStateTest {
|
||||
|
||||
use ContentModerationTestTrait {
|
||||
createEditorialWorkflow as traitCreateEditorialWorkflow;
|
||||
addEntityTypeAndBundleToWorkflow as traitAddEntityTypeAndBundleToWorkflow;
|
||||
}
|
||||
use ContentTypeCreationTrait {
|
||||
createContentType as traitCreateContentType;
|
||||
}
|
||||
use UserCreationTrait;
|
||||
use WorkspaceTestTrait;
|
||||
|
||||
/**
|
||||
* The ID of the revisionable entity type used in the tests.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $revEntityTypeId = 'entity_test_revpub';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installSchema('system', ['key_value_expire', 'sequences']);
|
||||
|
||||
$this->initializeWorkspacesModule();
|
||||
$this->switchToWorkspace('stage');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the integration between Content Moderation and Workspaces.
|
||||
*
|
||||
* @see content_moderation_workspace_access()
|
||||
*/
|
||||
public function testContentModerationIntegrationWithWorkspaces() {
|
||||
$editorial = $this->createEditorialWorkflow();
|
||||
$access_handler = \Drupal::entityTypeManager()->getAccessControlHandler('workspace');
|
||||
|
||||
// Create another workflow which has the same states as the 'editorial' one,
|
||||
// but it doesn't create default revisions for the 'archived' state. This
|
||||
// covers the case when two bundles of the same entity type use different
|
||||
// workflows with same moderation state names but with different settings.
|
||||
$editorial_2_values = $editorial->toArray();
|
||||
unset($editorial_2_values['uuid']);
|
||||
$editorial_2_values['id'] = 'editorial_2';
|
||||
$editorial_2_values['type_settings']['states']['archived']['default_revision'] = FALSE;
|
||||
|
||||
$editorial_2 = Workflow::create($editorial_2_values);
|
||||
$this->workspaceManager->executeOutsideWorkspace(function () use ($editorial_2) {
|
||||
$editorial_2->save();
|
||||
});
|
||||
|
||||
// Create two bundles and assign the two workflows for each of them.
|
||||
$this->createContentType(['type' => 'page']);
|
||||
$this->addEntityTypeAndBundleToWorkflow($editorial, 'node', 'page');
|
||||
$this->createContentType(['type' => 'article']);
|
||||
$this->addEntityTypeAndBundleToWorkflow($editorial_2, 'node', 'article');
|
||||
|
||||
// Create three entities for each bundle, covering all the available
|
||||
// moderation states.
|
||||
$page_archived = Node::create(['type' => 'page', 'title' => 'Test page - archived', 'moderation_state' => 'archived']);
|
||||
$page_archived->save();
|
||||
$page_draft = Node::create(['type' => 'page', 'title' => 'Test page - draft', 'moderation_state' => 'draft']);
|
||||
$page_draft->save();
|
||||
$page_published = Node::create(['type' => 'page', 'title' => 'Test page - published', 'moderation_state' => 'published']);
|
||||
$page_published->save();
|
||||
|
||||
$article_archived = Node::create(['type' => 'article', 'title' => 'Test article - archived', 'moderation_state' => 'archived']);
|
||||
$article_archived->save();
|
||||
$article_draft = Node::create(['type' => 'article', 'title' => 'Test article - draft', 'moderation_state' => 'draft']);
|
||||
$article_draft->save();
|
||||
$article_published = Node::create(['type' => 'article', 'title' => 'Test article - published', 'moderation_state' => 'published']);
|
||||
$article_published->save();
|
||||
|
||||
// We have three items in a non-default moderation state:
|
||||
// - $page_draft
|
||||
// - $article_archived
|
||||
// - $article_draft
|
||||
// Therefore the workspace can not be published.
|
||||
// This assertion also covers two moderation states from different workflows
|
||||
// with the same name ('archived'), but with different default revision
|
||||
// settings.
|
||||
try {
|
||||
$this->workspaces['stage']->publish();
|
||||
$this->fail('The expected exception was not thrown.');
|
||||
}
|
||||
catch (WorkspaceAccessException $e) {
|
||||
$this->assertEquals('The Stage workspace can not be published because it contains 3 items in an unpublished moderation state.', $e->getMessage());
|
||||
}
|
||||
|
||||
// Get the $page_draft node to a publishable state and try again.
|
||||
$page_draft->moderation_state->value = 'published';
|
||||
$page_draft->save();
|
||||
try {
|
||||
$access_handler->resetCache();
|
||||
$this->workspaces['stage']->publish();
|
||||
$this->fail('The expected exception was not thrown.');
|
||||
}
|
||||
catch (WorkspaceAccessException $e) {
|
||||
$this->assertEquals('The Stage workspace can not be published because it contains 2 items in an unpublished moderation state.', $e->getMessage());
|
||||
}
|
||||
|
||||
// Get the $article_archived node to a publishable state and try again.
|
||||
$article_archived->moderation_state->value = 'published';
|
||||
$article_archived->save();
|
||||
try {
|
||||
$access_handler->resetCache();
|
||||
$this->workspaces['stage']->publish();
|
||||
$this->fail('The expected exception was not thrown.');
|
||||
}
|
||||
catch (WorkspaceAccessException $e) {
|
||||
$this->assertEquals('The Stage workspace can not be published because it contains 1 item in an unpublished moderation state.', $e->getMessage());
|
||||
}
|
||||
|
||||
// Get the $article_draft node to a publishable state and try again.
|
||||
$article_draft->moderation_state->value = 'published';
|
||||
$article_draft->save();
|
||||
$access_handler->resetCache();
|
||||
$this->workspaces['stage']->publish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cases for basic moderation test.
|
||||
*/
|
||||
public function basicModerationTestCases() {
|
||||
return [
|
||||
'Nodes' => [
|
||||
'node',
|
||||
],
|
||||
'Block content' => [
|
||||
'block_content',
|
||||
],
|
||||
'Media' => [
|
||||
'media',
|
||||
],
|
||||
'Test entity - revisions, data table, and published interface' => [
|
||||
'entity_test_mulrevpub',
|
||||
],
|
||||
'Entity Test with revisions and published status' => [
|
||||
'entity_test_revpub',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testModerationWithFieldConfigOverride() {
|
||||
// This test does not assert anything that can be workspace-specific.
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testWorkflowDependencies() {
|
||||
// This test does not assert anything that can be workspace-specific.
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testWorkflowNonConfigBundleDependencies() {
|
||||
// This test does not assert anything that can be workspace-specific.
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function testGetCurrentUserId() {
|
||||
// This test does not assert anything that can be workspace-specific.
|
||||
$this->markTestSkipped();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEntity($entity_type_id, $moderation_state = 'published', $create_workflow = TRUE) {
|
||||
$entity = $this->workspaceManager->executeOutsideWorkspace(function () use ($entity_type_id, $moderation_state, $create_workflow) {
|
||||
return parent::createEntity($entity_type_id, $moderation_state, $create_workflow);
|
||||
});
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createEditorialWorkflow() {
|
||||
$workflow = $this->workspaceManager->executeOutsideWorkspace(function () {
|
||||
return $this->traitCreateEditorialWorkflow();
|
||||
});
|
||||
|
||||
return $workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function addEntityTypeAndBundleToWorkflow(WorkflowInterface $workflow, $entity_type_id, $bundle) {
|
||||
$this->workspaceManager->executeOutsideWorkspace(function () use ($workflow, $entity_type_id, $bundle) {
|
||||
$this->traitAddEntityTypeAndBundleToWorkflow($workflow, $entity_type_id, $bundle);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function createContentType(array $values = []) {
|
||||
$note_type = $this->workspaceManager->executeOutsideWorkspace(function () use ($values) {
|
||||
return $this->traitCreateContentType($values);
|
||||
});
|
||||
|
||||
return $note_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function assertDefaultRevision(EntityInterface $entity, $revision_id, $published = TRUE) {
|
||||
// In the context of a workspace, the default revision ID is always the
|
||||
// latest workspace-specific revision, so we need to adjust the expectation
|
||||
// of the parent assertion.
|
||||
$revision_id = $this->entityTypeManager->getStorage($entity->getEntityTypeId())->load($entity->id())->getRevisionId();
|
||||
|
||||
// Additionally, the publishing status of the default revision is not
|
||||
// relevant in a workspace, because getting an entity to a "published"
|
||||
// moderation state doesn't automatically make it the default revision, so
|
||||
// we have to disable that assertion.
|
||||
$published = NULL;
|
||||
|
||||
parent::assertDefaultRevision($entity, $revision_id, $published);
|
||||
}
|
||||
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
namespace Drupal\Tests\content_moderation\Traits;
|
||||
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
use Drupal\workflows\WorkflowInterface;
|
||||
|
||||
/**
|
||||
* Trait ContentModerationTestTraint.
|
||||
|
@ -85,4 +86,19 @@ trait ContentModerationTestTrait {
|
|||
return $workflow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an entity type ID / bundle ID to the given workflow.
|
||||
*
|
||||
* @param \Drupal\workflows\WorkflowInterface $workflow
|
||||
* A workflow object.
|
||||
* @param string $entity_type_id
|
||||
* The entity type ID to add.
|
||||
* @param string $bundle
|
||||
* The bundle ID to add.
|
||||
*/
|
||||
protected function addEntityTypeAndBundleToWorkflow(WorkflowInterface $workflow, $entity_type_id, $bundle) {
|
||||
$workflow->getTypePlugin()->addEntityTypeAndBundle($entity_type_id, $bundle);
|
||||
$workflow->save();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -97,6 +97,7 @@ function entity_test_entity_type_alter(array &$entity_types) {
|
|||
|
||||
// Allow entity_test_rev tests to override the entity type definition.
|
||||
$entity_types['entity_test_rev'] = $state->get('entity_test_rev.entity_type', $entity_types['entity_test_rev']);
|
||||
$entity_types['entity_test_revpub'] = $state->get('entity_test_revpub.entity_type', $entity_types['entity_test_revpub']);
|
||||
|
||||
// Enable the entity_test_new only when needed.
|
||||
if (!$state->get('entity_test_new')) {
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\entity_test\Entity;
|
||||
|
||||
use Drupal\Core\Entity\EntityPublishedInterface;
|
||||
use Drupal\Core\Entity\EntityPublishedTrait;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
|
||||
/**
|
||||
* Defines the test entity class.
|
||||
*
|
||||
* @ContentEntityType(
|
||||
* id = "entity_test_revpub",
|
||||
* label = @Translation("Test entity - revisions and publishing status"),
|
||||
* handlers = {
|
||||
* "access" = "Drupal\entity_test\EntityTestAccessControlHandler",
|
||||
* "view_builder" = "Drupal\entity_test\EntityTestViewBuilder",
|
||||
* "form" = {
|
||||
* "default" = "Drupal\entity_test\EntityTestForm",
|
||||
* "delete" = "Drupal\entity_test\EntityTestDeleteForm",
|
||||
* "delete-multiple-confirm" = "Drupal\Core\Entity\Form\DeleteMultipleForm"
|
||||
* },
|
||||
* "view_builder" = "Drupal\entity_test\EntityTestViewBuilder",
|
||||
* },
|
||||
* base_table = "entity_test_revpub",
|
||||
* revision_table = "entity_test_revpub_revision",
|
||||
* admin_permission = "administer entity_test content",
|
||||
* show_revision_ui = TRUE,
|
||||
* entity_keys = {
|
||||
* "id" = "id",
|
||||
* "uuid" = "uuid",
|
||||
* "revision" = "revision_id",
|
||||
* "bundle" = "type",
|
||||
* "label" = "name",
|
||||
* "langcode" = "langcode",
|
||||
* "published" = "status",
|
||||
* },
|
||||
* links = {
|
||||
* "add-form" = "/entity_test_rev/add",
|
||||
* "canonical" = "/entity_test_rev/manage/{entity_test_rev}",
|
||||
* "delete-form" = "/entity_test/delete/entity_test_rev/{entity_test_rev}",
|
||||
* "delete-multiple-form" = "/entity_test_rev/delete_multiple",
|
||||
* "edit-form" = "/entity_test_rev/manage/{entity_test_rev}/edit",
|
||||
* "revision" = "/entity_test_rev/{entity_test_rev}/revision/{entity_test_rev_revision}/view",
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class EntityTestRevPub extends EntityTestRev implements EntityPublishedInterface {
|
||||
|
||||
use EntityPublishedTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
|
||||
$fields = parent::baseFieldDefinitions($entity_type);
|
||||
|
||||
// Add the publishing status field.
|
||||
$fields += static::publishedBaseFieldDefinitions($entity_type);
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
}
|
|
@ -77,6 +77,24 @@ class EntityTypeInfo implements ContainerInjectionInterface {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the 'latest-version' link template provided by Content Moderation.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_types
|
||||
* An array of entity types.
|
||||
*
|
||||
* @see hook_entity_type_alter()
|
||||
*/
|
||||
public function entityTypeAlter(array &$entity_types) {
|
||||
foreach ($entity_types as $entity_type_id => $entity_type) {
|
||||
// Non-default workspaces display the active revision on the canonical
|
||||
// route of an entity, so the latest version route is no longer needed.
|
||||
$link_templates = $entity_type->get('links');
|
||||
unset($link_templates['latest-version']);
|
||||
$entity_type->set('links', $link_templates);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters field plugin definitions.
|
||||
*
|
||||
|
|
|
@ -8,6 +8,7 @@ use Drupal\Core\Entity\EntityRepositoryInterface;
|
|||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\workspaces\WorkspaceAccessException;
|
||||
use Drupal\workspaces\WorkspaceOperationFactory;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
|
@ -153,6 +154,9 @@ class WorkspaceDeployForm extends ContentEntityForm implements WorkspaceFormInte
|
|||
$workspace->publish();
|
||||
$this->messenger->addMessage($this->t('Successful deployment.'));
|
||||
}
|
||||
catch (WorkspaceAccessException $e) {
|
||||
$this->messenger->addMessage($e->getMessage(), 'error');
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
$this->messenger->addMessage($this->t('Deployment failed. All errors have been logged.'), 'error');
|
||||
}
|
||||
|
|
|
@ -23,7 +23,9 @@ class WorkspaceAccessControlHandler extends EntityAccessControlHandler {
|
|||
return AccessResult::allowed()->cachePerPermissions();
|
||||
}
|
||||
|
||||
$permission_operation = $operation === 'update' ? 'edit' : $operation;
|
||||
// @todo Consider adding explicit "publish any|own workspace" permissions in
|
||||
// https://www.drupal.org/project/drupal/issues/3084260.
|
||||
$permission_operation = ($operation === 'update' || $operation === 'publish') ? 'edit' : $operation;
|
||||
|
||||
// Check if the user has permission to access all workspaces.
|
||||
$access_result = AccessResult::allowedIfHasPermission($account, $permission_operation . ' any workspace');
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Drupal\workspaces;
|
||||
|
||||
use Drupal\Core\Access\AccessResultReasonInterface;
|
||||
use Drupal\Core\Database\Connection;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
|
@ -74,6 +75,12 @@ class WorkspacePublisher implements WorkspacePublisherInterface {
|
|||
* {@inheritdoc}
|
||||
*/
|
||||
public function publish() {
|
||||
$publish_access = $this->sourceWorkspace->access('publish', NULL, TRUE);
|
||||
if (!$publish_access->isAllowed()) {
|
||||
$message = $publish_access instanceof AccessResultReasonInterface ? $publish_access->getReason() : '';
|
||||
throw new WorkspaceAccessException($message);
|
||||
}
|
||||
|
||||
if ($this->checkConflictsOnTarget()) {
|
||||
throw new WorkspaceConflictException();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
name: 'Workspace Access Test'
|
||||
type: module
|
||||
description: 'Provides supporting code for testing access for workspaces.'
|
||||
package: Testing
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
dependencies:
|
||||
- drupal:workspaces
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Provides supporting code for testing access for workspaces.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
|
||||
/**
|
||||
* Implements hook_ENTITY_TYPE_access() for the 'workspace' entity type.
|
||||
*/
|
||||
function workspace_access_test_workspace_access(EntityInterface $entity, $operation, AccountInterface $account) {
|
||||
return \Drupal::state()->get("workspace_access_test.result.$operation", AccessResult::neutral());
|
||||
}
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
namespace Drupal\Tests\workspaces\Kernel;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\Tests\user\Traits\UserCreationTrait;
|
||||
use Drupal\workspaces\Entity\Workspace;
|
||||
use Drupal\workspaces\WorkspaceAccessException;
|
||||
|
||||
/**
|
||||
* Tests access on workspaces.
|
||||
|
@ -22,6 +24,7 @@ class WorkspaceAccessTest extends KernelTestBase {
|
|||
'user',
|
||||
'system',
|
||||
'workspaces',
|
||||
'workspace_access_test',
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -31,6 +34,7 @@ class WorkspaceAccessTest extends KernelTestBase {
|
|||
parent::setUp();
|
||||
|
||||
$this->installSchema('system', ['sequences']);
|
||||
$this->installSchema('workspaces', ['workspace_association']);
|
||||
|
||||
$this->installEntitySchema('workspace');
|
||||
$this->installEntitySchema('user');
|
||||
|
@ -81,4 +85,29 @@ class WorkspaceAccessTest extends KernelTestBase {
|
|||
$this->assertTrue($workspace->access($operation, $user));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests workspace publishing access.
|
||||
*/
|
||||
public function testPublishWorkspaceAccess() {
|
||||
$user = $this->createUser([
|
||||
'view own workspace',
|
||||
'edit own workspace',
|
||||
]);
|
||||
$this->setCurrentUser($user);
|
||||
|
||||
$workspace = Workspace::create(['id' => 'stage']);
|
||||
$workspace->save();
|
||||
|
||||
// Check that, by default, an admin user is allowed to publish a workspace.
|
||||
$workspace->publish();
|
||||
|
||||
// Simulate an external factor which decides that a workspace can not be
|
||||
// published.
|
||||
\Drupal::state()->set('workspace_access_test.result.publish', AccessResult::forbidden());
|
||||
\Drupal::entityTypeManager()->getAccessControlHandler('workspace')->resetCache();
|
||||
|
||||
$this->expectException(WorkspaceAccessException::class);
|
||||
$workspace->publish();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -227,8 +227,9 @@ class WorkspaceCRUDTest extends KernelTestBase {
|
|||
$admin = $this->createUser([
|
||||
'administer nodes',
|
||||
'create workspace',
|
||||
'view any workspace',
|
||||
'delete any workspace',
|
||||
'view own workspace',
|
||||
'edit own workspace',
|
||||
'delete own workspace',
|
||||
]);
|
||||
$this->setCurrentUser($admin);
|
||||
|
||||
|
|
|
@ -38,9 +38,9 @@ trait WorkspaceTestTrait {
|
|||
$this->installSchema('workspaces', ['workspace_association']);
|
||||
|
||||
// Create two workspaces by default, 'live' and 'stage'.
|
||||
$this->workspaces['live'] = Workspace::create(['id' => 'live']);
|
||||
$this->workspaces['live'] = Workspace::create(['id' => 'live', 'label' => 'Live']);
|
||||
$this->workspaces['live']->save();
|
||||
$this->workspaces['stage'] = Workspace::create(['id' => 'stage']);
|
||||
$this->workspaces['stage'] = Workspace::create(['id' => 'stage', 'label' => 'Stage']);
|
||||
$this->workspaces['stage']->save();
|
||||
|
||||
$permissions = array_intersect([
|
||||
|
|
|
@ -15,12 +15,6 @@ use Drupal\workspaces\Entity\Workspace;
|
|||
function workspaces_requirements($phase) {
|
||||
$requirements = [];
|
||||
if ($phase === 'install') {
|
||||
if (\Drupal::moduleHandler()->moduleExists('content_moderation')) {
|
||||
$requirements['content_moderation_incompatibility'] = [
|
||||
'severity' => REQUIREMENT_ERROR,
|
||||
'description' => t('Workspaces can not be installed when Content Moderation is also installed.'),
|
||||
];
|
||||
}
|
||||
if (\Drupal::moduleHandler()->moduleExists('workspace')) {
|
||||
$requirements['workspace_incompatibility'] = [
|
||||
'severity' => REQUIREMENT_ERROR,
|
||||
|
|
|
@ -44,6 +44,15 @@ function workspaces_entity_type_build(array &$entity_types) {
|
|||
->entityTypeBuild($entity_types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_type_alter().
|
||||
*/
|
||||
function workspaces_entity_type_alter(array &$entity_types) {
|
||||
\Drupal::service('class_resolver')
|
||||
->getInstanceFromDefinition(EntityTypeInfo::class)
|
||||
->entityTypeAlter($entity_types);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_alter().
|
||||
*/
|
||||
|
|
|
@ -49,6 +49,12 @@ class DeleteActionTest extends KernelTestBase {
|
|||
'action_label' => 'Delete',
|
||||
'confirm_form_route_name' => 'entity.entity_test_mulrevpub.delete_multiple_form',
|
||||
],
|
||||
'entity_test_revpub' => [
|
||||
'type' => 'entity_test_revpub',
|
||||
'label' => 'Delete test entity - revisions and publishing status',
|
||||
'action_label' => 'Delete',
|
||||
'confirm_form_route_name' => 'entity.entity_test_revpub.delete_multiple_form',
|
||||
],
|
||||
'entity_test_rev' => [
|
||||
'type' => 'entity_test_rev',
|
||||
'label' => 'Delete test entity - revisions',
|
||||
|
|
Loading…
Reference in New Issue