Issue #2830740 by timmillwood, amateescu, Sam152, alexpott, martin107, plach, catch: Allow workflow types to lock certain changes to workflows once things are in use
parent
e6cd4b0dc2
commit
832d7695ac
|
@ -19,4 +19,9 @@ services:
|
|||
class: Drupal\content_moderation\RevisionTracker
|
||||
arguments: ['@database']
|
||||
tags:
|
||||
- { name: backend_overridable }
|
||||
- { name: backend_overridable }
|
||||
content_moderation.config_import_subscriber:
|
||||
class: Drupal\content_moderation\EventSubscriber\ConfigImportSubscriber
|
||||
arguments: ['@config.manager', '@entity_type.manager']
|
||||
tags:
|
||||
- { name: event_subscriber }
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\content_moderation\EventSubscriber;
|
||||
|
||||
use Drupal\Core\Config\ConfigImporterEvent;
|
||||
use Drupal\Core\Config\ConfigImportValidateEventSubscriberBase;
|
||||
use Drupal\Core\Config\ConfigManagerInterface;
|
||||
use Drupal\Core\Config\Entity\ConfigEntityStorage;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
|
||||
/**
|
||||
* Check moderation states are not being used before updating workflow config.
|
||||
*/
|
||||
class ConfigImportSubscriber extends ConfigImportValidateEventSubscriberBase {
|
||||
|
||||
/**
|
||||
* The config manager.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigManagerInterface
|
||||
*/
|
||||
protected $configManager;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Constructs the event subscriber.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigManagerInterface $config_manager
|
||||
* The config manager
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
*/
|
||||
public function __construct(ConfigManagerInterface $config_manager, EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->configManager = $config_manager;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function onConfigImporterValidate(ConfigImporterEvent $event) {
|
||||
foreach (['update', 'delete'] as $op) {
|
||||
$unprocessed_configurations = $event->getConfigImporter()->getUnprocessedConfiguration($op);
|
||||
foreach ($unprocessed_configurations as $unprocessed_configuration) {
|
||||
if ($workflow = $this->getWorkflow($unprocessed_configuration)) {
|
||||
if ($op === 'update') {
|
||||
$original_workflow_config = $event->getConfigImporter()
|
||||
->getStorageComparer()
|
||||
->getSourceStorage()
|
||||
->read($unprocessed_configuration);
|
||||
$workflow_config = $event->getConfigImporter()
|
||||
->getStorageComparer()
|
||||
->getTargetStorage()
|
||||
->read($unprocessed_configuration);
|
||||
$diff = array_diff_key($workflow_config['type_settings']['states'], $original_workflow_config['type_settings']['states']);
|
||||
foreach (array_keys($diff) as $state_id) {
|
||||
$state = $workflow->getState($state_id);
|
||||
if ($workflow->getTypePlugin()->workflowStateHasData($workflow, $state)) {
|
||||
$event->getConfigImporter()->logError($this->t('The moderation state @state_label is being used, but is not in the source storage.', ['@state_label' => $state->label()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($op === 'delete') {
|
||||
if ($workflow->getTypePlugin()->workflowHasData($workflow)) {
|
||||
$event->getConfigImporter()->logError($this->t('The workflow @workflow_label is being used, and cannot be deleted.', ['@workflow_label' => $workflow->label()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the workflow entity object from the configuration name.
|
||||
*
|
||||
* @param string $config_name
|
||||
* The configuration object name.
|
||||
*
|
||||
* @return \Drupal\Core\Entity\EntityInterface|null
|
||||
* An entity object. NULL if no matching entity is found.
|
||||
*/
|
||||
protected function getWorkflow($config_name) {
|
||||
$entity_type_id = $this->configManager->getEntityTypeIdByName($config_name);
|
||||
/** @var \Drupal\Core\Config\Entity\ConfigEntityTypeInterface $entity_type */
|
||||
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
|
||||
$entity_id = ConfigEntityStorage::getIDFromConfigName($config_name, $entity_type->getConfigPrefix());
|
||||
/** @var \Drupal\workflows\WorkflowInterface $workflow */
|
||||
return $this->entityTypeManager->getStorage($entity_type_id)->load($entity_id);
|
||||
}
|
||||
|
||||
}
|
|
@ -114,6 +114,35 @@ class ContentModeration extends WorkflowTypeFormBase implements ContainerFactory
|
|||
return $state;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function workflowHasData(WorkflowInterface $workflow) {
|
||||
return (bool) $this->entityTypeManager
|
||||
->getStorage('content_moderation_state')
|
||||
->getQuery()
|
||||
->condition('workflow', $workflow->id())
|
||||
->count()
|
||||
->accessCheck(FALSE)
|
||||
->range(0, 1)
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function workflowStateHasData(WorkflowInterface $workflow, StateInterface $state) {
|
||||
return (bool) $this->entityTypeManager
|
||||
->getStorage('content_moderation_state')
|
||||
->getQuery()
|
||||
->condition('workflow', $workflow->id())
|
||||
->condition('moderation_state', $state->id())
|
||||
->count()
|
||||
->accessCheck(FALSE)
|
||||
->range(0, 1)
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
|
|
@ -451,4 +451,53 @@ class ModerationFormTest extends ModerationStateTestBase {
|
|||
$this->drupalPostForm(NULL, [], t('Save and Create New Draft (this translation)'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that workflows and states can not be deleted if they are in use.
|
||||
*
|
||||
* @covers \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration::workflowHasData
|
||||
* @covers \Drupal\content_moderation\Plugin\WorkflowType\ContentModeration::workflowStateHasData
|
||||
*/
|
||||
public function testWorkflowInUse() {
|
||||
$user = $this->createUser([
|
||||
'administer workflows',
|
||||
'create moderated_content content',
|
||||
'edit own moderated_content content',
|
||||
'use editorial transition create_new_draft',
|
||||
'use editorial transition publish',
|
||||
'use editorial transition archive'
|
||||
]);
|
||||
$this->drupalLogin($user);
|
||||
$paths = [
|
||||
'archived_state' => 'admin/config/workflow/workflows/manage/editorial/state/archived/delete',
|
||||
'editorial_workflow' => 'admin/config/workflow/workflows/manage/editorial/delete',
|
||||
];
|
||||
foreach ($paths as $path) {
|
||||
$this->drupalGet($path);
|
||||
$this->assertSession()->buttonExists('Delete');
|
||||
}
|
||||
// Create new moderated content in draft.
|
||||
$this->drupalPostForm('node/add/moderated_content', [
|
||||
'title[0][value]' => 'Some moderated content',
|
||||
'body[0][value]' => 'First version of the content.',
|
||||
], 'Save and Create New Draft');
|
||||
|
||||
// The archived state is not used yet, so can still be deleted.
|
||||
$this->drupalGet($paths['archived_state']);
|
||||
$this->assertSession()->buttonExists('Delete');
|
||||
|
||||
// The workflow is being used, so can't be deleted.
|
||||
$this->drupalGet($paths['editorial_workflow']);
|
||||
$this->assertSession()->buttonNotExists('Delete');
|
||||
|
||||
$node = $this->drupalGetNodeByTitle('Some moderated content');
|
||||
$this->drupalPostForm('node/' . $node->id() . '/edit', [], 'Save and Publish');
|
||||
$this->drupalPostForm('node/' . $node->id() . '/edit', [], 'Save and Archive');
|
||||
|
||||
// Now the archived state is being used so it can not be deleted either.
|
||||
foreach ($paths as $path) {
|
||||
$this->drupalGet($path);
|
||||
$this->assertSession()->buttonNotExists('Delete');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\content_moderation\Kernel;
|
||||
|
||||
use Drupal\Core\Config\ConfigImporterException;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\workflows\Entity\Workflow;
|
||||
|
||||
/**
|
||||
* Tests how Content Moderation handles workflow config changes.
|
||||
*
|
||||
* @group content_moderation
|
||||
*/
|
||||
class ContentModerationWorkflowConfigTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = [
|
||||
'node',
|
||||
'content_moderation',
|
||||
'user',
|
||||
'system',
|
||||
'text',
|
||||
'workflows',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Entity\EntityTypeManager
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $configFactory;
|
||||
|
||||
/**
|
||||
* @var \Drupal\workflows\Entity\Workflow
|
||||
*/
|
||||
protected $workflow;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Core\Config\Entity\ConfigEntityStorage
|
||||
*/
|
||||
protected $workflowStorage;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installSchema('node', 'node_access');
|
||||
$this->installEntitySchema('node');
|
||||
$this->installEntitySchema('user');
|
||||
$this->installEntitySchema('content_moderation_state');
|
||||
$this->installConfig('content_moderation');
|
||||
|
||||
NodeType::create([
|
||||
'type' => 'example',
|
||||
])->save();
|
||||
|
||||
$workflow = Workflow::load('editorial');
|
||||
$workflow->getTypePlugin()
|
||||
->addState('test1', 'Test one')
|
||||
->addState('test2', 'Test two')
|
||||
->addState('test3', 'Test three')
|
||||
->addEntityTypeAndBundle('node', 'example');
|
||||
$workflow->save();
|
||||
$this->workflow = $workflow;
|
||||
|
||||
$this->copyConfig($this->container->get('config.storage'), $this->container->get('config.storage.sync'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test deleting a state via config import.
|
||||
*/
|
||||
public function testDeletingStateViaConfiguration() {
|
||||
$config_data = $this->config('workflows.workflow.editorial')->get();
|
||||
unset($config_data['type_settings']['states']['test1']);
|
||||
\Drupal::service('config.storage.sync')->write('workflows.workflow.editorial', $config_data);
|
||||
|
||||
// There are no Nodes with the moderation state test1, so this should run
|
||||
// with no errors.
|
||||
$this->configImporter()->reset()->import();
|
||||
|
||||
$node = Node::create([
|
||||
'type' => 'example',
|
||||
'title' => 'Test title',
|
||||
'moderation_state' => 'test2',
|
||||
]);
|
||||
$node->save();
|
||||
|
||||
$config_data = $this->config('workflows.workflow.editorial')->get();
|
||||
unset($config_data['type_settings']['states']['test2']);
|
||||
unset($config_data['type_settings']['states']['test3']);
|
||||
\Drupal::service('config.storage.sync')->write('workflows.workflow.editorial', $config_data);
|
||||
|
||||
// Now there is a Node with the moderation state test2, this will fail.
|
||||
try {
|
||||
$this->configImporter()->reset()->import();
|
||||
$this->fail('ConfigImporterException not thrown, invalid import was not stopped due to deleted state.');
|
||||
}
|
||||
catch (ConfigImporterException $e) {
|
||||
$this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.');
|
||||
$error_log = $this->configImporter->getErrors();
|
||||
$expected = ['The moderation state Test two is being used, but is not in the source storage.'];
|
||||
$this->assertEqual($expected, $error_log);
|
||||
}
|
||||
|
||||
\Drupal::service('config.storage.sync')->delete('workflows.workflow.editorial');
|
||||
|
||||
// An error should be thrown when trying to delete an in use workflow.
|
||||
try {
|
||||
$this->configImporter()->reset()->import();
|
||||
$this->fail('ConfigImporterException not thrown, invalid import was not stopped due to deleted workflow.');
|
||||
}
|
||||
catch (ConfigImporterException $e) {
|
||||
$this->assertEqual($e->getMessage(), 'There were errors validating the config synchronization.');
|
||||
$error_log = $this->configImporter->getErrors();
|
||||
$expected = [
|
||||
'The moderation state Test two is being used, but is not in the source storage.',
|
||||
'The workflow Editorial workflow is being used, and cannot be deleted.',
|
||||
];
|
||||
$this->assertEqual($expected, $error_log);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -11,6 +11,19 @@ use Drupal\Core\Url;
|
|||
*/
|
||||
class WorkflowDeleteForm extends EntityConfirmFormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
if ($this->entity->getTypePlugin()->workflowHasData($this->entity)) {
|
||||
$form['#title'] = $this->getQuestion();
|
||||
$form['description'] = ['#markup' => $this->t('This workflow is in use. You cannot remove this workflow until you have removed all content using it.')];
|
||||
return $form;
|
||||
}
|
||||
|
||||
return parent::buildForm($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
|
|
@ -75,6 +75,13 @@ class WorkflowStateDeleteForm extends ConfirmFormBase {
|
|||
}
|
||||
$this->workflow = $workflow;
|
||||
$this->stateId = $workflow_state;
|
||||
|
||||
if ($this->workflow->getTypePlugin()->workflowStateHasData($this->workflow, $this->workflow->getState($this->stateId))) {
|
||||
$form['#title'] = $this->getQuestion();
|
||||
$form['description'] = ['#markup' => $this->t('This workflow state is in use. You cannot remove this workflow state until you have removed all content using it.')];
|
||||
return $form;
|
||||
}
|
||||
|
||||
return parent::buildForm($form, $form_state);
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,20 @@ abstract class WorkflowTypeBase extends PluginBase implements WorkflowTypeInterf
|
|||
return AccessResult::neutral();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function workflowHasData(WorkflowInterface $workflow) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function workflowStateHasData(WorkflowInterface $workflow, StateInterface $state) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
|
|
@ -56,6 +56,38 @@ interface WorkflowTypeInterface extends PluginInspectionInterface, DerivativeIns
|
|||
*/
|
||||
public function checkWorkflowAccess(WorkflowInterface $entity, $operation, AccountInterface $account);
|
||||
|
||||
/**
|
||||
* Determines if the workflow is being has data associated with it.
|
||||
*
|
||||
* @internal
|
||||
* Marked as internal until it's validated this should form part of the
|
||||
* public API in https://www.drupal.org/node/2897148.
|
||||
*
|
||||
* @param \Drupal\workflows\WorkflowInterface $workflow
|
||||
* The workflow to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the workflow is being used, FALSE if not.
|
||||
*/
|
||||
public function workflowHasData(WorkflowInterface $workflow);
|
||||
|
||||
/**
|
||||
* Determines if the workflow state has data associated with it.
|
||||
*
|
||||
* @internal
|
||||
* Marked as internal until it's validated this should form part of the
|
||||
* public API in https://www.drupal.org/node/2897148.
|
||||
*
|
||||
* @param \Drupal\workflows\WorkflowInterface $workflow
|
||||
* The workflow to check.
|
||||
* @param \Drupal\workflows\StateInterface $state
|
||||
* The workflow state to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the workflow state is being used, FALSE if not.
|
||||
*/
|
||||
public function workflowStateHasData(WorkflowInterface $workflow, StateInterface $state);
|
||||
|
||||
/**
|
||||
* Decorates states so the WorkflowType can add additional information.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue