diff --git a/tasks/MigrateSiteTreeLinkingTask.php b/tasks/MigrateSiteTreeLinkingTask.php
new file mode 100755
index 00000000..88c76e66
--- /dev/null
+++ b/tasks/MigrateSiteTreeLinkingTask.php
@@ -0,0 +1,52 @@
+ID))->map();
+
+ foreach($tracking as $childID => $fieldName) {
+ $linked = DataObject::get_by_id('SiteTree', $childID);
+
+ // TOOD: Replace in all HTMLText fields
+ $page->Content = preg_replace (
+ "/href *= *([\"']?){$linked->URLSegment}\/?/i",
+ "href=$1[sitetree_link id={$linked->ID}]",
+ $page->Content,
+ -1,
+ $replaced
+ );
+
+ if($replaced) {
+ $links += $replaced;
+ }
+ }
+
+ $page->write();
+ $pages++;
+ }
+
+ echo "Rewrote $links link(s) on $pages page(s) to use shortcodes.\n";
+ }
+
+}
\ No newline at end of file
diff --git a/tasks/RemoveOrphanedPagesTask.php b/tasks/RemoveOrphanedPagesTask.php
new file mode 100644
index 00000000..30083d9f
--- /dev/null
+++ b/tasks/RemoveOrphanedPagesTask.php
@@ -0,0 +1,354 @@
+@silverstripe.com), SilverStripe Ltd.
+ *
+ * @package cms
+ * @subpackage tasks
+ */
+//class RemoveOrphanedPagesTask extends BuildTask {
+class RemoveOrphanedPagesTask extends Controller {
+
+ static $allowed_actions = array(
+ 'index' => 'ADMIN',
+ 'Form' => 'ADMIN',
+ 'run' => 'ADMIN',
+ 'handleAction' => 'ADMIN',
+ );
+
+ protected $title = 'Removed orphaned pages without existing parents from both stage and live';
+
+ protected $description = "
+
+Identify 'orphaned' pages which point to a parent
+that no longer exists in a specific stage.
+
+
+Caution: Pages also count as orphans if they don't
+have parents in this stage, even if the parent has a representation
+in the other stage:
+- A live child is orphaned if its parent was deleted from live, but still exists on stage
+- A stage child is orphaned if its parent was deleted from stage, but still exists on live
+
+ ";
+
+ protected $orphanedSearchClass = 'SiteTree';
+
+ function Link() {
+ return $this->class;
+ }
+
+ function init() {
+ parent::init();
+
+ if(!Permission::check('ADMIN')) {
+ return Security::permissionFailure($this);
+ }
+ }
+
+ function index() {
+ Requirements::javascript(SAPPHIRE_DIR . '/thirdparty/jquery/jquery.js');
+ Requirements::customCSS('#OrphanIDs .middleColumn {width: auto;}');
+ Requirements::customCSS('#OrphanIDs label {display: inline;}');
+
+ return $this->renderWith('BlankPage');
+ }
+
+ function Form() {
+ $fields = new FieldSet();
+ $source = array();
+
+ $fields->push(new HeaderField(
+ 'Header',
+ _t('RemoveOrphanedPagesTask.HEADER', 'Remove all orphaned pages task')
+ ));
+ $fields->push(new LiteralField(
+ 'Description',
+ $this->description
+ ));
+
+ $orphans = $this->getOrphanedPages($this->orphanedSearchClass);
+ if($orphans) foreach($orphans as $orphan) {
+ $latestVersion = Versioned::get_latest_version($this->orphanedSearchClass, $orphan->ID);
+ $latestAuthor = DataObject::get_by_id('Member', $latestVersion->AuthorID);
+ $stageRecord = Versioned::get_one_by_stage(
+ $this->orphanedSearchClass,
+ 'Stage',
+ sprintf("\"%s\".\"ID\" = %d",
+ ClassInfo::baseDataClass($this->orphanedSearchClass),
+ $orphan->ID
+ )
+ );
+ $liveRecord = Versioned::get_one_by_stage(
+ $this->orphanedSearchClass,
+ 'Live',
+ sprintf("\"%s\".\"ID\" = %d",
+ ClassInfo::baseDataClass($this->orphanedSearchClass),
+ $orphan->ID
+ )
+ );
+ $label = sprintf(
+ '%s (#%d, Last Modified Date: %s, Last Modifier: %s, %s)',
+ $orphan->ID,
+ $orphan->Title,
+ $orphan->ID,
+ DBField::create('Date', $orphan->LastEdited)->Nice(),
+ ($latestAuthor) ? $latestAuthor->Title : 'unknown',
+ ($liveRecord) ? 'is published' : 'not published'
+ );
+ $source[$orphan->ID] = $label;
+ }
+
+ if($orphans && $orphans->Count()) {
+ $fields->push(new CheckboxSetField('OrphanIDs', false, $source));
+ $fields->push(new LiteralField(
+ 'SelectAllLiteral',
+ sprintf(
+ '%s ',
+ _t('RemoveOrphanedPagesTask.SELECTALL', 'select all')
+ )
+ ));
+ $fields->push(new LiteralField(
+ 'UnselectAllLiteral',
+ sprintf(
+ '%s
',
+ _t('RemoveOrphanedPagesTask.UNSELECTALL', 'unselect all')
+ )
+ ));
+ $fields->push(new OptionSetField(
+ 'OrphanOperation',
+ _t('RemoveOrphanedPagesTask.CHOOSEOPERATION', 'Choose operation:'),
+ array(
+ 'rebase' => _t(
+ 'RemoveOrphanedPagesTask.OPERATION_REBASE',
+ sprintf(
+ 'Rebase selected to a new holder page "%s" and unpublish. None of these pages will show up for website visitors.',
+ $this->rebaseHolderTitle()
+ )
+ ),
+ 'remove' => _t('RemoveOrphanedPagesTask.OPERATION_REMOVE', 'Remove selected from all stages (WARNING: Will destroy all selected pages from both stage and live)'),
+ ),
+ 'rebase'
+ ));
+ $fields->push(new LiteralField(
+ 'Warning',
+ sprintf('%s
',
+ _t(
+ 'RemoveOrphanedPagesTask.DELETEWARNING',
+ 'Warning: These operations are not reversible. Please handle with care.'
+ )
+ )
+ ));
+ } else {
+ $fields->push(new LiteralField(
+ 'NotFoundLabel',
+ sprintf(
+ '%s
',
+ _t('RemoveOrphanedPagesTask.NONEFOUND', 'No orphans found')
+ )
+ ));
+ }
+
+ $form = new Form(
+ $this,
+ 'Form',
+ $fields,
+ new FieldSet(
+ new FormAction('doSubmit', _t('RemoveOrphanedPagesTask.BUTTONRUN', 'Run'))
+ )
+ );
+
+ if(!$orphans || !$orphans->Count()) {
+ $form->makeReadonly();
+ }
+
+ return $form;
+ }
+
+ function run($request) {
+ // @todo Merge with BuildTask functionality
+ }
+
+ function doSubmit($data, $form) {
+ set_time_limit(60*10); // 10 minutes
+
+ if(!isset($data['OrphanIDs']) || !isset($data['OrphanOperation'])) return false;
+
+ switch($data['OrphanOperation']) {
+ case 'remove':
+ $successIDs = $this->removeOrphans($data['OrphanIDs']);
+ break;
+ case 'rebase':
+ $successIDs = $this->rebaseOrphans($data['OrphanIDs']);
+ break;
+ default:
+ user_error(sprintf("Unknown operation: '%s'", $data['OrphanOperation']), E_USER_ERROR);
+ }
+
+ $content = '';
+ if($successIDs) {
+ $content .= "";
+ foreach($successIDs as $id => $label) {
+ $content .= sprintf('- %s
', $label);
+ }
+ $content .= "
";
+ } else {
+ $content = _t('RemoveOrphanedPagesTask.NONEREMOVED', 'None removed');
+ }
+
+ return $this->customise(array(
+ 'Content' => $content,
+ 'Form' => ' '
+ ))->renderWith('BlankPage');
+ }
+
+ protected function removeOrphans($orphanIDs) {
+ $removedOrphans = array();
+ foreach($orphanIDs as $id) {
+ $stageRecord = Versioned::get_one_by_stage(
+ $this->orphanedSearchClass,
+ 'Stage',
+ sprintf("\"%s\".\"ID\" = %d",
+ ClassInfo::baseDataClass($this->orphanedSearchClass),
+ $id
+ )
+ );
+ if($stageRecord) {
+ $removedOrphans[$stageRecord->ID] = sprintf('Removed %s (#%d) from Stage', $stageRecord->Title, $stageRecord->ID);
+ $stageRecord->delete();
+ $stageRecord->destroy();
+ unset($stageRecord);
+ }
+ $liveRecord = Versioned::get_one_by_stage(
+ $this->orphanedSearchClass,
+ 'Live',
+ sprintf("\"%s\".\"ID\" = %d",
+ ClassInfo::baseDataClass($this->orphanedSearchClass),
+ $id
+ )
+ );
+ if($liveRecord) {
+ $removedOrphans[$liveRecord->ID] = sprintf('Removed %s (#%d) from Live', $liveRecord->Title, $liveRecord->ID);
+ $liveRecord->doDeleteFromLive();
+ $liveRecord->destroy();
+ unset($liveRecord);
+ }
+ }
+
+ return $removedOrphans;
+ }
+
+ protected function rebaseHolderTitle() {
+ return sprintf('Rebased Orphans (%s)', date('d/m/Y g:ia', time()));
+ }
+
+ protected function rebaseOrphans($orphanIDs) {
+ $holder = new SiteTree();
+ $holder->ShowInMenus = 0;
+ $holder->ShowInSearch = 0;
+ $holder->ParentID = 0;
+ $holder->Title = $this->rebaseHolderTitle();
+ $holder->write();
+
+ $removedOrphans = array();
+ foreach($orphanIDs as $id) {
+ $stageRecord = Versioned::get_one_by_stage(
+ $this->orphanedSearchClass,
+ 'Stage',
+ sprintf("\"%s\".\"ID\" = %d",
+ ClassInfo::baseDataClass($this->orphanedSearchClass),
+ $id
+ )
+ );
+ if($stageRecord) {
+ $removedOrphans[$stageRecord->ID] = sprintf('Rebased %s (#%d)', $stageRecord->Title, $stageRecord->ID);
+ $stageRecord->ParentID = $holder->ID;
+ $stageRecord->ShowInMenus = 0;
+ $stageRecord->ShowInSearch = 0;
+ $stageRecord->write();
+ $stageRecord->doUnpublish();
+ $stageRecord->destroy();
+ //unset($stageRecord);
+ }
+ $liveRecord = Versioned::get_one_by_stage(
+ $this->orphanedSearchClass,
+ 'Live',
+ sprintf("\"%s\".\"ID\" = %d",
+ ClassInfo::baseDataClass($this->orphanedSearchClass),
+ $id
+ )
+ );
+ if($liveRecord) {
+ $removedOrphans[$liveRecord->ID] = sprintf('Rebased %s (#%d)', $liveRecord->Title, $liveRecord->ID);
+ $liveRecord->ParentID = $holder->ID;
+ $liveRecord->ShowInMenus = 0;
+ $liveRecord->ShowInSearch = 0;
+ $liveRecord->write();
+ if(!$stageRecord) $liveRecord->doRestoreToStage();
+ $liveRecord->doUnpublish();
+ $liveRecord->destroy();
+ unset($liveRecord);
+ }
+ if($stageRecord) {
+ unset($stageRecord);
+ }
+ }
+
+ return $removedOrphans;
+ }
+
+ /**
+ * Gets all orphans from "Stage" and "Live" stages.
+ *
+ * @param string $class
+ * @param string $filter
+ * @param string $sort
+ * @param string $join
+ * @param int|array $limit
+ * @return DataObjectSet
+ */
+ function getOrphanedPages($class = 'SiteTree', $filter = '', $sort = null, $join = null, $limit = null) {
+ $filter .= ($filter) ? ' AND ' : '';
+ $filter .= sprintf("\"%s\".\"ParentID\" != 0 AND \"Parents\".\"ID\" IS NULL", $class);
+
+ $orphans = new DataObjectSet();
+ foreach(array('Stage', 'Live') as $stage) {
+ $joinByStage = $join;
+ $table = $class;
+ $table .= ($stage == 'Live') ? '_Live' : '';
+ $joinByStage .= sprintf(
+ "LEFT JOIN \"%s\" AS \"Parents\" ON \"%s\".\"ParentID\" = \"Parents\".\"ID\"",
+ $table,
+ $table
+ );
+ $stageOrphans = Versioned::get_by_stage(
+ $class,
+ $stage,
+ $filter,
+ $sort,
+ $joinByStage,
+ $limit
+ );
+ $orphans->merge($stageOrphans);
+ }
+
+ $orphans->removeDuplicates();
+
+ return $orphans;
+ }
+}
+?>
\ No newline at end of file
diff --git a/tasks/UpgradeSiteTreePermissionSchemaTask.php b/tasks/UpgradeSiteTreePermissionSchemaTask.php
new file mode 100644
index 00000000..3e024b1a
--- /dev/null
+++ b/tasks/UpgradeSiteTreePermissionSchemaTask.php
@@ -0,0 +1,51 @@
+ 'ADMIN'
+ );
+
+ protected $title = 'Upgrade SiteTree Permissions Schema';
+
+ protected $description = "Move data from legacy columns to new schema introduced in SilverStripe 2.1.
+ SiteTree->Viewers to SiteTree->CanViewType
+ SiteTree->Editors to SiteTree->CanEditType
+ SiteTree->ViewersGroup to SiteTree->ViewerGroups (has_one to many_many)
+ SiteTree->Editorsroup to SiteTree->EditorGroups (has_one to many_many)
+ See http://open.silverstripe.com/ticket/2847
+ ";
+
+ function run($request) {
+ // transfer values for changed column name
+ foreach(array('SiteTree','SiteTree_Live','SiteTree_versions') as $table) {
+ DB::query("UPDATE \"{$table}\" SET \"CanViewType\" = 'Viewers';");
+ DB::query("UPDATE \"{$table}\" SET \"CanEditType\" = 'Editors';");
+ }
+ //Debug::message('Moved SiteTree->Viewers to SiteTree->CanViewType');
+ //Debug::message('Moved SiteTree->Editors to SiteTree->CanEditType');
+
+ // convert has_many to many_many
+ $pageIDs = DB::query("SELECT ID FROM SiteTree")->column('ID');
+ foreach($pageIDs as $pageID) {
+ $page = DataObject::get_by_id('SiteTree', $pageID);
+ if($page->ViewersGroup && DataObject::get_by_id("Group", $page->ViewersGroup)) $page->ViewerGroups()->add($page->ViewersGroup);
+ if($page->EditorsGroup && DataObject::get_by_id("Group", $page->EditorsGroup)) $page->EditorGroups()->add($page->EditorsGroup);
+
+ $page->destroy();
+ unset($page);
+ }
+ //Debug::message('SiteTree->ViewersGroup to SiteTree->ViewerGroups (has_one to many_many)');
+ //Debug::message('SiteTree->EditorsGroup to SiteTree->EditorGroups (has_one to many_many)');
+
+ // rename legacy columns
+ foreach(array('SiteTree','SiteTree_Live','SiteTree_versions') as $table) {
+ foreach(array('Viewers','Editors','ViewersGroup','EditorsGroup') as $field) {
+ DB::getConn()->dontRequireField($table, $field);
+ }
+ }
+ }
+}
+?>
\ No newline at end of file
diff --git a/tests/MigrateSiteTreeLinkingTaskTest.php b/tests/MigrateSiteTreeLinkingTaskTest.php
new file mode 100755
index 00000000..f27efbcc
--- /dev/null
+++ b/tests/MigrateSiteTreeLinkingTaskTest.php
@@ -0,0 +1,80 @@
+run(null);
+
+ $this->assertEquals (
+ "Rewrote 9 link(s) on 5 page(s) to use shortcodes.\n",
+ ob_get_contents(),
+ 'Rewritten links are correctly reported'
+ );
+ ob_end_clean();
+
+ $homeID = $this->idFromFixture('SiteTree', 'home');
+ $aboutID = $this->idFromFixture('SiteTree', 'about');
+ $staffID = $this->idFromFixture('SiteTree', 'staff');
+ $actionID = $this->idFromFixture('SiteTree', 'action');
+ $hashID = $this->idFromFixture('SiteTree', 'hash_link');
+
+ $homeContent = sprintf (
+ 'AboutStaffExternal Link',
+ $aboutID,
+ $staffID
+ );
+ $aboutContent = sprintf (
+ 'HomeStaff',
+ $homeID,
+ $staffID
+ );
+ $staffContent = sprintf (
+ 'HomeAbout',
+ $homeID,
+ $aboutID
+ );
+ $actionContent = sprintf (
+ 'Search Form', $homeID
+ );
+ $hashLinkContent = sprintf (
+ 'HomeAbout',
+ $homeID,
+ $aboutID
+ );
+
+ $this->assertEquals (
+ $homeContent,
+ DataObject::get_by_id('SiteTree', $homeID)->Content,
+ 'HTML URLSegment links are rewritten.'
+ );
+ $this->assertEquals (
+ $aboutContent,
+ DataObject::get_by_id('SiteTree', $aboutID)->Content
+ );
+ $this->assertEquals (
+ $staffContent,
+ DataObject::get_by_id('SiteTree', $staffID)->Content
+ );
+ $this->assertEquals (
+ $actionContent,
+ DataObject::get_by_id('SiteTree', $actionID)->Content,
+ 'Links to actions on pages are rewritten correctly.'
+ );
+ $this->assertEquals (
+ $hashLinkContent,
+ DataObject::get_by_id('SiteTree', $hashID)->Content,
+ 'Hash/anchor links are correctly handled.'
+ );
+ }
+
+}
\ No newline at end of file
diff --git a/tests/MigrateSiteTreeLinkingTaskTest.yml b/tests/MigrateSiteTreeLinkingTaskTest.yml
new file mode 100755
index 00000000..34faf015
--- /dev/null
+++ b/tests/MigrateSiteTreeLinkingTaskTest.yml
@@ -0,0 +1,67 @@
+SiteTree:
+ home:
+ Title: Home Page
+ URLSegment: home
+ Content: 'AboutStaffExternal Link'
+ about:
+ Title: About Us
+ URLSegment: about
+ Content: HomeStaff
+ staff:
+ Title: Staff
+ URLSegment: staff
+ Content: HomeAbout
+ Parent: =>SiteTree.about
+ action:
+ Title: Action Link
+ URLSegment: action
+ Content: Search Form
+ hash_link:
+ Title: Hash Link
+ URLSegment: hash-link
+ Content: 'HomeAbout'
+ admin_link:
+ Title: Admin Link
+ URLSegment: admin-link
+ Content: Admin
+ no_links:
+ Title: No Links
+ URLSegment: No Links
+
+SiteTree_LinkTracking:
+ home_about:
+ SiteTreeID: =>SiteTree.home
+ ChildID: =>SiteTree.about
+ FieldName: Content
+ home_staff:
+ SiteTreeID: =>SiteTree.home
+ ChildID: =>SiteTree.staff
+ FieldName: Content
+ about_home:
+ SiteTreeID: =>SiteTree.about
+ ChildID: =>SiteTree.home
+ FieldName: Content
+ about_staff:
+ SiteTreeID: =>SiteTree.about
+ ChildID: =>SiteTree.staff
+ FieldName: Content
+ staff_home:
+ SiteTreeID: =>SiteTree.staff
+ ChildID: =>SiteTree.home
+ FieldName: Content
+ staff_about:
+ SiteTreeID: =>SiteTree.staff
+ ChildID: =>SiteTree.about
+ FieldName: Content
+ action_home:
+ SiteTreeID: =>SiteTree.action
+ ChildID: =>SiteTree.home
+ FieldName: Content
+ hash_link_home:
+ SiteTreeID: =>SiteTree.hash_link
+ ChildID: =>SiteTree.home
+ FieldName: Content
+ hash_link_about:
+ SiteTreeID: =>SiteTree.hash_link
+ ChildID: =>SiteTree.about
+ FieldName: Content
diff --git a/tests/RemoveOrphanedPagesTaskTest.php b/tests/RemoveOrphanedPagesTaskTest.php
new file mode 100644
index 00000000..bd56308b
--- /dev/null
+++ b/tests/RemoveOrphanedPagesTaskTest.php
@@ -0,0 +1,105 @@
+Fixture tree
+ *
+ * parent1_published
+ * child1_1_published
+ * grandchild1_1_1
+ * grandchild1_1_2_published
+ * grandchild1_1_3_orphaned
+ * grandchild1_1_4_orphaned_published
+ * child1_2_published
+ * child1_3_orphaned
+ * child1_4_orphaned_published
+ * parent2
+ * child2_1_published_orphaned // is orphaned because parent is not published
+ *
+ *
+ * Cleaned up tree
+ *
+ * parent1_published
+ * child1_1_published
+ * grandchild1_1_1
+ * grandchild1_1_2_published
+ * child2_1_published_orphaned
+ * parent2
+ *
+ *
+ * @author Ingo Schommer (@silverstripe.com), SilverStripe Ltd.
+ *
+ * @package sapphire
+ * @subpackage tests
+ */
+class RemoveOrphanedPagesTaskTest extends FunctionalTest {
+
+ static $fixture_file = 'sapphire/tests/tasks/RemoveOrphanedPagesTaskTest.yml';
+
+ static $use_draft_site = false;
+
+ function setUp() {
+ parent::setUp();
+
+ $parent1_published = $this->objFromFixture('Page', 'parent1_published');
+ $parent1_published->publish('Stage', 'Live');
+
+ $child1_1_published = $this->objFromFixture('Page', 'child1_1_published');
+ $child1_1_published->publish('Stage', 'Live');
+
+ $child1_2_published = $this->objFromFixture('Page', 'child1_2_published');
+ $child1_2_published->publish('Stage', 'Live');
+
+ $child1_3_orphaned = $this->objFromFixture('Page', 'child1_3_orphaned');
+ $child1_3_orphaned->ParentID = 9999;
+ $child1_3_orphaned->write();
+
+ $child1_4_orphaned_published = $this->objFromFixture('Page', 'child1_4_orphaned_published');
+ $child1_4_orphaned_published->ParentID = 9999;
+ $child1_4_orphaned_published->write();
+ $child1_4_orphaned_published->publish('Stage', 'Live');
+
+ $grandchild1_1_2_published = $this->objFromFixture('Page', 'grandchild1_1_2_published');
+ $grandchild1_1_2_published->publish('Stage', 'Live');
+
+ $grandchild1_1_3_orphaned = $this->objFromFixture('Page', 'grandchild1_1_3_orphaned');
+ $grandchild1_1_3_orphaned->ParentID = 9999;
+ $grandchild1_1_3_orphaned->write();
+
+ $grandchild1_1_4_orphaned_published = $this->objFromFixture('Page',
+ 'grandchild1_1_4_orphaned_published'
+ );
+ $grandchild1_1_4_orphaned_published->ParentID = 9999;
+ $grandchild1_1_4_orphaned_published->write();
+ $grandchild1_1_4_orphaned_published->publish('Stage', 'Live');
+
+ $child2_1_published_orphaned = $this->objFromFixture('Page', 'child2_1_published_orphaned');
+ $child2_1_published_orphaned->publish('Stage', 'Live');
+ }
+
+ function testGetOrphansByStage() {
+ // all orphans
+ $child1_3_orphaned = $this->objFromFixture('Page', 'child1_3_orphaned');
+ $child1_4_orphaned_published = $this->objFromFixture('Page', 'child1_4_orphaned_published');
+ $grandchild1_1_3_orphaned = $this->objFromFixture('Page', 'grandchild1_1_3_orphaned');
+ $grandchild1_1_4_orphaned_published = $this->objFromFixture('Page',
+ 'grandchild1_1_4_orphaned_published'
+ );
+ $child2_1_published_orphaned = $this->objFromFixture('Page', 'child2_1_published_orphaned');
+
+ $task = singleton('RemoveOrphanedPagesTask');
+ $orphans = $task->getOrphanedPages();
+ $orphanIDs = $orphans->column('ID');
+ sort($orphanIDs);
+ $compareIDs = array(
+ $child1_3_orphaned->ID,
+ $child1_4_orphaned_published->ID,
+ $grandchild1_1_3_orphaned->ID,
+ $grandchild1_1_4_orphaned_published->ID,
+ $child2_1_published_orphaned->ID
+ );
+ sort($compareIDs);
+
+ $this->assertEquals($orphanIDs, $compareIDs);
+ }
+
+}
+?>
\ No newline at end of file
diff --git a/tests/RemoveOrphanedPagesTaskTest.yml b/tests/RemoveOrphanedPagesTaskTest.yml
new file mode 100644
index 00000000..47719b8d
--- /dev/null
+++ b/tests/RemoveOrphanedPagesTaskTest.yml
@@ -0,0 +1,32 @@
+Page:
+ parent1_published:
+ Title: Parent1
+ child1_1_published:
+ Title: Child1.1
+ Parent: =>Page.parent1_published
+ child1_2_published:
+ Title: Child1.2
+ Parent: =>Page.parent1_published
+ child1_3_orphaned:
+ Title: Child1.3
+ Parent: =>Page.parent1_published
+ child1_4_orphaned_published:
+ Title: Child1.4
+ Parent: =>Page.parent1_published
+ grandchild1_1_1:
+ Title: Grandchild1.1.1
+ Parent: =>Page.child1_1_published
+ grandchild1_1_2_published:
+ Title: Grandchild1.1.2
+ Parent: =>Page.child1_1_published
+ grandchild1_1_3_orphaned:
+ Title: Grandchild1.1.3
+ Parent: =>Page.child1_1_published
+ grandchild1_1_4_orphaned_published:
+ Title: Grandchild1.1.4
+ Parent: =>Page.child1_1_published
+ parent2:
+ Title: Parent2
+ child2_1_published_orphaned:
+ Title: Child2.1
+ Parent: =>Page.parent2
\ No newline at end of file