diff --git a/core/model/Hierarchy.php b/core/model/Hierarchy.php
index 88a51de6c..5ca46d200 100644
--- a/core/model/Hierarchy.php
+++ b/core/model/Hierarchy.php
@@ -25,15 +25,21 @@ class Hierarchy extends DataObjectDecorator {
* @param string $titleEval PHP code to evaluate to start each child - this should include '
'
* @param string $extraArg Extra arguments that will be passed on to children, for if they overload this function.
* @param boolean $limitToMarked Display only marked children.
+ * @param string $childrenMethod The name of the method used to get children from each object
* @param boolean $rootCall Set to true for this first call, and then to false for calls inside the recursion. You should not change this.
* @return string
*/
- public function getChildrenAsUL($attributes = "", $titleEval = '"" . $child->Title', $extraArg = null, $limitToMarked = false, $rootCall = true) {
+ public function getChildrenAsUL($attributes = "", $titleEval = '"" . $child->Title', $extraArg = null, $limitToMarked = false, $childrenMethod = "AllChildrenIncludingDeleted", $rootCall = true) {
if($limitToMarked && $rootCall) {
$this->markingFinished();
}
- $children = $this->owner->AllChildrenIncludingDeleted($extraArg);
+ if($this->owner->hasMethod($childrenMethod)) {
+ $children = $this->owner->$childrenMethod($extraArg);
+ } else {
+ user_error(sprintf("Can't find the method '%s' on class '%s' for getting tree children",
+ $childrenMethod, get_class($this->owner)), E_USER_ERROR);
+ }
if($children) {
if($attributes) {
@@ -47,7 +53,7 @@ class Hierarchy extends DataObjectDecorator {
if(!$limitToMarked || $child->isMarked()) {
$foundAChild = true;
$output .= eval("return $titleEval;") . "\n" .
- $child->getChildrenAsUL("", $titleEval, $extraArg, $limitToMarked, false) . "\n";
+ $child->getChildrenAsUL("", $titleEval, $extraArg, $limitToMarked, $childrenMethod, false) . "\n";
}
}
@@ -70,13 +76,13 @@ class Hierarchy extends DataObjectDecorator {
* @param int $minCount The minimum amount of nodes to mark.
* @return int The actual number of nodes marked.
*/
- public function markPartialTree($minCount = 30, $context = null) {
+ public function markPartialTree($minCount = 30, $context = null, $childrenMethod = "AllChildrenIncludingDeleted") {
$this->markedNodes = array($this->owner->ID => $this->owner);
$this->owner->markUnexpanded();
// foreach can't handle an ever-growing $nodes list
while(list($id, $node) = each($this->markedNodes)) {
- $this->markChildren($node, $context);
+ $this->markChildren($node, $context, $childrenMethod);
if($minCount && sizeof($this->markedNodes) >= $minCount) {
break;
@@ -140,8 +146,14 @@ class Hierarchy extends DataObjectDecorator {
* Mark all children of the given node that match the marking filter.
* @param DataObject $node Parent node.
*/
- public function markChildren($node, $context = null) {
- $children = $node->AllChildrenIncludingDeleted($context);
+ public function markChildren($node, $context = null, $childrenMethod = "AllChildrenIncludingDeleted") {
+ if($node->hasMethod($childrenMethod)) {
+ $children = $node->$childrenMethod($context);
+ } else {
+ user_error(sprintf("Can't find the method '%s' on class '%s' for getting tree children",
+ $childrenMethod, get_class($this->owner)), E_USER_ERROR);
+ }
+
$node->markExpanded();
if($children) {
foreach($children as $child) {
@@ -172,10 +184,10 @@ class Hierarchy extends DataObjectDecorator {
*/
public function markingClasses() {
$classes = '';
- if(!$this->expanded) {
+ if(!$this->isExpanded()) {
$classes .= " unexpanded";
}
- if(!$this->treeOpened) {
+ if(!$this->isTreeOpened()) {
$classes .= " closed";
}
return $classes;
@@ -230,42 +242,42 @@ class Hierarchy extends DataObjectDecorator {
* True if this DataObject is marked.
* @var boolean
*/
- protected $marked = false;
+ protected static $marked = array();
/**
* True if this DataObject is expanded.
* @var boolean
*/
- protected $expanded = false;
+ protected static $expanded = array();
/**
* True if this DataObject is opened.
* @var boolean
*/
- protected $treeOpened = false;
+ protected static $treeOpened = array();
/**
* Mark this DataObject as expanded.
*/
public function markExpanded() {
- $this->marked = true;
- $this->expanded = true;
+ self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
+ self::$expanded[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
}
/**
* Mark this DataObject as unexpanded.
*/
public function markUnexpanded() {
- $this->marked = true;
- $this->expanded = false;
+ self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
+ self::$expanded[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = false;
}
/**
* Mark this DataObject's tree as opened.
*/
public function markOpened() {
- $this->marked = true;
- $this->treeOpened = true;
+ self::$marked[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
+ self::$treeOpened[ClassInfo::baseDataClass($this->owner->class)][$this->owner->ID] = true;
}
/**
@@ -273,7 +285,9 @@ class Hierarchy extends DataObjectDecorator {
* @return boolean
*/
public function isMarked() {
- return $this->marked;
+ $baseClass = ClassInfo::baseDataClass($this->owner->class);
+ $id = $this->owner->ID;
+ return isset(self::$marked[$baseClass][$id]) ? self::$marked[$baseClass][$id] : false;
}
/**
@@ -281,14 +295,18 @@ class Hierarchy extends DataObjectDecorator {
* @return boolean
*/
public function isExpanded() {
- return $this->expanded;
+ $baseClass = ClassInfo::baseDataClass($this->owner->class);
+ $id = $this->owner->ID;
+ return isset(self::$expanded[$baseClass][$id]) ? self::$expanded[$baseClass][$id] : false;
}
/**
* Check if this DataObject's tree is opened.
*/
public function isTreeOpened() {
- return $this->treeOpened;
+ $baseClass = ClassInfo::baseDataClass($this->owner->class);
+ $id = $this->owner->ID;
+ return isset(self::$treeOpened[$baseClass][$id]) ? self::$treeOpened[$baseClass][$id] : false;
}
/**
@@ -481,6 +499,15 @@ class Hierarchy extends DataObjectDecorator {
return $this->allChildrenIncludingDeleted;
}
+
+ /**
+ * Return all the children that this page had, including pages that were deleted
+ * from both stage & live.
+ */
+ public function AllHistoricalChildren() {
+ return Versioned::get_including_deleted(ClassInfo::baseDataClass($this->owner->class),
+ "ParentID = " . (int)$this->owner->ID);
+ }
/**
* Return the number of children
diff --git a/core/model/SiteTree.php b/core/model/SiteTree.php
index 161eb9a1f..945e8e474 100644
--- a/core/model/SiteTree.php
+++ b/core/model/SiteTree.php
@@ -1362,11 +1362,15 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
if($this->IsDeletedFromStage) {
if($this->can('CMSEdit')) {
- // "restore"
- $actions->push(new FormAction('revert',_t('CMSMain.RESTORE','Restore')));
-
- // "delete from live"
- $actions->push(new FormAction('deletefromlive',_t('CMSMain.DELETEFP','Delete from the published site')));
+ if($this->ExistsOnLive) {
+ // "restore"
+ $actions->push(new FormAction('revert',_t('CMSMain.RESTORE','Restore')));
+ // "delete from live"
+ $actions->push(new FormAction('deletefromlive',_t('CMSMain.DELETEFP','Delete from the published site')));
+ } else {
+ // "restore"
+ $actions->push(new FormAction('restore',_t('CMSMain.RESTORE','Restore')));
+ }
}
} else {
if($this->canEdit()) {
@@ -1461,6 +1465,29 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
$this->extend('onAfterRevertToLive');
}
+
+ /**
+ * Restore the content in the active copy of this SiteTree page to the stage site.
+ * @return The SiteTree object.
+ */
+ function doRestoreToStage() {
+ // if no record can be found on draft stage (meaning it has been "deleted from draft" before),
+ // create an empty record
+ if(!DB::query("SELECT ID FROM SiteTree WHERE ID = $this->ID")->value()) {
+ DB::query("INSERT INTO SiteTree SET ID = $this->ID");
+ }
+
+ $oldStage = Versioned::current_stage();
+ Versioned::reading_stage('Stage');
+ $this->forceChange();
+ $this->writeWithoutVersion();
+
+ $result = DataObject::get_by_id($this->class, $this->ID);
+
+ Versioned::reading_stage($oldStage);
+
+ return $result;
+ }
/**
* Check if this page is new - that is, if it has yet to have been written
@@ -1569,11 +1596,13 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
// sort alphabetically, and put current on top
asort($result);
- $currentPageTypeName = $result[$currentClass];
- unset($result[$currentClass]);
- $result = array_reverse($result);
- $result[$currentClass] = $currentPageTypeName;
- $result = array_reverse($result);
+ if($currentClass) {
+ $currentPageTypeName = $result[$currentClass];
+ unset($result[$currentClass]);
+ $result = array_reverse($result);
+ $result[$currentClass] = $currentPageTypeName;
+ $result = array_reverse($result);
+ }
return $result;
}
@@ -1675,7 +1704,11 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
*/
function TreeTitle() {
if($this->IsDeletedFromStage) {
- $tag ="del title=\"" . _t('SiteTree.REMOVEDFROMDRAFT', 'Removed from draft site') . "\"";
+ if($this->ExistsOnLive) {
+ $tag ="del title=\"" . _t('SiteTree.REMOVEDFROMDRAFT', 'Removed from draft site') . "\"";
+ } else {
+ $tag ="del class=\"deletedOnLive\" title=\"" . _t('SiteTree.DELETEDPAGE', 'Deleted page') . "\"";
+ }
} elseif($this->IsAddedToStage) {
$tag = "ins title=\"" . _t('SiteTree.ADDEDTODRAFT', 'Added to draft site') . "\"";
} elseif($this->IsModifiedOnStage) {
@@ -1754,9 +1787,16 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid
if(!is_numeric($this->ID)) return false;
$stageVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Stage', $this->ID);
- $liveVersion = Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
- return (!$stageVersion && $liveVersion);
+ // Return true for both completely deleted pages and for pages just deleted from stage.
+ return !$stageVersion;
+ }
+
+ /**
+ * Return true if this page exists on the live site
+ */
+ function getExistsOnLive() {
+ return (bool)Versioned::get_versionnumber_by_stage('SiteTree', 'Live', $this->ID);
}
/**
diff --git a/core/model/Versioned.php b/core/model/Versioned.php
index db14da5a7..eb46628e6 100755
--- a/core/model/Versioned.php
+++ b/core/model/Versioned.php
@@ -106,9 +106,9 @@ class Versioned extends DataObjectDecorator {
* Create a temporary table mapping each database record to its version on the given date.
* This is used by the versioning system to return database content on that date.
* @param string $baseTable The base table.
- * @param string $date The date.
+ * @param string $date The date. If omitted, then the latest version of each page will be returned.
*/
- protected function requireArchiveTempTable($baseTable, $date) {
+ protected static function requireArchiveTempTable($baseTable, $date = null) {
if(!isset(self::$createdArchiveTempTable[$baseTable])) {
self::$createdArchiveTempTable[$baseTable] = true;
@@ -116,12 +116,17 @@ class Versioned extends DataObjectDecorator {
RecordID INT NOT NULL PRIMARY KEY,
Version INT NOT NULL
)");
+
+ if($date) $dateClause = "WHERE LastEdited <= '$date'";
+ else $dateClause = "";
+
DB::query("INSERT INTO _Archive$baseTable
SELECT RecordID, max(Version) FROM {$baseTable}_versions
- WHERE LastEdited <= '$date'
+ $dateClause
GROUP BY RecordID");
}
}
+
/**
* An array of DataObject extensions that may require versioning for extra tables
* The array value is a set of suffixes to form these table names, assuming a preceding '_'.
@@ -719,6 +724,33 @@ class Versioned extends DataObjectDecorator {
return new $className($record);
}
+
+ /**
+ * Return the equivalent of a DataObject::get() call, querying the latest
+ * version of each page stored in the (class)_versions tables.
+ *
+ * In particular, this will query deleted records as well as active ones.
+ */
+ static function get_including_deleted($class, $filter = "", $sort = "") {
+ $oldStage = Versioned::$reading_stage;
+ Versioned::$reading_stage = null;
+
+ $SNG = singleton($class);
+
+ // Build query
+ $query = $SNG->buildVersionSQL($filter, $sort);
+ $baseTable = ClassInfo::baseDataClass($class);
+ self::requireArchiveTempTable($baseTable);
+ $query->from["_Archive$baseTable"] = "INNER JOIN `_Archive$baseTable`
+ ON `_Archive$baseTable`.RecordID = `{$baseTable}_versions`.RecordID
+ AND `_Archive$baseTable`.Version = `{$baseTable}_versions`.Version";
+
+ // Process into a DataObjectSet
+ $result = $SNG->buildDataObjectSet($query->execute());
+
+ Versioned::$reading_stage = $oldStage;
+ return $result;
+ }
/**
* @return DataObject
diff --git a/tests/SiteTreeActionsTest.php b/tests/SiteTreeActionsTest.php
index 2199e7bcd..38b5bbb51 100644
--- a/tests/SiteTreeActionsTest.php
+++ b/tests/SiteTreeActionsTest.php
@@ -66,9 +66,13 @@ if(class_exists('SiteTreeCMSWorkflow')) {
function testActionsDeletedFromStageRecord() {
$page = new Page();
$page->write();
+ $pageID = $page->ID;
$page->publish('Stage', 'Live');
$page->deleteFromStage('Stage');
+ // Get the live version of the page
+ $page = Versioned::get_one_by_stage("SiteTree", "Live", "`SiteTree`.ID = $pageID");
+
$author = $this->objFromFixture('Member', 'cmseditor');
$this->session()->inst_set('loggedInAs', $author->ID);
diff --git a/tests/SiteTreeTest.php b/tests/SiteTreeTest.php
index 699dba78f..2f1efe338 100644
--- a/tests/SiteTreeTest.php
+++ b/tests/SiteTreeTest.php
@@ -167,6 +167,45 @@ class SiteTreeTest extends SapphireTest {
$this->assertFalse($modifiedOnDraftPage->IsAddedToStage);
$this->assertTrue($modifiedOnDraftPage->IsModifiedOnStage);
}
+
+ /**
+ * Test that a page can be completely deleted and restored to the stage site
+ */
+ function testRestoreToStage() {
+ $page = $this->objFromFixture('Page', 'about');
+ $pageID = $page->ID;
+ $page->delete();
+ $this->assertTrue(!DataObject::get_by_id("Page", $pageID));
+
+ $deletedPage = Versioned::get_latest_version('SiteTree', $pageID);
+ $resultPage = $deletedPage->doRestoreToStage();
+
+ $requeriedPage = DataObject::get_by_id("Page", $pageID);
+
+ $this->assertEquals($pageID, $resultPage->ID);
+ $this->assertEquals($pageID, $requeriedPage->ID);
+ $this->assertEquals('About Us', $requeriedPage->Title);
+ $this->assertEquals('Page', $requeriedPage->class);
+
+
+ $page2 = $this->objFromFixture('Page', 'staff');
+ $page2ID = $page2->ID;
+ $page2->doUnpublish();
+ $page2->delete();
+
+ // Check that if we restore while on the live site that the content still gets pushed to
+ // stage
+ Versioned::reading_stage('Live');
+ $deletedPage = Versioned::get_latest_version('SiteTree', $page2ID);
+ $deletedPage->doRestoreToStage();
+ $this->assertTrue(!Versioned::get_one_by_stage("Page", "Live", "`SiteTree`.ID = " . $page2ID));
+
+ Versioned::reading_stage('Stage');
+ $requeriedPage = DataObject::get_by_id("Page", $page2ID);
+ $this->assertEquals('Staff', $requeriedPage->Title);
+ $this->assertEquals('Page', $requeriedPage->class);
+
+ }
}
// We make these extend page since that's what all page types are expected to do