diff --git a/README.md b/README.md index 441edc6..0230e78 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,13 @@ # DataObjectAsPage Module # - * v1.0 - ## Maintainers * Aram Balakjian - + -## Requirements +## Branch Requirements - * Master -> SilverStripe 3.1.0 + * 3.1 -> SilverStripe 3.1.x * 3.0 -> SilverStripe 3.0.x * 2.4 -> SilverStripe 2.4.x diff --git a/code/DataObjects/DataObjectAsPage.php b/code/DataObjects/DataObjectAsPage.php index 03a2dba..f49c499 100644 --- a/code/DataObjects/DataObjectAsPage.php +++ b/code/DataObjects/DataObjectAsPage.php @@ -142,81 +142,6 @@ public function canDeleteFromLive($member = null) return $this->canPublish($member); } - /** - * Override the CMS acctions to create a "duplicate" button - * - * @return FieldList The updated list of actions - */ - public function getCMSActions() - { - $minorActions = CompositeField::create()->setTag('fieldset')->addExtraClass('ss-ui-buttonset'); - $actions = new FieldList($minorActions); - - if($this->ID) - { - if($this->isPublished() && $this->canPublish() && $this->canDeleteFromLive()) { - // "unpublish" - $minorActions->push( - FormAction::create('unpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete') - ->setDescription(_t('SiteTree.BUTTONUNPUBLISHDESC', 'Remove this page from the published site')) - ->addExtraClass('ss-ui-action-destructive')->setAttribute('data-icon', 'unpublish') - ); - } - - if($this->canEdit()) { - - if($this->canDelete()) { - // "delete" - $minorActions->push( - FormAction::create('delete','Delete')->addExtraClass('delete ss-ui-action-destructive') - ->setAttribute('data-icon', 'decline') - ); - } - - if($this->hasChangesOnStage()) { - if($this->isPublished() && $this->canEdit()) { - // "rollback" - $minorActions->push( - FormAction::create('rollback', _t('SiteTree.BUTTONCANCELDRAFT', 'Cancel draft changes'), 'delete') - ->setDescription(_t('SiteTree.BUTTONCANCELDRAFTDESC', 'Delete your draft and revert to the currently published page')) - ); - } - } - - if ($this->canCreate()) - { - //Create the Duplicate action - $minorActions->push( FormAction::create('duplicate', 'Duplicate') - ->setDescription("Duplicate this item") - ); - } - // "save" - $minorActions->push( - FormAction::create('doSave',_t('CMSMain.SAVEDRAFT','Save Draft'))->setAttribute('data-icon', 'addpage') - ); - } - - if($this->canPublish()) { - // "publish" - $actions->push( - FormAction::create('publish', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & Publish')) - ->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept') - ); - } - - } - else - { - //Change the Save label to 'Create' - $actions->push(FormAction::create('doSave', _t('GridFieldDetailForm.Create', 'Create')) - ->setUseButtonTag(true) - ->addExtraClass('ss-ui-action-constructive') - ->setAttribute('data-icon', 'add')); - } - - return $actions; - } - /** * Overload getCMSFields for our custom fields * @@ -231,18 +156,18 @@ public function getCMSFields() { if($this->isVersioned) { - $status = $this->Status; + $status = $this->getStatus(); $color = '#E88F31'; $links = sprintf( - "%s", $this->Link() . '?Stage=stage', 'Draft' + "%s", $this->Link() . '?stage=Stage', 'Draft' ); - if($this->Status == 'Published') + if($status == 'Published') { $color = '#000'; $links .= sprintf( - "%s", $this->Link() . '?Stage=live', 'Published' + "%s", $this->Link() . '?stage=Live', 'Published' ); if($this->hasChangesOnStage()) @@ -257,7 +182,7 @@ public function getCMSFields() else { $links = sprintf( - "%s", $this->Link() . '?Stage=stage', 'View' + "%s", $this->Link() . '?stage=Stage', 'View' ); $statusPill = ""; @@ -382,6 +307,11 @@ public function MetaTags($includeTitle = true) return $tags; } + public function getStatus() + { + return $this->isPublished() ? "Published" : "Draft"; + } + /** * Check if this page has been published. * @@ -393,126 +323,6 @@ public function isPublished() ? true : false; } - - /** - * Create a duplicate of this node. Doesn't affect joined data - create a - * custom overloading of this if you need such behaviour. - * - * @return SiteTree The duplicated object. - */ - public function doDuplicate($doWrite = true) - { - $item = parent::duplicate(false); - $this->extend('onBeforeDuplicate', $item); - - //Change the title so we know we are looking at the copy - $item->Title = 'Copy of ' . $this->Title; - $item->Status = 'Draft'; - - if($doWrite) { - $item->write(); - } - - $this->extend('onAfterDuplicate', $page); - - return $item; - } - - /** - * Publish this page. - * - * @uses SiteTreeDecorator->onBeforePublish() - * @uses SiteTreeDecorator->onAfterPublish() - */ - public function doPublish() - { - if (!$this->canPublish()) return false; - - $original = Versioned::get_one_by_stage("DataObjectAsPage", "Live", "\"DataObjectAsPage\".\"ID\" = $this->ID"); - if(!$original) $original = new DataObjectAsPage(); - - // Handle activities undertaken by decorators - $this->invokeWithExtensions('onBeforePublish', $original); - - $this->Status = "Published"; - //$this->PublishedByID = Member::currentUser()->ID; - $this->write(); - $this->publish("Stage", "Live"); - - // Handle activities undertaken by decorators - $this->invokeWithExtensions('onAfterPublish', $original); - - return true; - } - - /** - * Unpublish this DataObject - remove it from the live site - * - */ - public function doUnpublish() - { - if(!$this->ID) return false; - if (!$this->canDeleteFromLive()) return false; - - $this->extend('onBeforeUnpublish'); - - $origStage = Versioned::current_stage(); - Versioned::reading_stage('Live'); - - // This way our ID won't be unset - $clone = clone $this; - $clone->delete(); - - Versioned::reading_stage($origStage); - - // If we're on the draft site, then we can update the status. - // Otherwise, these lines will resurrect an inappropriate record - if(DB::query("SELECT \"ID\" FROM \"DataObjectAsPage\" WHERE \"ID\" = $this->ID")->value() - && Versioned::current_stage() != 'Live') { - $this->Status = "Draft"; - $this->write(); - } - - $this->extend('onAfterUnpublish'); - - return true; - } - - /** - * Function to delete the DOAP including from versioned tables - */ - public function doDelete() - { - $this->doUnpublish(); - - $oldMode = Versioned::get_reading_mode(); - Versioned::reading_stage('Draft'); - - //delete all versioned objects with this ID - $result = DB::query("DELETE FROM DataObjectAsPage_versions WHERE RecordID = '$this->ID'"); - $result = $this->delete(); - - Versioned::set_reading_mode($oldMode); - - return $result; - } - - - /** - * Revert the draft changes: replace the draft content with the content on live - */ - public function doRevertToLive() - { - $this->publish("Live", "Stage", false); - - // Use a clone to get the updates made by $this->publish - $clone = DataObject::get_by_id("DataObjectAsPage", $this->ID); - $clone->writeWithoutVersion(); - - $this->extend('onAfterRevertToLive'); - - return $clone; - } /** * Check whether this DO has changes which are not published diff --git a/code/Decorators/VersionedDataObjectAsPage.php b/code/Decorators/VersionedDataObjectAsPage.php index 134b14f..7c670a8 100644 --- a/code/Decorators/VersionedDataObjectAsPage.php +++ b/code/Decorators/VersionedDataObjectAsPage.php @@ -2,17 +2,9 @@ class VersionedDataObjectAsPage extends DataExtension{ - private static $db = array( - "Status" => "Varchar" - ); - private static $summary_fields = array( 'Status' => 'Status' ); - - private static $defaults = array( - 'Status' => 'Draft' - ); private static $versioning = array( "Stage", "Live" diff --git a/code/Forms/VersionedGridFieldDetailForm.php b/code/Forms/VersionedGridFieldDetailForm.php index fa84dc7..7665bed 100644 --- a/code/Forms/VersionedGridFieldDetailForm.php +++ b/code/Forms/VersionedGridFieldDetailForm.php @@ -1,4 +1,11 @@ + */ class VersionedGridFieldDetailForm extends GridFieldDetailForm { @@ -11,201 +18,344 @@ class VersionedGridFieldDetailForm_ItemRequest extends GridFieldDetailForm_ItemR 'view', 'ItemEditForm' ); + + function isNew() { + /** + * This check was a problem for a self-hosted site, and may indicate a + * bug in the interpreter on their server, or a bug here + * Changing the condition from empty($this->ID) to + * !$this->ID && !$this->record['ID'] fixed this. + */ + if(empty($this->record->ID)) return true; + + if(is_numeric($this->record->ID)) return false; - public function ItemEditForm() - { - $form = parent::ItemEditForm(); - $actions = $this->record->getCMSActions(); - $form->setActions($actions); - return $form; + return stripos($this->record->ID, 'new') === 0; } - - /* - //Unable to get preview working for now - public function LinkPreview() - { + + + /** + * Check if this page has been published. + * + * @return boolean True if this page has been published. + */ + function isPublished() { + if($this->isNew()) + return false; + $record = $this->record; - - $baseLink = $record->CMSEditLink(); - - return $baseLink; + + return (DB::query("SELECT \"ID\" FROM \"{$this->baseTable()}_Live\" WHERE \"ID\" = $record->ID")->value()) + ? true + : false; } - */ - public function doSave($data, $form) { - $new_record = $this->record->ID == 0; - $controller = Controller::curr(); + function baseTable() { + $record = $this->record; + $classes = ClassInfo::dataClassesFor($record->ClassName); + return array_pop($classes); + } - try { - $form->saveInto($this->record); - $this->record->write(); - $this->gridField->getList()->add($this->record); - } catch(ValidationException $e) { - $form->sessionMessage($e->getResult()->message(), 'bad'); - $responseNegotiator = new PjaxResponseNegotiator(array( - 'CurrentForm' => function() use(&$form) { - return $form->forTemplate(); - }, - 'default' => function() use(&$controller) { - return $controller->redirectBack(); - } - )); - if($controller->getRequest()->isAjax()){ - $controller->getRequest()->addHeader('X-Pjax', 'CurrentForm'); - } - return $responseNegotiator->respond($controller->getRequest()); - } + function canPublish() { + return $this->record->canPublish(); + } + + function canDeleteFromLive() { + return $this->canPublish(); + } + + function stagesDiffer($from, $to) { + return $this->record->stagesDiffer($from, $to); + } + + function canEdit() { + return $this->record->canEdit(); + } - // TODO Save this item into the given relationship + function canDelete() { + return $this->record->canDelete(); + } - if(isset($data['publish']) && $data['publish']) - { - $this->record->doPublish(); + function canPreview() { + $can = false; + $can = in_array('CMSPreviewable', class_implements($this->record)); + if(method_exists("canPreview", $this->record)) { + $can = $this->record->canPreview(); } - else - { - $message = sprintf( - _t('GridFieldDetailForm.Saved', 'Saved %s %s'), - $this->record->singular_name(), - '"' . htmlspecialchars($this->record->Title, ENT_QUOTES) . '"' + + return ($can && !$this->isNew()); + } + + function getCMSActions() { + + $record = $this->record; + $classname = $record->class; + + $minorActions = CompositeField::create()->setTag('fieldset')->addExtraClass('ss-ui-buttonset'); + $actions = new FieldList($minorActions); + + + $this->IsDeletedFromStage = $this->getIsDeletedFromStage(); + $this->ExistsOnLive = $this->getExistsOnLive(); + + if($this->isPublished() && $this->canPublish() && !$this->IsDeletedFromStage && $this->canDeleteFromLive()) { + // "unpublish" + $minorActions->push( + FormAction::create('doUnpublish', _t('SiteTree.BUTTONUNPUBLISH', 'Unpublish'), 'delete') + ->setUseButtonTag(true)->setDescription("Remove this {$classname} from the published site") + ->addExtraClass('ss-ui-action-destructive')->setAttribute('data-icon', 'unpublish') ); - - $form->sessionMessage($message, 'good'); - - if($new_record) { - return Controller::curr()->redirect($this->Link()); - } elseif($this->gridField->getList()->byId($this->record->ID)) { - // Return new view, as we can't do a "virtual redirect" via the CMS Ajax - // to the same URL (it assumes that its content is already current, and doesn't reload) - return $this->edit(Controller::curr()->getRequest()); - } else { - // Changes to the record properties might've excluded the record from - // a filtered list, so return back to the main view if it can't be found - $noActionURL = $controller->removeAction($data['url']); - $controller->getRequest()->addHeader('X-Pjax', 'Content'); - return $controller->redirect($noActionURL, 302); - } } - } - public function publish($data, $form) - { - try { - - if($record = $this->record) - { - if (!$record->canPublish()) { - throw new ValidationException(_t('GridFieldDetailForm.DeletePermissionsFailure',"No publish permissions"),0); - } - - $data['publish'] = true; - $this->doSave($data, $form); + if($this->stagesDiffer('Stage', 'Live') && !$this->IsDeletedFromStage) { + if($this->isPublished() && $this->canEdit()) { + // "rollback" + $minorActions->push( + FormAction::create('doRollback', 'Cancel draft changes', 'delete') + ->setUseButtonTag(true)->setDescription(_t('SiteTree.BUTTONCANCELDRAFTDESC', 'Delete your draft and revert to the currently published page')) + ); } - - } catch(ValidationException $e) { - $this->executeException($form,$e); } - return $this->completeAction($form, $data, 'Published'); + if($this->canEdit()) { + if($this->canDelete() && !$this->isNew() && !$this->isPublished()) { + // "delete" + $minorActions->push( + FormAction::create('doDelete', 'Delete')->addExtraClass('delete ss-ui-action-destructive') + ->setAttribute('data-icon', 'decline')->setUseButtonTag(true) + ); + } + + // "save" + $minorActions->push( + FormAction::create('doSave',_t('CMSMain.SAVEDRAFT','Save Draft'))->setAttribute('data-icon', 'addpage')->setUseButtonTag(true) + ); + } + + if($this->canPublish() && !$this->IsDeletedFromStage) { + // "publish" + $actions->push( + FormAction::create('doPublish', _t('SiteTree.BUTTONSAVEPUBLISH', 'Save & Publish')) + ->setUseButtonTag(true)->addExtraClass('ss-ui-action-constructive')->setAttribute('data-icon', 'accept') + ); + } + // This is a bit hacky, however from what I understand ModelAdmin / GridField dont use the SilverStripe navigator, this will do for now just fine. + if($this->canPreview()) { + //Ensure Link method is defined & non-null before allowing preview + if(method_exists($this->record, 'Link') && $this->record->Link()){ + $actions->push( + LiteralField::create("preview", + sprintf("%s »", + $this->record->Link()."?stage=Stage", + _t('LeftAndMain.PreviewButton', 'Preview') + ) + ) + ); + } + } + + return $actions; } - - public function unpublish($data, $form) - { - try { - if($record = $this->record) - { - if (!$record->canDeleteFromLive()) { - throw new ValidationException(_t('GridFieldDetailForm.DeletePermissionsFailure',"No unpublish permissions"),0); - } + public function ItemEditForm() { + $form = parent::ItemEditForm(); + $actions = $this->getCMSActions(); - $record->doUnpublish(); - } - } catch(ValidationException $e) { - $this->executeException($form,$e); + $form->setActions($actions); + return $form; + } + + + public function doPublish($data, $form) { + $record = $this->record; + + if($record && !$record->canPublish()) { + return Security::permissionFailure($this); } - return $this->completeAction($form, $data, 'Unplublished'); + $form->saveInto($record); + $record->write(); + $this->gridField->getList()->add($record); + $record->publish("Stage", "Live"); + + $message = sprintf( + _t('GridFieldDetailForm.Published', 'Published %s %s'), + $this->record->singular_name(), + '"'.htmlspecialchars($this->record->Title, ENT_QUOTES).'"' + ); + + $form->sessionMessage($message, 'good'); + return $this->edit(Controller::curr()->getRequest()); + } + + + public function doUnpublish($data, $form) { + $record = $this->record; + + if($record && !$record->canPublish()) + return Security::permissionFailure($this); + + $origStage = Versioned::current_stage(); + Versioned::reading_stage('Live'); + + // This way our ID won't be unset + $clone = clone $record; + $clone->delete(); + $message = sprintf( + 'Unpublished %s %s', + $this->record->singular_name(), + '"'.htmlspecialchars($this->record->Title, ENT_QUOTES).'"' + ); + $form->sessionMessage($message, 'good'); + return $this->edit(Controller::curr()->getRequest()); } - public function delete($data, $form) { + + function doRollback($data, $form) { + $record = $this->record; + + //$clone = clone $record; + $record->publish("Live", "Stage", false); + //$record->writeWithoutVersion(); + $message = "Cancelled Draft changes for \"".htmlspecialchars($record->Title, ENT_QUOTES)."\""; + + $form->sessionMessage($message, 'good'); + return Controller::curr()->redirect($this->Link('edit')); + } + + + public function doDelete($data, $form) { + $record = $this->record; + try { - $record = $this->record; if (!$record->canDelete()) { throw new ValidationException(_t('GridFieldDetailForm.DeletePermissionsFailure',"No delete permissions"),0); } - - //This extra line is needed to remove the records with this ID from the versions table. - DB::query("DELETE FROM DataObjectAsPage_versions WHERE RecordID = '$record->ID'"); - $record->doDelete(); } catch(ValidationException $e) { - $this->executeException($form,$e); + $form->sessionMessage($e->getResult()->message(), 'bad'); + return Controller::curr()->redirectBack(); } - return $this->completeAction($form, $data, 'Deleted'); - } - - public function rollback($data, $form) { - try { - if($record = $this->record) - { - $reverted = $record->doRevertToLive(); - } - } catch(ValidationException $e) { - $this->executeException($form,$e); - } - - $this->record = $reverted; - return $this->completeAction($form, $data, 'Draft changed cancelled for'); - } - - public function duplicate($data, $form, $request) { - try { - if($record = $this->record) - { - //Duplicate the object - $clone = $record->doDuplicate(); - } - } catch(ValidationException $e) { - $this->executeException($form,$e); + + $message = sprintf( + _t('GridFieldDetailForm.Deleted', 'Deleted %s %s'), + $this->record->singular_name(), + '"'.htmlspecialchars($this->record->Title, ENT_QUOTES).'"' + ); + + $form->sessionMessage($message, 'good'); + + $controller = Controller::curr(); + $noActionURL = $controller->removeAction($data['url']); + $controller->getRequest()->addHeader('X-Pjax', 'Content'); // Force a content refresh + //double check that this deletes all versions + + $clone = clone $record; + $clone->deleteFromStage("Stage"); + $clone->delete(); + //manually deleting all orphaned _version records + DB::query("DELETE FROM \"{$this->baseTable()}_versions\" WHERE \"RecordID\" = '{$record->ID}'"); + return $controller->redirect($noActionURL, 302); //redirect back to admin section + } + + + /** + * Restore the content in the active copy of this SiteTree page to the stage site. + * @return The SiteTree object. + */ + function doRestoreToStage() { + $record = $this->record; + // 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 \"{$this->baseTable()}\" WHERE \"ID\" = $record->ID")->value()) { + $conn = DB::getConn(); + if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing($record->class, true); + DB::query("INSERT INTO \"{$this->baseTable()}\" (\"ID\") VALUES ($this->ID)"); + if(method_exists($conn, 'allowPrimaryKeyEditing')) $conn->allowPrimaryKeyEditing($record->class, false); } - $this->record = $clone; - return $this->completeAction($form, $data, 'Duplicated'); - } + $oldStage = Versioned::current_stage(); + Versioned::reading_stage('Stage'); + $record->forceChange(); + $record->write(); + + $result = DataObject::get_by_id($this->class, $this->ID); + + Versioned::reading_stage($oldStage); + + return $result; + } - /* - * Consolidating code, repeated in each action funciton above + /** + * Synonym of {@link doUnpublish} */ - private function completeAction($form, $data, $message) - { - $fullMessage = $message . " " . $this->record->singular_name() . " " . htmlspecialchars($this->record->Title, ENT_QUOTES); - - $form->sessionMessage($fullMessage, 'good'); + function doDeleteFromLive() { + return $this->doUnpublish(); + } + + + + + + + + /** + * Compares current draft with live version, + * and returns TRUE if no draft version of this page exists, + * but the page is still published (after triggering "Delete from draft site" in the CMS). + * + * @return boolean + */ + function getIsDeletedFromStage() { + //if(!$this->record->ID) return true; + if($this->isNew()) return false; - $controller = Controller::curr(); + $stageVersion = Versioned::get_versionnumber_by_stage($this->record->class, 'Stage', $this->record->ID); + + // 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($this->record->class, 'Live', $this->record->ID); + } + + /** + * Compares current draft with live version, + * and returns TRUE if these versions differ, + * meaning there have been unpublished changes to the draft site. + * + * @return boolean + */ + public function getIsModifiedOnStage() { + // new unsaved pages could be never be published + if($this->isNew()) return false; - if($this->gridField->getList()->byId($this->record->ID)) - { - return $this->edit($controller->getRequest()); - } - else - { - // Changes to the record properties might've excluded the record from - // a filtered list, so return back to the main view if it can't be found - $noActionURL = $controller->removeAction($data['url']); - $controller->getRequest()->addHeader('X-Pjax', 'Content'); - return $controller->redirect($noActionURL, 302); - } + $stageVersion = Versioned::get_versionnumber_by_stage($this->record->class, 'Stage', $this->record->ID); + $liveVersion = Versioned::get_versionnumber_by_stage($this->record->class, 'Live', $this->record->ID); + + return ($stageVersion && $stageVersion != $liveVersion); } + /** + * Compares current draft with live version, + * and returns true if no live version exists, + * meaning the page was never published. + * + * @return boolean + */ + public function getIsAddedToStage() { + // new unsaved pages could be never be published + if($this->isNew()) return false; + + $stageVersion = Versioned::get_versionnumber_by_stage($this->record->class, 'Stage', $this->record->ID); + $liveVersion = Versioned::get_versionnumber_by_stage($this->record->class, 'Live', $this->record->ID); - /* - * Consolidating code, repeated in each action funciton above - */ - private function executeException($form, $e) - { - $form->sessionMessage($e->getResult()->message(), 'bad'); - return Controller::curr()->redirectBack(); - } -} \ No newline at end of file + return ($stageVersion && !$liveVersion); + } + + +} diff --git a/code/ModelAdmin/DataObjectAsPageAdmin.php b/code/ModelAdmin/DataObjectAsPageAdmin.php index 0e27dda..cbb248f 100644 --- a/code/ModelAdmin/DataObjectAsPageAdmin.php +++ b/code/ModelAdmin/DataObjectAsPageAdmin.php @@ -7,6 +7,7 @@ public function init() { parent::init(); + Versioned::reading_stage('Stage'); //Styling for preview links and status Requirements::CSS(MOD_DOAP_DIR . '/css/dataobjectaspageadmin.css'); }