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