Skip to content
Snippets Groups Projects
Verified Commit da97aa7e authored by Alex Pott's avatar Alex Pott
Browse files

Issue #3300639 by amateescu, acrazyanimal, smustgrave: Improve and add...

Issue #3300639 by amateescu, acrazyanimal, smustgrave: Improve and add explicit test coverage for the workspace conflict validator

(cherry picked from commit d9a1c6e8)
parent 1cc7dc97
No related branches found
No related tags found
22 merge requests!10602Issue #3438769 by vinmayiswamy, antonnavi, michelle, amateescu: Sub workspace does not clear,!10301Issue #3469309 by mstrelan, smustgrave, moshe weitzman: Use one-time login...,!10187Issue #3487488 by dakwamine: ExtensionMimeTypeGuesser::guessMimeType must support file names with "0" (zero) like foo.0.zip,!9944Issue #3483353: Consider making the createCopy config action optionally fail...,!9929Issue #3445469 by pooja_sharma, smustgrave: Add additional test coverage for...,!9787Resolve issue 3479427 - bootstrap barrio issue under Windows,!9742Issue #3463908 by catch, quietone: Split OptionsFieldUiTest into two,!9526Issue #3458177 by mondrake, catch, quietone, godotislate, longwave, larowlan,...,!8738Issue #3424162 by camilledavis, dineshkumarbollu, smustgrave: Claro...,!8704Make greek characters available in ckeditor5,!8597Draft: Issue #3442259 by catch, quietone, dww: Reduce time of Migrate Upgrade tests...,!8533Issue #3446962 by kim.pepper: Remove incorrectly added...,!8517Issue #3443748 by NexusNovaz, smustgrave: Testcase creates false positive,!8325Update file Sort.php,!8095Expose document root on install,!7930Resolve #3427374 "Taxonomytid viewsargumentdefault plugin",!7627Issue #3439440 by nicxvan, Binoli Lalani, longwave: Remove country support from DateFormatter,!7445Issue #3440169: When using drupalGet(), provide an associative array for $headers,!7384Add constraints to system.advisories,!6502Draft: Resolve #2938524 "Plach testing issue",!38582585169-10.1.x,!3226Issue #2987537: Custom menu link entity type should not declare "bundle" entity key
Pipeline #135535 passed with warnings
......@@ -21,6 +21,6 @@ class EntityWorkspaceConflictConstraint extends SymfonyConstraint {
*
* @var string
*/
public $message = 'The content is being edited in the %label workspace. As a result, your changes cannot be saved.';
public $message = 'The content is being edited in the @label workspace. As a result, your changes cannot be saved.';
}
......@@ -6,62 +6,22 @@
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\workspaces\WorkspaceAssociationInterface;
use Drupal\workspaces\WorkspaceManagerInterface;
use Drupal\workspaces\WorkspaceRepositoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the EntityWorkspaceConflict constraint.
*
* @internal
*/
class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The workspace manager service.
*
* @var \Drupal\workspaces\WorkspaceManagerInterface
*/
protected $workspaceManager;
/**
* The workspace association service.
*
* @var \Drupal\workspaces\WorkspaceAssociationInterface
*/
protected $workspaceAssociation;
/**
* The workspace repository service.
*
* @var \Drupal\workspaces\WorkspaceRepositoryInterface
*/
protected $workspaceRepository;
/**
* Constructs an EntityUntranslatableFieldsConstraintValidator object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\workspaces\WorkspaceManagerInterface $workspace_manager
* The workspace manager service.
* @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association
* The workspace association service.
* @param \Drupal\workspaces\WorkspaceRepositoryInterface $workspace_repository
* The Workspace repository service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association, WorkspaceRepositoryInterface $workspace_repository) {
$this->entityTypeManager = $entity_type_manager;
$this->workspaceManager = $workspace_manager;
$this->workspaceAssociation = $workspace_association;
$this->workspaceRepository = $workspace_repository;
}
public function __construct(
protected readonly EntityTypeManagerInterface $entityTypeManager,
protected readonly WorkspaceManagerInterface $workspaceManager,
protected readonly WorkspaceAssociationInterface $workspaceAssociation,
) {}
/**
* {@inheritdoc}
......@@ -71,7 +31,6 @@ public static function create(ContainerInterface $container) {
$container->get('entity_type.manager'),
$container->get('workspaces.manager'),
$container->get('workspaces.association'),
$container->get('workspaces.repository')
);
}
......@@ -83,22 +42,16 @@ public function validate($entity, Constraint $constraint) {
if (isset($entity) && !$entity->isNew()) {
$active_workspace = $this->workspaceManager->getActiveWorkspace();
// Get the latest revision of the entity in order to check if it's being
// edited in a different workspace.
$latest_revision = $this->workspaceManager->executeOutsideWorkspace(function () use ($entity) {
/** @var \Drupal\Core\Entity\RevisionableStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
return $storage->loadRevision($storage->getLatestRevisionId($entity->id()));
});
// If the latest revision of the entity is tracked in a workspace, it can
// only be edited in that workspace or one of its descendants.
if ($latest_revision_workspace = $latest_revision->workspace->entity) {
$descendants_and_self = $this->workspaceRepository->getDescendantsAndSelf($latest_revision_workspace->id());
// If the entity is tracked in a workspace, it can only be edited in
// that workspace or one of its descendants.
if ($tracking_workspace_ids = $this->workspaceAssociation->getEntityTrackingWorkspaceIds($entity, TRUE)) {
if (!$active_workspace || !in_array($active_workspace->id(), $tracking_workspace_ids, TRUE)) {
$first_tracking_workspace_id = reset($tracking_workspace_ids);
$workspace = $this->entityTypeManager->getStorage('workspace')
->load($first_tracking_workspace_id);
if (!$active_workspace || !in_array($active_workspace->id(), $descendants_and_self, TRUE)) {
$this->context->buildViolation($constraint->message)
->setParameter('%label', $latest_revision_workspace->label())
->setParameter('@label', $workspace->label())
->addViolation();
}
}
......
......@@ -304,13 +304,33 @@ public function getAssociatedInitialRevisions(string $workspace_id, string $enti
/**
* {@inheritdoc}
*/
public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity) {
$query = $this->database->select(static::TABLE)
->fields(static::TABLE, ['workspace'])
->condition('target_entity_type_id', $entity->getEntityTypeId())
->condition('target_entity_id', $entity->id());
public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity, bool $latest_revision = FALSE) {
$query = $this->database->select(static::TABLE, 'wa')
->fields('wa', ['workspace'])
->condition('[wa].[target_entity_type_id]', $entity->getEntityTypeId())
->condition('[wa].[target_entity_id]', $entity->id());
// Use a self-join to get only the workspaces in which the latest revision
// of the entity is tracked.
if ($latest_revision) {
$inner_select = $this->database->select(static::TABLE, 'wai')
->condition('[wai].[target_entity_type_id]', $entity->getEntityTypeId())
->condition('[wai].[target_entity_id]', $entity->id());
$inner_select->addExpression('MAX([wai].[target_entity_revision_id])', 'max_revision_id');
$query->join($inner_select, 'waj', '[wa].[target_entity_revision_id] = [waj].[max_revision_id]');
}
$result = $query->execute()->fetchCol();
// Return early if the entity is not tracked in any workspace.
if (empty($result)) {
return [];
}
return $query->execute()->fetchCol();
// Return workspace IDs sorted in tree order.
$tree = $this->workspaceRepository->loadTree();
return array_keys(array_intersect_key($tree, array_flip($result)));
}
/**
......
......@@ -96,12 +96,15 @@ public function getAssociatedInitialRevisions(string $workspace_id, string $enti
*
* @param \Drupal\Core\Entity\RevisionableInterface $entity
* An entity object.
* @param bool $latest_revision
* (optional) Whether to return only the workspaces in which the latest
* revision of the entity is tracked. Defaults to FALSE.
*
* @return string[]
* An array of workspace IDs where the given entity is tracked, or an empty
* array if it is not tracked anywhere.
*/
public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity);
public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity, bool $latest_revision = FALSE);
/**
* Triggers clean-up operations after publishing a workspace.
......
<?php
declare(strict_types=1);
namespace Drupal\Tests\workspaces\Kernel;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\entity_test\Entity\EntityTestMulRevPub;
use Drupal\KernelTests\KernelTestBase;
use Drupal\Tests\user\Traits\UserCreationTrait;
use Drupal\workspaces\Entity\Workspace;
/**
* @coversDefaultClass \Drupal\workspaces\Plugin\Validation\Constraint\EntityWorkspaceConflictConstraintValidator
* @group workspaces
*/
class EntityWorkspaceConflictConstraintValidatorTest extends KernelTestBase {
use UserCreationTrait;
use WorkspaceTestTrait;
/**
* {@inheritdoc}
*/
protected static $modules = [
'entity_test',
'path_alias',
'system',
'user',
'workspaces',
];
/**
* The entity type manager.
*/
protected EntityTypeManagerInterface $entityTypeManager;
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entityTypeManager = \Drupal::entityTypeManager();
$this->installSchema('workspaces', ['workspace_association']);
$this->installEntitySchema('entity_test_mulrevpub');
$this->installEntitySchema('workspace');
$this->installEntitySchema('user');
$this->createUser();
}
/**
* @covers ::validate
*/
public function testNewEntitiesAllowedInDefaultWorkspace(): void {
// Create two top-level workspaces and a second-level one.
$stage = Workspace::create(['id' => 'stage', 'label' => 'Stage']);
$stage->save();
$dev = Workspace::create(['id' => 'dev', 'label' => 'Dev', 'parent' => 'stage']);
$dev->save();
$other = Workspace::create(['id' => 'other', 'label' => 'Other']);
$other->save();
// Create an entity in Live, and check that the validation is skipped.
$entity = EntityTestMulRevPub::create();
$this->assertCount(0, $entity->validate());
$entity->save();
$entity = $this->reloadEntity($entity);
$this->assertCount(0, $entity->validate());
// Edit the entity in Stage.
$this->switchToWorkspace('stage');
$entity->save();
$entity = $this->reloadEntity($entity);
$this->assertCount(0, $entity->validate());
$expected_message = 'The content is being edited in the Stage workspace. As a result, your changes cannot be saved.';
// Check that the entity can no longer be edited in Live.
$this->switchToLive();
$entity = $this->reloadEntity($entity);
$violations = $entity->validate();
$this->assertCount(1, $violations);
$this->assertSame($expected_message, (string) $violations->get(0)->getMessage());
// Check that the entity can no longer be edited in another top-level
// workspace.
$this->switchToWorkspace('other');
$entity = $this->reloadEntity($entity);
$violations = $entity->validate();
$this->assertCount(1, $violations);
$this->assertSame($expected_message, (string) $violations->get(0)->getMessage());
// Check that the entity can still be edited in a sub-workspace of Stage.
$this->switchToWorkspace('dev');
$entity = $this->reloadEntity($entity);
$this->assertCount(0, $entity->validate());
// Edit the entity in Dev.
$this->switchToWorkspace('dev');
$entity->save();
$entity = $this->reloadEntity($entity);
$this->assertCount(0, $entity->validate());
$expected_message = 'The content is being edited in the Dev workspace. As a result, your changes cannot be saved.';
// Check that the entity can no longer be edited in Live.
$this->switchToLive();
$entity = $this->reloadEntity($entity);
$violations = $entity->validate();
$this->assertCount(1, $violations);
$this->assertSame($expected_message, (string) $violations->get(0)->getMessage());
// Check that the entity can no longer be edited in the parent workspace.
$this->switchToWorkspace('stage');
$entity = $this->reloadEntity($entity);
$violations = $entity->validate();
$this->assertCount(1, $violations);
$this->assertSame($expected_message, (string) $violations->get(0)->getMessage());
// Check that the entity can no longer be edited in another top-level
// workspace.
$this->switchToWorkspace('other');
$entity = $this->reloadEntity($entity);
$violations = $entity->validate();
$this->assertCount(1, $violations);
$this->assertSame($expected_message, (string) $violations->get(0)->getMessage());
}
/**
* Reloads the given entity from the storage and returns it.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to be reloaded.
*
* @return \Drupal\Core\Entity\EntityInterface
* The reloaded entity.
*/
protected function reloadEntity(EntityInterface $entity): EntityInterface {
$storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
$storage->resetCache([$entity->id()]);
return $storage->load($entity->id());
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment