From 9b75cb19870e86e26eb96a774129ec033bb949df Mon Sep 17 00:00:00 2001 From: Sean Harvey Date: Fri, 1 May 2009 03:49:34 +0000 Subject: [PATCH] Merged from branches/2.3 git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@75845 467b73ca-7a2a-4603-9d3b-597d59a354a9 --- core/SSViewer.php | 5 +- core/model/ComponentSet.php | 40 ++--- core/model/Hierarchy.php | 69 +++++--- core/model/SiteTree.php | 79 +++++++-- core/model/Versioned.php | 32 +++- email/Email.php | 6 +- forms/ComplexTableField.php | 117 +++++++----- forms/Form.php | 38 +++- tests/DataObjectTest.php | 34 ++-- tests/ErrorPageTest.php | 13 +- tests/SiteTreeActionsTest.php | 4 + tests/SiteTreeTest.php | 39 ++++ tests/forms/ComplexTableFieldTest.php | 246 ++++++++++++++++++++++++++ tests/forms/ComplexTableFieldTest.yml | 15 ++ 14 files changed, 611 insertions(+), 126 deletions(-) create mode 100644 tests/forms/ComplexTableFieldTest.php create mode 100644 tests/forms/ComplexTableFieldTest.yml diff --git a/core/SSViewer.php b/core/SSViewer.php index d62e15da1..3690d26c4 100644 --- a/core/SSViewer.php +++ b/core/SSViewer.php @@ -364,11 +364,10 @@ class SSViewer extends Object { if(isset($_GET['debug_profile'])) Profiler::unmark("SSViewer::process", " for $template"); - // If we have our crazy base tag, then fix # links referencing the current page. - if(strpos($output, '+]href *= *")#/i', '\\1' . $thisURLRelativeToBase . '#', $output); + $output = preg_replace('/(+]href *= *)"#/i', '\\1"' . $thisURLRelativeToBase . '#', $output); } return $output; diff --git a/core/model/ComponentSet.php b/core/model/ComponentSet.php index 913db1dae..9ea77ce4c 100755 --- a/core/model/ComponentSet.php +++ b/core/model/ComponentSet.php @@ -63,36 +63,36 @@ class ComponentSet extends DataObjectSet { /** * Find the extra field data for a single row of the relationship * join table, given the known child ID. + * + * @todo This should return casted fields, like Enum, Varchar, Date + * instead of just the raw value of the field. * * @param string $componentName The name of the component * @param int $childID The ID of the child for the relationship - * @return array Map of fieldName => fieldValue + * @param string|null $fieldName To get a specific extra data field, specify it here + * @return array|string Array of field => value or single string of value */ - function getExtraData($componentName, $childID) { + function getExtraData($componentName, $childID, $fieldName = null) { $ownerObj = $this->ownerObj; $parentField = $this->ownerClass . 'ID'; $childField = ($this->childClass == $this->ownerClass) ? 'ChildID' : ($this->childClass . 'ID'); - $result = array(); - if(!isset($componentName)) { - user_error('ComponentSet::getExtraData() passed a NULL component name', E_USER_ERROR); - } - - if(!is_numeric($childID)) { - user_error('ComponentSet::getExtraData() passed a non-numeric child ID', E_USER_ERROR); - } + if(!$componentName) return false; - // @todo Optimize into a single query instead of one per extra field $extraFields = $ownerObj->many_many_extraFields($componentName); - if($extraFields) { - foreach($extraFields as $fieldName => $dbFieldSpec) { - $query = DB::query("SELECT \"$fieldName\" FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID} AND \"$childField\" = {$childID}"); - $value = $query->value(); - $result[$fieldName] = $value; - } - } + if(!$extraFields) return false; - return $result; + if($fieldName && !empty($extraFields[$fieldName])) { + $query = DB::query("SELECT $fieldName FROM {$this->tableName} WHERE $parentField = '{$this->ownerObj->ID}' AND $childField = '{$childID}'"); + return $query->value(); + } else { + $fields = array(); + foreach($extraFields as $fieldName => $fieldSpec) { + $query = DB::query("SELECT $fieldName FROM {$this->tableName} WHERE $parentField = '{$this->ownerObj->ID}' AND $childField = '{$childID}'"); + $fields[$fieldName] = $query->value(); + } + return $fields; + } } /** @@ -325,4 +325,4 @@ OUT; } } -?> \ No newline at end of file +?> diff --git a/core/model/Hierarchy.php b/core/model/Hierarchy.php index 33e440790..80036e108 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) { @@ -46,7 +52,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"; } } @@ -69,13 +75,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; @@ -139,8 +145,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) { @@ -171,10 +183,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; @@ -229,42 +241,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; } /** @@ -272,7 +284,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; } /** @@ -280,14 +294,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; } /** @@ -477,6 +495,15 @@ class Hierarchy extends DataObjectDecorator { return $this->_cache_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 b91687585..187608561 100644 --- a/core/model/SiteTree.php +++ b/core/model/SiteTree.php @@ -1222,6 +1222,13 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid $this->fieldLabel('ClassName'), $this->getClassDropdown() ), + + new OptionsetField("ParentType", "Page location", array( + "root" => _t("SiteTree.PARENTTYPE_ROOT", "Top-level page"), + "subpage" => _t("SiteTree.PARENTTYPE_SUBPAGE", "Sub-page underneath a parent page (choose below)"), + )), + new TreeDropdownField("ParentID", $this->fieldLabel('ParentID'), 'SiteTree'), + new CheckboxField("ShowInMenus", $this->fieldLabel('ShowInMenus')), new CheckboxField("ShowInSearch", $this->fieldLabel('ShowInSearch')), /*, new TreeMultiselectField("MultipleParents", "Page appears within", "SiteTree")*/ @@ -1318,6 +1325,8 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid $labels['MetaKeywords'] = _t('SiteTree.METAKEYWORDS', "Keywords"); $labels['ExtraMeta'] = _t('SiteTree.METAEXTRA', "Custom Meta Tags"); $labels['ClassName'] = _t('SiteTree.PAGETYPE', "Page type", PR_MEDIUM, 'Classname of a page object'); + $labels['ParentType'] = _t('SiteTree.PARENTTYPE', "Page location", PR_MEDIUM); + $labels['ParentID'] = _t('SiteTree.PARENTID', "Parent page", PR_MEDIUM); $labels['ShowInMenus'] =_t('SiteTree.SHOWINMENUS', "Show in menus?"); $labels['ShowInSearch'] = _t('SiteTree.SHOWINSEARCH', "Show in search?"); $labels['ProvideComments'] = _t('SiteTree.ALLOWCOMMENTS', "Allow comments on this page?"); @@ -1370,11 +1379,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()) { @@ -1480,6 +1493,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 @@ -1589,11 +1625,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; } @@ -1695,7 +1733,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) { @@ -1775,9 +1817,16 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid if($this->isNew()) 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); } /** @@ -1859,6 +1908,10 @@ class SiteTree extends DataObject implements PermissionProvider,i18nEntityProvid return $entities; } + + function getParentType() { + return $this->ParentID == 0 ? 'root' : 'subpage'; + } } ?> \ No newline at end of file diff --git a/core/model/Versioned.php b/core/model/Versioned.php index 7d2591f16..82a825896 100755 --- a/core/model/Versioned.php +++ b/core/model/Versioned.php @@ -106,10 +106,10 @@ 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. * @todo Ensure that this is DB abstracted */ - protected function requireArchiveTempTable($baseTable, $date) { + protected static function requireArchiveTempTable($baseTable, $date = null) { if(!isset(self::$createdArchiveTempTable[$baseTable])) { self::$createdArchiveTempTable[$baseTable] = true; @@ -123,6 +123,7 @@ class Versioned extends DataObjectDecorator { 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 '_'. @@ -723,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/email/Email.php b/email/Email.php index bf2ba2aa8..7e2702389 100755 --- a/email/Email.php +++ b/email/Email.php @@ -158,7 +158,7 @@ class Email extends ViewableData { 'mimetype' => $mimetype, ); } - + public function setBounceHandlerURL( $bounceHandlerURL ) { if($bounceHandlerURL) { $this->bounceHandlerURL = $bounceHandlerURL; @@ -166,7 +166,7 @@ class Email extends ViewableData { $this->bounceHandlerURL = $_SERVER['HTTP_HOST'] . Director::baseURL() . 'Email_BounceHandler'; } } - + public function attachFile($filename, $attachedFilename = null, $mimetype = null) { $absoluteFileName = Director::getAbsFile($filename); if(file_exists($absoluteFileName)) { @@ -787,4 +787,4 @@ class Email_BounceRecord extends DataObject { } } -?> \ No newline at end of file +?> diff --git a/forms/ComplexTableField.php b/forms/ComplexTableField.php index ca77cd2b8..c243c18f2 100755 --- a/forms/ComplexTableField.php +++ b/forms/ComplexTableField.php @@ -458,69 +458,85 @@ JS; * this method. */ function getCustomFieldsFor($childData) { - if($this->detailFormFields instanceof Fieldset) { + if($this->detailFormFields instanceof FieldSet) { return $this->detailFormFields; - } else { - $fieldsMethod = $this->detailFormFields; - - if(!is_string($fieldsMethod)) { - $this->detailFormFields = 'getCMSFields'; - $fieldsMethod = 'getCMSFields'; - } - - if(!$childData->hasMethod($fieldsMethod)) { - $fieldsMethod = 'getCMSFields'; - } - - $fields = $childData->$fieldsMethod(); - } - - if(!$this->relationAutoSetting) { - return $fields; } - $parentClass = DataObject::get_by_id($this->getParentClass(), $this->sourceID()); - $manyManyExtraFields = $parentClass->many_many_extraFields($this->name); - if($manyManyExtraFields) { - foreach($manyManyExtraFields as $fieldName => $fieldSpec) { - $dbField = new Varchar('ctf[extraFields][' . $fieldName . ']'); - $fields->addFieldToTab('Root.Main', $dbField->scaffoldFormField($fieldName)); - } + $fieldsMethod = $this->detailFormFields; + + if(!is_string($fieldsMethod)) { + $this->detailFormFields = 'getCMSFields'; + $fieldsMethod = 'getCMSFields'; } - return $fields; + if(!$childData->hasMethod($fieldsMethod)) { + $fieldsMethod = 'getCMSFields'; + } + + return $childData->$fieldsMethod(); } - + function getFieldsFor($childData) { // See if our parent class has any many_many relations by this source class - $parentClass = DataObject::get_by_id($this->getParentClass(), $this->sourceID()); + if($this->sourceID()) { + $parentClass = DataObject::get_by_id($this->getParentClass(), $this->sourceID()); + } else { + $parentClass = singleton($this->getParentClass()); + } + $manyManyRelations = $parentClass->many_many(); $manyManyRelationName = null; $manyManyComponentSet = null; + $hasManyRelations = $parentClass->has_many(); + $hasManyRelationName = null; + $hasManyComponentSet = null; + if($manyManyRelations) foreach($manyManyRelations as $relation => $class) { if($class == $this->sourceClass()) { $manyManyRelationName = $relation; } } + if($hasManyRelations) foreach($hasManyRelations as $relation => $class) { + if($class == $this->sourceClass()) { + $hasManyRelationName = $relation; + } + } + // Add the relation value to related records if(!$childData->ID && $this->getParentClass()) { // make sure the relation-link is existing, even if we just add the sourceClass and didn't save it - $parentIDName = $this->getParentIdName( $this->getParentClass(), $this->sourceClass() ); + $parentIDName = $this->getParentIdName($this->getParentClass(), $this->sourceClass()); $childData->$parentIDName = $this->sourceID(); } - + $detailFields = $this->getCustomFieldsFor($childData); // Loading of extra field values for editing an existing record - if($manyManyRelationName && $childData->ID) { + if($manyManyRelationName) { $manyManyComponentSet = $parentClass->getManyManyComponents($manyManyRelationName); - $extraData = $manyManyComponentSet->getExtraData($manyManyRelationName, $childData->ID); - if($extraData) foreach($extraData as $fieldName => $fieldValue) { - $field = $detailFields->dataFieldByName('ctf[extraFields][' . $fieldName . ']'); - $field->setValue($fieldValue); + $extraFieldsSpec = $parentClass->many_many_extraFields($this->name); + + $extraData = null; + if($childData && $childData->ID) { + $extraData = $manyManyComponentSet->getExtraData($manyManyRelationName, $childData->ID); } + + if($extraFieldsSpec) foreach($extraFieldsSpec as $fieldName => $fieldSpec) { + // @todo Create the proper DBField type instead of hardcoding Varchar + $fieldObj = new Varchar($fieldName); + + if(isset($extraData[$fieldName])) { + $fieldObj->setValue($extraData[$fieldName]); + } + + $detailFields->addFieldToTab('Root.Main', $fieldObj->scaffoldFormField($fieldName)); + } + } + + if($hasManyRelationName && $childData->ID) { + $hasManyComponentSet = $parentClass->getComponents($hasManyRelationName); } // the ID field confuses the Controller-logic in finding the right view for ReferencedField @@ -530,22 +546,28 @@ JS; if($childData->ID) { $detailFields->push(new HiddenField('ctf[childID]', '', $childData->ID)); } - + // add a namespaced ID instead thats "converted" by saveComplexTableField() $detailFields->push(new HiddenField('ctf[ClassName]', '', $this->sourceClass())); if($this->getParentClass()) { + $detailFields->push(new HiddenField('ctf[parentClass]', '', $this->getParentClass())); + if($manyManyRelationName && $this->relationAutoSetting) { $detailFields->push(new HiddenField('ctf[manyManyRelation]', '', $manyManyRelationName)); - $detailFields->push(new HiddenField('ctf[parentClass]', '', $this->getParentClass())); + } + + if($hasManyRelationName && $this->relationAutoSetting) { + $detailFields->push(new HiddenField('ctf[hasManyRelation]', '', $hasManyRelationName)); + } + + if($manyManyRelationName || $hasManyRelationName) { $detailFields->push(new HiddenField('ctf[sourceID]', '', $this->sourceID())); } $parentIdName = $this->getParentIdName($this->getParentClass(), $this->sourceClass()); + if($parentIdName) { - // add relational fields - $detailFields->push(new HiddenField('ctf[parentClass]', '', $this->getParentClass())); - if($this->relationAutoSetting) { // Hack for model admin: model admin will have included a dropdown for the relation itself $detailFields->removeByName($parentIdName); @@ -614,6 +636,8 @@ JS; * even if there is no action relevant for the main controller (to provide the instance of ComplexTableField * which in turn saves the record. * + * This is for adding new item records. {@link ComplexTableField_ItemRequest::saveComplexTableField()} + * * @see Form::ReferencedField */ function saveComplexTableField($data, $form, $params) { @@ -621,7 +645,7 @@ JS; $childData = new $className(); $form->saveInto($childData); $childData->write(); - + // Save the many many relationship if it's available if(isset($data['ctf']['manyManyRelation'])) { $parentRecord = DataObject::get_by_id($data['ctf']['parentClass'], (int) $data['ctf']['sourceID']); @@ -638,6 +662,14 @@ JS; $componentSet->add($childData, $extraFields); } + if(isset($data['ctf']['hasManyRelation'])) { + $parentRecord = DataObject::get_by_id($data['ctf']['parentClass'], (int) $data['ctf']['sourceID']); + $relationName = $data['ctf']['hasManyRelation']; + + $componentSet = $parentRecord->getComponents($relationName); + $componentSet->add($childData); + } + $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null; $closeLink = sprintf( @@ -784,6 +816,8 @@ class ComplexTableField_ItemRequest extends RequestHandler { * even if there is no action relevant for the main controller (to provide the instance of ComplexTableField * which in turn saves the record. * + * This is for editing existing item records. {@link ComplexTableField::saveComplexTableField()} + * * @see Form::ReferencedField */ function saveComplexTableField($data, $form, $request) { @@ -819,6 +853,7 @@ class ComplexTableField_ItemRequest extends RequestHandler { '"' . $dataObject->Title . '"', $closeLink ); + $form->sessionMessage($message, 'good'); Director::redirectBack(); diff --git a/forms/Form.php b/forms/Form.php index 5df643030..5d0b02230 100644 --- a/forms/Form.php +++ b/forms/Form.php @@ -95,6 +95,14 @@ class Form extends RequestHandler { protected $messageType; + /** + * Should we redirect the user back down to the + * the form on validation errors rather then just the page + * + * @var bool + */ + protected $redirectToFormOnValidationError = false; + protected $security = true; /** @@ -214,11 +222,16 @@ class Form extends RequestHandler { return $response; } } else { - Director::redirectBack(); - return; + if($this->getRedirectToFormOnValidationError()) { + if($pageURL = $request->getHeader('Referer')) { + return Director::redirect($pageURL . '#' . $this->FormName()); + } + } + return Director::redirectBack(); } } + // Protection against CSRF attacks if($this->securityTokenEnabled()) { $securityID = Session::get('SecurityID'); @@ -278,6 +291,27 @@ class Form extends RequestHandler { function makeReadonly() { $this->transform(new ReadonlyTransformation()); } + + /** + * Set whether the user should be redirected back down to the + * form on the page upon validation errors in the form or if + * they just need to redirect back to the page + * + * @param bool Redirect to the form + */ + public function setRedirectToFormOnValidationError($bool) { + $this->redirectToFormOnValidationError = $bool; + } + + /** + * Get whether the user should be redirected back down to the + * form on the page upon validation errors + * + * @return bool + */ + public function getRedirectToFormOnValidationError() { + return $this->redirectToFormOnValidationError; + } /** * Add an error message to a field on this form. It will be saved into the session diff --git a/tests/DataObjectTest.php b/tests/DataObjectTest.php index fd6650128..49717de68 100644 --- a/tests/DataObjectTest.php +++ b/tests/DataObjectTest.php @@ -579,23 +579,6 @@ class DataObjectTest extends SapphireTest { // @todo test has_many and many_many relations } - function testManyManyExtraFields() { - $player = $this->fixture->objFromFixture('DataObjectTest_Player', 'player1'); - $team = $this->fixture->objFromFixture('DataObjectTest_Team', 'team1'); - - // Extra fields are immediately available on the Team class (defined in $many_many_extraFields) - $teamExtraFields = $team->many_many_extraFields('Players'); - $this->assertEquals($teamExtraFields, array( - 'Position' => 'Varchar(100)' - )); - - // We'll have to go through the relation to get the extra fields on Player - $playerExtraFields = $player->many_many_extraFields('Teams'); - $this->assertEquals($playerExtraFields, array( - 'Position' => 'Varchar(100)' - )); - } - function testPopulateDefaults() { $obj = new DataObjectTest_WithDefaults(); $this->assertEquals( @@ -623,6 +606,23 @@ class DataObjectTest extends SapphireTest { $this->assertType('RedirectorPage', $changedPage); $this->assertEquals($changedPage->ClassName, 'RedirectorPage'); } + + function testManyManyExtraFields() { + $player = $this->fixture->objFromFixture('DataObjectTest_Player', 'player1'); + $team = $this->fixture->objFromFixture('DataObjectTest_Team', 'team1'); + + // Extra fields are immediately available on the Team class (defined in $many_many_extraFields) + $teamExtraFields = $team->many_many_extraFields('Players'); + $this->assertEquals($teamExtraFields, array( + 'Position' => 'Varchar(100)' + )); + + // We'll have to go through the relation to get the extra fields on Player + $playerExtraFields = $player->many_many_extraFields('Teams'); + $this->assertEquals($playerExtraFields, array( + 'Position' => 'Varchar(100)' + )); + } } diff --git a/tests/ErrorPageTest.php b/tests/ErrorPageTest.php index f5c84ffaa..7f3eeaa5b 100644 --- a/tests/ErrorPageTest.php +++ b/tests/ErrorPageTest.php @@ -9,15 +9,20 @@ class ErrorPageTest extends FunctionalTest { function test404ErrorPage() { $page = $this->objFromFixture('ErrorPage', '404'); + + $response = $this->get($page->URLSegment); - /* The page is an instance of ErrorPage */ - $this->assertTrue($page instanceof ErrorPage, 'The page is an instance of ErrorPage'); + /* A standard error is shown */ + $this->assertEquals($response->getBody(), 'The requested page couldn\'t be found.', 'A standard error is shown'); + + /* When the page is published, an error page with the theme is shown instead */ + $page->publish('Stage', 'Live', false); $response = $this->get($page->URLSegment); - /* We have body text from the error page */ + /* There is body text from the error page */ $this->assertNotNull($response->getBody(), 'We have body text from the error page'); - + /* Status code of the HTTPResponse for error page is "404" */ $this->assertEquals($response->getStatusCode(), '404', 'Status cod eof the HTTPResponse for error page is "404"'); 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 1ce819b8a..59817509c 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 diff --git a/tests/forms/ComplexTableFieldTest.php b/tests/forms/ComplexTableFieldTest.php new file mode 100644 index 000000000..3a9d6e854 --- /dev/null +++ b/tests/forms/ComplexTableFieldTest.php @@ -0,0 +1,246 @@ +controller = new ComplexTableFieldTest_Controller(); + $this->manyManyForm = $this->controller->ManyManyForm(); + } + + function testCorrectNumberOfRowsInTable() { + $field = $this->manyManyForm->dataFieldByName('Players'); + $parser = new CSSContentParser($field->FieldHolder()); + + /* There are 2 players (rows) in the table */ + $this->assertEquals(count($parser->getBySelector('tbody tr')), 2, 'There are 2 players (rows) in the table'); + + /* There are 2 CTF items in the DataObjectSet */ + $this->assertEquals($field->Items()->Count(), 2, 'There are 2 CTF items in the DataObjectSet'); + } + + function testDetailFormDisplaysWithCorrectFields() { + $field = $this->manyManyForm->dataFieldByName('Players'); + $detailForm = $field->add(); + $parser = new CSSContentParser($detailForm); + + /* There is a field called "Name", which is a text input */ + $this->assertEquals(count($parser->getBySelector('#Name input')), 1, 'There is a field called "Name", which is a text input'); + + /* There is a field called "Role" - this field is the extra field for $many_many_extraFields */ + $this->assertEquals(count($parser->getBySelector('#Role input')), 1, 'There is a field called "Role" - this field is the extra field for $many_many_extraFields'); + } + + function testAddingManyManyNewPlayerWithExtraData() { + $team = DataObject::get_one('ComplexTableFieldTest_Team', "Name = 'The Awesome People'"); + + $this->post('ComplexTableFieldTest_Controller/ManyManyForm/field/Players/AddForm', array( + 'Name' => 'Bobby Joe', + 'ctf' => array( + 'extraFields' => array( + 'Role' => 'Goalie', + 'Position' => 'Player', + 'DateJoined' => '2008-10-10' + ), + 'ClassName' => 'ComplexTableFieldTest_Player', + 'manyManyRelation' => 'Players', + 'parentClass' => 'ComplexTableFieldTest_Team', + 'sourceID' => $team->ID + ) + )); + + /* Retrieve the new player record we created */ + $newPlayer = DataObject::get_one('ComplexTableFieldTest_Player', "Name = 'Bobby Joe'"); + + /* A new ComplexTableFieldTest_Player record was created, Name = "Bobby Joe" */ + $this->assertNotNull($newPlayer, 'A new ComplexTableFieldTest_Player record was created, Name = "Bobby Joe"'); + + /* Get the many-many related Teams to the new player that were automatically linked by CTF */ + $teams = $newPlayer->getManyManyComponents('Teams'); + + /* Automatic many-many relation was set correctly on the new player */ + $this->assertEquals($teams->Count(), 1, 'Automatic many-many relation was set correctly on the new player'); + + /* The extra fields have the correct value */ + $extraFields = $teams->getExtraData('Teams', $team->ID); + + /* There are 3 extra fields */ + $this->assertEquals(count($extraFields), 3, 'There are 3 extra fields'); + + /* The three extra fields have the correct values */ + $this->assertEquals($extraFields['Role'], 'Goalie', 'The extra field "Role" has the correct value'); + $this->assertEquals($extraFields['Position'], 'Player', 'The extra field "Position" has the correct value'); + $this->assertEquals($extraFields['DateJoined'], '2008-10-10', 'The extra field "DateJoined" has the correct value'); + } + + function testAddingHasManyData() { + $team = DataObject::get_one('ComplexTableFieldTest_Team', "Name = 'The Awesome People'"); + + $this->post('ComplexTableFieldTest_Controller/HasManyForm/field/Sponsors/AddForm', array( + 'Name' => 'Jim Beam', + 'ctf' => array( + 'ClassName' => 'ComplexTableFieldTest_Sponsor', + 'hasManyRelation' => 'Sponsors', + 'parentClass' => 'ComplexTableFieldTest_Team', + 'sourceID' => $team->ID + ) + )); + + /* Retrieve the new sponsor record we created */ + $newSponsor = DataObject::get_one('ComplexTableFieldTest_Sponsor', "Name = 'Jim Beam'"); + + /* A new ComplexTableFieldTest_Sponsor record was created, Name = "Jim Beam" */ + $this->assertNotNull($newSponsor, 'A new ComplexTableFieldTest_Sponsor record was created, Name = "Jim Beam"'); + + /* Get the has-one related Team to the new sponsor that were automatically linked by CTF */ + $teamID = $newSponsor->TeamID; + + /* Automatic many-many relation was set correctly on the new player */ + $this->assertTrue($teamID > 0, 'Automatic has-many/has-one relation was set correctly on the sponsor'); + + /* The other side of the relation works as well */ + $team = DataObject::get_by_id('ComplexTableFieldTest_Team', $teamID); + + /* Let's get the Sponsors component */ + $sponsor = $team->getComponents('Sponsors')->First(); + + /* The sponsor is the same as the one we added */ + $this->assertEquals($newSponsor->ID, $sponsor->ID, 'The sponsor is the same as the one we added'); + } + +} +class ComplexTableFieldTest_Controller extends Controller { + + function Link($action = null) { + return "ComplexTableFieldTest_Controller/$action"; + } + + function ManyManyForm() { + $team = DataObject::get_one('ComplexTableFieldTest_Team', "Name = 'The Awesome People'"); + + $playersField = new ComplexTableField( + $this, + 'Players', + 'ComplexTableFieldTest_Player', + ComplexTableFieldTest_Player::$summary_fields, + 'getCMSFields' + ); + + $playersField->setParentClass('ComplexTableFieldTest_Team'); + + $form = new Form( + $this, + 'ManyManyForm', + new FieldSet( + new HiddenField('ID', '', $team->ID), + $playersField + ), + new FieldSet( + new FormAction('doSubmit', 'Submit') + ) + ); + + $form->disableSecurityToken(); + + return $form; + } + + function HasManyForm() { + $team = DataObject::get_one('ComplexTableFieldTest_Team', "Name = 'The Awesome People'"); + + $sponsorsField = new ComplexTableField( + $this, + 'Sponsors', + 'ComplexTableFieldTest_Sponsor', + ComplexTableFieldTest_Sponsor::$summary_fields, + 'getCMSFields' + ); + + $sponsorsField->setParentClass('ComplexTableFieldTest_Team'); + + $form = new Form( + $this, + 'HasManyForm', + new FieldSet( + new HiddenField('ID', '', $team->ID), + $sponsorsField + ), + new FieldSet( + new FormAction('doSubmit', 'Submit') + ) + ); + + $form->disableSecurityToken(); + + return $form; + } + +} +class ComplexTableFieldTest_Player extends DataObject implements TestOnly { + + public static $db = array( + 'Name' => 'Varchar(100)' + ); + + public static $many_many = array( + 'Teams' => 'ComplexTableFieldTest_Team' + ); + + public static $many_many_extraFields = array( + 'Teams' => array( + 'Role' => 'Varchar(100)', + 'Position' => "Enum('Admin,Player,Coach','Admin')", + 'DateJoined' => 'Date' + ) + ); + +} +class ComplexTableFieldTest_Team extends DataObject implements TestOnly { + + public static $db = array( + 'Name' => 'Varchar(100)' + ); + + public static $belongs_many_many = array( + 'Players' => 'ComplexTableFieldTest_Player' + ); + + public static $has_many = array( + 'Sponsors' => 'ComplexTableFieldTest_Sponsor' + ); + +} +class ComplexTableFieldTest_Sponsor extends DataObject implements TestOnly { + + public static $db = array( + 'Name' => 'Varchar(100)' + ); + + public static $has_one = array( + 'Team' => 'ComplexTableFieldTest_Team' + ); + +} +?> \ No newline at end of file diff --git a/tests/forms/ComplexTableFieldTest.yml b/tests/forms/ComplexTableFieldTest.yml new file mode 100644 index 000000000..9173a8725 --- /dev/null +++ b/tests/forms/ComplexTableFieldTest.yml @@ -0,0 +1,15 @@ +ComplexTableFieldTest_Player: + p1: + Name: Joe Bloggs + p2: + Name: Some Guy +ComplexTableFieldTest_Team: + t1: + Name: The Awesome People + t2: + Name: Incredible Four +ComplexTableFieldTest_Sponsor: + s1: + Name: Coca Cola + s2: + Name: Pepsi \ No newline at end of file