mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 12:05:37 +00:00
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:
parent
2622c152d5
commit
7861a8f740
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user