ENHANCEMENT: Added Versioned::get_including_deleted(), for querying deleted pages.

ENHANCEMENT: Added childrenMethod argument to Hierarchy tree-generation functions.
ENHANCEMENT: Added 'show deleted pages' function to CMS, with a restore page option.

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/branches/2.3@75736 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Sam Minnee 2009-04-30 05:38:48 +00:00
parent 2622c152d5
commit 7861a8f740
5 changed files with 179 additions and 37 deletions

View File

@ -25,15 +25,21 @@ class Hierarchy extends DataObjectDecorator {
* @param string $titleEval PHP code to evaluate to start each child - this should include '<li>'
* @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 = '"<li>" . $child->Title', $extraArg = null, $limitToMarked = false, $rootCall = true) {
public function getChildrenAsUL($attributes = "", $titleEval = '"<li>" . $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) . "</li>\n";
$child->getChildrenAsUL("", $titleEval, $extraArg, $limitToMarked, $childrenMethod, false) . "</li>\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

View File

@ -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);
}
/**

View File

@ -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

View File

@ -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);

View File

@ -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