Skip to content

Commit

Permalink
NEW Content migration task has a bunch of new features...:
Browse files Browse the repository at this point in the history
- Fixes #617 - The task now checks for pages with no content instead of no elemental area
- Developers can configure a specific element type and attribute to migrate content too
- Content is now published during the migration
- Added a flag to control whether content is published
- Whether this task clears content from the page during migration is configurable. If it is not cleared out, content will not migrate if the page already has elements.
- Added some extension points and extracted some logic into methods to allow flexible subclassing / extensions
  • Loading branch information
ScopeyNZ committed Apr 29, 2019
1 parent a4d9ead commit b5dd71b
Show file tree
Hide file tree
Showing 5 changed files with 299 additions and 14 deletions.
4 changes: 4 additions & 0 deletions src/Extensions/ElementalPageExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
use SilverStripe\Core\Config\Config;
use SilverStripe\View\SSViewer;

/**
* @method ElementalArea ElementalArea
* @property ElementalArea ElementalArea
*/
class ElementalPageExtension extends ElementalAreasExtension
{
private static $has_one = [
Expand Down
3 changes: 2 additions & 1 deletion src/Models/ElementalArea.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\FieldType\DBField;
use SilverStripe\ORM\FieldType\DBHTMLText;
use SilverStripe\ORM\HasManyList;
use SilverStripe\ORM\UnsavedRelationList;
use SilverStripe\Versioned\Versioned;

Expand Down Expand Up @@ -117,7 +118,7 @@ public function setOwnerPageCached(DataObject $page)

/**
* A cache-aware accessor for the elements
* @return ArrayList|DataList|BaseElement[]
* @return ArrayList|HasManyList|BaseElement[]
*/
public function Elements()
{
Expand Down
149 changes: 136 additions & 13 deletions src/Tasks/MigrateContentToElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,50 @@

namespace DNADesign\Elemental\Tasks;

use DNADesign\Elemental\Extensions\ElementalAreasExtension;
use DNADesign\Elemental\Extensions\ElementalPageExtension;
use DNADesign\Elemental\Models\BaseElement;
use DNADesign\Elemental\Models\ElementalArea;
use DNADesign\Elemental\Models\ElementContent;

use SilverStripe\CMS\Model\SiteTree;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\Dev\BuildTask;
use SilverStripe\Versioned\Versioned;

class MigrateContentToElement extends BuildTask
{
/**
* Configures if the existing content should be left blank.
*
* @config
* @var bool
*/
private static $clear_content = true;

/**
* The FQN of an element that will be the target of the content
*
* @config
* @var string
*/
private static $target_element = ElementContent::class;

/**
* The name of the field on the `target_element` where the content should be placed
*
* @config
* @var string
*/
private static $target_element_field = 'HTML';

/**
* Indicates that the updated page and elements should be immediately published (provided the Versioned extension
* is present)
*
* @config
* @var bool
*/
private static $publish_changes = true;

protected $title = 'MigrateContentToElement';

Expand All @@ -17,26 +54,112 @@ class MigrateContentToElement extends BuildTask

public function run($request)
{
// TODO: needs rewriting for multiple elemental areas
$pageTypes = singleton(ElementalArea::class)->supportedPageTypes();
$count = 0;
foreach ($pageTypes as $pageType) {
$pages = $pageType::get()->filter('ElementalAreaID', 0);
// Only pages that have the ElementalPageExtension have a known ElementalArea relation
if (!$this->isMigratable($pageType)) {
continue;
}

$pages = $pageType::get()->filter('Content:not', ['', null]);;
$clearContent = $this->config()->get('clear_content');

$this->extend('updatePageFilter', $pages, $pageType);
$pageTypeCount = 0;

/** @var SiteTree&ElementalAreasExtension $page */
foreach ($pages as $page) {
if ($this->shouldSkipMigration($page)) {
continue;
}
// Fetch and clear existing content (if configured)
$content = $page->Content;
$page->Content = '';
// trigger area relations to be setup
$page->write();
$area = $page->ElementalArea();
$element = new ElementContent();
if ($clearContent) {
$page->Content = '';
}

// Get the area
$area = $this->getAreaRelationFromPage($page);

// Write the page if we're clearing content or if the area doesn't exist - we write to trigger a
// relationship update
if ($clearContent || !$area->exists()) {
$page->write();

if (!$area->exists()) {
$area = $this->getAreaRelationFromPage($page);
}
}

// Create a new element
/** @var BaseElement $element */
$element = Injector::inst()->create($this->config()->get('target_element'));
$element->Title = 'Auto migrated content';
$element->HTML = $content;
$element->ParentID = $area->ID;
$element->write();

// Set the configured field
$element->setField($this->config()->get('target_element_field'), $content);

// Provide an extension hook for further updates to the new element
$this->extend('updateMigratedElement', $element);

// Add and write to the area
$area->Elements()->add($element);

// Publish the record if configured
if ($this->config()->get('publish_changes')) {
$page->publishRecursive();
}

$pageTypeCount++;
}
$count += $pages->Count();
echo 'Migrated ' . $pages->Count() . ' ' . $pageType . ' pages\' content<br>';
$count += $pageTypeCount;
echo 'Migrated ' . $pageTypeCount . ' ' . $pageType . ' pages\' content<br>';
}
echo 'Finished migrating ' . $count . ' pages\' content<br>';
}

/**
* Indicates if the given page type is migratable
*
* @param string|SiteTree $pageType
* @return bool
*/
protected function isMigratable($pageType)
{
$migratable = SiteTree::has_extension($pageType, ElementalPageExtension::class);

$this->extend('updateIsMigratable', $migratable, $pageType);

return $migratable;
}

/**
* Extracts the relevant ElementalArea from the given page. This can be overloaded for custom page types that might
* prefer an alternate area to hold the migrated content
*
* @param SiteTree&ElementalPageExtension $page
* @return ElementalArea
*/
protected function getAreaRelationFromPage(SiteTree $page)
{
return $page->ElementalArea;
}

/**
* Assert that the given page should actually have content migrated. By default this asserts that no elements
* currently exist IFF the "clear_content" config is on
*
* @param SiteTree $page
* @return bool
*/
protected function shouldSkipMigration(SiteTree $page)
{
$skip = !$this->config()->get('clear_content')
&& $this->getAreaRelationFromPage($page)->Elements()->count() > 0;

$this->extend('updatePageShouldSkip', $skip, $page);

return $skip;
}
}
140 changes: 140 additions & 0 deletions tests/Tasks/MigrateContentToElementTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php
namespace DNADesign\Elemental\Tests\Tasks;

use DNADesign\Elemental\Extensions\ElementalPageExtension;
use DNADesign\Elemental\Models\ElementContent;
use DNADesign\Elemental\Tasks\MigrateContentToElement;
use DNADesign\Elemental\Tests\Src\TestElement;
use DNADesign\Elemental\Tests\Src\TestPage;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Config\Config;
use SilverStripe\Dev\SapphireTest;
use SilverStripe\ORM\HasManyList;
use SilverStripe\Versioned\Versioned;

class MigrateContentToElementTest extends SapphireTest
{
protected static $fixture_file = 'MigrateContentToElementTest.yml';

protected static $required_extensions = [
TestPage::class => [
ElementalPageExtension::class,
],
];

protected static $extra_dataobjects = [
TestElement::class,
TestPage::class,
];

public function testContentIsMigratedFromPagesToNewElements()
{
$task = new MigrateContentToElement();

ob_start();
$task->run(new HTTPRequest('GET', ''));
$output = ob_get_clean();

$this->assertContains('Finished migrating 1 pages\' content', $output);

// Get the page that should've been updated and the content should be removed
/** @var TestPage&Versioned $page */
$page = $this->objFromFixture(TestPage::class, 'page3');
$this->assertEmpty($page->Content);

// Check that there's one element and it contains the old content
/** @var HasManyList $elements */
$elements = $page->ElementalArea->Elements();
$this->assertCount(1, $elements);

/** @var ElementContent&Versioned $contentElement */
$contentElement = $elements->first();
$this->assertSame('This is page 3', $contentElement->HTML);

// Assert that the element and page are "live"
$this->assertTrue($contentElement->isLiveVersion());
$this->assertTrue($page->isLiveVersion());
}

public function testContentIsNotClearedWhenConfigured()
{
Config::modify()->set(MigrateContentToElement::class, 'clear_content', false);

$task = new MigrateContentToElement();

ob_start();
$task->run(new HTTPRequest('GET', ''));
$output = ob_get_clean();

$this->assertContains('Finished migrating 1 pages\' content', $output);

$page = $this->objFromFixture(TestPage::class, 'page3');
$this->assertContains('This is page 3', $page->Content, 'Content is not removed from the page');

$element = $page->ElementalArea->Elements()->first();
$this->assertContains('This is page 3', $element->HTML, 'Content is still added to a new element');

// Run the task again and assert the page is not picked up again
ob_start();
$task->run(new HTTPRequest('GET', ''));
$output = ob_get_clean();

$this->assertContains('Finished migrating 0 pages\' content', $output);
$page = $this->objFromFixture(TestPage::class, 'page3');
$this->assertCount(1, $page->ElementalArea->Elements());
}

public function testTargetElementConfigurationIsRespected()
{
Config::modify()->set(MigrateContentToElement::class, 'target_element', TestElement::class);
Config::modify()->set(MigrateContentToElement::class, 'target_element_field', 'TestValue');

$task = new MigrateContentToElement();

ob_start();
$task->run(new HTTPRequest('GET', ''));
$output = ob_get_clean();

$this->assertContains('Finished migrating 1 pages\' content', $output);

// Get the page that should've been updated and the content should be removed
$element = $this->objFromFixture(TestPage::class, 'page3')->ElementalArea->Elements()->first();

$this->assertInstanceOf(TestElement::class, $element);
$this->assertSame('This is page 3', $element->TestValue);
}

public function testPublishingConfigurationIsRespected()
{
Config::modify()->set(MigrateContentToElement::class, 'publish_changes', false);

// Ensure the page is published to begin with
/** @var TestPage&Versioned $page */
$page = $this->objFromFixture(TestPage::class, 'page3');
$page->publishSingle();

$task = new MigrateContentToElement();

ob_start();
$task->run(new HTTPRequest('GET', ''));
$output = ob_get_clean();

$this->assertContains('Finished migrating 1 pages\' content', $output);

// Get the page that should've been updated and the content should be removed
$page = $this->objFromFixture(TestPage::class, 'page3');
/** @var ElementContent&Versioned $element */
$element = $page->ElementalArea->Elements()->first();

$this->assertSame('This is page 3', $element->HTML);

$this->assertFalse($page->isLiveVersion());
$this->assertFalse($element->isLiveVersion());

$this->assertFalse($element->isPublished());
$this->assertEmpty($page->Content);

$livePage = Versioned::get_by_stage(TestPage::class, Versioned::LIVE)->byID($page->ID);
$this->assertSame('This is page 3', $livePage->Content);
}
}
17 changes: 17 additions & 0 deletions tests/Tasks/MigrateContentToElementTest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Page:
page1:
Title: Page 1
URLSegment: test-page-1
Content: "This is page 1"
page2:
Title: Page 2
URLSegment: test-page-2

DNADesign\Elemental\Tests\Src\TestPage:
page3:
Title: Page 3
URLSegment: test-page-3
Content: "This is page 3"
page4:
Title: Page 4
URLSegment: test-page-4

0 comments on commit b5dd71b

Please # to comment.