Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IBX-8418: Remove draft when trashing or deleting its parent or ancestor location #439

Merged
merged 3 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/contracts/Persistence/Content/Location/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ public function create(CreateStruct $location);
*/
public function removeSubtree($locationId);

/**
* Removes all draft contents that have no location assigned to them under the given parent location.
*/
public function deleteChildrenDrafts(int $locationId): void;

/**
* Set section on all content objects in the subtree.
* Only main locations will be updated.
Expand Down
6 changes: 6 additions & 0 deletions src/contracts/Test/IbexaKernelTestTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use Ibexa\Contracts\Core\Repository\RoleService;
use Ibexa\Contracts\Core\Repository\SearchService;
use Ibexa\Contracts\Core\Repository\SectionService;
use Ibexa\Contracts\Core\Repository\TrashService;
use Ibexa\Contracts\Core\Repository\URLAliasService;
use Ibexa\Contracts\Core\Repository\UserService;
use Ibexa\Contracts\Core\Test\Persistence\Fixture\FixtureImporter;
Expand Down Expand Up @@ -169,6 +170,11 @@ protected static function getUrlAliasService(): URLAliasService
return self::getServiceByClassName(URLAliasService::class);
}

protected static function getTrashService(): TrashService
{
return self::getServiceByClassName(TrashService::class);
}

protected static function setAnonymousUser(): void
{
$anonymousUserId = 10;
Expand Down
1 change: 1 addition & 0 deletions src/contracts/Test/IbexaTestKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ class IbexaTestKernel extends Kernel implements IbexaTestKernelInterface
Repository\TokenService::class,
Repository\URLAliasService::class,
Repository\BookmarkService::class,
Repository\TrashService::class,
Handler::class,
];

Expand Down
14 changes: 14 additions & 0 deletions src/lib/Persistence/Cache/LocationHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,20 @@ public function removeSubtree($locationId)
return $return;
}

public function deleteChildrenDrafts(int $locationId): void
{
$this->logger->logCall(__METHOD__, ['location' => $locationId]);

$this->persistenceHandler->locationHandler()->deleteChildrenDrafts($locationId);

$this->cache->invalidateTags([
$this->cacheIdentifierGenerator->generateTag(
self::LOCATION_PATH_IDENTIFIER,
[$locationId],
),
]);
}

/**
* {@inheritdoc}
*/
Expand Down
1 change: 1 addition & 0 deletions src/lib/Persistence/Legacy/Content/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,7 @@ public function deleteContent($contentId)
$this->removeRawContent($contentId);
} else {
foreach ($contentLocations as $locationId) {
$this->treeHandler->deleteChildrenDrafts($locationId);
$this->treeHandler->removeSubtree($locationId);
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/lib/Persistence/Legacy/Content/Location/Gateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ abstract public function loadParentLocationsDataForDraftContent(int $contentId):
*/
abstract public function getSubtreeContent(int $sourceId, bool $onlyIds = false): array;

/**
* Finds draft contents created under the given parent location.
*
* @return array<int>
*/
abstract public function getSubtreeChildrenDraftContentIds(int $sourceId): array;

abstract public function getSubtreeSize(string $path): int;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,29 @@ public function getSubtreeContent(int $sourceId, bool $onlyIds = false): array
: $results;
}

/**
* @return array<int>
*
* @throws \Doctrine\DBAL\Exception
* @throws \Doctrine\DBAL\Driver\Exception
*/
public function getSubtreeChildrenDraftContentIds(int $sourceId): array
{
$query = $this->connection->createQueryBuilder();
$query
->select('contentobject_id')
->from('eznode_assignment', 'n')
->innerJoin('n', 'ezcontentobject', 'c', 'n.contentobject_id = c.id')
->andWhere('n.parent_node = :parentNode')
->andWhere('c.status = :status')
->setParameter(':parentNode', $sourceId, ParameterType::INTEGER)
->setParameter(':status', ContentInfo::STATUS_DRAFT, ParameterType::INTEGER);

$statement = $query->execute();

return $statement->fetchFirstColumn();
}

public function getSubtreeSize(string $path): int
{
$query = $this->createNodeQueryBuilder([$this->dbPlatform->getCountExpression('node_id')]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,18 @@ public function getSubtreeContent(int $sourceId, bool $onlyIds = false): array
}
}

/**
* @return array<int>
*/
public function getSubtreeChildrenDraftContentIds(int $sourceId): array
{
try {
return $this->innerGateway->getSubtreeChildrenDraftContentIds($sourceId);
} catch (PDOException $e) {
throw DatabaseException::wrap($e);
}
}

public function getSubtreeSize(string $path): int
{
try {
Expand Down
5 changes: 5 additions & 0 deletions src/lib/Persistence/Legacy/Content/Location/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,11 @@ public function removeSubtree($locationId)
$this->treeHandler->removeSubtree($locationId);
}

public function deleteChildrenDrafts(int $locationId): void
{
$this->treeHandler->deleteChildrenDrafts($locationId);
}

/**
* Set section on all content objects in the subtree.
*
Expand Down
20 changes: 20 additions & 0 deletions src/lib/Persistence/Legacy/Content/TreeHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,26 @@ public function removeSubtree($locationId)
$this->locationGateway->deleteNodeAssignment($contentId);
}

/**
* Removes draft contents assigned to the given parent location and its descendant locations.
*
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException
*/
public function deleteChildrenDrafts(int $locationId): void
{
$subLocations = $this->locationGateway->getChildren($locationId);
foreach ($subLocations as $subLocation) {
$this->deleteChildrenDrafts($subLocation['node_id']);
}

// Fetch child draft content ids
$subtreeChildrenDraftIds = $this->locationGateway->getSubtreeChildrenDraftContentIds($locationId);

foreach ($subtreeChildrenDraftIds as $contentId) {
$this->removeRawContent($contentId);
}
}

/**
* Set section on all content objects in the subtree.
*
Expand Down
1 change: 1 addition & 0 deletions src/lib/Repository/TrashService.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ public function trash(Location $location): ?APITrashItem

$this->repository->beginTransaction();
try {
$this->persistenceHandler->locationHandler()->deleteChildrenDrafts($location->id);
$spiTrashItem = $this->persistenceHandler->trashHandler()->trashSubtree($location->id);
$this->persistenceHandler->urlAliasHandler()->locationDeleted($location->id);
$this->repository->commit();
Expand Down
111 changes: 111 additions & 0 deletions tests/integration/Core/Repository/ContentService/DeleteContentTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\Integration\Core\Repository\ContentService;

use Ibexa\Contracts\Core\Repository\Values\Content\Content;
use Ibexa\Tests\Integration\Core\RepositoryTestCase;
use PHPUnit\Framework\Assert;

/**
* @covers \Ibexa\Contracts\Core\Repository\ContentService
*/
final class DeleteContentTest extends RepositoryTestCase
{
/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\Exception
*/
public function testDeleteContentDeletesChildrenDrafts(): void
{
$contentService = self::getContentService();

[$folder, $draft1, $draft2, $draft3, $draftSecondDepth] = $this->prepareContentStructure();

$contentService->deleteContent($folder->getContentInfo());

$contentInfos = $contentService->loadContentInfoList([
$draft1->getId(),
$draft2->getId(),
$draft3->getId(),
$draftSecondDepth->getId(),
]);

self::assertEmpty($contentInfos);
}

/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\Exception
*/
public function testTrashLocationDeletesChildrenDrafts(): void
{
$trashService = self::getTrashService();
$contentService = self::getContentService();

[$folder, $draft1, $draft2, $draft3, $draftSecondDepth] = $this->prepareContentStructure();

$folderMainLocationId = $folder->getVersionInfo()->getContentInfo()->getMainLocationId();
Assert::assertIsNumeric($folderMainLocationId);

$locationToTrash = self::getLocationService()->loadLocation($folderMainLocationId);

$trashService->trash($locationToTrash);

$contentInfos = $contentService->loadContentInfoList([
$draft1->getId(),
$draft2->getId(),
$draft3->getId(),
$draftSecondDepth->getId(),
]);

self::assertEmpty($contentInfos);
}

/**
* @return array<Content>
*/
private function prepareContentStructure(): array
{
$folder = $this->createFolder(['eng-GB' => 'Folder'], 2);
$folderMainLocationId = $folder->getVersionInfo()->getContentInfo()->getMainLocationId();
Assert::assertIsNumeric($folderMainLocationId);

$childFolder = $this->createFolder(
['eng-GB' => 'Child folder'],
$folderMainLocationId,
);
$childFolderMainLocationId = $childFolder->getVersionInfo()->getContentInfo()->getMainLocationId();
Assert::assertIsNumeric($childFolderMainLocationId);

$secondDepthChildFolder = $this->createFolder(
['eng-GB' => 'Second depth folder'],
$childFolderMainLocationId,
);
$secondDepthChildFolderLocationId = $secondDepthChildFolder
->getVersionInfo()
->getContentInfo()
->getMainLocationId()
;
Assert::assertIsNumeric($secondDepthChildFolderLocationId);

$draft1 = $this->createFolderDraft(['eng-GB' => 'Folder draft 1'], $folderMainLocationId);
$draft2 = $this->createFolderDraft(['eng-GB' => 'Folder draft 2'], $childFolderMainLocationId);
$draft3 = $this->createFolderDraft(['eng-GB' => 'Folder draft 3'], $childFolderMainLocationId);
$draftSecondDepth = $this->createFolderDraft(
['eng-GB' => 'Folder draft 4'],
$secondDepthChildFolderLocationId,
);

return [
$folder,
$draft1,
$draft2,
$draft3,
$draftSecondDepth,
];
}
}
1 change: 1 addition & 0 deletions tests/lib/Persistence/Cache/LocationHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public function providerForUnCachedMethods(): array
['c-4', 'ragl-4'],
],
['removeSubtree', [12], [['location_path', [12], false]], null, ['lp-12']],
['deleteChildrenDrafts', [12], [['location_path', [12], false]], null, ['lp-12']],
['setSectionForSubtree', [12, 2], [['location_path', [12], false]], null, ['lp-12']],
['changeMainLocation', [4, 12], [['content', [4], false]], null, ['c-4']],
['countLocationsByContent', [4]],
Expand Down
12 changes: 12 additions & 0 deletions tests/lib/Persistence/Legacy/Content/LocationHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,18 @@ public function testRemoveSubtree()
$handler->removeSubtree(42);
}

public function testDeleteChildrenDrafts(): void
{
$handler = $this->getLocationHandler();

$this->treeHandler
->expects(self::once())
->method('deleteChildrenDrafts')
->with(42);

$handler->deleteChildrenDrafts(42);
}

/**
* Test for the copySubtree() method.
*/
Expand Down
55 changes: 55 additions & 0 deletions tests/lib/Persistence/Legacy/Content/TreeHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,61 @@ public function testLoadLocation()
$this->assertTrue($location instanceof Location);
}

public function testDeleteChildrenDraftsRecursive(): void
{
$locationGatewayMock = $this->getLocationGatewayMock();
$contentGatewayMock = $this->getContentGatewayMock();
$contentMapperMock = $this->getContentMapperMock();

$locationGatewayMock
->expects(self::exactly(3))
->method('getChildren')
->willReturnMap([
[42, [
['node_id' => 201],
['node_id' => 202],
]],
[201, []],
[202, []],
]);

$locationGatewayMock
->expects(self::exactly(3))
->method('getSubtreeChildrenDraftContentIds')
->willReturnMap([
[201, [101]],
[202, [102]],
[42, [99]],
]);

$contentGatewayMock
->expects(self::exactly(3))
->method('loadContentInfo')
->willReturnMap([
[101, ['main_node_id' => 201]],
[102, ['main_node_id' => 202]],
[99, ['main_node_id' => 42]],
]);

$contentMapperMock
->expects(self::exactly(3))
->method('extractContentInfoFromRow')
->willReturnCallback(static function (array $row): ContentInfo {
return new ContentInfo(['mainLocationId' => $row['main_node_id']]);
});

$contentGatewayMock
->expects(self::exactly(3))
->method('deleteContent')
->willReturnCallback(static function (int $contentId): void {
self::assertContains($contentId, [99, 101, 102]);
});

$treeHandler = $this->getTreeHandler();

$treeHandler->deleteChildrenDrafts(42);
}

/** @var \PHPUnit\Framework\MockObject\MockObject|\Ibexa\Core\Persistence\Legacy\Content\Location\Gateway */
protected $locationGatewayMock;

Expand Down
Loading