From 3619eae9eb40d5c898a05af501adddfd2131774b Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Sat, 21 Nov 2009 17:43:49 +1300 Subject: [PATCH 01/64] MINOR: Don't couple GroupTest to the Member_GroupSet object. --- tests/security/GroupTest.php | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tests/security/GroupTest.php b/tests/security/GroupTest.php index 009511dee..5f73b42e3 100644 --- a/tests/security/GroupTest.php +++ b/tests/security/GroupTest.php @@ -62,13 +62,9 @@ class GroupTest extends FunctionalTest { $form->saveInto($member); $updatedGroups = $member->Groups(); - $controlGroups = new Member_GroupSet( - $adminGroup, - $parentGroup - ); $this->assertEquals( - $updatedGroups->Map('ID','ID'), - $controlGroups->Map('ID','ID'), + array($adminGroup->ID, $parentGroup->ID), + $updatedGroups->column(), "Adding a toplevel group works" ); @@ -82,12 +78,9 @@ class GroupTest extends FunctionalTest { $form->saveInto($member); $member->flushCache(); $updatedGroups = $member->Groups(); - $controlGroups = new Member_GroupSet( - $adminGroup - ); $this->assertEquals( - $updatedGroups->Map('ID','ID'), - $controlGroups->Map('ID','ID'), + array($adminGroup->ID), + $updatedGroups->column(), "Removing a previously added toplevel group works" ); From 199e267bbf8121645922f7f0e5caeeff2a767f6b Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Sun, 22 Nov 2009 16:26:48 +1300 Subject: [PATCH 02/64] MINOR: Don't add 'OFFSET 0' to a query unnecessarily. --- model/Database.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/model/Database.php b/model/Database.php index 0d08f9ac1..72a359b7f 100755 --- a/model/Database.php +++ b/model/Database.php @@ -728,7 +728,7 @@ abstract class SS_Database { if(!array_key_exists('limit',$limit)) user_error('SQLQuery::limit(): Wrong format for $limit', E_USER_ERROR); if(isset($limit['start']) && is_numeric($limit['start']) && isset($limit['limit']) && is_numeric($limit['limit'])) { - $combinedLimit = "$limit[limit] OFFSET $limit[start]"; + $combinedLimit = $limit['start'] ? "$limit[limit] OFFSET $limit[start]" : "$limit[limit]"; } elseif(isset($limit['limit']) && is_numeric($limit['limit'])) { $combinedLimit = (int)$limit['limit']; } else { @@ -740,7 +740,6 @@ abstract class SS_Database { $text .= " LIMIT " . $sqlQuery->limit; } } - return $text; } From 8a9903988fb71a6fad20a7269bf61e884f85bc69 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Sun, 22 Nov 2009 16:40:32 +1300 Subject: [PATCH 03/64] MINOR: Always store SQLQuery::$limit as a 2-element array internally. --- model/SQLQuery.php | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/model/SQLQuery.php b/model/SQLQuery.php index 06e08a230..f1da1bea1 100755 --- a/model/SQLQuery.php +++ b/model/SQLQuery.php @@ -186,12 +186,28 @@ class SQLQuery { /** * Pass LIMIT clause either as SQL snippet or in array format. + * Internally, limit will always be stored as a map containing the keys 'start' and 'limit' * * @param string|array $limit * @return SQLQuery This instance */ public function limit($limit) { - $this->limit = $limit; + if($limit && is_numeric($limit)) { + $this->limit = array( + 'start' => 0, + 'limit' => $limit, + ); + } else if($limit && is_string($limit)) { + if(strpos($limit,',') !== false) list($start, $innerLimit) = explode(',', $limit, 2); + else list($innerLimit, $start) = explode(' OFFSET ', strtoupper($limit), 2); + $this->limit = array( + 'start' => trim($start), + 'limit' => trim($innerLimit), + ); + + } else { + $this->limit = $limit; + } return $this; } From 7fab93f7cba013e9219e2386b4e6097b6c23c5ce Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Sun, 22 Nov 2009 16:42:29 +1300 Subject: [PATCH 04/64] API CHANGE: Added SQLQuery::count(), SQLQuery::firstRow(), and SQLQuery::lastRow() --- model/SQLQuery.php | 57 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/model/SQLQuery.php b/model/SQLQuery.php index f1da1bea1..bdbc83b89 100755 --- a/model/SQLQuery.php +++ b/model/SQLQuery.php @@ -503,6 +503,63 @@ class SQLQuery { return (in_array($SQL_fieldName,$selects) || stripos($sql,"AS {$SQL_fieldName}")); } + + /** + * Return the number of rows in this query if the limit were removed. Useful in paged data sets. + * @return int + * + * TODO Respect HAVING and GROUPBY, which can affect the result-count + */ + function count( $column = null) { + // Choose a default column + if($column == null) { + if($this->groupby) { + $column = 'DISTINCT ' . implode(", ", $this->groupby); + } else { + $column = '*'; + } + } + + $clone = clone $this; + $clone->select = array("count($column)"); + $clone->limit = null; + $clone->orderby = null; + $clone->groupby = null; + + $count = $clone->execute()->value(); + // If there's a limit set, then that limit is going to heavily affect the count + if($this->limit) { + if($count >= ($this->limit['start'] + $this->limit['limit'])) + return $this->limit['limit']; + else + return max(0, $count - $this->limit['start']); + + // Otherwise, the count is going to be the output of the SQL query + } else { + return $count; + } + } + + /** + * Returns a query that returns only the first row of this query + */ + function firstRow() { + $query = clone $this; + $offset = $this->limit ? $this->limit['start'] : 0; + $query->limit(array('start' => $offset, 'limit' => 1)); + return $query; + } + + /** + * Returns a query that returns only the last row of this query + */ + function lastRow() { + $query = clone $this; + $offset = $this->limit ? $this->limit['start'] : 0; + $query->limit(array('start' => $this->count() + $offset - 1, 'limit' => 1)); + return $query; + } + } ?> \ No newline at end of file From 5dd03ca9fb0e7cb407b8fe651d9d65be5443759b Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Sun, 22 Nov 2009 16:47:18 +1300 Subject: [PATCH 05/64] MINOR: Added additional tests for the ORM. --- tests/model/DataObjectTest.php | 65 ++++++++++++++++++++++++++++++---- tests/model/DataObjectTest.yml | 3 +- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/tests/model/DataObjectTest.php b/tests/model/DataObjectTest.php index b252ba3fc..5dc0b68b6 100755 --- a/tests/model/DataObjectTest.php +++ b/tests/model/DataObjectTest.php @@ -177,6 +177,26 @@ class DataObjectTest extends SapphireTest { $comment = DataObject::get_one('DataObjectTest_TeamComment', '', true, '"Name" DESC'); $this->assertEquals('Phil', $comment->Name); } + + function testGetSubclassFields() { + /* Test that fields / has_one relations from the parent table and the subclass tables are extracted */ + $captain1 = $this->objFromFixture("DataObjectTest_Player", "captain1"); + // Base field + $this->assertEquals('Captain 1', $captain1->FirstName); + // Subclass field + $this->assertEquals('007', $captain1->ShirtNumber); + // Subclass has_one relation + $this->assertEquals($this->idFromFixture('DataObjectTest_Team', 'team1'), $captain1->FavouriteTeamID); + } + + function testGetHasOneRelations() { + $captain1 = $this->objFromFixture("DataObjectTest_Player", "captain1"); + /* There will be a field called (relname)ID that contains the ID of the object linked to via the has_one relation */ + $this->assertEquals($this->idFromFixture('DataObjectTest_Team', 'team1'), $captain1->FavouriteTeamID); + /* There will be a method called $obj->relname() that returns the object itself */ + $this->assertEquals($this->idFromFixture('DataObjectTest_Team', 'team1'), $captain1->FavouriteTeam()->ID); + } + /** * Test writing of database columns which don't correlate to a DBField, @@ -201,12 +221,27 @@ class DataObjectTest extends SapphireTest { $team = $this->objFromFixture('DataObjectTest_Team', 'team1'); // Test getComponents() gets the ComponentSet of the other side of the relation - $this->assertTrue($team->getComponents('Comments')->Count() == 2); + $this->assertTrue($page->Comments()->Count() == 2); // Test the IDs on the DataObjects are set correctly - foreach($team->getComponents('Comments') as $comment) { - $this->assertTrue($comment->TeamID == $team->ID); + foreach($page->Comments() as $comment) { + $this->assertTrue($comment->ParentID == $page->ID); } + + // Test that we can add and remove items that already exist in the database + $newComment = new PageComment(); + $newComment->Name = "Automated commenter"; + $newComment->Comment = "This is a new comment"; + $newComment->write(); + $page->Comments()->add($newComment); + $this->assertEquals($page->ID, $newComment->ParentID); + + $comment1 = $this->fixture->objFromFixture('PageComment', 'comment1'); + $comment2 = $this->fixture->objFromFixture('PageComment', 'comment2'); + $page->Comments()->remove($comment2); + + $commentIDs = $page->Comments()->column('ID'); + $this->assertEquals(array($comment1->ID, $newComment->ID), $commentIDs); } function testHasOneRelationship() { @@ -272,6 +307,14 @@ class DataObjectTest extends SapphireTest { $compareTeams->column('ID'), "Removing single record as ID from many_many" ); + + // Set a many-many relationship by and idList + $player1->Teams()->setByIdList(array($team1->ID, $team2->ID)); + $this->assertEquals(array($team1->ID, $team2->ID), $player1->Teams()->column()); + $player1->Teams()->setByIdList(array($team1->ID)); + $this->assertEquals(array($team1->ID), $player1->Teams()->column()); + $player1->Teams()->setByIdList(array($team2->ID)); + $this->assertEquals(array($team2->ID), $player1->Teams()->column()); } /** @@ -378,8 +421,17 @@ class DataObjectTest extends SapphireTest { $obj->FirstName = "New Player"; $this->assertTrue($obj->isChanged()); - $obj->write(); - $this->assertFalse($obj->isChanged()); + $page->write(); + $this->assertFalse($page->isChanged()); + + /* If we perform the same random query twice, it shouldn't return the same results */ + $itemsA = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random()); + foreach($itemsA as $item) $keysA[] = $item->ID; + + $itemsB = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random()); + foreach($itemsB as $item) $keysB[] = $item->ID; + + $this->assertNotEquals($keysA, $keysB); } function testWriteSavesToHasOneRelations() { @@ -1003,7 +1055,8 @@ class DataObjectTest extends SapphireTest { class DataObjectTest_Player extends Member implements TestOnly { static $db = array( - 'IsRetired' => 'Boolean' + 'IsRetired' => 'Boolean', + 'ShirtNumber' => 'Varchar', ); static $has_one = array( diff --git a/tests/model/DataObjectTest.yml b/tests/model/DataObjectTest.yml index 541dc8e50..1bd3be7bc 100644 --- a/tests/model/DataObjectTest.yml +++ b/tests/model/DataObjectTest.yml @@ -6,7 +6,8 @@ DataObjectTest_Team: DataObjectTest_Player: captain1: - FirstName: Captain + FirstName: Captain 1 + ShirtNumber: 007 FavouriteTeam: =>DataObjectTest_Team.team1 Teams: =>DataObjectTest_Team.team1 IsRetired: 1 From 2b991629b8b456f41b9809fab488a59b08f8eaee Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Sun, 22 Nov 2009 16:48:13 +1300 Subject: [PATCH 06/64] API CHANGE: In FieldSet::removeByName(), only match on field Title() if field Name() isn't set. --- forms/FieldSet.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/forms/FieldSet.php b/forms/FieldSet.php index 0f686b14c..9cfc3896d 100755 --- a/forms/FieldSet.php +++ b/forms/FieldSet.php @@ -179,7 +179,10 @@ class FieldSet extends DataObjectSet { foreach($this->items as $i => $child) { if(is_object($child)){ - if(($child->Name() == $fieldName || $child->Title() == $fieldName) && (!$dataFieldOnly || $child->hasData())) { + $childName = $child->Name(); + if(!$childName) $childName = $child->Title(); + + if(($childName == $fieldName) && (!$dataFieldOnly || $child->hasData())) { array_splice( $this->items, $i, 1 ); break; } else if($child->isComposite()) { From de1494e3a8a466d5b529927672b0a0e8ac5e1ef0 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Sun, 22 Nov 2009 18:16:38 +1300 Subject: [PATCH 07/64] ENHANCEMENT: Implemented DataList as the successor of DataObjectSet. DataList doesn't execute the query until it's actually needed, allowing for a more flexible ORM. API CHANGE: augmentSQL is now passed a DataQuery object from which query parameters can be extracted. API CHANGE: DataObjectDecorators that manipulate the query can now define augmentDataQueryCreation(). API CHANGE: The container class argument for DataObject::get() is deprecated. API CHANGE: DataObject::buildSQL() and DataObject::extendedSQL() are deprecated; just use DataObject::get() now. API CHANGE: DataObject::instance_get() and DataObject::instance_get_one() are deprecated, and can no longer be overloaded. API CHANGE: DataObject::buildDataObjectSet() is deprecated. API CHANGE: Cant't call manual manipulation methods on DataList such as insertFirst() --- admin/code/SecurityAdmin.php | 1 - core/model/DataList.php | 383 ++++++++++++++++++ core/model/DataQuery.php | 428 ++++++++++++++++++++ dev/BulkLoader.php | 10 +- forms/CheckboxSetField.php | 3 +- forms/HtmlEditorField.php | 8 +- forms/TableListField.php | 30 +- model/DataObject.php | 281 +++---------- model/DataObjectSet.php | 68 ++-- model/Hierarchy.php | 24 +- model/SQLQuery.php | 99 ++++- model/Versioned.php | 76 ++-- security/Group.php | 1 - security/Member.php | 11 +- security/Permission.php | 5 +- security/Security.php | 16 +- tests/DataQueryTest.php | 14 + tests/api/RestfulServerTest.php | 2 +- tests/forms/TableFieldTest.php | 2 +- tests/model/DataObjectSetTest.php | 5 +- tests/model/DataObjectTest.php | 68 ++-- tests/model/DataObjectTest.yml | 2 +- tests/model/VersionedTest.php | 8 +- tests/security/GroupTest.php | 4 +- tests/security/PermissionRoleTest.php | 4 +- tests/security/PermissionTest.php | 15 + tests/security/SecurityDefaultAdminTest.php | 2 +- 27 files changed, 1170 insertions(+), 400 deletions(-) create mode 100644 core/model/DataList.php create mode 100644 core/model/DataQuery.php create mode 100644 tests/DataQueryTest.php diff --git a/admin/code/SecurityAdmin.php b/admin/code/SecurityAdmin.php index 809877fb7..f13e3b556 100644 --- a/admin/code/SecurityAdmin.php +++ b/admin/code/SecurityAdmin.php @@ -109,7 +109,6 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider { ); // unset 'inlineadd' permission, we don't want inline addition $memberList->setPermissions(array('edit', 'delete', 'add')); - $memberList->setRelationAutoSetting(false); $fields = new FieldSet( new TabSet( diff --git a/core/model/DataList.php b/core/model/DataList.php new file mode 100644 index 000000000..51f491c8a --- /dev/null +++ b/core/model/DataList.php @@ -0,0 +1,383 @@ +sort("Title") is legal, but + * new DataList("SiteTree")->sort("Title") is not. + */ + static function create($dataClass) { + return new DataList($dataClass); + } + + /** + * Create a new DataList. + * No querying is done on construction, but the initial query schema is set up. + * @param $dataClass The DataObject class to query. + */ + public function __construct($dataClass) { + $this->dataClass = $dataClass; + $this->dataQuery = new DataQuery($this->dataClass); + parent::__construct(); + } + + public function dataClass() { + return $this->dataClass; + } + + /** + * Clone this object + */ + function __clone() { + $this->dataQuery = clone $this->dataQuery; + } + + /** + * Return the internal {@link DataQuery} object for direct manipulation + */ + public function dataQuery() { + return $this->dataQuery; + } + /** + * Returns the SQL query that will be used to get this DataList's records. Good for debugging. :-) + */ + public function sql() { + return $this->dataQuery->query()->sql(); + } + + /** + * Filter this data list by a WHERE clause + * @todo Implement array syntax for this. Perhaps the WHERE clause should be $this->where()? + */ + public function filter($filter) { + $this->dataQuery->filter($filter); + return $this; + } + + /** + * Set the sort order of this data list + */ + public function sort($sort, $direction = "ASC") { + if($direction && strtoupper($direction) != 'ASC') $sort = "$sort $direction"; + $this->dataQuery->sort($sort); + return $this; + } + + /** + * Add an join clause to this data list's query. + */ + public function join($join) { + $this->dataQuery->join($join); + return $this; + } + + /** + * Restrict the records returned in this query by a limit clause + */ + public function limit($limit) { + $this->dataQuery->limit($limit); + return $this; + } + + /** + * Add an inner join clause to this data list's query. + */ + public function innerJoin($table, $onClause, $alias = null) { + $this->dataQuery->innerJoin($table, $onClause, $alias); + return $this; + } + + /** + * Add an left join clause to this data list's query. + */ + public function leftJoin($table, $onClause, $alias = null) { + $this->dataQuery->leftJoin($table, $onClause, $alias); + return $this; + } + + /** + * Return an array of the actual items that this DataList contains at this stage. + * This is when the query is actually executed. + */ + protected function generateItems() { + $query = $this->dataQuery->query(); + $this->parseQueryLimit($query); + $rows = $query->execute(); + $results = array(); + foreach($rows as $row) { + $results[] = $this->createDataObject($row); + } + return $results; + } + + /** + * Create a data object from the given SQL row + */ + protected function createDataObject($row) { + $defaultClass = $this->dataClass; + + // Failover from RecordClassName to ClassName + if(empty($row['RecordClassName'])) $row['RecordClassName'] = $row['ClassName']; + + // Instantiate the class mentioned in RecordClassName only if it exists, otherwise default to $this->dataClass + if(class_exists($row['RecordClassName'])) return new $row['RecordClassName']($row); + else return new $defaultClass($row); + } + + /** + * Returns an Iterator for this DataObjectSet. + * This function allows you to use DataObjectSets in foreach loops + * @return DataObjectSet_Iterator + */ + public function getIterator() { + return new DataObjectSet_Iterator($this->generateItems()); + } + + /** + * Convert this DataList to a DataObjectSet. + * Useful if you want to push additional records onto the list. + */ + public function toDataObjectSet() { + $array = array(); + foreach($this as $item) $array[] = $item; + return new DataObjectSet($array); + } + + /** + * Return the number of items in this DataList + */ + function Count() { + return $this->dataQuery->count(); + } + + /** + * Return the maximum value of the given field in this DataList + */ + function Max($field) { + return $this->dataQuery->max($field); + } + + /** + * Return the minimum value of the given field in this DataList + */ + function Min($field) { + return $this->dataQuery->min($field); + } + + /** + * Return the average value of the given field in this DataList + */ + function Avg($field) { + return $this->dataQuery->avg($field); + } + + /** + * Return the sum of the values of the given field in this DataList + */ + function Sum($field) { + return $this->dataQuery->sum($field); + } + + + /** + * Returns the first item in this DataList + */ + function First() { + foreach($this->dataQuery->firstRow()->execute() as $row) { + return $this->createDataObject($row); + } + } + + /** + * Returns the last item in this DataList + */ + function Last() { + foreach($this->dataQuery->lastRow()->execute() as $row) { + return $this->createDataObject($row); + } + } + + /** + * Returns true if this DataList has items + */ + function exists() { + return $this->count() > 0; + } + + /** + * Get a sub-range of this dataobjectset as an array + */ + public function getRange($offset, $length) { + return $this->limit(array('start' => $offset, 'limit' => $length)); + } + + /** + * Find an element of this DataList where the given key = value + */ + public function find($key, $value) { + return $this->filter("\"$key\" = '" . Convert::raw2sql($value) . "'")->First(); + } + + + /** + * Filter this list to only contain the given IDs + */ + public function byIDs(array $ids) { + $baseClass = ClassInfo::baseDataClass($this->dataClass); + $this->filter("\"$baseClass\".\"ID\" IN (" . implode(',', $ids) .")"); + + return $this; + } + + /** + * Return a single column from this DataList. + * @param $colNum The DataObject field to return. + */ + function column($colName = "ID") { + return $this->dataQuery->column($colName); + } + + + // Member altering methods + /** + * Sets the ComponentSet to be the given ID list. + * Records will be added and deleted as appropriate. + * @param array $idList List of IDs. + */ + function setByIDList($idList) { + $has = array(); + + // Index current data + foreach($this->column() as $id) { + $has[$id] = true; + } + + // Keep track of items to delete + $itemsToDelete = $has; + + // add items in the list + // $id is the database ID of the record + if($idList) foreach($idList as $id) { + unset($itemsToDelete[$id]); + if($id && !isset($has[$id])) $this->add($id); + } + + // Remove any items that haven't been mentioned + $this->removeMany(array_keys($itemsToDelete)); + } + + /** + * Returns an array with both the keys and values set to the IDs of the records in this list. + */ + function getIDList() { + $ids = $this->column("ID"); + return $ids ? array_combine($ids, $ids) : array(); + } + + /** + * Returns a HasManyList or ManyMany list representing the querying of a relation across all + * objects in this data list. For it to work, the relation must be defined on the data class + * that you used to create this DataList. + * + * Example: Get members from all Groups: + * + * DataObject::get("Group")->relation("Members") + */ + + function relation($relationName) { + $ids = $this->column('ID'); + return singleton($this->dataClass)->$relationName()->forForeignID($ids); + } + + /** + * Add a number of items to the component set. + * @param array $items Items to add, as either DataObjects or IDs. + */ + function addMany($items) { + foreach($items as $item) { + $this->add($item); + } + } + + /** + * Remove the items from this list with the given IDs + */ + function removeMany($idList) { + foreach($idList as $id) { + $this->remove($id); + } + } + + /** + * Remove every element in this DataList matching the given $filter. + */ + function removeByFilter($filter) { + foreach($this->filter($filter) as $item) { + $this->remove($item); + } + } + + /** + * Remove every element in this DataList. + */ + function removeAll() { + foreach($this as $item) { + $this->remove($item); + } + } + + // These methods are overloaded by HasManyList and ManyMany list to perform + // more sophisticated list manipulation + + function add($item) { + // Nothing needs to happen by default + // TO DO: If a filter is given to this data list then + } + + function remove($item) { + // TO DO: Allow for amendment of this behaviour - for exmaple, we can remove an item from + // an "ActiveItems" DataList by chaning the status to inactive. + + // By default, we remove an item from a DataList by deleting it. + if($item instanceof $this->dataClass) $item->delete(); + + } + + // Methods that won't function on DataLists + + function push($item) { + user_error("Can't call DataList::push() because its data comes from a specific query.", E_USER_ERROR); + } + function insertFirst($item) { + user_error("Can't call DataList::insertFirst() because its data comes from a specific query.", E_USER_ERROR); + } + function shift() { + user_error("Can't call DataList::shift() because its data comes from a specific query.", E_USER_ERROR); + } + function replace() { + user_error("Can't call DataList::replace() because its data comes from a specific query.", E_USER_ERROR); + } + function merge() { + user_error("Can't call DataList::merge() because its data comes from a specific query.", E_USER_ERROR); + } + function removeDuplicates() { + user_error("Can't call DataList::removeDuplicates() because its data comes from a specific query.", E_USER_ERROR); + } + +} + +?> \ No newline at end of file diff --git a/core/model/DataQuery.php b/core/model/DataQuery.php new file mode 100644 index 000000000..534db3568 --- /dev/null +++ b/core/model/DataQuery.php @@ -0,0 +1,428 @@ +dataClass = $dataClass; + $this->initialiseQuery(); + } + + /** + * Clone this object + */ + function __clone() { + $this->query = clone $this->query; + } + + /** + * Return the {@link DataObject} class that is being queried. + */ + function dataClass() { + return $this->dataClass; + } + + /** + * Return the {@link SQLQuery} object that represents the current query; note that it will + * be a clone of the object. + */ + function query() { + return $this->getFinalisedQuery(); + } + + + /** + * Remove a filter from the query + */ + function removeFilterOn($fieldExpression) { + $matched = false; + foreach($this->query->where as $i=>$item) { + if(strpos($item, $fieldExpression) !== false) { + unset($this->query->where[$i]); + $matched = true; + } + } + + if(!$matched) user_error("Couldn't find $fieldExpression in the query filter.", E_USER_WARNING); + + return $this; + } + + /** + * Set up the simplest intial query + */ + function initialiseQuery() { + // Get the tables to join to + $tableClasses = ClassInfo::dataClassesFor($this->dataClass); + + // Error checking + if(!$tableClasses) { + if(!ManifestBuilder::has_been_included()) { + user_error("DataObjects have been requested before the manifest is loaded. Please ensure you are not querying the database in _config.php.", E_USER_ERROR); + } else { + user_error("DataObject::buildSQL: Can't find data classes (classes linked to tables) for $this->dataClass. Please ensure you run dev/build after creating a new DataObject.", E_USER_ERROR); + } + } + + $baseClass = array_shift($tableClasses); + $select = array("\"$baseClass\".*"); + + // Build our intial query + $this->query = new SQLQuery(array()); + $this->query->distinct = true; + + if($sort = singleton($this->dataClass)->stat('default_sort')) { + $this->sort($sort); + } + + $this->query->from("\"$baseClass\""); + $this->selectAllFromTable($this->query, $baseClass); + + singleton($this->dataClass)->extend('augmentDataQueryCreation', $this->query, $this); + } + + /** + * Ensure that the query is ready to execute. + */ + function getFinalisedQuery() { + $query = clone $this->query; + + // Get the tables to join to + $tableClasses = ClassInfo::dataClassesFor($this->dataClass); + $baseClass = array_shift($tableClasses); + + $collidingFields = array(); + + // Join all the tables + if($this->querySubclasses) { + foreach($tableClasses as $tableClass) { + $query->leftJoin($tableClass, "\"$tableClass\".\"ID\" = \"$baseClass\".\"ID\"") ; + $this->selectAllFromTable($query, $tableClass); + } + } + + // Resolve colliding fields + if($this->collidingFields) { + foreach($this->collidingFields as $k => $collisions) { + $caseClauses = array(); + foreach($collisions as $collision) { + if(preg_match('/^"([^"]+)"/', $collision, $matches)) { + $collisionBase = $matches[1]; + $collisionClasses = ClassInfo::subclassesFor($collisionBase); + $caseClauses[] = "WHEN \"$baseClass\".\"ClassName\" IN ('" + . implode("', '", $collisionClasses) . "') THEN $collision"; + } else { + user_error("Bad collision item '$collision'", E_USER_WARNING); + } + } + $query->select[$k] = "CASE " . implode( " ", $caseClauses) . " ELSE NULL END" + . " AS \"$k\""; + } + } + + + if($this->filterByClassName) { + // If querying the base class, don't bother filtering on class name + if($this->dataClass != $baseClass) { + // Get the ClassName values to filter to + $classNames = ClassInfo::subclassesFor($this->dataClass); + if(!$classNames) user_error("DataObject::get() Can't find data sub-classes for '$callerClass'"); + $query->where[] = "\"$baseClass\".\"ClassName\" IN ('" . implode("','", $classNames) . "')"; + } + } + + $query->select[] = "\"$baseClass\".\"ID\""; + $query->select[] = "CASE WHEN \"$baseClass\".\"ClassName\" IS NOT NULL THEN \"$baseClass\".\"ClassName\" ELSE '$baseClass' END AS \"RecordClassName\""; + + // TODO: Versioned, Translatable, SiteTreeSubsites, etc, could probably be better implemented as subclasses of DataQuery + singleton($this->dataClass)->extend('augmentSQL', $query, $this); + + return $query; + } + + /** + * Execute the query and return the result as {@link Query} object. + */ + function execute() { + return $this->getFinalisedQuery()->execute(); + } + + /** + * Return this query's SQL + */ + function sql() { + return $this->getFinalisedQuery()->sql(); + } + + /** + * Return the number of records in this query. + * Note that this will issue a separate SELECT COUNT() query. + */ + function count() { + $baseClass = ClassInfo::baseDataClass($this->dataClass); + return $this->getFinalisedQuery()->count("DISTINCT \"$baseClass\".\"ID\""); + } + + /** + * Return the maximum value of the given field in this DataList + */ + function Max($field) { + return $this->getFinalisedQuery()->aggregate("MAX(\"$field\")")->execute()->value(); + } + + /** + * Return the minimum value of the given field in this DataList + */ + function Min($field) { + return $this->getFinalisedQuery()->aggregate("MIN(\"$field\")")->execute()->value(); + } + + /** + * Return the average value of the given field in this DataList + */ + function Avg($field) { + return $this->getFinalisedQuery()->aggregate("AVG(\"$field\")")->execute()->value(); + } + + /** + * Return the sum of the values of the given field in this DataList + */ + function Sum($field) { + return $this->getFinalisedQuery()->aggregate("SUM(\"$field\")")->execute()->value(); + } + + /** + * Return the first row that would be returned by this full DataQuery + * Note that this will issue a separate SELECT ... LIMIT 1 query. + */ + function firstRow() { + return $this->getFinalisedQuery()->firstRow(); + } + + /** + * Return the last row that would be returned by this full DataQuery + * Note that this will issue a separate SELECT ... LIMIT query. + */ + function lastRow() { + return $this->getFinalisedQuery()->lastRow(); + } + + /** + * Update the SELECT clause of the query with the columns from the given table + */ + protected function selectAllFromTable(SQLQuery &$query, $tableClass) { + // Add SQL for multi-value fields + $databaseFields = DataObject::database_fields($tableClass); + $compositeFields = DataObject::composite_fields($tableClass, false); + if($databaseFields) foreach($databaseFields as $k => $v) { + if(!isset($compositeFields[$k])) { + // Update $collidingFields if necessary + if(isset($query->select[$k])) { + if(!isset($this->collidingFields[$k])) $this->collidingFields[$k] = array($query->select[$k]); + $this->collidingFields[$k][] = "\"$tableClass\".\"$k\""; + + } else { + $query->select[$k] = "\"$tableClass\".\"$k\""; + } + } + } + if($compositeFields) foreach($compositeFields as $k => $v) { + if($v) { + $dbO = Object::create_from_string($v, $k); + $dbO->addToQuery($query); + } + } + } + + /** + * Set the HAVING clause of this query + */ + function having($having) { + if($having) { + $clone = $this; + $clone->query->having[] = $having; + return $clone; + } else { + return $this; + } + } + + /** + * Set the WHERE clause of this query + */ + function filter($filter) { + if($filter) { + $clone = $this; + $clone->query->where($filter); + return $clone; + } else { + return $this; + } + } + + /** + * Set the ORDER BY clause of this query + */ + function sort($sort) { + if($sort) { + $clone = $this; + // Add quoting to sort expression if it's a simple column name + if(!is_array($sort) && preg_match('/^[A-Z][A-Z0-9_]*$/i', $sort)) $sort = "\"$sort\""; + $clone->query->orderby($sort); + return $clone; + } else { + return $this; + } + } + + /** + * Set the limit of this query + */ + function limit($limit) { + if($limit) { + $clone = $this; + $clone->query->limit($limit); + return $clone; + } else { + return $this; + } + } + + /** + * Add a join clause to this query + * @deprecated Use innerJoin() or leftJoin() instead. + */ + function join($join) { + if($join) { + $clone = $this; + $clone->query->from[] = $join; + // TODO: This needs to be resolved for all databases + if(DB::getConn() instanceof MySQLDatabase) $clone->query->groupby[] = reset($clone->query->from) . ".\"ID\""; + return $clone; + } else { + return $this; + } + } + + /** + * Add an INNER JOIN clause to this queyr + * @param $table The table to join to. + * @param $onClause The filter for the join. + */ + public function innerJoin($table, $onClause, $alias = null) { + if($table) { + $clone = $this; + $clone->query->innerJoin($table, $onClause, $alias); + return $clone; + } else { + return $this; + } + } + + /** + * Add a LEFT JOIN clause to this queyr + * @param $table The table to join to. + * @param $onClause The filter for the join. + */ + public function leftJoin($table, $onClause, $alias = null) { + if($table) { + $clone = $this; + $clone->query->leftJoin($table, $onClause, $alias); + return $clone; + } else { + return $this; + } + } + + /** + * Select the given fields from the given table + */ + public function selectFromTable($table, $fields) { + $fieldExpressions = array_map(create_function('$item', + "return '\"$table\".\"' . \$item . '\"';"), $fields); + + $this->select($fieldExpressions); + } + + /** + * Query the given field column from the database and return as an array. + */ + public function column($field = 'ID') { + $query = $this->getFinalisedQuery(); + $query->select($this->expressionForField($field, $query)); + return $query->execute()->column(); + } + + protected function expressionForField($field, $query) { + // Special case for ID + if($field == 'ID') { + $baseClass = ClassInfo::baseDataClass($this->dataClass); + return "\"$baseClass\".\"ID\""; + + } else { + return $query->expressionForField($field); + } + } + + /** + * Clear the selected fields to start over + */ + public function clearSelect() { + $this->query->select = array(); + + return $this; + } + + /** + * Select the given field expressions. You must do your own escaping + */ + protected function select($fieldExpressions) { + $this->query->select = array_merge($this->query->select, $fieldExpressions); + } + + //// QUERY PARAMS + + /** + * An arbitrary store of query parameters that can be used by decorators. + * @todo This will probably be made obsolete if we have subclasses of DataList and/or DataQuery. + */ + private $queryParams; + + /** + * Set an arbitrary query parameter, that can be used by decorators to add additional meta-data to the query. + * It's expected that the $key will be namespaced, e.g, 'Versioned.stage' instead of just 'stage'. + */ + function setQueryParam($key, $value) { + $this->queryParams[$key] = $value; + } + + /** + * Set an arbitrary query parameter, that can be used by decorators to add additional meta-data to the query. + */ + function getQueryParam($key) { + if(isset($this->queryParams[$key])) return $this->queryParams[$key]; + else return null; + } + +} + +?> \ No newline at end of file diff --git a/dev/BulkLoader.php b/dev/BulkLoader.php index 5f471625d..20d82743b 100644 --- a/dev/BulkLoader.php +++ b/dev/BulkLoader.php @@ -140,15 +140,7 @@ abstract class BulkLoader extends ViewableData { //get all instances of the to be imported data object if($this->deleteExistingRecords) { - $q = singleton($this->objectClass)->buildSQL(); - $q->select = array('"ID"'); - $ids = $q->execute()->column('ID'); - foreach($ids as $id) { - $obj = DataObject::get_by_id($this->objectClass, $id); - $obj->delete(); - $obj->destroy(); - unset($obj); - } + DataObject::get($this->objectClass)->removeAll(); } return $this->processAll($filepath); diff --git a/forms/CheckboxSetField.php b/forms/CheckboxSetField.php index d828432cb..ef5a1635e 100755 --- a/forms/CheckboxSetField.php +++ b/forms/CheckboxSetField.php @@ -164,8 +164,7 @@ class CheckboxSetField extends OptionsetField { // If we're not passed a value directly, we can look for it in a relation method on the object passed as a second arg if(!$value && $obj && $obj instanceof DataObject && $obj->hasMethod($this->name)) { $funcName = $this->name; - $selected = $obj->$funcName(); - $value = $selected->toDropdownMap('ID', 'ID'); + $value = $obj->$funcName()->getIDList(); } parent::setValue($value, $obj); diff --git a/forms/HtmlEditorField.php b/forms/HtmlEditorField.php index 63f2c310d..3a7afb1c4 100755 --- a/forms/HtmlEditorField.php +++ b/forms/HtmlEditorField.php @@ -142,8 +142,7 @@ class HtmlEditorField extends TextareaField { // Save file & link tracking data. if(class_exists('SiteTree')) { if($record->ID && $record->many_many('LinkTracking') && $tracker = $record->LinkTracking()) { - $filter = sprintf('"FieldName" = \'%s\' AND "SiteTreeID" = %d', $this->name, $record->ID); - DB::query("DELETE FROM \"$tracker->tableName\" WHERE $filter"); + $tracker->removeByFilter(sprintf('"FieldName" = \'%s\' AND "SiteTreeID" = %d', $this->name, $record->ID)); if($linkedPages) foreach($linkedPages as $item) { $SQL_fieldName = Convert::raw2sql($this->name); @@ -151,10 +150,9 @@ class HtmlEditorField extends TextareaField { VALUES ($record->ID, $item, '$SQL_fieldName')"); } } - + if($record->ID && $record->many_many('ImageTracking') && $tracker = $record->ImageTracking()) { - $filter = sprintf('"FieldName" = \'%s\' AND "SiteTreeID" = %d', $this->name, $record->ID); - DB::query("DELETE FROM \"$tracker->tableName\" WHERE $filter"); + $tracker->removeByFilter(sprintf('"FieldName" = \'%s\' AND "SiteTreeID" = %d', $this->name, $record->ID)); $fieldName = $this->name; if($linkedFiles) foreach($linkedFiles as $item) { diff --git a/forms/TableListField.php b/forms/TableListField.php index 4dd94650f..06b45e7fe 100755 --- a/forms/TableListField.php +++ b/forms/TableListField.php @@ -496,12 +496,19 @@ JS $query = singleton($this->sourceClass)->extendedSQL($this->sourceFilter(), $this->sourceSort, null, $this->sourceJoin); } - if(!empty($_REQUEST['ctf'][$this->Name()]['sort'])) { - $column = $_REQUEST['ctf'][$this->Name()]['sort']; - $dir = 'ASC'; - if(!empty($_REQUEST['ctf'][$this->Name()]['dir'])) { - $dir = $_REQUEST['ctf'][$this->Name()]['dir']; - if(strtoupper(trim($dir)) == 'DESC') $dir = 'DESC'; + if(!$this->dataList) { + user_error(get_class($this). ' is missing a DataList', E_USER_ERROR); + } + + $dl = clone $this->dataList; + + if(isset($_REQUEST['ctf'][$this->Name()]['sort'])) { + $query = $this->dataList->dataQuery()->query(); + $SQL_sort = Convert::raw2sql($_REQUEST['ctf'][$this->Name()]['sort']); + $sql = $query->sql(); + // see {isFieldSortable} + if(in_array($SQL_sort,$query->select) || stripos($sql,"AS {$SQL_sort}")) { + $dl->sort($SQL_sort); } if($query->canSortBy($column)) $query->orderby = $column.' '.$dir; } @@ -1210,17 +1217,6 @@ JS return $this->Link(); } - /** - * @return Int - */ - function sourceID() { - $idField = $this->form->dataFieldByName('ID'); - if(!isset($idField)) { - user_error("TableListField needs a formfield named 'ID' to be present", E_USER_ERROR); - } - return $idField->Value(); - } - /** * Helper method to determine permissions for a scaffolded * TableListField (or subclasses) - currently used in {@link ModelAdmin} and {@link DataObject->scaffoldFormFields()}. diff --git a/model/DataObject.php b/model/DataObject.php index b6991cd00..5e22b1aec 100755 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -444,7 +444,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity private function duplicateRelations($sourceObject, $destinationObject, $name) { $relations = $sourceObject->$name(); if ($relations) { - if ($relations instanceOf ComponentSet) { //many-to-something relation + if ($relations instanceOf RelationList) { //many-to-something relation if ($relations->Count() > 0) { //with more than one thing it is related to foreach($relations as $relation) { $destinationObject->$name()->add($relation); @@ -1192,18 +1192,19 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity user_error("$this->class has a broken onBeforeDelete() function. Make sure that you call parent::onBeforeDelete().", E_USER_ERROR); } - // Deleting a record without an ID shouldn't do anything - if(!$this->ID) throw new Exception("DataObject::delete() called on a DataObject without an ID"); - - foreach($this->getClassAncestry() as $ancestor) { - if(self::has_own_table($ancestor)) { - $sql = new SQLQuery(); - $sql->delete = true; - $sql->from[$ancestor] = "\"$ancestor\""; - $sql->where[] = "\"ID\" = $this->ID"; - $this->extend('augmentSQL', $sql); - $sql->execute(); - } + // Deleting a record without an ID shouldn't do anything + if(!$this->ID) throw new Exception("DataObject::delete() called on a DataObject without an ID"); + + // TODO: This is quite ugly. To improve: + // - move the details of the delete code in the DataQuery system + // - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest + // obviously, that means getting requireTable() to configure cascading deletes ;-) + $srcQuery = DataList::create($this->class)->filter("ID = $this->ID")->dataQuery()->query(); + foreach($srcQuery->queriedTables() as $table) { + $query = new SQLQuery("*", array('"'.$table.'"')); + $query->where("\"ID\" = $this->ID"); + $query->delete = true; + $query->execute(); } // Remove this item out of any caches $this->flushCache(); @@ -2590,162 +2591,12 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } /** - * Build a {@link SQLQuery} object to perform the given query. - * - * @param string $filter A filter to be inserted into the WHERE clause. - * @param string|array $sort A sort expression to be inserted into the ORDER BY clause. If omitted, self::$default_sort will be used. - * @param string|array $limit A limit expression to be inserted into the LIMIT clause. - * @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned. - * @param boolean $restictClasses Restrict results to only objects of either this class of a subclass of this class - * @param string $having A filter to be inserted into the HAVING clause. - * - * @return SQLQuery Query built. + * @deprecated 2.5 Use DataObject::get() instead, with the new data mapper there's no reason not to. */ public function buildSQL($filter = "", $sort = "", $limit = "", $join = "", $restrictClasses = true, $having = "") { - // Cache the big hairy part of buildSQL - if(!isset(self::$cache_buildSQL_query[$this->class])) { - // Get the tables to join to - $tableClasses = ClassInfo::dataClassesFor($this->class); - if(!$tableClasses) { - if (!DB::getConn()) { - throw new Exception('DataObjects have been requested before' - . ' a DB connection has been made. Please ensure you' - . ' are not querying the database in _config.php.'); - } else { - user_error("DataObject::buildSQL: Can't find data classes (classes linked to tables) for $this->class. Please ensure you run dev/build after creating a new DataObject.", E_USER_ERROR); - } - } + user_error("DataObject::buildSQL() deprecated; just use DataObject::get() with the new data mapper", E_USER_NOTICE); + return $this->extendedSQL($filter, $sort, $limit, $join, $having); - $baseClass = array_shift($tableClasses); - - - // $collidingFields will keep a list fields that appear in mulitple places in the class - // heirarchy for this table. They will be dealt with more explicitly in the SQL query - // to ensure that junk data from other tables doesn't corrupt data objects - $collidingFields = array(); - - // Build our intial query - $query = new SQLQuery(array()); - $query->from("\"$baseClass\""); - - // Add SQL for multi-value fields on the base table - $databaseFields = self::database_fields($baseClass); - if($databaseFields) foreach($databaseFields as $k => $v) { - if(!in_array($k, array('ClassName', 'LastEdited', 'Created')) && ClassInfo::classImplements($v, 'CompositeDBField')) { - $this->dbObject($k)->addToQuery($query); - } else { - $query->select[$k] = "\"$baseClass\".\"$k\""; - } - } - // Join all the tables - if($tableClasses && self::$subclass_access) { - foreach($tableClasses as $tableClass) { - $query->from[$tableClass] = "LEFT JOIN \"$tableClass\" ON \"$tableClass\".\"ID\" = \"$baseClass\".\"ID\""; - - // Add SQL for multi-value fields - $databaseFields = self::database_fields($tableClass); - $compositeFields = self::composite_fields($tableClass, false); - if($databaseFields) foreach($databaseFields as $k => $v) { - if(!isset($compositeFields[$k])) { - // Update $collidingFields if necessary - if(isset($query->select[$k])) { - if(!isset($collidingFields[$k])) $collidingFields[$k] = array($query->select[$k]); - $collidingFields[$k][] = "\"$tableClass\".\"$k\""; - - } else { - $query->select[$k] = "\"$tableClass\".\"$k\""; - } - } - } - if($compositeFields) foreach($compositeFields as $k => $v) { - $dbO = $this->dbObject($k); - if($dbO) $dbO->addToQuery($query); - } - } - } - - // Resolve colliding fields - if($collidingFields) { - foreach($collidingFields as $k => $collisions) { - $caseClauses = array(); - foreach($collisions as $collision) { - if(preg_match('/^"([^"]+)"/', $collision, $matches)) { - $collisionBase = $matches[1]; - $collisionClasses = ClassInfo::subclassesFor($collisionBase); - $caseClauses[] = "WHEN \"$baseClass\".\"ClassName\" IN ('" - . implode("', '", $collisionClasses) . "') THEN $collision"; - } else { - user_error("Bad collision item '$collision'", E_USER_WARNING); - } - } - $query->select[$k] = "CASE " . implode( " ", $caseClauses) . " ELSE NULL END" - . " AS \"$k\""; - } - } - - - $query->select[] = "\"$baseClass\".\"ID\""; - $query->select[] = "CASE WHEN \"$baseClass\".\"ClassName\" IS NOT NULL THEN \"$baseClass\".\"ClassName\" ELSE '$baseClass' END AS \"RecordClassName\""; - - // Get the ClassName values to filter to - $classNames = ClassInfo::subclassesFor($this->class); - - if(!$classNames) { - user_error("DataObject::get() Can't find data sub-classes for '$callerClass'"); - } - - // If querying the base class, don't bother filtering on class name - if($restrictClasses && $this->class != $baseClass) { - // Get the ClassName values to filter to - $classNames = ClassInfo::subclassesFor($this->class); - if(!$classNames) { - user_error("DataObject::get() Can't find data sub-classes for '$callerClass'"); - } - - $query->where[] = "\"$baseClass\".\"ClassName\" IN ('" . implode("','", $classNames) . "')"; - } - self::$cache_buildSQL_query[$this->class] = clone $query; - } else { - $query = clone self::$cache_buildSQL_query[$this->class]; - - } - - // Find a default sort - if(!$sort) { - $sort = $this->stat('default_sort'); - } - // Add quoting to sort expression if it's a simple column name - if(preg_match('/^[A-Z][A-Z0-9_]*$/i', $sort)) $sort = "\"$sort\""; - - $query->where($filter); - $query->orderby($sort); - $query->limit($limit); - - - if($having) { - $query->having[] = $having; - } - - if($join) { - $query->from[] = $join; - // In order to group by unique columns we have to group by everything listed in the select - foreach($query->select as $field) { - // Skip the _SortColumns; these are only going to be aggregate functions - if(preg_match('/AS\s+\"?_SortColumn/', $field, $matches)) { - - // Identify columns with aliases, and ignore the alias. Making use of the alias in - // group by was causing problems when those queries were subsequently passed into - // SQLQuery::unlimitedRowCount. - } else if(preg_match('/^(.*)\s+AS\s+(\"[^"]+\")\s*$/', $field, $matches)) { - $query->groupby[] = $matches[1]; - // Otherwise just use the field as is - } else { - $query->groupby[] = $field; - } - } - } - - return $query; } /** @@ -2754,21 +2605,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity private static $cache_buildSQL_query; /** - * Like {@link buildSQL}, but applies the extension modifications. - * - * @uses DataExtension->augmentSQL() - * - * @param string $filter A filter to be inserted into the WHERE clause. - * @param string|array $sort A sort expression to be inserted into the ORDER BY clause. If omitted, self::$default_sort will be used. - * @param string|array $limit A limit expression to be inserted into the LIMIT clause. - * @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned. - * @param string $having A filter to be inserted into the HAVING clause. - * @return SQLQuery Query built + * @deprecated 2.5 Use DataObject::get() instead, with the new data mapper there's no reason not to. */ - public function extendedSQL($filter = "", $sort = "", $limit = "", $join = "", $having = ""){ - $query = $this->buildSQL($filter, $sort, $limit, $join, true, $having); - $this->extend('augmentSQL', $query); - return $query; + public function extendedSQL($filter = "", $sort = "", $limit = "", $join = ""){ + $dataList = DataObject::get($this->class, $filter, $sort, $join, $limit); + return $dataList->dataQuery()->query(); } /** @@ -2784,14 +2625,36 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * * @return mixed The objects matching the filter, in the class specified by $containerClass */ - public static function get($callerClass, $filter = "", $sort = "", $join = "", $limit = "", $containerClass = "DataObjectSet") { - return singleton($callerClass)->instance_get($filter, $sort, $join, $limit, $containerClass); + public static function get($callerClass, $filter = "", $sort = "", $join = "", $limit = "", $containerClass = "DataList") { + // Deprecated 2.5? + // Todo: Make the $containerClass method redundant + if($containerClass != "DataList") user_error("The DataObject::get() \$containerClass argument has been deprecated", E_USER_NOTICE); + $result = DataList::create($callerClass)->filter($filter)->sort($sort)->join($join)->limit($limit); + return $result; + } + + /** + * @deprecated + */ + public function Aggregate($class = null) { + if($class) return new DataList($class); + else if(isset($this)) return new DataList(get_class($this)); + else throw new InvalidArgumentException("DataObject::aggregate() must be called as an instance method or passed a classname"); + } + + /** + * @deprecated + */ + public function RelationshipAggregate($relationship) { + return $this->$relationship(); } /** * The internal function that actually performs the querying for get(). * DataObject::get("Table","filter") is the same as singleton("Table")->instance_get("filter") * + * @deprecated 2.5 Use DataObject::get() + * * @param string $filter A filter to be inserted into the WHERE clause. * @param string $sort A sort expression to be inserted into the ORDER BY clause. If omitted, self::$default_sort will be used. * @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned. @@ -2801,22 +2664,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @return mixed The objects matching the filter, in the class specified by $containerClass */ public function instance_get($filter = "", $sort = "", $join = "", $limit="", $containerClass = "DataObjectSet") { - if(!DB::isActive()) { - user_error("DataObjects have been requested before the database is ready. Please ensure your database connection details are correct, your database has been built, and that you are not trying to query the database in _config.php.", E_USER_ERROR); - } - - $query = $this->extendedSQL($filter, $sort, $limit, $join); - - $records = $query->execute(); - - $ret = $this->buildDataObjectSet($records, $containerClass, $query, $this->class); - if($ret) $ret->parseQueryLimit($query); + user_error("instance_get deprecated", E_USER_NOTICE); + return self::get($this->class, $filter, $sort, $join, $limit, $containerClass); - return $ret; } /** * Take a database {@link SS_Query} and instanciate an object for each record. + * + * @deprecated 2.5 Use DataObject::get(), you don't need to side-step it any more * * @param SS_Query|array $records The database records, a {@link SS_Query} object or an array of maps. * @param string $containerClass The class to place all of the objects into. @@ -2824,6 +2680,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @return mixed The new objects in an object of type $containerClass */ function buildDataObjectSet($records, $containerClass = "DataObjectSet", $query = null, $baseClass = null) { + user_error('buildDataObjectSet is deprecated; use DataList to do your querying', E_USER_NOTICE); + foreach($records as $record) { if(empty($record['RecordClassName'])) { $record['RecordClassName'] = $record['ClassName']; @@ -2879,7 +2737,9 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity ] = false; } if(!$cache || !isset(DataObject::$cache_get_one[$callerClass][$cacheKey])) { - $item = $SNG->instance_get_one($filter, $orderby); + $dl = DataList::create($callerClass)->filter($filter)->sort($orderby); + $item = $dl->First(); + if($cache) { DataObject::$cache_get_one[$callerClass][$cacheKey] = $item; if(!DataObject::$cache_get_one[$callerClass][$cacheKey]) { @@ -2935,6 +2795,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity /** * Does the hard work for get_one() + * + * @deprecated 2.5 Use DataObject::get_one() instead * * @uses DataExtension->augmentSQL() * @@ -2943,35 +2805,8 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @return DataObject The first item matching the query */ public function instance_get_one($filter, $orderby = null) { - if(!DB::isActive()) { - user_error("DataObjects have been requested before the database is ready. Please ensure your database connection details are correct, your database has been built, and that you are not trying to query the database in _config.php.", E_USER_ERROR); - } - - $query = $this->buildSQL($filter); - $query->limit = "1"; - if($orderby) { - $query->orderby = $orderby; - } - - $this->extend('augmentSQL', $query); - - $records = $query->execute(); - $records->rewind(); - $record = $records->current(); - - if($record) { - // Mid-upgrade, the database can have invalid RecordClassName values that need to be guarded against. - if(class_exists($record['RecordClassName'])) { - $record = new $record['RecordClassName']($record); - } else { - $record = new $this->class($record); - } - - // Rather than restrict classes at the SQL-query level, we now check once the object has been instantiated - // This lets us check up on weird errors where the class has been incorrectly set, and give warnings to our - // developers - return $record; - } + user_error("DataObjct::instance_get_one is deprecated", E_USER_NOTICE); + return DataObject::get_one($this->class, $filter, true, $orderby); } /** diff --git a/model/DataObjectSet.php b/model/DataObjectSet.php index daa67ab24..854947c57 100644 --- a/model/DataObjectSet.php +++ b/model/DataObjectSet.php @@ -126,7 +126,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable * Destory all of the DataObjects in this set. */ public function destroy() { - foreach($this->items as $item) { + foreach($this as $item) { $item->destroy(); } } @@ -144,14 +144,10 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable * @return array */ public function toArray($index = null) { - if(!$index) { - return $this->items; - } - $map = array(); - - foreach($this->items as $item) { - $map[$item->$index] = $item; + foreach($this as $item) { + if($index) $map[$item->$index] = $item; + else $map[] = $item; } return $map; @@ -169,7 +165,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable $map = array(); - foreach( $this->items as $item ) { + foreach( $this as $item ) { $map[$item->$index] = $item->getAllFields(); } @@ -584,7 +580,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable * @return boolean */ public function exists() { - return (bool)$this->items; + return $this->count() > 0; } /** @@ -616,7 +612,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable * @return int */ public function TotalItems() { - return $this->totalSize ? $this->totalSize : sizeof($this->items); + return $this->totalSize ? $this->totalSize : $this->Count(); } /** @@ -632,9 +628,9 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable * @return string */ public function UL() { - if($this->items) { + if($this->exists()) { $result = "
    \n"; - foreach($this->items as $item) { + foreach($this as $item) { $result .= "
  • Link\">$item->Title
  • \n"; } $result .= "
\n"; @@ -662,29 +658,27 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable */ public function map($index = 'ID', $titleField = 'Title', $emptyString = null, $sort = false) { $map = array(); - - if($this->items) { - foreach($this->items as $item) { - $map[$item->$index] = ($item->hasMethod($titleField)) ? $item->$titleField() : $item->$titleField; - } + foreach($this as $item) { + $map[$item->$index] = ($item->hasMethod($titleField)) + ? $item->$titleField() : $item->$titleField; } + if($emptyString) $map = array('' => $emptyString) + $map; - if($emptyString) $map = array('' => "$emptyString") + $map; if($sort) asort($map); return $map; } - - /** - * Find an item in this list where the field $key is equal to $value - * Eg: $doSet->find('ID', 4); - * @return ViewableData The first matching item. - */ - public function find($key, $value) { - foreach($this->items as $item) { - if($item->$key == $value) return $item; - } - } + + /** + * Find an item in this list where the field $key is equal to $value + * Eg: $doSet->find('ID', 4); + * @return ViewableData The first matching item. + */ + public function find($key, $value) { + foreach($this as $item) { + if($item->$key == $value) return $item; + } + } /** * Return a column of the given field @@ -693,7 +687,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable */ public function column($value = "ID") { $list = array(); - foreach($this->items as $item ){ + foreach($this as $item ){ $list[] = ($item->hasMethod($value)) ? $item->$value() : $item->$value; } return $list; @@ -705,11 +699,9 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable * @param string $index The field name to index the array by. * @return array */ - public function groupBy($index) { - $result = array(); - foreach($this->items as $item) { + public function groupBy($index){ + foreach($this as $item ){ $key = ($item->hasMethod($index)) ? $item->$index() : $item->$index; - if(!isset($result[$key])) { $result[$key] = new DataObjectSet(); } @@ -898,7 +890,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable // Put this item into the array indexed by $groupField. // the keys are later used to retrieve the top-level records - foreach( $this->items as $item ) { + foreach( $this as $item ) { $groupedSet[$item->$groupField][] = $item; } @@ -994,7 +986,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable */ function containsIDs($idList) { foreach($idList as $item) $wants[$item] = true; - foreach($this->items as $item) if($item) unset($wants[$item->ID]); + foreach($this as $item) if($item) unset($wants[$item->ID]); return !$wants; } @@ -1004,7 +996,7 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable * @param $idList An array of object IDs */ function onlyContainsIDs($idList) { - return $this->containsIDs($idList) && sizeof($idList) == sizeof($this->items); + return $this->containsIDs($idList) && sizeof($idList) == $this->count(); } } diff --git a/model/Hierarchy.php b/model/Hierarchy.php index 4fc7f5360..eb71f50d3 100755 --- a/model/Hierarchy.php +++ b/model/Hierarchy.php @@ -484,11 +484,9 @@ class Hierarchy extends DataExtension { */ public function numHistoricalChildren() { if(!$this->owner->hasExtension('Versioned')) throw new Exception('Hierarchy->AllHistoricalChildren() only works with Versioned extension applied'); - - $query = Versioned::get_including_deleted_query(ClassInfo::baseDataClass($this->owner->class), - "\"ParentID\" = " . (int)$this->owner->ID); - - return $query->unlimitedRowCount(); + + return Versioned::get_including_deleted(ClassInfo::baseDataClass($this->owner->class), + "\"ParentID\" = " . (int)$this->owner->ID)->count(); } /** @@ -500,20 +498,11 @@ class Hierarchy extends DataExtension { * @return int */ public function numChildren($cache = true) { - $baseClass = ClassInfo::baseDataClass($this->owner->class); - // Build the cache for this class if it doesn't exist. if(!$cache || !is_numeric($this->_cache_numChildren)) { - // We build the query in an extension-friendly way. - $query = new SQLQuery( - "COUNT(*)", - "\"$baseClass\"", - sprintf('"ParentID" = %d', $this->owner->ID) - ); - $this->owner->extend('augmentSQL', $query); - $this->owner->extend('augmentNumChildrenCountQuery', $query); - - $this->_cache_numChildren = (int)$query->execute()->value(); + // Hey, this is efficient now! + // We call stageChildren(), because Children() has canView() filtering + $this->_cache_numChildren = (int)$this->owner->stageChildren(true)->Count(); } // If theres no value in the cache, it just means that it doesn't have any children. @@ -540,7 +529,6 @@ class Hierarchy extends DataExtension { . (int)$this->owner->ID . " AND \"{$baseClass}\".\"ID\" != " . (int)$this->owner->ID . $extraFilter, ""); - if(!$staged) $staged = new DataObjectSet(); $this->owner->extend("augmentStageChildren", $staged, $showAll); return $staged; } diff --git a/model/SQLQuery.php b/model/SQLQuery.php index bdbc83b89..d655ce3e1 100755 --- a/model/SQLQuery.php +++ b/model/SQLQuery.php @@ -127,6 +127,31 @@ class SQLQuery { return $this; } + /** + * Add addition columns to the select clause + */ + public function selectMore($fields) { + if (func_num_args() > 1) $fields = func_get_args(); + if(is_array($fields)) { + foreach($fields as $field) $this->select[] = $field; + } else { + $this->select[] = $fields; + } + } + + /** + * Return the SQL expression for the given field + * @todo This should be refactored after $this->select is changed to make that easier + */ + public function expressionForField($field) { + foreach($this->select as $sel) { + if(preg_match('/AS +"?([^"]*)"?/i', $sel, $matches)) $selField = $matches[1]; + else if(preg_match('/"([^"]*)"\."([^"]*)"/', $sel, $matches)) $selField = $matches[2]; + else if(preg_match('/"?([^"]*)"?/', $sel, $matches)) $selField = $matches[2]; + if($selField == $field) return $sel; + } + } + /** * Specify the target table to select from. * @@ -156,7 +181,7 @@ class SQLQuery { if( !$tableAlias ) { $tableAlias = $table; } - $this->from[$tableAlias] = "LEFT JOIN \"$table\" AS \"$tableAlias\" ON $onPredicate"; + $this->from[$tableAlias] = array('type' => 'LEFT', 'table' => $table, 'filter' => array($onPredicate)); return $this; } @@ -173,10 +198,25 @@ class SQLQuery { if( !$tableAlias ) { $tableAlias = $table; } - $this->from[$tableAlias] = "INNER JOIN \"$table\" AS \"$tableAlias\" ON $onPredicate"; + $this->from[$tableAlias] = array('type' => 'INNER', 'table' => $table, 'filter' => array($onPredicate)); return $this; } + /** + * Add an additional filter (part of the ON clause) on a join + */ + public function addFilterToJoin($tableAlias, $filter) { + $this->from[$tableAlias]['filter'][] = $filter; + } + + /** + * Replace the existing filter (ON clause) on a join + */ + public function setJoinFilter($tableAlias, $filter) { + if(is_string($this->from[$tableAlias])) {Debug::message($tableAlias); Debug::dump($this->from);} + $this->from[$tableAlias]['filter'] = array($filter); + } + /** * Returns true if we are already joining to the given table alias */ @@ -184,6 +224,27 @@ class SQLQuery { return isset($this->from[$tableAlias]); } + /** + * Return a list of tables that this query is selecting from. + */ + public function queriedTables() { + $tables = array(); + foreach($this->from as $key => $tableClause) { + if(is_array($tableClause)) $table = $tableClause['table']; + else if(is_string($tableClause) && preg_match('/JOIN +("[^"]+") +(AS|ON) +/i', $tableClause, $matches)) $table = $matches[1]; + else $table = $tableClause; + + // Handle string replacements + if($this->replacementsOld) $table = str_replace($this->replacementsOld, $this->replacementsNew, $table); + + $tables[] = preg_replace('/^"|"$/','',$table); + } + return $tables; + + } + + + /** * Pass LIMIT clause either as SQL snippet or in array format. * Internally, limit will always be stored as a map containing the keys 'start' and 'limit' @@ -394,6 +455,20 @@ class SQLQuery { * @return string */ function sql() { + // Build from clauses + foreach($this->from as $alias => $join) { + // $join can be something like this array structure + // array('type' => 'inner', 'table' => 'SiteTree', 'filter' => array("SiteTree.ID = 1", "Status = 'approved'")) + if(is_array($join)) { + if(is_string($join['filter'])) $filter = $join['filter']; + else if(sizeof($join['filter']) == 1) $filter = $join['filter'][0]; + else $filter = "(" . implode(") AND (", $join['filter']) . ")"; + + $this->from[$alias] = strtoupper($join['type']) . " JOIN \"{$join['table']}\" AS \"$alias\" ON $filter"; + } + } + + $sql = DB::getConn()->sqlQueryToString($this); if($this->replacementsOld) $sql = str_replace($this->replacementsOld, $this->replacementsNew, $sql); return $sql; @@ -539,6 +614,26 @@ class SQLQuery { return $count; } } + + /** + * Return a new SQLQuery that calls the given aggregate functions on this data. + * @param $columns An aggregate expression, such as 'MAX("Balance")', or an array of them. + */ + function aggregate($columns) { + if(!is_array($columns)) $columns = array($columns); + + if($this->groupby || $this->limit) { + throw new Exception("SQLQuery::aggregate() doesn't work with groupby or limit, yet"); + } + + $clone = clone $this; + $clone->limit = null; + $clone->orderby = null; + $clone->groupby = null; + $clone->select = $columns; + + return $clone; + } /** * Returns a query that returns only the first row of this query diff --git a/model/Versioned.php b/model/Versioned.php index bf74321bd..1b56c924a 100644 --- a/model/Versioned.php +++ b/model/Versioned.php @@ -106,9 +106,31 @@ class Versioned extends DataExtension { ); } - function augmentSQL(SQLQuery &$query) { - // Get the content at a specific date - if($date = Versioned::current_archived_date()) { + + /** + * Amend freshly created DataQuery objects with versioned-specific information + */ + function augmentDataQueryCreation(SQLQuery &$query, DataQuery &$dataQuery) { + if($date = Versioned::$reading_archived_date) { + $dataQuery->setQueryParam('Versioned.mode', 'archive'); + $dataQuery->setQueryParam('Versioned.date', Versioned::$reading_archived_date); + + } else if(Versioned::$reading_stage && Versioned::$reading_stage != $this->defaultStage && array_search(Versioned::$reading_stage,$this->stages) !== false) { + $dataQuery->setQueryParam('Versioned.mode', 'stage'); + $dataQuery->setQueryParam('Versioned.stage', Versioned::$reading_stage); + } + + } + + /** + * Augment the the SQLQuery that is created by the DataQuery + * @todo Should this all go into VersionedDataQuery? + */ + function augmentSQL(SQLQuery &$query, DataQuery &$dataQuery) { + switch($dataQuery->getQueryParam('Versioned.mode')) { + // Reading a specific data from the archive + case 'archive': + $date = $dataQuery->getQueryParam('Versioned.date'); foreach($query->from as $table => $dummy) { if(!isset($baseTable)) { $baseTable = $table; @@ -132,13 +154,24 @@ class Versioned extends DataExtension { $query->from[$archiveTable] = "INNER JOIN \"$archiveTable\" ON \"$archiveTable\".\"ID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$archiveTable\".\"Version\" = \"{$baseTable}_versions\".\"Version\""; - - // Get a specific stage - } else if(Versioned::current_stage() && Versioned::current_stage() != $this->defaultStage - && array_search(Versioned::current_stage(), $this->stages) !== false) { - foreach($query->from as $table => $dummy) { - $query->renameTable($table, $table . '_' . Versioned::current_stage()); + break; + + // Reading a specific stage (Stage or Live) + case 'stage': + $stage = $dataQuery->getQueryParam('Versioned.stage'); + if($stage && ($stage != $this->defaultStage)) { + foreach($query->from as $table => $dummy) { + // Only rewrite table names that are actually part of the subclass tree + // This helps prevent rewriting of other tables that get joined in, in + // particular, many_many tables + if(class_exists($table) && ($table == $this->owner->class + || is_subclass_of($table, $this->owner->class) + || is_subclass_of($this->owner->class, $table))) { + $query->renameTable($table, $table . '_' . $stage); + } + } } + break; } } @@ -846,11 +879,11 @@ class Versioned extends DataExtension { * @param string $containerClass The container class for the result set (default is DataObjectSet) * @return DataObjectSet */ - static function get_by_stage($class, $stage, $filter = '', $sort = '', $join = '', $limit = '', $containerClass = 'DataObjectSet') { - $oldMode = Versioned::get_reading_mode(); - Versioned::reading_stage($stage); + static function get_by_stage($class, $stage, $filter = '', $sort = '', $join = '', $limit = '', $containerClass = 'DataList') { $result = DataObject::get($class, $filter, $sort, $join, $limit, $containerClass); - Versioned::set_reading_mode($oldMode); + $dq = $result->dataQuery(); + $dq->setQueryParam('Versioned.mode', 'stage'); + $dq->setQueryParam('Versioned.stage', $stage); return $result; } @@ -959,20 +992,6 @@ class Versioned extends DataExtension { * In particular, this will query deleted records as well as active ones. */ static function get_including_deleted($class, $filter = "", $sort = "") { - $query = self::get_including_deleted_query($class, $filter, $sort); - - // Process into a DataObjectSet - $SNG = singleton($class); - return $SNG->buildDataObjectSet($query->execute(), 'DataObjectSet', null, $class); - } - - /** - * Return the query for 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_query($class, $filter = "", $sort = "") { $oldMode = Versioned::get_reading_mode(); Versioned::set_reading_mode(''); @@ -985,6 +1004,9 @@ class Versioned extends DataExtension { $query->from[$archiveTable] = "INNER JOIN \"$archiveTable\" ON \"$archiveTable\".\"ID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$archiveTable\".\"Version\" = \"{$baseTable}_versions\".\"Version\""; + + // Process into a DataObjectSet + $result = $SNG->buildDataObjectSet($query->execute(), 'DataObjectSet', null, $class); Versioned::set_reading_mode($oldMode); return $query; diff --git a/security/Group.php b/security/Group.php index 58e0813a7..403bf7ca2 100644 --- a/security/Group.php +++ b/security/Group.php @@ -155,7 +155,6 @@ class Group extends DataObject { $memberList->setPermissions(array('edit', 'delete', 'export', 'add', 'inlineadd')); $memberList->setParentClass('Group'); $memberList->setPopupCaption(_t('SecurityAdmin.VIEWUSER', 'View User')); - $memberList->setRelationAutoSetting(false); $fields->push($idField = new HiddenField("ID")); diff --git a/security/Member.php b/security/Member.php index 73e5aba41..e1d661842 100755 --- a/security/Member.php +++ b/security/Member.php @@ -139,16 +139,15 @@ class Member extends DataObject { // Default groups should've been built by Group->requireDefaultRecords() already // Find or create ADMIN group - $adminGroups = Permission::get_groups_by_permission('ADMIN'); - if(!$adminGroups) { + $adminGroup = Permission::get_groups_by_permission('ADMIN')->First(); + if(!$adminGroup) { singleton('Group')->requireDefaultRecords(); - $adminGroups = Permission::get_groups_by_permission('ADMIN'); + $adminGroup = Permission::get_groups_by_permission('ADMIN')->First(); } - $adminGroup = $adminGroups->First(); // Add a default administrator to the first ADMIN group found (most likely the default // group created through Group->requireDefaultRecords()). - $admins = Permission::get_members_by_permission('ADMIN'); + $admins = Permission::get_members_by_permission('ADMIN')->First(); if(!$admins) { // Leave 'Email' and 'Password' are not set to avoid creating // persistent logins in the database. See Security::setDefaultAdmin(). @@ -156,7 +155,7 @@ class Member extends DataObject { $admin->FirstName = _t('Member.DefaultAdminFirstname', 'Default Admin'); $admin->write(); $admin->Groups()->add($adminGroup); - } + } } /** diff --git a/security/Permission.php b/security/Permission.php index a85540fd7..3d9ede9ab 100755 --- a/security/Permission.php +++ b/security/Permission.php @@ -379,7 +379,7 @@ class Permission extends DataObject { */ public static function get_members_by_permission($code) { $toplevelGroups = self::get_groups_by_permission($code); - if (!$toplevelGroups) return false; + if (!$toplevelGroups) return new DataObjectSet(); $groupIDs = array(); foreach($toplevelGroups as $group) { @@ -389,8 +389,7 @@ class Permission extends DataObject { } } - if(!count($groupIDs)) - return false; + if(!count($groupIDs)) return new DataObjectSet(); $members = DataObject::get( Object::getCustomClass('Member'), diff --git a/security/Security.php b/security/Security.php index f22de44f2..b0019d8ef 100644 --- a/security/Security.php +++ b/security/Security.php @@ -668,32 +668,30 @@ class Security extends Controller { Subsite::changeSubsite(0); } + $member = null; + // find a group with ADMIN permission $adminGroup = DataObject::get('Group', "\"Permission\".\"Code\" = 'ADMIN'", "\"Group\".\"ID\"", "JOIN \"Permission\" ON \"Group\".\"ID\"=\"Permission\".\"GroupID\"", - '1'); + '1')->First(); if(is_callable('Subsite::changeSubsite')) { Subsite::changeSubsite($origSubsite); } + if ($adminGroup) { - $adminGroup = $adminGroup->First(); - - if($adminGroup->Members()->First()) { - $member = $adminGroup->Members()->First(); - } + $member = $adminGroup->Members()->First(); } if(!$adminGroup) { singleton('Group')->requireDefaultRecords(); } - if(!isset($member)) { + if(!$member) { singleton('Member')->requireDefaultRecords(); - $members = Permission::get_members_by_permission('ADMIN'); - $member = $members->First(); + $member = Permission::get_members_by_permission('ADMIN')->First(); } return $member; diff --git a/tests/DataQueryTest.php b/tests/DataQueryTest.php new file mode 100644 index 000000000..972377a88 --- /dev/null +++ b/tests/DataQueryTest.php @@ -0,0 +1,14 @@ +join("INNER JOIN \"Group_Members\" ON \"Group_Members\".\"MemberID\" = \"Member\".\"ID\""); + $this->assertContains("INNER JOIN \"Group_Members\" ON \"Group_Members\".\"MemberID\" = \"Member\".\"ID\"", $dq->sql()); + } +} + +?> \ No newline at end of file diff --git a/tests/api/RestfulServerTest.php b/tests/api/RestfulServerTest.php index 0b0d10e63..fc57b7f4d 100644 --- a/tests/api/RestfulServerTest.php +++ b/tests/api/RestfulServerTest.php @@ -121,7 +121,7 @@ class RestfulServerTest extends SapphireTest { $url = "/api/v1/RestfulServerTest_Author/" . $author4->ID . '/RelatedAuthors'; $response = Director::test($url, null, null, 'GET'); - $this->assertEquals($response->getStatusCode(), 200); + $this->assertEquals(200, $response->getStatusCode()); $arr = Convert::xml2array($response->getBody()); $authorsArr = $arr['RestfulServerTest_Author']; diff --git a/tests/forms/TableFieldTest.php b/tests/forms/TableFieldTest.php index ef009f323..0ec277208 100755 --- a/tests/forms/TableFieldTest.php +++ b/tests/forms/TableFieldTest.php @@ -110,7 +110,7 @@ class TableFieldTest extends SapphireTest { new FieldSet() ); - $this->assertEquals($tableField->sourceItems()->Count(), 2); + $this->assertEquals(2, $tableField->sourceItems()->Count()); // We have replicated the array structure that the specific layout of the form generates. $tableField->setValue(array( diff --git a/tests/model/DataObjectSetTest.php b/tests/model/DataObjectSetTest.php index bedd9fcce..54d6c687c 100644 --- a/tests/model/DataObjectSetTest.php +++ b/tests/model/DataObjectSetTest.php @@ -112,7 +112,7 @@ class DataObjectSetTest extends SapphireTest { $commArr = $comments->toArray(); $multiplesOf3 = 0; - foreach($comments as $comment) { + foreach($commArr as $comment) { if($comment->MultipleOf(3)) { $comment->IsMultipleOf3 = true; $multiplesOf3++; @@ -295,6 +295,8 @@ class DataObjectSetTest extends SapphireTest { * Test {@link DataObjectSet->insertFirst()} */ function testInsertFirst() { + // inserFirst doesn't work with DataLists any more, because of new ORM. + /* // Get one comment $comment = DataObject::get_one('DataObjectSetTest_TeamComment', "\"Name\" = 'Joe'"); @@ -316,6 +318,7 @@ class DataObjectSetTest extends SapphireTest { // insert with a non-numeric key $set->insertFirst($comment, 'SomeRandomKey'); $this->assertEquals($comment, $set->First(), 'Comment should be first'); + */ } /** diff --git a/tests/model/DataObjectTest.php b/tests/model/DataObjectTest.php index 5dc0b68b6..3f028108f 100755 --- a/tests/model/DataObjectTest.php +++ b/tests/model/DataObjectTest.php @@ -137,14 +137,6 @@ class DataObjectTest extends SapphireTest { $this->assertEquals('Joe', $comments->First()->Name); $this->assertEquals('Phil', $comments->Last()->Name); - // Test container class - $comments = DataObject::get('DataObjectTest_TeamComment', '', '', '', '', 'DataObjectSet'); - $this->assertEquals('DataObjectSet', get_class($comments)); - - $comments = DataObject::get('DataObjectTest_TeamComment', '', '', '', '', 'ComponentSet'); - $this->assertEquals('ComponentSet', get_class($comments)); - - // Test get_by_id() $captain1ID = $this->idFromFixture('DataObjectTest_Player', 'captain1'); $captain1 = DataObject::get_by_id('DataObjectTest_Player', $captain1ID); @@ -182,7 +174,7 @@ class DataObjectTest extends SapphireTest { /* Test that fields / has_one relations from the parent table and the subclass tables are extracted */ $captain1 = $this->objFromFixture("DataObjectTest_Player", "captain1"); // Base field - $this->assertEquals('Captain 1', $captain1->FirstName); + $this->assertEquals('Captain', $captain1->FirstName); // Subclass field $this->assertEquals('007', $captain1->ShirtNumber); // Subclass has_one relation @@ -197,6 +189,25 @@ class DataObjectTest extends SapphireTest { $this->assertEquals($this->idFromFixture('DataObjectTest_Team', 'team1'), $captain1->FavouriteTeam()->ID); } + function testLimitAndCount() { + $players = DataObject::get("DataObjectTest_Player"); + + // There's 4 records in total + $this->assertEquals(4, $players->count()); + + // Testing "## offset ##" syntax + $this->assertEquals(4, $players->limit("20 OFFSET 0")->count()); + $this->assertEquals(0, $players->limit("20 OFFSET 20")->count()); + $this->assertEquals(2, $players->limit("2 OFFSET 0")->count()); + $this->assertEquals(1, $players->limit("5 OFFSET 3")->count()); + + // Testing "##, ##" syntax + $this->assertEquals(4, $players->limit("20")->count()); + $this->assertEquals(4, $players->limit("0, 20")->count()); + $this->assertEquals(0, $players->limit("20, 20")->count()); + $this->assertEquals(2, $players->limit("0, 2")->count()); + $this->assertEquals(1, $players->limit("3, 5")->count()); + } /** * Test writing of database columns which don't correlate to a DBField, @@ -221,26 +232,26 @@ class DataObjectTest extends SapphireTest { $team = $this->objFromFixture('DataObjectTest_Team', 'team1'); // Test getComponents() gets the ComponentSet of the other side of the relation - $this->assertTrue($page->Comments()->Count() == 2); + $this->assertTrue($team->Comments()->Count() == 2); // Test the IDs on the DataObjects are set correctly - foreach($page->Comments() as $comment) { - $this->assertTrue($comment->ParentID == $page->ID); + foreach($team->Comments() as $comment) { + $this->assertEquals($team->ID, $comment->TeamID); } // Test that we can add and remove items that already exist in the database - $newComment = new PageComment(); + $newComment = new DataObjectTest_TeamComment(); $newComment->Name = "Automated commenter"; $newComment->Comment = "This is a new comment"; $newComment->write(); - $page->Comments()->add($newComment); - $this->assertEquals($page->ID, $newComment->ParentID); + $team->Comments()->add($newComment); + $this->assertEquals($team->ID, $newComment->TeamID); - $comment1 = $this->fixture->objFromFixture('PageComment', 'comment1'); - $comment2 = $this->fixture->objFromFixture('PageComment', 'comment2'); - $page->Comments()->remove($comment2); + $comment1 = $this->fixture->objFromFixture('DataObjectTest_TeamComment', 'comment1'); + $comment2 = $this->fixture->objFromFixture('DataObjectTest_TeamComment', 'comment2'); + $team->Comments()->remove($comment2); - $commentIDs = $page->Comments()->column('ID'); + $commentIDs = $team->Comments()->column('ID'); $this->assertEquals(array($comment1->ID, $newComment->ID), $commentIDs); } @@ -421,17 +432,22 @@ class DataObjectTest extends SapphireTest { $obj->FirstName = "New Player"; $this->assertTrue($obj->isChanged()); - $page->write(); - $this->assertFalse($page->isChanged()); + $obj->write(); + $this->assertFalse($obj->isChanged()); /* If we perform the same random query twice, it shouldn't return the same results */ $itemsA = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random()); - foreach($itemsA as $item) $keysA[] = $item->ID; - $itemsB = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random()); + $itemsC = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random()); + $itemsD = DataObject::get("DataObjectTest_TeamComment", "", DB::getConn()->random()); + foreach($itemsA as $item) $keysA[] = $item->ID; foreach($itemsB as $item) $keysB[] = $item->ID; + foreach($itemsC as $item) $keysC[] = $item->ID; + foreach($itemsD as $item) $keysD[] = $item->ID; - $this->assertNotEquals($keysA, $keysB); + // These shouldn't all be the same (run it 4 times to minimise chance of an accidental collision) + // There's about a 1 in a billion chance of an accidental collision + $this->assertTrue($keysA != $keysB || $keysB != $keysC || $keysC != $keysD); } function testWriteSavesToHasOneRelations() { @@ -815,8 +831,8 @@ class DataObjectTest extends SapphireTest { */ function testManyManyUnlimitedRowCount() { $player = $this->objFromFixture('DataObjectTest_Player', 'player2'); - $query = $player->getManyManyComponentsQuery('Teams'); - $this->assertEquals(2, $query->unlimitedRowCount()); + // TODO: What's going on here? + $this->assertEquals(2, $player->Teams()->dataQuery()->query()->unlimitedRowCount()); } /** diff --git a/tests/model/DataObjectTest.yml b/tests/model/DataObjectTest.yml index 1bd3be7bc..df1f695e3 100644 --- a/tests/model/DataObjectTest.yml +++ b/tests/model/DataObjectTest.yml @@ -6,7 +6,7 @@ DataObjectTest_Team: DataObjectTest_Player: captain1: - FirstName: Captain 1 + FirstName: Captain ShirtNumber: 007 FavouriteTeam: =>DataObjectTest_Team.team1 Teams: =>DataObjectTest_Team.team1 diff --git a/tests/model/VersionedTest.php b/tests/model/VersionedTest.php index 53efece11..1b60aa824 100644 --- a/tests/model/VersionedTest.php +++ b/tests/model/VersionedTest.php @@ -171,10 +171,10 @@ class VersionedTest extends SapphireTest { $page->write(); $live = Versioned::get_by_stage('VersionedTest_DataObject', 'Live', "\"VersionedTest_DataObject_Live\".\"ID\"='$page->ID'"); - $this->assertNull($live); + $this->assertEquals(0, $live->count()); $stage = Versioned::get_by_stage('VersionedTest_DataObject', 'Stage', "\"VersionedTest_DataObject\".\"ID\"='$page->ID'"); - $this->assertNotNull($stage); + $this->assertEquals(1, $stage->count()); $this->assertEquals($stage->First()->Title, 'testWritingNewToStage'); Versioned::reading_stage($origStage); @@ -195,11 +195,11 @@ class VersionedTest extends SapphireTest { $page->write(); $live = Versioned::get_by_stage('VersionedTest_DataObject', 'Live', "\"VersionedTest_DataObject_Live\".\"ID\"='$page->ID'"); - $this->assertNotNull($live->First()); + $this->assertEquals(1, $live->count()); $this->assertEquals($live->First()->Title, 'testWritingNewToLive'); $stage = Versioned::get_by_stage('VersionedTest_DataObject', 'Stage', "\"VersionedTest_DataObject\".\"ID\"='$page->ID'"); - $this->assertNull($stage); + $this->assertEquals(0, $stage->count()); Versioned::reading_stage($origStage); } diff --git a/tests/security/GroupTest.php b/tests/security/GroupTest.php index 5f73b42e3..4f378426a 100644 --- a/tests/security/GroupTest.php +++ b/tests/security/GroupTest.php @@ -93,8 +93,8 @@ class GroupTest extends FunctionalTest { $adminGroup->delete(); - $this->assertNull(DataObject::get('Group', "\"ID\"={$adminGroup->ID}"), 'Group is removed'); - $this->assertNull(DataObject::get('Permission',"\"GroupID\"={$adminGroup->ID}"), 'Permissions removed along with the group'); + $this->assertEquals(0, DataObject::get('Group', "\"ID\"={$adminGroup->ID}")->count(), 'Group is removed'); + $this->assertEquals(0, DataObject::get('Permission',"\"GroupID\"={$adminGroup->ID}")->count(), 'Permissions removed along with the group'); } function testCollateAncestorIDs() { diff --git a/tests/security/PermissionRoleTest.php b/tests/security/PermissionRoleTest.php index c387d9ad4..81ecc8b29 100644 --- a/tests/security/PermissionRoleTest.php +++ b/tests/security/PermissionRoleTest.php @@ -11,7 +11,7 @@ class PermissionRoleTest extends FunctionalTest { $role->delete(); - $this->assertNull(DataObject::get('PermissionRole', "\"ID\"={$role->ID}"), 'Role is removed'); - $this->assertNull(DataObject::get('PermissionRoleCode',"\"RoleID\"={$role->ID}"), 'Permissions removed along with the role'); + $this->assertEquals(0, DataObject::get('PermissionRole', "\"ID\"={$role->ID}")->count(), 'Role is removed'); + $this->assertEquals(0, DataObject::get('PermissionRoleCode',"\"RoleID\"={$role->ID}")->count(), 'Permissions removed along with the role'); } } diff --git a/tests/security/PermissionTest.php b/tests/security/PermissionTest.php index bdae7fffa..85455b33f 100644 --- a/tests/security/PermissionTest.php +++ b/tests/security/PermissionTest.php @@ -64,4 +64,19 @@ class PermissionTest extends SapphireTest { 'Member is found via a permission attached to a role'); $this->assertNotContains($accessAuthor->ID, $resultIDs); } + + + function testHiddenPermissions(){ + $permissionCheckboxSet = new PermissionCheckboxSetField('Permissions','Permissions','Permission','GroupID'); + $this->assertContains('CMS_ACCESS_CMSMain', $permissionCheckboxSet->Field()); + $this->assertContains('CMS_ACCESS_AssetAdmin', $permissionCheckboxSet->Field()); + + Permission::add_to_hidden_permissions('CMS_ACCESS_CMSMain'); + Permission::add_to_hidden_permissions('CMS_ACCESS_AssetAdmin'); + $this->assertNotContains('CMS_ACCESS_CMSMain', $permissionCheckboxSet->Field()); + $this->assertNotContains('CMS_ACCESS_AssetAdmin', $permissionCheckboxSet->Field()); + + Permission::remove_from_hidden_permissions('CMS_ACCESS_AssetAdmin'); + $this->assertContains('CMS_ACCESS_AssetAdmin', $permissionCheckboxSet->Field()); + } } \ No newline at end of file diff --git a/tests/security/SecurityDefaultAdminTest.php b/tests/security/SecurityDefaultAdminTest.php index b5a695d10..1b94eb2ad 100644 --- a/tests/security/SecurityDefaultAdminTest.php +++ b/tests/security/SecurityDefaultAdminTest.php @@ -36,7 +36,7 @@ class SecurityDefaultAdminTest extends SapphireTest { function testFindAnAdministratorCreatesNewUser() { $adminMembers = Permission::get_members_by_permission('ADMIN'); - $this->assertFalse($adminMembers); + $this->assertEquals(0, $adminMembers->count()); $admin = Security::findAnAdministrator(); From 050e0675ce0ad2520e34f98352593236a599dd60 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Sun, 22 Nov 2009 18:29:24 +1300 Subject: [PATCH 08/64] ENHANCEMENT: Created HasManyList and ManyManyList objects that represent relations. API CHANGE: Relation methods no longer cache their results. API CHANGE: ComponentSet is deprecated. API CHANGE: DataObject::getManyManyComponentsQuery() no longer exists; just use the ManyManyList itself. API CHANGE: DataObject::getManyManyJoin() no longer exists; just use the ManyManyList itself. API CHANGE: DataObject::getManyManyFilter() no longer exists; just use the ManyManyList itself. --- core/model/HasManyList.php | 76 +++++++++ core/model/ManyManyList.php | 166 ++++++++++++++++++++ core/model/RelationList.php | 34 ++++ model/ComponentSet.php | 299 +----------------------------------- model/DataObject.php | 178 +++------------------ 5 files changed, 296 insertions(+), 457 deletions(-) create mode 100644 core/model/HasManyList.php create mode 100644 core/model/ManyManyList.php create mode 100644 core/model/RelationList.php diff --git a/core/model/HasManyList.php b/core/model/HasManyList.php new file mode 100644 index 000000000..194d49443 --- /dev/null +++ b/core/model/HasManyList.php @@ -0,0 +1,76 @@ + value filters that define which records + * in the $dataClass table actually belong to this relationship. + */ + function __construct($dataClass, $foreignKey) { + parent::__construct($dataClass); + $this->foreignKey = $foreignKey; + } + + protected function foreignIDFilter() { + // Apply relation filter + if(is_array($this->foreignID)) { + return "\"$this->foreignKey\" IN ('" . + implode(', ', array_map('Convert::raw2sql', $this->foreignID)) . "')"; + } else if($this->foreignID){ + return "\"$this->foreignKey\" = '" . + Convert::raw2sql($this->foreignID) . "'"; + } + } + + /** + * Adds the item to this relation. + * It does so by setting the relationFilters. + * @param $item The DataObject to be added, or its ID + */ + function add($item) { + if(is_numeric($item)) $item = DataObject::get_by_id($this->dataClass, $item); + else if(!($item instanceof $this->dataClass)) user_eror("HasManyList::add() expecting a $this->dataClass object, or ID value", E_USER_ERROR); + + // Validate foreignID + if(!$this->foreignID) { + user_error("ManyManyList::add() can't be called until a foreign ID is set", E_USER_WARNING); + return; + } + if(is_array($this->foreignID)) { + user_error("ManyManyList::add() can't be called on a list linked to mulitple foreign IDs", E_USER_WARNING); + return; + } + + $fk = $this->foreignKey; + $item->$fk = $this->foreignID; + + $item->write(); + } + + /** + * Remove an item from this relation. + * Doesn't actually remove the item, it just clears the foreign key value. + * @param $item The DataObject to be removed, or its ID + * @todo Maybe we should delete the object instead? + */ + function remove($item) { + if(is_numeric($item)) $item = DataObject::get_by_id($this->dataClass, $item); + else if(!($item instanceof $this->dataClass)) user_eror("HasManyList::remove() expecting a $this->dataClass object, or ID value", E_USER_ERROR); + + $fk = $this->foreignKey; + $item->$fk = null; + + $item->write(); + } + +} diff --git a/core/model/ManyManyList.php b/core/model/ManyManyList.php new file mode 100644 index 000000000..5814644d2 --- /dev/null +++ b/core/model/ManyManyList.php @@ -0,0 +1,166 @@ + fieldtype of extra fields on the join table. + */ + function __construct($dataClass, $joinTable, $localKey, $foreignKey, $extraFields = array()) { + parent::__construct($dataClass); + $this->joinTable = $joinTable; + $this->localKey = $localKey; + $this->foreignKey = $foreignKey; + $this->extraFields = $extraFields; + + $baseClass = ClassInfo::baseDataClass($dataClass); + + // Join to the many-many join table + $this->dataQuery->innerJoin($joinTable, "\"$this->localKey\" = \"$baseClass\".\"ID\""); + + // Query the extra fields from the join table + if($extraFields) $this->dataQuery->selectFromTable($joinTable, array_keys($extraFields)); + } + + /** + * Return a filter expression for the foreign ID. + */ + protected function foreignIDFilter() { + // Apply relation filter + if(is_array($this->foreignID)) { + return "\"$this->joinTable\".\"$this->foreignKey\" IN ('" . + implode(', ', array_map('Convert::raw2sql', $this->foreignID)) . "')"; + } else if($this->foreignID){ + return "\"$this->joinTable\".\"$this->foreignKey\" = '" . + Convert::raw2sql($this->foreignID) . "'"; + } + } + + /** + * Add an item to this many_many relationship + * Does so by adding an entry to the joinTable. + * @param $extraFields A map of additional columns to insert into the joinTable + */ + function add($item, $extraFields = null) { + if(is_numeric($item)) $itemID = $item; + else if($item instanceof $this->dataClass) $itemID = $item->ID; + else throw new InvalidArgumentException("ManyManyList::add() expecting a $this->dataClass object, or ID value", E_USER_ERROR); + + // Validate foreignID + if(!$this->foreignID) { + throw new Exception("ManyManyList::add() can't be called until a foreign ID is set", E_USER_WARNING); + } + if(is_array($this->foreignID)) { + throw new Exception("ManyManyList::add() can't be called on a list linked to mulitple foreign IDs", E_USER_WARNING); + } + + // Delete old entries, to prevent duplication + $this->remove($itemID); + + // Insert new entry + $manipulation = array(); + $manipulation[$this->joinTable]['command'] = 'insert'; + + if($extraFields) foreach($extraFields as $k => $v) { + $manipulation[$this->joinTable]['fields'][$k] = "'" . Convert::raw2sql($v) . "'"; + } + + $manipulation[$this->joinTable]['fields'][$this->localKey] = $itemID; + $manipulation[$this->joinTable]['fields'][$this->foreignKey] = $this->foreignID; + + DB::manipulate($manipulation); + } + + /** + * Remove the given item from this list. + * Note that for a ManyManyList, the item is never actually deleted, only the join table is affected + * @param $item The data object or its ID + */ + function remove($item) { + if(is_numeric($item)) $itemID = $item; + else if($item instanceof $this->dataClass) $itemID = $item->ID; + else user_eror("ManyManyList::remove() expecting a $this->dataClass object, or ID value", E_USER_ERROR); + + $query = new SQLQuery("*", array($this->joinTable)); + $query->delete = true; + + if($filter = $this->foreignIDFilter()) { + $query->where($filter); + } else { + user_error("Can't call ManyManyList::remove() until a foreign ID is set", E_USER_WARNING); + } + + $query->where("\"$this->localKey\" = {$itemID}"); + $query->execute(); + } + + /** + * Remove all items from this many-many join that match the given filter + * @deprecated this is experimental and will change. Don't use it in your projects. + */ + function removeByFilter($filter) { + $query = new SQLQuery("*", array($this->joinTable)); + $query->delete = true; + $query->where($filter); + $query->execute(); + } + + /** + * Find the extra field data for a single row of the relationship + * join table, given the known child ID. + * + * @todo Add tests for this / refactor it / something + * + * @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 + */ + function getExtraData($componentName, $childID) { + $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); + } + + // @todo Optimize into a single query instead of one per extra field + if($this->extraFields) { + foreach($this->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; + } + } + + return $result; + } +} diff --git a/core/model/RelationList.php b/core/model/RelationList.php new file mode 100644 index 000000000..05321f2f1 --- /dev/null +++ b/core/model/RelationList.php @@ -0,0 +1,34 @@ +foreignID = $id; + + $this->dataQuery->filter($this->foreignIDFilter()); + } + + /** + * Returns this ManyMany relationship linked to the given foreign ID. + * @param $id An ID or an array of IDs. + */ + function forForeignID($id) { + $this->setForeignID($id); + return $this; + } + + abstract protected function foreignIDFilter(); +} \ No newline at end of file diff --git a/model/ComponentSet.php b/model/ComponentSet.php index 01684066d..800878f34 100755 --- a/model/ComponentSet.php +++ b/model/ComponentSet.php @@ -1,307 +1,12 @@ type = $type; - $this->ownerObj = $ownerObj; - $this->ownerClass = $ownerClass ? $ownerClass : $ownerObj->class; - $this->tableName = $tableName; - $this->childClass = $childClass; - $this->joinField = $joinField; - } - - /** - * Get the ComponentSet specific information - * - * Returns an array on the format array( - * 'type' => , - * 'ownerObj' => , - * 'ownerClass' => , - * 'tableName' => , - * 'childClass' => , - * 'joinField' => |null ); - * - * @return array - */ - public function getComponentInfo() { - return array( - 'type' => $this->type, - 'ownerObj' => $this->ownerObj, - 'ownerClass' => $this->ownerClass, - 'tableName' => $this->tableName, - 'childClass' => $this->childClass, - 'joinField' => $this->joinField - ); - } - - /** - * Get an array of all the IDs in this component set, where the keys are the same as the - * values. - * @return array - */ - function getIdList() { - $list = array(); - foreach($this->items as $item) { - $list[$item->ID] = $item->ID; - } - return $list; - } - - /** - * Add an item to this set. - * @param DataObject|int|string $item Item to add, either as a DataObject or as the ID. - * @param array $extraFields A map of extra fields to add. - */ - function add($item, $extraFields = null) { - if(!isset($item)) { - user_error("ComponentSet::add() Not passed an object or ID", E_USER_ERROR); - } - - if(is_object($item)) { - if(!is_a($item, $this->childClass)) { - user_error("ComponentSet::add() Tried to add an '{$item->class}' object, but a '{$this->childClass}' object expected", E_USER_ERROR); - } - } else { - if(!$this->childClass) { - user_error("ComponentSet::add() \$this->childClass not set", E_USER_ERROR); - } - - $item = DataObject::get_by_id($this->childClass, $item); - if(!$item) return; - } - - // If we've already got a database object, then update the database - if($this->ownerObj->ID && is_numeric($this->ownerObj->ID)) { - $this->loadChildIntoDatabase($item, $extraFields); - } - - // In either case, add something to $this->items - $this->items[] = $item; - } - - /** - * Method to save many-many join data into the database for the given $item. - * Used by add() and write(). - * @param DataObject|string|int The item to save, as either a DataObject or the ID. - * @param array $extraFields Map of extra fields. - */ - protected function loadChildIntoDatabase($item, $extraFields = null) { - if($this->type == '1-to-many') { - $child = DataObject::get_by_id($this->childClass,$item->ID); - if (!$child) $child = $item; - $joinField = $this->joinField; - $child->$joinField = $this->ownerObj->ID; - $child->write(); - - } else { - $parentField = $this->ownerClass . 'ID'; - $childField = ($this->childClass == $this->ownerClass) ? "ChildID" : ($this->childClass . 'ID'); - - DB::query( "DELETE FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID} AND \"$childField\" = {$item->ID}" ); - - $extraKeys = $extraValues = ''; - if($extraFields) foreach($extraFields as $k => $v) { - $extraKeys .= ", \"$k\""; - $extraValues .= ", '" . DB::getConn()->addslashes($v) . "'"; - } - - DB::query("INSERT INTO \"$this->tableName\" (\"$parentField\",\"$childField\" $extraKeys) VALUES ({$this->ownerObj->ID}, {$item->ID} $extraValues)"); - } - } - - /** - * Add a number of items to the component set. - * @param array $items Items to add, as either DataObjects or IDs. - */ - function addMany($items) { - foreach($items as $item) { - $this->add($item); - } - } - - /** - * Sets the ComponentSet to be the given ID list. - * Records will be added and deleted as appropriate. - * @param array $idList List of IDs. - */ - function setByIDList($idList) { - $has = array(); - // Index current data - if($this->items) foreach($this->items as $item) { - $has[$item->ID] = true; - } - - // Keep track of items to delete - $itemsToDelete = $has; - - // add items in the list - // $id is the database ID of the record - if($idList) foreach($idList as $id) { - $itemsToDelete[$id] = false; - if($id && !isset($has[$id])) $this->add($id); - } - - // delete items not in the list - $removeList = array(); - foreach($itemsToDelete as $id => $actuallyDelete) { - if($actuallyDelete) $removeList[] = $id; - } - $this->removeMany($removeList); - } - - /** - * Remove an item from this set. - * - * @param DataObject|string|int $item Item to remove, either as a DataObject or as the ID. - */ - function remove($item) { - if(is_object($item)) { - if(!is_a($item, $this->childClass)) { - user_error("ComponentSet::remove() Tried to remove an '{$item->class}' object, but a '{$this->childClass}' object expected", E_USER_ERROR); - } - } else { - $item = DataObject::get_by_id($this->childClass, $item); - } - - // Manipulate the database, if it's in there - if($this->ownerObj->ID && is_numeric($this->ownerObj->ID)) { - if($this->type == '1-to-many') { - $child = DataObject::get_by_id($this->childClass,$item->ID); - $joinField = $this->joinField; - if($child->$joinField == $this->ownerObj->ID) { - $child->$joinField = null; - $child->write(); - } - - } else { - $parentField = $this->ownerClass . 'ID'; - $childField = ($this->childClass == $this->ownerClass) ? "ChildID" : ($this->childClass . 'ID'); - DB::query("DELETE FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID} AND \"$childField\" = {$item->ID}"); - } - } - - // Manipulate the in-memory array of items - if($this->items) foreach($this->items as $i => $candidateItem) { - if($candidateItem->ID == $item->ID) { - unset($this->items[$i]); - break; - } - } - } - - /** - * Remove many items from this set. - * @param array $itemList The items to remove, as a numerical array with IDs or as a DataObjectSet - */ - function removeMany($itemList) { - if(!count($itemList)) return false; - - if($this->type == '1-to-many') { - foreach($itemList as $item) $this->remove($item); - } else { - $itemCSV = implode(", ", $itemList); - $parentField = $this->ownerClass . 'ID'; - $childField = ($this->childClass == $this->ownerClass) ? "ChildID" : ($this->childClass . 'ID'); - DB::query("DELETE FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID} AND \"$childField\" IN ($itemCSV)"); - } + user_error("ComponentSet is deprecated; use HasManyList or ManyManyList", E_USER_WARNING); } - /** - * Remove all items in this set. - */ - function removeAll() { - if(!empty($this->tableName)) { - $parentField = $this->ownerClass . 'ID'; - DB::query("DELETE FROM \"$this->tableName\" WHERE \"$parentField\" = {$this->ownerObj->ID}"); - } else { - foreach($this->items as $item) { - $this->remove($item); - } - } - } - - /** - * Write this set to the database. - * Called by DataObject::write(). - * @param boolean $firstWrite This should be set to true if it the first time the set is being written. - */ - function write($firstWrite = false) { - if($firstWrite) { - foreach($this->items as $item) { - $this->loadChildIntoDatabase($item); - } - } - } - - /** - * Returns information about this set in HTML format for debugging. - * - * @return string - */ - function debug() { - $size = count($this->items); - - $output = <<ComponentSet -
    -
  • Type: {$this->type}
  • -
  • Size: $size
  • -
- -OUT; - - return $output; - } } ?> diff --git a/model/DataObject.php b/model/DataObject.php index 5e22b1aec..37ef4c05e 100755 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -1144,7 +1144,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $this->extend('onAfterSkippedWrite'); } - // Write ComponentSets as necessary + // Write relations as necessary if($writeComponents) { $this->writeComponents(true); } @@ -1307,10 +1307,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity protected $componentCache; /** - * Returns a one-to-many component, as a ComponentSet. - * The return value will be cached on this object instance, - * but only when no related objects are found (to avoid unnecessary empty checks in the database). - * If related objects exist, no caching is applied. + * Returns a one-to-many relation as a HasManyList * * @param string $componentName Name of the component * @param string $filter A filter to be inserted into the WHERE clause @@ -1318,36 +1315,21 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @param string $join A single join clause. This can be used for filtering, only 1 instance of each DataObject will be returned. * @param string|array $limit A limit expression to be inserted into the LIMIT clause * - * @return ComponentSet The components of the one-to-many relationship. + * @return HasManyList The components of the one-to-many relationship. */ public function getComponents($componentName, $filter = "", $sort = "", $join = "", $limit = "") { $result = null; - $sum = md5("{$filter}_{$sort}_{$join}_{$limit}"); - if(isset($this->componentCache[$componentName . '_' . $sum]) && false != $this->componentCache[$componentName . '_' . $sum]) { - return $this->componentCache[$componentName . '_' . $sum]; - } - if(!$componentClass = $this->has_many($componentName)) { user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName' on class '$this->class'", E_USER_ERROR); } $joinField = $this->getRemoteJoinField($componentName, 'has_many'); + + $result = new HasManyList($componentClass, $joinField); + if($this->ID) $result->setForeignID($this->ID); - if($this->isInDB()) { //Check to see whether we should query the db - $query = $this->getComponentsQuery($componentName, $filter, $sort, $join, $limit); - $result = $this->buildDataObjectSet($query->execute(), 'ComponentSet', $query, $componentClass); - if($result) $result->parseQueryLimit($query); - } - - if(!$result) { - // If this record isn't in the database, then we want to hold onto this specific ComponentSet, - // because it's the only copy of the data that we have. - $result = new ComponentSet(); - $this->setComponent($componentName . '_' . $sum, $result); - } - - $result->setComponentInfo("1-to-many", $this, null, null, $componentClass, $joinField); + $result = $result->filter($filter)->limit($limit)->sort($sort)->join($join); return $result; } @@ -1355,11 +1337,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity /** * Get the query object for a $has_many Component. * - * Use {@link DataObjectSet->setComponentInfo()} to attach metadata to the - * resultset you're building with this query. - * Use {@link DataObject->buildDataObjectSet()} to build a set out of the {@link SQLQuery} - * object, and pass "ComponentSet" as a $containerClass. - * * @param string $componentName * @param string $filter * @param string|array $sort @@ -1419,149 +1396,30 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * and {@link getManyManyComponents()}. * * @param string $componentName Name of the component - * @param DataObject|ComponentSet $componentValue Value of the component + * @param DataObject|HasManyList|ManyManyList $componentValue Value of the component */ public function setComponent($componentName, $componentValue) { $this->componentCache[$componentName] = $componentValue; } /** - * Returns a many-to-many component, as a ComponentSet. - * The return value will be cached on this object instance, - * but only when no related objects are found (to avoid unnecessary empty checks in the database). - * If related objects exist, no caching is applied. - * + * Returns a many-to-many component, as a ManyManyList. * @param string $componentName Name of the many-many component - * @return ComponentSet The set of components + * @return ManyManyList The set of components * * @todo Implement query-params */ public function getManyManyComponents($componentName, $filter = "", $sort = "", $join = "", $limit = "") { - $sum = md5("{$filter}_{$sort}_{$join}_{$limit}"); - if(isset($this->componentCache[$componentName . '_' . $sum]) && false != $this->componentCache[$componentName . '_' . $sum]) { - return $this->componentCache[$componentName . '_' . $sum]; - } - list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName); - - // Join expression is done on SiteTree.ID even if we link to Page; it helps work around - // database inconsistencies - $componentBaseClass = ClassInfo::baseDataClass($componentClass); - - if($this->ID && is_numeric($this->ID)) { - - if($componentClass) { - $query = $this->getManyManyComponentsQuery($componentName, $filter, $sort, $join, $limit); - $records = $query->execute(); - $result = $this->buildDataObjectSet($records, "ComponentSet", $query, $componentBaseClass); - if($result) $result->parseQueryLimit($query); // for pagination support - if(!$result) { - $result = new ComponentSet(); - } - } - } else { - $result = new ComponentSet(); - } - $result->setComponentInfo("many-to-many", $this, $parentClass, $table, $componentClass); - - // If this record isn't in the database, then we want to hold onto this specific ComponentSet, - // because it's the only copy of the data that we have. - if(!$this->isInDB()) { - $this->setComponent($componentName . '_' . $sum, $result); - } - - return $result; - } - - /** - * Get the query object for a $many_many Component. - * Use {@link DataObjectSet->setComponentInfo()} to attach metadata to the - * resultset you're building with this query. - * Use {@link DataObject->buildDataObjectSet()} to build a set out of the {@link SQLQuery} - * object, and pass "ComponentSet" as a $containerClass. - * - * @param string $componentName - * @param string $filter - * @param string|array $sort - * @param string $join - * @param string|array $limit - * @return SQLQuery - */ - public function getManyManyComponentsQuery($componentName, $filter = "", $sort = "", $join = "", $limit = "") { - list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName); - - $componentObj = singleton($componentClass); - - // Join expression is done on SiteTree.ID even if we link to Page; it helps work around - // database inconsistencies - $componentBaseClass = ClassInfo::baseDataClass($componentClass); - - - $query = $componentObj->extendedSQL( - "\"$table\".\"$parentField\" = $this->ID", // filter - $sort, - $limit, - "INNER JOIN \"$table\" ON \"$table\".\"$componentField\" = \"$componentBaseClass\".\"ID\"" // join - ); - foreach((array)$this->many_many_extraFields($componentName) as $extraField => $extraFieldType) { - $query->select[] = "\"$table\".\"$extraField\""; - $query->groupby[] = "\"$table\".\"$extraField\""; - } + $result = new ManyManyList($componentClass, $table, $componentField, $parentField, + $this->many_many_extraFields($componentName)); - if($filter) $query->where[] = $filter; - if($join) $query->from[] = $join; - - return $query; - } - - /** - * Pull out a join clause for a many-many relationship. - * - * @param string $componentName The many_many or belongs_many_many relation to join to. - * @param string $baseTable The classtable that will already be included in the SQL query to which this join will be added. - * @return string SQL join clause - */ - function getManyManyJoin($componentName, $baseTable) { - if(!$componentClass = $this->many_many($componentName)) { - user_error("DataObject::getComponents(): Unknown many-to-many component '$componentName' on class '$this->class'", E_USER_ERROR); - } - $classes = array_reverse(ClassInfo::ancestry($this->class)); - - list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName); - - $baseComponentClass = ClassInfo::baseDataClass($componentClass); - if($baseTable == $parentClass) { - return "LEFT JOIN \"$table\" ON (\"$table\".\"$parentField\" = \"$parentClass\".\"ID\" AND \"$table\".\"$componentField\" = '{$this->ID}')"; - } else { - return "LEFT JOIN \"$table\" ON (\"$table\".\"$componentField\" = \"$baseComponentClass\".\"ID\" AND \"$table\".\"$parentField\" = '{$this->ID}')"; - } - } - - function getManyManyFilter($componentName, $baseTable) { - list($parentClass, $componentClass, $parentField, $componentField, $table) = $this->many_many($componentName); - - return "\"$table\".\"$parentField\" = '{$this->ID}'"; - } - - /** - * Return an aggregate object. An aggregate object returns the result of running some SQL aggregate function on a field of - * this dataobject type. - * - * It can be called with no arguments, in which case it returns an object that calculates aggregates on this object's type, - * or with an argument (possibly statically), in which case it returns an object for that type - */ - function Aggregate($type = null, $filter = '') { - return new Aggregate($type ? $type : $this->class, $filter); - } - - /** - * Return an relationship aggregate object. A relationship aggregate does the same thing as an aggregate object, but operates - * on a has_many rather than directly on the type specified - */ - function RelationshipAggregate($object = null, $relationship = '', $filter = '') { - if (is_string($object)) { $filter = $relationship; $relationship = $object; $object = $this; } - return new Aggregate_Relationship($object ? $object : $this->owner, $relationship, $filter); + // If this is called on a singleton, then we return an 'orphaned relation' that can have the + // foreignID set elsewhere. + if($this->ID) $result->setForeignID($this->ID); + + return $result->filter($filter)->sort($sort)->limit($limit); } /** @@ -2560,7 +2418,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $object = $component->dbObject($fieldName); - if (!($object instanceof DBField) && !($object instanceof ComponentSet)) { + if (!($object instanceof DBField) && !($object instanceof DataList)) { // Todo: come up with a broader range of exception objects to describe differnet kinds of errors programatically throw new Exception("Unable to traverse to related object field [$fieldPath] on [$this->class]"); } From 33fa7825f90e90bb728e56de8927aee51ff29f47 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Sun, 22 Nov 2009 18:30:14 +1300 Subject: [PATCH 09/64] BUGFIX: Updated Member <-> Group relations to work with new ManyManyList. API CHANGE: Deprecated the special methods in Member_GroupSet. --- security/Group.php | 44 +++------- security/Member.php | 201 +++++++++++++++----------------------------- 2 files changed, 80 insertions(+), 165 deletions(-) diff --git a/security/Group.php b/security/Group.php index 403bf7ca2..6ceceb8b5 100644 --- a/security/Group.php +++ b/security/Group.php @@ -206,38 +206,20 @@ class Group extends DataObject { * @param $join string SQL * @return ComponentSet */ - public function Members($limit = "", $offset = "", $filter = "", $sort = "", $join = "") { - $table = "Group_Members"; - if($filter) $filter = is_array($filter) ? $filter : array($filter); - - if( is_numeric( $limit ) ) { - if( is_numeric( $offset ) ) - $limit = "$limit OFFSET $offset"; - else - $limit = "$limit OFFSET 0"; - } else { - $limit = ""; - } - - // Get all of groups that this group contains - $groupFamily = implode(", ", $this->collateFamilyIDs()); - - $filter[] = "\"$table\".\"GroupID\" IN ($groupFamily)"; - $join .= " INNER JOIN \"$table\" ON \"$table\".\"MemberID\" = \"Member\".\"ID\"" . Convert::raw2sql($join); - - $result = singleton("Member")->instance_get( - $filter, - $sort, - $join, - $limit, - "ComponentSet" // datatype - ); - - if(!$result) $result = new ComponentSet(); + public function Members($filter = "", $sort = "", $join = "", $limit = "") { + // Get a DataList of the relevant groups + $groups = DataList::create("Group")->byIDs($this->collateFamilyIDs()); - $result->setComponentInfo("many-to-many", $this, "Group", $table, "Member"); - foreach($result as $item) $item->GroupID = $this->ID; - return $result; + // Call the relation method on the DataList to get the members from all the groups + return $groups->relation('DirectMembers') + ->filter($filter)->sort($sort)->join($join)->limit($limit); + } + + /** + * Return only the members directly added to this group + */ + public function DirectMembers() { + return $this->getManyManyComponents('Members'); } public function map($filter = "", $sort = "", $blank="") { diff --git a/security/Member.php b/security/Member.php index e1d661842..33b1b5bd4 100755 --- a/security/Member.php +++ b/security/Member.php @@ -941,40 +941,22 @@ class Member extends DataObject { * Get a "many-to-many" map that holds for all members their group * memberships * - * @return Member_GroupSet Returns a map holding for all members their - * group memberships. + * @todo Push all this logic into Member_GroupSet's getIterator()? */ public function Groups() { - $groups = $this->getManyManyComponents("Groups"); - $groupIDs = $groups->column(); - $collatedGroups = array(); - - if($groups) { - foreach($groups as $group) { - $collatedGroups = array_merge((array)$collatedGroups, $group->collateAncestorIDs()); - } + $groups = new Member_GroupSet('Group', 'Group_Members', 'GroupID', 'MemberID'); + if($this->ID) $groups->setForeignID($this->ID); + + // Filter out groups that aren't allowed from this IP + $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null; + $disallowedGroups = array(); + foreach($groups as $group) { + if(!$group->allowedIPAddress($ip)) $disallowedGroups[] = $groupID; } + if($disallowedGroups) $group->filter("\"Group\".\"ID\" NOT IN (" . + implode(',',$disallowedGroups) . ")"); - $table = "Group_Members"; - - if(count($collatedGroups) > 0) { - $collatedGroups = implode(", ", array_unique($collatedGroups)); - - $unfilteredGroups = singleton('Group')->instance_get("\"Group\".\"ID\" IN ($collatedGroups)", "\"Group\".\"ID\"", "", "", "Member_GroupSet"); - $result = new ComponentSet(); - - // Only include groups where allowedIPAddress() returns true - $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null; - foreach($unfilteredGroups as $group) { - if($group->allowedIPAddress($ip)) $result->push($group); - } - } else { - $result = new Member_GroupSet(); - } - - $result->setComponentInfo("many-to-many", $this, "Member", $table, "Group"); - - return $result; + return $groups; } @@ -1414,38 +1396,52 @@ class Member extends DataObject { } /** - * Special kind of {@link ComponentSet} that has special methods for - * manipulating a user's membership + * Represents a set of Groups attached to a member. + * Handles the hierarchy logic. * @package sapphire * @subpackage security */ -class Member_GroupSet extends ComponentSet { +class Member_GroupSet extends ManyManyList { + function __construct($dataClass, $joinTable, $localKey, $foreignKey, $extraFields = array()) { + // Bypass the many-many constructor + DataList::__construct($dataClass); + + $this->joinTable = $joinTable; + $this->localKey = $localKey; + $this->foreignKey = $foreignKey; + $this->extraFields = $extraFields; + } + /** - * Control group membership with a number of checkboxes. - * - If the checkbox fields are present in $data, then the member will be - * added to the group with the same codename. - * - If the checkbox fields are *NOT* present in $data, then the member - * will be removed from the group with the same codename. - * - * @param array $checkboxes An array list of the checkbox fieldnames (only - * values are used). E.g. array(0, 1, 2) - * @param array $data The form data. Uually in the format array(0 => 2) - * (just pass the checkbox data from your form) + * Link this group set to a specific member. + */ + public function setForeignID($id) { + // Turn a 1-element array into a simple value + if(is_array($id) && sizeof($id) == 1) $id = reset($id); + $this->foreignID = $id; + + // Find directly applied groups + $manymanyFilter = $this->foreignIDFilter(); + $groupIDs = DB::query('SELECT "GroupID" FROM Group_Members WHERE ' . $manymanyFilter)->column(); + + // Get all ancestors + $allGroupIDs = array(); + while($groupIDs) { + $allGroupIDs = array_merge($allGroupIDs, $groupIDs); + $groupIDs = DataObject::get("Group")->byIDs($groupIDs)->column("ParentID"); + $groupIDs = array_filter($groupIDs); + } + + // Add a filter to this DataList + if($allGroupIDs) $this->byIDs($allGroupIDs); + else $this->byIDs(array(0)); + } + + /** + * @deprecated Use setByIdList() and/or a CheckboxSetField */ function setByCheckboxes(array $checkboxes, array $data) { - foreach($checkboxes as $checkbox) { - if($data[$checkbox]) { - $add[] = $checkbox; - } else { - $remove[] = $checkbox; - } - } - - if($add) - $this->addManyByCodename($add); - - if($remove) - $this->removeManyByCodename($remove); + user_error("Member_GroupSet is deprecated and no longer works", E_USER_WARNING); } @@ -1515,108 +1511,45 @@ class Member_GroupSet extends ComponentSet { /** - * Adds this member to the groups based on the group IDs - * - * @param array $ids Group identifiers. + * @deprecated Use DataList::addMany */ - function addManyByGroupID($groupIds){ - $groups = $this->getGroupsFromIDs($groupIds); - if($groups) { - foreach($groups as $group) { - $this->add($group); - } - } + function addManyByGroupID($ids){ + user_error('addManyByGroupID is deprecated, use addMany', E_USER_NOTICE); + return $this->addMany($ids); } /** - * Removes the member from many groups based on the group IDs - * - * @param array $ids Group identifiers. + * @deprecated Use DataList::removeMany */ function removeManyByGroupID($groupIds) { - $groups = $this->getGroupsFromIDs($groupIds); - if($groups) { - foreach($groups as $group) { - $this->remove($group); - } - } + user_error('removeManyByGroupID is deprecated, use removeMany', E_USER_NOTICE); + return $this->removeMany($ids); } /** - * Returns the groups from an array of group IDs - * - * @param array $ids Group identifiers. - * @return mixed Returns the groups from the array of Group IDs. + * @deprecated Use DataObject::get("Group")->byIds() */ - function getGroupsFromIDs($ids){ - if($ids && count($ids) > 1) { - return DataObject::get("Group", "\"ID\" IN (" . implode(",", $ids) . ")"); - } else { - return DataObject::get_by_id("Group", $ids[0]); - } + function getGroupsFromIDs($ids) { + user_error('getGroupsFromIDs is deprecated, use DataObject::get("Group")->byIds()', E_USER_NOTICE); + return DataObject::get("Group")->byIDs($ids); } /** - * Adds this member to the groups based on the group codenames - * - * @param array $codenames Group codenames + * @deprecated Group.Code is deprecated */ function addManyByCodename($codenames) { - $groups = $this->codenamesToGroups($codenames); - if($groups) { - foreach($groups as $group){ - $this->add($group); - } - } + user_error("addManyByCodename is deprecated and no longer works", E_USER_WARNING); } /** - * Removes this member from the groups based on the group codenames - * - * @param array $codenames Group codenames + * @deprecated Group.Code is deprecated */ function removeManyByCodename($codenames) { - $groups = $this->codenamesToGroups($codenames); - if($groups) { - foreach($groups as $group) { - $this->remove($group); - } - } - } - - - /** - * Helper function to return the appropriate groups via a codenames - * - * @param array $codenames Group codenames - * @return array Returns the the appropriate groups. - */ - protected function codenamesToGroups($codenames) { - $list = "'" . implode("', '", $codenames) . "'"; - $output = DataObject::get("Group", "\"Code\" IN ($list)"); - - // Some are missing - throw warnings - if(!$output || ($output->Count() != sizeof($list))) { - foreach($codenames as $codename) - $missing[$codename] = $codename; - - if($output) { - foreach($output as $record) - unset($missing[$record->Code]); - } - - if($missing) - user_error("The following group-codes aren't matched to any groups: " . - implode(", ", $missing) . - ". You probably need to link up the correct group codes in phpMyAdmin", - E_USER_WARNING); - } - - return $output; + user_error("removeManyByCodename is deprecated and no longer works", E_USER_WARNING); } } From 165f38361b4e6fc6fb2afc10c9dca7af0eda64e3 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Sun, 22 Nov 2009 18:58:17 +1300 Subject: [PATCH 10/64] BUGFIX: Updated Hierarchy::liveChildren() to use DataList over buildDataObjectSet. --- model/Hierarchy.php | 55 ++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 33 deletions(-) diff --git a/model/Hierarchy.php b/model/Hierarchy.php index eb71f50d3..1ddc08bf4 100755 --- a/model/Hierarchy.php +++ b/model/Hierarchy.php @@ -452,9 +452,10 @@ class Hierarchy extends DataExtension { // Next, go through the live children. Only some of these will be listed $liveChildren = $this->owner->liveChildren(true, true); if($liveChildren) { - foreach($liveChildren as $child) { - $stageChildren->push($child); - } + $merged = new DataObjectSet(); + $merged->merge($stageChildren); + $merged->merge($liveChildren); + $stageChildren = $merged; } } @@ -543,43 +544,31 @@ class Hierarchy extends DataExtension { */ public function liveChildren($showAll = false, $onlyDeletedFromStage = false) { if(!$this->owner->hasExtension('Versioned')) throw new Exception('Hierarchy->liveChildren() only works with Versioned extension applied'); - - if($this->owner->db('ShowInMenus')) { - $extraFilter = ($showAll) ? '' : " AND \"ShowInMenus\"=1"; - } else { - $extraFilter = ''; - } - $join = ""; $baseClass = ClassInfo::baseDataClass($this->owner->class); + $id = $this->owner->ID; + + $children = DataObject::get($baseClass)->filter("\"{$baseClass}\".\"ParentID\" = $id AND \"{$baseClass}\".\"ID\" != $id"); + if(!$showAll) $children = $children->filter('"ShowInMenus" = 1'); - $filter = "\"{$baseClass}\".\"ParentID\" = " . (int)$this->owner->ID - . " AND \"{$baseClass}\".\"ID\" != " . (int)$this->owner->ID; + // Query the live site + $children->dataQuery()->setQueryParam('Versioned.mode', 'stage'); + $children->dataQuery()->setQueryParam('Versioned.stage', 'Live'); if($onlyDeletedFromStage) { - // Note that the lack of double-quotes around $baseClass are the only thing preventing - // it from being rewritten to {$baseClass}_Live. This is brittle and a little clumsy - $join = "LEFT JOIN {$baseClass} ON {$baseClass}.\"ID\" = \"{$baseClass}\".\"ID\""; - $filter .= " AND {$baseClass}.\"ID\" IS NULL"; + // Note that this makes a second query, and could be optimised to be a joi; + $stageChildren = DataObject::get($baseClass) + ->filter("\"{$baseClass}\".\"ParentID\" = $id AND \"{$baseClass}\".\"ID\" != $id"); + $stageChildren->dataQuery()->setQueryParam('Versioned.mode', 'stage'); + $stageChildren->dataQuery()->setQueryParam('Versioned.stage', ''); + + $ids = $stageChildren->column("ID"); + if($ids) { + $children->filter("\"$baseClass\".\"ID\" NOT IN (" . implode(',',$ids) . ")"); + } } - - $oldStage = Versioned::current_stage(); - Versioned::reading_stage('Live'); - // Singleton is necessary and not $this->owner so as not to muck with Translatable's - // behaviour. - $query = singleton($baseClass)->extendedSQL($filter, null, null, $join); - - // Since we didn't include double quotes in the join & filter, we need to add them into the - // SQL now, after Versioned has done is query rewriting - $correctedSQL = str_replace(array("LEFT JOIN {$baseClass}", "{$baseClass}.\"ID\""), - array("LEFT JOIN \"{$baseClass}\"", "\"{$baseClass}\".\"ID\""), $query->sql()); - - $result = $this->owner->buildDataObjectSet(DB::query($correctedSQL)); - - Versioned::reading_stage($oldStage); - - return $result; + return $children; } /** From 319d2f49520c7282443da8c553b93272ff2efc6f Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Sun, 22 Nov 2009 19:07:45 +1300 Subject: [PATCH 11/64] ENHANCEMENT: Refactored TableListField and subclasses to rely on the DataList to handle data manipulatation. API CHANGE: A DataList can be passed as the 2nd argument to the constructor; this is the recommended approach for editing non-relations. API CHANGE: You can't set a custom query; only a custom DataList. API CHANGE: You can't have one of these fields editing data that doesn't correspond to a DataList - there must be some kind of DataObject behind it. API CHANGE: If the field's name corresponds to a relation on the object being edited, then the relation is used as the data set - all the source* parameters are ignored. API CHANGE: relationAutoSetting only works if your form has had the corresponding data object loaded with $form->loadDataFrom(). API CHANGE: relationAutoSetting can't be turned off; attach a non-relation DataList instead. --- admin/code/MemberTableField.php | 3 + forms/ComplexTableField.php | 108 +++-------------- forms/FormScaffolder.php | 15 +-- forms/TableField.php | 55 ++------- forms/TableListField.php | 165 +++++++++++--------------- tests/forms/ComplexTableFieldTest.php | 2 + tests/forms/ComplexTableFieldTest.yml | 1 + tests/forms/FormScaffolderTest.php | 22 ++++ tests/forms/TableFieldTest.php | 65 ++-------- 9 files changed, 136 insertions(+), 300 deletions(-) diff --git a/admin/code/MemberTableField.php b/admin/code/MemberTableField.php index c517c2642..934daf13e 100755 --- a/admin/code/MemberTableField.php +++ b/admin/code/MemberTableField.php @@ -104,12 +104,15 @@ class MemberTableField extends ComplexTableField { } if($this->group) { + user_error("MemberTableField's group setting doesn't yet work in the new-orm branch", E_USER_WARNING); + /* $groupIDs = array($this->group->ID); if($this->group->AllChildren()) $groupIDs = array_merge($groupIDs, $this->group->AllChildren()->column('ID')); $this->sourceFilter[] = sprintf( '"Group_Members"."GroupID" IN (%s)', implode(',', $groupIDs) ); + */ } $this->sourceJoin = " INNER JOIN \"Group_Members\" ON \"MemberID\"=\"Member\".\"ID\""; diff --git a/forms/ComplexTableField.php b/forms/ComplexTableField.php index 6c9e0794f..0839daf3a 100644 --- a/forms/ComplexTableField.php +++ b/forms/ComplexTableField.php @@ -39,7 +39,7 @@ class ComplexTableField extends TableListField { protected $detailFormFields; - protected $viewAction, $sourceJoin, $sourceItems; + protected $viewAction; /** * @var Controller @@ -119,14 +119,6 @@ class ComplexTableField extends TableListField { */ protected $detailFormValidator = null; - /** - * Automatically detect a has-one relationship - * in the popup (=child-class) and save the relation ID. - * - * @var boolean - */ - protected $relationAutoSetting = true; - /** * Default size for the popup box */ @@ -210,31 +202,6 @@ class ComplexTableField extends TableListField { parent::__construct($name, $sourceClass, $fieldList, $sourceFilter, $sourceSort, $sourceJoin); } - /** - * Return the record filter for this table. - * It will automatically add a relation filter if relationAutoSetting is true, and it can determine an appropriate - * filter. - */ - function sourceFilter() { - $sourceFilter = parent::sourceFilter(); - - if($this->relationAutoSetting - && $this->getParentClass() - && ($filterKey = $this->getParentIdName($this->getParentClass(), $this->sourceClass())) - && ($filterValue = $this->sourceID()) ) { - - $newFilter = "\"$filterKey\" = '" . Convert::raw2sql($filterValue) . "'"; - - if($sourceFilter && is_array($sourceFilter)) { - // Note that the brackets below are taken into account when building this - $sourceFilter = implode(") AND (", $sourceFilter); - } - - $sourceFilter = $sourceFilter ? "($sourceFilter) AND ($newFilter)" : $newFilter; - } - return $sourceFilter; - } - function isComposite() { return false; } @@ -277,26 +244,22 @@ JS; return $this->renderWith($this->template); } - function sourceClass() { - return $this->sourceClass; - } - /** * @return DataObjectSet */ function Items() { - $this->sourceItems = $this->sourceItems(); + $sourceItems = $this->sourceItems(); - if(!$this->sourceItems) { + if(!$sourceItems) { return null; } $pageStart = (isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0; - $this->sourceItems->setPageLimits($pageStart, $this->pageSize, $this->totalCount); + $sourceItems->setPageLimits($pageStart, $this->pageSize, $this->totalCount); $output = new DataObjectSet(); - foreach($this->sourceItems as $pageIndex=>$item) { - $output->push(new $this->itemClass($item, $this)); + foreach($sourceItems as $pageIndex=>$item) { + $output->push(Object::create($this->itemClass,$item, $this, $pageStart+$pageIndex)); } return $output; } @@ -547,27 +510,12 @@ JS; if($this->getParentClass()) { $detailFields->push(new HiddenField('ctf[parentClass]', '', $this->getParentClass())); - - if($manyManyRelationName && $this->relationAutoSetting) { - $detailFields->push(new HiddenField('ctf[manyManyRelation]', '', $manyManyRelationName)); - } - - if($hasManyRelationName && $this->relationAutoSetting) { - $detailFields->push(new HiddenField('ctf[hasManyRelation]', '', $hasManyRelationName)); - } - - if($manyManyRelationName || $hasManyRelationName) { - $detailFields->push(new HiddenField('ctf[sourceID]', '', $this->sourceID())); - } + // Hack for model admin: model admin will have included a dropdown for the relation itself $parentIdName = $this->getParentIdName($this->getParentClass(), $this->sourceClass()); - if($parentIdName) { - if($this->relationAutoSetting) { - // Hack for model admin: model admin will have included a dropdown for the relation itself - $detailFields->removeByName($parentIdName); - $detailFields->push(new HiddenField($parentIdName, '', $this->sourceID())); - } + $detailFields->removeByName($parentIdName); + $detailFields->push(new HiddenField($parentIdName, '', $this->sourceID())); } } @@ -614,16 +562,10 @@ JS; } /** - * By default, a ComplexTableField will assume that the field name is the name of a has-many relation on the object being - * edited. It will identify the foreign key in the object being listed, and filter on that column, as well as auto-setting - * that column for newly created records. - * - * Calling $this->setRelationAutoSetting(false) will disable this functionality. - * - * @param boolean $value Should the relation auto-setting functionality be enabled? + * @deprecated */ function setRelationAutoSetting($value) { - $this->relationAutoSetting = $value; + user_error("ComplexTableField::setRelationAutoSetting() is deprecated; manipulate the DataList instead", E_USER_WARNING); } /** @@ -648,21 +590,8 @@ JS; return Director::redirectBack(); } - // 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']); - $relationName = $data['ctf']['manyManyRelation']; - $componentSet = $parentRecord ? $parentRecord->getManyManyComponents($relationName) : null; - if($componentSet) $componentSet->add($childData); - } - - if(isset($data['ctf']['hasManyRelation'])) { - $parentRecord = DataObject::get_by_id($data['ctf']['parentClass'], (int) $data['ctf']['sourceID']); - $relationName = $data['ctf']['hasManyRelation']; - - $componentSet = $parentRecord ? $parentRecord->getComponents($relationName) : null; - if($componentSet) $componentSet->add($childData); - } + // Save this item into the given relationship + $this->getDataList()->add($childData); $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null; @@ -830,14 +759,9 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest { $form->sessionMessage($e->getResult()->message(), 'bad'); return Director::redirectBack(); } - - // 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']); - $relationName = $data['ctf']['manyManyRelation']; - $componentSet = $parentRecord->getManyManyComponents($relationName); - $componentSet->add($dataObject); - } + + // Save this item into the given relationship + $this->ctf->getDataList()->add($childData); $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null; diff --git a/forms/FormScaffolder.php b/forms/FormScaffolder.php index b0e7c735d..05bf8800e 100755 --- a/forms/FormScaffolder.php +++ b/forms/FormScaffolder.php @@ -118,14 +118,12 @@ class FormScaffolder extends Object { ); } $relationshipFields = singleton($component)->summaryFields(); - $foreignKey = $this->obj->getRemoteJoinField($relationship); $ctf = new ComplexTableField( $this, $relationship, - $component, + null, $relationshipFields, - "getCMSFields", - "\"$foreignKey\" = " . $this->obj->ID + "getCMSFields" ); $ctf->setPermissions(TableListField::permissions_for_object($component)); if($this->tabbed) { @@ -146,17 +144,12 @@ class FormScaffolder extends Object { } $relationshipFields = singleton($component)->summaryFields(); - $filterWhere = $this->obj->getManyManyFilter($relationship, $component); - $filterJoin = $this->obj->getManyManyJoin($relationship, $component); $ctf = new ComplexTableField( $this, $relationship, - $component, + null, $relationshipFields, - "getCMSFields", - $filterWhere, - '', - $filterJoin + "getCMSFields" ); $ctf->setPermissions(TableListField::permissions_for_object($component)); $ctf->popupClass = "ScaffoldingComplexTableField_Popup"; diff --git a/forms/TableField.php b/forms/TableField.php index 8a83d51ea..5c7aedf5f 100644 --- a/forms/TableField.php +++ b/forms/TableField.php @@ -28,10 +28,6 @@ class TableField extends TableListField { - protected $sourceClass; - - protected $sourceFilter; - protected $fieldList; /** @@ -52,10 +48,6 @@ class TableField extends TableListField { * preset relations or other default data. */ protected $fieldTypes; - - protected $sourceSort; - - protected $sourceJoin; /** * @var $template string Template-Overrides @@ -105,8 +97,6 @@ class TableField extends TableListField { * * @var boolean */ - protected $relationAutoSetting = true; - function __construct($name, $sourceClass, $fieldList = null, $fieldTypes, $filterField = null, $sourceFilter = null, $editExisting = true, $sourceSort = null, $sourceJoin = null) { @@ -174,6 +164,9 @@ class TableField extends TableListField { $rows = $this->sortData(ArrayLib::invert($this->value)); // ignore all rows which are already saved if(isset($rows['new'])) { + if($sourceItems instanceof DataList) $sourceItems = $sourceItems->toDataObjectSet(); + + $newRows = $this->sortData($rows['new']); // iterate over each value (not each row) $i = 0; @@ -189,7 +182,7 @@ class TableField extends TableListField { } // generate a temporary DataObject container (not saved in the database) - $sourceClass = $this->sourceClass; + $sourceClass = $this->sourceClass(); $sourceItems->push(new $sourceClass($newRow)); $i++; @@ -264,17 +257,10 @@ class TableField extends TableListField { $savedObjIds = $this->saveData($newFields,false); } - // Optionally save the newly created records into a relationship - // on $record. This assumes the name of this formfield instance - // is set to a relationship name on $record. - if($this->relationAutoSetting) { - $relationName = $this->Name(); - if($record->has_many($relationName) || $record->many_many($relationName)) { - if($savedObjIds) foreach($savedObjIds as $id => $status) { - $record->$relationName()->add($id); - } - } - } + // Add the new records to the DataList + if($savedObjIds) foreach($savedObjIds as $id => $status) { + $this->getDataList()->add($id); + } // Update the internal source items cache $this->value = null; @@ -386,9 +372,9 @@ class TableField extends TableListField { // either look for an existing object, or create a new one if($existingValues) { - $obj = DataObject::get_by_id($this->sourceClass, $objectid); + $obj = DataObject::get_by_id($this->sourceClass(), $objectid); } else { - $sourceClass = $this->sourceClass; + $sourceClass = $this->sourceClass(); $obj = new $sourceClass(); } @@ -490,13 +476,6 @@ class TableField extends TableListField { return $this->renderWith($this->template); } - - /** - * @return Int - */ - function sourceID() { - return $this->filterField; - } function setTransformationConditions($conditions) { $this->transformationConditions = $conditions; @@ -601,20 +580,6 @@ JS; function setRequiredFields($fields) { $this->requiredFields = $fields; } - - /** - * @param boolean $value - */ - function setRelationAutoSetting($value) { - $this->relationAutoSetting = $value; - } - - /** - * @return boolean - */ - function getRelationAutoSetting() { - return $this->relationAutoSetting; - } } /** diff --git a/forms/TableListField.php b/forms/TableListField.php index 06b45e7fe..5d99d4585 100755 --- a/forms/TableListField.php +++ b/forms/TableListField.php @@ -21,19 +21,10 @@ * @subpackage fields-relational */ class TableListField extends FormField { - /** - * @var $cachedSourceItems DataObjectSet Prevent {@sourceItems()} from being called multiple times. + * The {@link DataList} object defining the source data for this view/ */ - protected $cachedSourceItems; - - protected $sourceClass; - - protected $sourceFilter = ""; - - protected $sourceSort = ""; - - protected $sourceJoin = array(); + protected $dataList; protected $fieldList; @@ -137,14 +128,6 @@ class TableListField extends FormField { * Mostly needed in ComplexTableField-subclass. */ public $defaultAction = ''; - - /** - * @var $customQuery Specify custom query, e.g. for complicated having/groupby-constructs. - * Caution: TableListField automatically selects the ID from the {@sourceClass}, because it relies - * on this information e.g. in saving a TableField. Please use a custom select if you want to filter - * for other IDs in joined tables: $query->select[] = "MyJoinedTable.ID AS MyJoinedTableID" - */ - protected $customQuery; /** * @var $customCsvQuery Query for CSV-export (might need different fields or further filtering) @@ -257,26 +240,27 @@ class TableListField extends FormField { protected $__cachedQuery; - function __construct($name, $sourceClass, $fieldList = null, $sourceFilter = null, + function __construct($name, $sourceClass = null, $fieldList = null, $sourceFilter = null, $sourceSort = null, $sourceJoin = null) { $this->fieldList = ($fieldList) ? $fieldList : singleton($sourceClass)->summaryFields(); - $this->sourceClass = $sourceClass; - $this->sourceFilter = $sourceFilter; - $this->sourceSort = $sourceSort; - $this->sourceJoin = $sourceJoin; + + if($sourceClass) { + // You can optionally pass a DataList as the 2nd argument to the constructor + if($sourceClass instanceof DataList) { + $this->dataList = $sourceClass; + + } else { + $this->dataList = DataObject::get($sourceClass)->filter($sourceFilter) + ->sort($sourceSort)->join($sourceJoin); + } + } + $this->readOnly = false; parent::__construct($name); } - /** - * Get the filter - */ - function sourceFilter() { - return $this->sourceFilter; - } - function index() { return $this->FieldHolder(); } @@ -287,7 +271,7 @@ class TableListField extends FormField { ); function sourceClass() { - return $this->sourceClass; + return $this->getDataList()->dataClass(); } function handleItem($request) { @@ -351,7 +335,7 @@ JS $headings[] = new ArrayData(array( "Name" => $fieldName, - "Title" => ($this->sourceClass) ? singleton($this->sourceClass)->fieldLabel($fieldTitle) : $fieldTitle, + "Title" => ($this->sourceClass()) ? singleton($this->sourceClass())->fieldLabel($fieldTitle) : $fieldTitle, "IsSortable" => $isSortable, "SortLink" => $sortLink, "SortBy" => $isSorted, @@ -403,27 +387,16 @@ JS /** * Provide a custom query to compute sourceItems. This is the preferred way to using * {@setSourceItems}, because we can still paginate. - * Caution: Other parameters such as {@sourceFilter} will be ignored. * Please use this only as a fallback for really complex queries (e.g. involving HAVING and GROUPBY). * - * @param $query SS_Query + * @param $query DataList */ - function setCustomQuery(SQLQuery $query) { - // The type-hinting above doesn't seem to work consistently - if($query instanceof SQLQuery) { - $this->customQuery = $query; - } else { - user_error('TableList::setCustomQuery() should be passed a SQLQuery', E_USER_WARNING); - } + function setCustomQuery(DataList $dataList) { + $this->dataList = $dataList; } - function setCustomCsvQuery(SQLQuery $query) { - // The type-hinting above doesn't seem to work consistently - if($query instanceof SQLQuery) { - $this->customCsvQuery = $query; - } else { - user_error('TableList::setCustomCsvQuery() should be passed a SQLQuery', E_USER_WARNING); - } + function setCustomCsvQuery(DataList $dataList) { + $this->customCsvQuery = $query; } function setCustomSourceItems(DataObjectSet $items) { @@ -436,45 +409,43 @@ JS } function sourceItems() { + // Determine pagination limit, offset $SQL_limit = ($this->showPagination && $this->pageSize) ? "{$this->pageSize}" : null; if(isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) { $SQL_start = (isset($_REQUEST['ctf'][$this->Name()]['start'])) ? intval($_REQUEST['ctf'][$this->Name()]['start']) : "0"; } else { $SQL_start = 0; } + + // Custom source items can be explicitly passed if(isset($this->customSourceItems)) { if($this->showPagination && $this->pageSize) { $items = $this->customSourceItems->getRange($SQL_start, $SQL_limit); } else { $items = $this->customSourceItems; } - } elseif(isset($this->cachedSourceItems)) { - $items = $this->cachedSourceItems; + + // Otherwise we use the internal data list } else { - // get query - $dataQuery = $this->getQuery(); + // get the DataList of items + $items = $this->getDataList(); // we don't limit when doing certain actions T $methodName = isset($_REQUEST['url']) ? array_pop(explode('/', $_REQUEST['url'])) : null; if(!$methodName || !in_array($methodName,array('printall','export'))) { - $dataQuery->limit(array( + $items->limit(array( 'limit' => $SQL_limit, 'start' => (isset($SQL_start)) ? $SQL_start : null )); } - - // get data - $records = $dataQuery->execute(); - $sourceClass = $this->sourceClass; - $dataobject = new $sourceClass(); - $items = $dataobject->buildDataObjectSet($records, 'DataObjectSet'); - - $this->cachedSourceItems = $items; } return $items; } - + + /** + * Return a DataObjectSet of TableListField_Item objects, suitable for display in the template. + */ function Items() { $fieldItems = new DataObjectSet(); if($items = $this->sourceItems()) foreach($items as $item) { @@ -484,16 +455,19 @@ JS } /** - * Generates the query for sourceitems (without pagination/limit-clause) - * - * @return string + * Returns the DataList for this field. */ - function getQuery() { - if($this->customQuery) { - $query = clone $this->customQuery; - $baseClass = ClassInfo::baseDataClass($this->sourceClass); - } else { - $query = singleton($this->sourceClass)->extendedSQL($this->sourceFilter(), $this->sourceSort, null, $this->sourceJoin); + function getDataList() { + // Load the data from the form + // Note that this will override any specific. This is so that explicitly-passed sets of + // parameters that represent a relation can be replaced with the relation itself. This is + // a little clumsy and won't work if people have used a field name that is the same as a + // relation but have specified alternative parameters. + if($this->form) { + $relation = $this->name; + if($record = $this->form->getRecord()) { + if($record->hasMethod($relation)) $this->dataList = $record->$relation(); + } } if(!$this->dataList) { @@ -513,20 +487,28 @@ JS if($query->canSortBy($column)) $query->orderby = $column.' '.$dir; } - return $query; + return $dl; } - function getCsvQuery() { - $baseClass = ClassInfo::baseDataClass($this->sourceClass); - if($this->customCsvQuery || $this->customQuery) { - $query = $this->customCsvQuery ? $this->customCsvQuery : $this->customQuery; - } else { - $query = singleton($this->sourceClass)->extendedSQL($this->sourceFilter(), $this->sourceSort, null, $this->sourceJoin); - } - - return clone $query; + function getCsvDataList() { + if($this->customCsvQuery) return $this->customCsvQuery; + else return $this->getDataList(); } + /** + * @deprecated Use getDataList() instead. + */ + function getQuery() { + return $this->getDataList()->dataQuery()->query(); + } + + /** + * @deprecated Use getCsvDataList() instead. + */ + function getCsvQuery() { + return $this->getCsvDataList()->dataQuery()->query(); + } + function FieldList() { return $this->fieldList; } @@ -580,7 +562,7 @@ JS $childId = Convert::raw2sql($_REQUEST['ctf']['childID']); if (is_numeric($childId)) { - $childObject = DataObject::get_by_id($this->sourceClass, $childId); + $childObject = DataObject::get_by_id($this->sourceClass(), $childId); if($childObject) $childObject->delete(); } @@ -903,19 +885,10 @@ JS } function TotalCount() { - if($this->totalCount) { - return $this->totalCount; - } - if($this->customSourceItems) { - return $this->customSourceItems->Count(); - } - - $this->totalCount = $this->getQuery()->unlimitedRowCount(); - return $this->totalCount; + return $this->getDataList()->Count(); } - /** * ################################# * Search @@ -1158,19 +1131,19 @@ JS // adding this to TODO probably add a method to the classes // to return they're translated string // added by ruibarreiros @ 27/11/2007 - return $this->sourceClass ? singleton($this->sourceClass)->singular_name() : $this->Name(); + return $this->sourceClass() ? singleton($this->sourceClass())->singular_name() : $this->Name(); } function NameSingular() { // same as Title() // added by ruibarreiros @ 27/11/2007 - return $this->sourceClass ? singleton($this->sourceClass)->singular_name() : $this->Name(); + return $this->sourceClass() ? singleton($this->sourceClass())->singular_name() : $this->Name(); } function NamePlural() { // same as Title() // added by ruibarreiros @ 27/11/2007 - return $this->sourceClass ? singleton($this->sourceClass)->plural_name() : $this->Name(); + return $this->sourceClass() ? singleton($this->sourceClass())->plural_name() : $this->Name(); } function setTemplate($template) { diff --git a/tests/forms/ComplexTableFieldTest.php b/tests/forms/ComplexTableFieldTest.php index 6ff4b8e41..d49f664ff 100644 --- a/tests/forms/ComplexTableFieldTest.php +++ b/tests/forms/ComplexTableFieldTest.php @@ -118,6 +118,7 @@ class ComplexTableFieldTest_Controller extends Controller { new FormAction('doSubmit', 'Submit') ) ); + $form->loadDataFrom($team); $form->disableSecurityToken(); @@ -148,6 +149,7 @@ class ComplexTableFieldTest_Controller extends Controller { new FormAction('doSubmit', 'Submit') ) ); + $form->loadDataFrom($team); $form->disableSecurityToken(); diff --git a/tests/forms/ComplexTableFieldTest.yml b/tests/forms/ComplexTableFieldTest.yml index 9173a8725..3f9d1cd2c 100644 --- a/tests/forms/ComplexTableFieldTest.yml +++ b/tests/forms/ComplexTableFieldTest.yml @@ -6,6 +6,7 @@ ComplexTableFieldTest_Player: ComplexTableFieldTest_Team: t1: Name: The Awesome People + Players: =>ComplexTableFieldTest_Player.p1,=>ComplexTableFieldTest_Player.p2 t2: Name: Incredible Four ComplexTableFieldTest_Sponsor: diff --git a/tests/forms/FormScaffolderTest.php b/tests/forms/FormScaffolderTest.php index 8a5620b32..48a0e9a8a 100644 --- a/tests/forms/FormScaffolderTest.php +++ b/tests/forms/FormScaffolderTest.php @@ -20,6 +20,9 @@ class FormScaffolderTest extends SapphireTest { function testGetCMSFieldsSingleton() { $fields = singleton('FormScaffolderTest_Article')->getCMSFields(); + $form = new Form(new Controller(), 'TestForm', $fields, new FieldSet()); + $form->loadDataFrom(singleton('FormScaffolderTest_Article')); + $this->assertTrue($fields->hasTabSet(), 'getCMSFields() produces a TabSet'); $this->assertNotNull($fields->dataFieldByName('Title'), 'getCMSFields() includes db fields'); $this->assertNotNull($fields->dataFieldByName('Content'), 'getCMSFields() includes db fields'); @@ -29,14 +32,22 @@ class FormScaffolderTest extends SapphireTest { function testGetCMSFieldsInstance() { $article1 = $this->objFromFixture('FormScaffolderTest_Article', 'article1'); + $fields = $article1->getCMSFields(); + $form = new Form(new Controller(), 'TestForm', $fields, new FieldSet()); + $form->loadDataFrom($article1); + $this->assertNotNull($fields->dataFieldByName('AuthorID'), 'getCMSFields() includes has_one fields on instances'); $this->assertNotNull($fields->dataFieldByName('Tags'), 'getCMSFields() includes many_many fields if ID is present on instances'); } function testUpdateCMSFields() { $article1 = $this->objFromFixture('FormScaffolderTest_Article', 'article1'); + $fields = $article1->getCMSFields(); + $form = new Form(new Controller(), 'TestForm', $fields, new FieldSet()); + $form->loadDataFrom($article1); + $this->assertNotNull( $fields->dataFieldByName('AddedExtensionField'), 'getCMSFields() includes extended fields' @@ -45,18 +56,26 @@ class FormScaffolderTest extends SapphireTest { function testRestrictCMSFields() { $article1 = $this->objFromFixture('FormScaffolderTest_Article', 'article1'); + $fields = $article1->scaffoldFormFields(array( 'restrictFields' => array('Title') )); + $form = new Form(new Controller(), 'TestForm', $fields, new FieldSet()); + $form->loadDataFrom($article1); + $this->assertNotNull($fields->dataFieldByName('Title'), 'scaffoldCMSFields() includes explitly defined "restrictFields"'); $this->assertNull($fields->dataFieldByName('Content'), 'getCMSFields() doesnt include fields left out in a "restrictFields" definition'); } function testFieldClassesOnGetCMSFields() { $article1 = $this->objFromFixture('FormScaffolderTest_Article', 'article1'); + $fields = $article1->scaffoldFormFields(array( 'fieldClasses' => array('Title' => 'HtmlEditorField') )); + $form = new Form(new Controller(), 'TestForm', $fields, new FieldSet()); + $form->loadDataFrom($article1); + $this->assertNotNull( $fields->dataFieldByName('Title') ); @@ -69,6 +88,9 @@ class FormScaffolderTest extends SapphireTest { function testGetFormFields() { $fields = singleton('FormScaffolderTest_Article')->getFrontEndFields(); + $form = new Form(new Controller(), 'TestForm', $fields, new FieldSet()); + $form->loadDataFrom(singleton('FormScaffolderTest_Article')); + $this->assertFalse($fields->hasTabSet(), 'getFrontEndFields() doesnt produce a TabSet by default'); } } diff --git a/tests/forms/TableFieldTest.php b/tests/forms/TableFieldTest.php index 0ec277208..d5922ac16 100755 --- a/tests/forms/TableFieldTest.php +++ b/tests/forms/TableFieldTest.php @@ -166,7 +166,13 @@ class TableFieldTest extends SapphireTest { $this->assertNotContains($perm1->ID, $tableField->sourceItems()->column('ID')); } + /** + * Relation auto-setting is now the only option + */ function testAutoRelationSettingOn() { + $o = new TableFieldTest_Object(); + $o->write(); + $tf = new TableField( 'HasManyRelations', 'TableFieldTest_HasManyRelation', @@ -180,69 +186,16 @@ class TableFieldTest extends SapphireTest { // Test with auto relation setting $form = new Form(new TableFieldTest_Controller(), "Form", new FieldSet($tf), new FieldSet()); + $form->loadDataFrom($o); + $tf->setValue(array( 'new' => array( 'Value' => array('one','two',) ) )); - $tf->setRelationAutoSetting(true); - $o = new TableFieldTest_Object(); - $o->write(); - $form->saveInto($o); - $this->assertEquals($o->HasManyRelations()->Count(), 2); - } - - function testAutoRelationSettingOff() { - $tf = new TableField( - 'HasManyRelations', - 'TableFieldTest_HasManyRelation', - array( - 'Value' => 'Value' - ), - array( - 'Value' => 'TextField' - ) - ); - // Test with auto relation setting - $form = new Form(new TableFieldTest_Controller(), "Form", new FieldSet($tf), new FieldSet()); - $tf->setValue(array( - 'new' => array( - 'Value' => array('one','two',) - ) - )); - $tf->setRelationAutoSetting(false); - $o = new TableFieldTest_Object(); - $o->write(); $form->saveInto($o); - $this->assertEquals($o->HasManyRelations()->Count(), 0); - } - - function testDataValue() { - $tf = new TableField( - 'TestTableField', - 'TestTableField', - array( - 'Currency' => 'Currency' - ), - array( - 'Currency' => 'CurrencyField' - ) - ); - $form = new Form(new TableFieldTest_Controller(), "Form", new FieldSet($tf), new FieldSet()); - $tf->setValue(array( - 'new' => array( - 'Currency' => array( - '$1,234.56', - '1234.57', - ) - ) - )); - $data = $form->getData(); - - // @todo Fix getData() - //$this->assertEquals($data['TestTableField']['new']['Currency'][0], 1234.56); - //$this->assertEquals($data['TestTableField']['new']['Currency'][1], 1234.57); + $this->assertEquals(2, $o->HasManyRelations()->Count()); } function testHasItemsWhenSetAsArray() { From c57378753ddb77fad1baea1760c2f769c08c4159 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Mon, 21 Mar 2011 19:53:09 +1300 Subject: [PATCH 12/64] ENHANCEMENT: Improved Versioned to pass different query styles as DataQuery modes. --- model/Versioned.php | 191 ++++++++++++++++---------------------------- 1 file changed, 68 insertions(+), 123 deletions(-) diff --git a/model/Versioned.php b/model/Versioned.php index 1b56c924a..4444bd5c1 100644 --- a/model/Versioned.php +++ b/model/Versioned.php @@ -111,13 +111,14 @@ class Versioned extends DataExtension { * Amend freshly created DataQuery objects with versioned-specific information */ function augmentDataQueryCreation(SQLQuery &$query, DataQuery &$dataQuery) { - if($date = Versioned::$reading_archived_date) { + $parts = explode('.', Versioned::get_reading_mode()); + if($parts[0] == 'Archive') { $dataQuery->setQueryParam('Versioned.mode', 'archive'); - $dataQuery->setQueryParam('Versioned.date', Versioned::$reading_archived_date); + $dataQuery->setQueryParam('Versioned.date', $parts[1]); - } else if(Versioned::$reading_stage && Versioned::$reading_stage != $this->defaultStage && array_search(Versioned::$reading_stage,$this->stages) !== false) { + } else if($parts[0] == 'Stage' && $parts[1] != $this->defaultStage && array_search($parts[1],$this->stages) !== false) { $dataQuery->setQueryParam('Versioned.mode', 'stage'); - $dataQuery->setQueryParam('Versioned.stage', Versioned::$reading_stage); + $dataQuery->setQueryParam('Versioned.stage', $parts[1]); } } @@ -127,14 +128,17 @@ class Versioned extends DataExtension { * @todo Should this all go into VersionedDataQuery? */ function augmentSQL(SQLQuery &$query, DataQuery &$dataQuery) { + $baseTable = ClassInfo::baseDataClass($dataQuery->dataClass()); + switch($dataQuery->getQueryParam('Versioned.mode')) { + // Noop + case '': + break; + // Reading a specific data from the archive case 'archive': $date = $dataQuery->getQueryParam('Versioned.date'); foreach($query->from as $table => $dummy) { - if(!isset($baseTable)) { - $baseTable = $table; - } $query->renameTable($table, $table . '_versions'); $query->replaceText("\"$table\".\"ID\"", "\"$table\".\"RecordID\""); @@ -172,6 +176,36 @@ class Versioned extends DataExtension { } } break; + + + // Return all version instances + case 'all_versions': + case 'latest_versions': + foreach($query->from as $alias => $join) { + if($alias != $baseTable) { + $query->setJoinFilter($alias, "\"$alias\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$alias\".\"Version\" = \"{$baseTable}_versions\".\"Version\""); + } + $query->renameTable($alias, $alias . '_versions'); + } + + // Add all _versions columns + foreach(self::$db_for_versions_table as $name => $type) { + $query->selectMore(sprintf('"%s_versions"."%s"', $baseTable, $name)); + } + $query->selectMore(sprintf('"%s_versions"."%s" AS "ID"', $baseTable, 'RecordID')); + + // latest_version has one more step + // Return latest version instances, regardless of whether they are on a particular stage + // This provides "show all, including deleted" functonality + if($dataQuery->getQueryParam('Versioned.mode') == 'latest_versions') { + $archiveTable = self::requireArchiveTempTable($baseTable); + $query->innerJoin($archiveTable, "\"$archiveTable\".\"ID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$archiveTable\".\"Version\" = \"{$baseTable}_versions\".\"Version\""); + } + + break; + + default: + throw new InvalidArgumentException("Bad value for query parameter Versioned.mode: " . $dataQuery->getQueryParam('Versioned.mode')); } } @@ -803,17 +837,11 @@ class Versioned extends DataExtension { * @param string $orderby A sort expression to be inserted into the ORDER BY clause. * @return DataObject */ - static function get_one_by_stage($class, $stage, $filter = '', $cache = true, $orderby = '') { - $oldMode = Versioned::get_reading_mode(); - Versioned::reading_stage($stage); - - singleton($class)->flushCache(); - $result = DataObject::get_one($class, $filter, $cache, $orderby); - singleton($class)->flushCache(); - - Versioned::set_reading_mode($oldMode); - return $result; - } + static function get_one_by_stage($class, $stage, $filter = '', $cache = true, $sort = '') { + // TODO: No identity cache operating + $items = self::get_by_stage($class, $stage, $filter, $sort, null, 1); + return $items->First(); + } /** * Gets the current version number of a specific record. @@ -832,7 +860,7 @@ class Versioned extends DataExtension { if($cache && isset(self::$cache_versionnumber[$baseClass][$stage][$id])) { return self::$cache_versionnumber[$baseClass][$stage][$id]; } - + // get version as performance-optimized SQL query (gets called for each page in the sitetree) $version = DB::query("SELECT \"Version\" FROM \"$stageTable\" WHERE \"ID\" = $id")->value(); @@ -908,7 +936,7 @@ class Versioned extends DataExtension { Versioned::set_reading_mode($oldMode); return $result; } - + /** * Roll the draft version of this page to match the published page. * Caution: Doesn't overwrite the object properties with the rolled back version. @@ -919,44 +947,6 @@ class Versioned extends DataExtension { $this->publish($version, "Stage", true); $this->owner->writeWithoutVersion(); } - - /** - * Build a SQL query to get data from the _version table. - * This function is similar in style to {@link DataObject::buildSQL} - */ - function buildVersionSQL($filter = "", $sort = "") { - $query = $this->owner->extendedSQL($filter,$sort); - foreach($query->from as $table => $join) { - if($join[0] == '"') $baseTable = str_replace('"','',$join); - else $query->from[$table] = "LEFT JOIN \"$table\" ON \"$table\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$table\".\"Version\" = \"{$baseTable}_versions\".\"Version\""; - $query->renameTable($table, $table . '_versions'); - } - - // Add all _versions columns - foreach(self::$db_for_versions_table as $name => $type) { - $query->select[] = sprintf('"%s_versions"."%s"', $baseTable, $name); - } - $query->select[] = sprintf('"%s_versions"."%s" AS "ID"', $baseTable, 'RecordID'); - - return $query; - } - - static function build_version_sql($className, $filter = "", $sort = "") { - $query = singleton($className)->extendedSQL($filter,$sort); - foreach($query->from as $table => $join) { - if($join[0] == '"') $baseTable = str_replace('"','',$join); - else $query->from[$table] = "LEFT JOIN \"$table\" ON \"$table\".\"RecordID\" = \"{$baseTable}_versions\".\"RecordID\" AND \"$table\".\"Version\" = \"{$baseTable}_versions\".\"Version\""; - $query->renameTable($table, $table . '_versions'); - } - - // Add all _versions columns - foreach(self::$db_for_versions_table as $name => $type) { - $query->select[] = sprintf('"%s_versions"."%s"', $baseTable, $name); - } - $query->select[] = sprintf('"%s_versions"."%s" AS "ID"', $baseTable, 'RecordID'); - - return $query; - } /** * Return the latest version of the given page. @@ -964,25 +954,10 @@ class Versioned extends DataExtension { * @return DataObject */ static function get_latest_version($class, $id) { - $oldMode = Versioned::get_reading_mode(); - Versioned::set_reading_mode(''); - - $baseTable = ClassInfo::baseDataClass($class); - $query = singleton($class)->buildVersionSQL("\"{$baseTable}\".\"RecordID\" = $id", "\"{$baseTable}\".\"Version\" DESC"); - $query->limit = 1; - $record = $query->execute()->record(); - if(!$record) return; - - $className = $record['ClassName']; - if(!$className) { - Debug::show($query->sql()); - Debug::show($record); - user_error("Versioned::get_version: Couldn't get $class.$id", E_USER_ERROR); - } - - Versioned::set_reading_mode($oldMode); - - return new $className($record); + $baseClass = ClassInfo::baseDataClass($class); + $list = DataList::create($class)->filter("\"$baseClass\".\"RecordID\" = $id"); + $list->dataQuery()->setQueryParam("Versioned.mode", "latest_versions"); + return $list->First(); } /** @@ -992,61 +967,31 @@ class Versioned extends DataExtension { * In particular, this will query deleted records as well as active ones. */ static function get_including_deleted($class, $filter = "", $sort = "") { - $oldMode = Versioned::get_reading_mode(); - Versioned::set_reading_mode(''); - - $SNG = singleton($class); - - // Build query - $query = $SNG->buildVersionSQL($filter, $sort); - $baseTable = ClassInfo::baseDataClass($class); - $archiveTable = self::requireArchiveTempTable($baseTable); - $query->from[$archiveTable] = "INNER JOIN \"$archiveTable\" - ON \"$archiveTable\".\"ID\" = \"{$baseTable}_versions\".\"RecordID\" - AND \"$archiveTable\".\"Version\" = \"{$baseTable}_versions\".\"Version\""; - - // Process into a DataObjectSet - $result = $SNG->buildDataObjectSet($query->execute(), 'DataObjectSet', null, $class); - - Versioned::set_reading_mode($oldMode); - return $query; + $list = DataList::create($class)->filter($filter)->sort($sort); + $list->dataQuery()->setQueryParam("Versioned.mode", "latest_versions"); + return $list; } /** + * Return the specific version of the given id * @return DataObject */ static function get_version($class, $id, $version) { - $oldMode = Versioned::get_reading_mode(); - Versioned::set_reading_mode(''); - - $baseTable = ClassInfo::baseDataClass($class); - $query = singleton($class)->buildVersionSQL("\"{$baseTable}\".\"RecordID\" = $id AND \"{$baseTable}\".\"Version\" = $version"); - $record = $query->execute()->record(); - $className = $record['ClassName']; - if(!$className) { - Debug::show($query->sql()); - Debug::show($record); - user_error("Versioned::get_version: Couldn't get $class.$id, version $version", E_USER_ERROR); - } - - Versioned::set_reading_mode($oldMode); - return new $className($record); + $baseClass = ClassInfo::baseDataClass($class); + $list = DataList::create($class)->filter("\"$baseClass\".\"RecordID\" = $id")->filter("\"$baseClass\".\"Version\" = " . (int)$version); + $list->dataQuery()->setQueryParam('Versioned.mode', 'all_versions'); + return $list->First(); } /** - * @return DataObject + * Return a list of all versions for a given id + * @return DataList */ - static function get_all_versions($class, $id, $version) { - $baseTable = ClassInfo::baseDataClass($class); - $query = singleton($class)->buildVersionSQL("\"{$baseTable}\".\"RecordID\" = $id AND \"{$baseTable}\".\"Version\" = $version"); - $record = $query->execute()->record(); - $className = $record['ClassName']; - if(!$className) { - Debug::show($query->sql()); - Debug::show($record); - user_error("Versioned::get_version: Couldn't get $class.$id, version $version", E_USER_ERROR); - } - return new $className($record); + static function get_all_versions($class, $id) { + $baseClass = ClassInfo::baseDataClass($class); + $list = DataList::create($class)->filter("\"$baseClass\".\"RecordID\" = $id"); + $list->dataQuery()->setQueryParam('Versioned.mode', 'all_versions'); + return $list; } function contentcontrollerInit($controller) { @@ -1070,7 +1015,7 @@ class Versioned extends DataExtension { * Return a piece of text to keep DataObject cache keys appropriately specific */ function cacheKeyComponent() { - return 'stage-'.self::current_stage(); + return 'versionedmode-'.self::get_reading_mode(); } } From 0ba86971e2f2aa545998ff0abb065a71f3951cfd Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Mon, 21 Mar 2011 19:58:01 +1300 Subject: [PATCH 13/64] API CHANGE: Removed context object - it's a hack. --- model/DataObject.php | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/model/DataObject.php b/model/DataObject.php index 37ef4c05e..da301ecb8 100755 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -3040,29 +3040,6 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity return is_numeric( $this->ID ) && $this->ID > 0; } - /** - * Sets a 'context object' that can be used to provide hints about how to process a particular get / get_one request. - * In particular, DataExtensions can use this to amend queries more effectively. - * Care must be taken to unset the context object after you're done with it, otherwise you will have a stale context, - * which could cause horrible bugs. - */ - public static function set_context_obj($obj) { - if($obj && self::$context_obj) user_error("Dataobject::set_context_obj passed " . $obj->class . "." . $obj->ID . " when there is already a context: " . self::$context_obj->class . '.' . self::$context_obj->ID, E_USER_WARNING); - self::$context_obj = $obj; - } - - /** - * Retrieve the current context object. - */ - public static function context_obj() { - return self::$context_obj; - } - - /** - * @ignore - */ - protected static $context_obj = null; - /* * @ignore */ From f83abe416c278af350c7d19abbb6ce3bc83c3084 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Mon, 21 Mar 2011 20:01:28 +1300 Subject: [PATCH 14/64] MINOR: Improved some exception handling. --- model/Database.php | 2 +- model/SQLQuery.php | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/model/Database.php b/model/Database.php index 72a359b7f..0ff12451e 100755 --- a/model/Database.php +++ b/model/Database.php @@ -725,7 +725,7 @@ abstract class SS_Database { $limit = $sqlQuery->limit; // Pass limit as array or SQL string value if(is_array($limit)) { - if(!array_key_exists('limit',$limit)) user_error('SQLQuery::limit(): Wrong format for $limit', E_USER_ERROR); + if(!array_key_exists('limit',$limit)) throw new InvalidArgumentException('SQLQuery::limit(): Wrong format for $limit: ' . var_export($limit, true)); if(isset($limit['start']) && is_numeric($limit['start']) && isset($limit['limit']) && is_numeric($limit['limit'])) { $combinedLimit = $limit['start'] ? "$limit[limit] OFFSET $limit[start]" : "$limit[limit]"; diff --git a/model/SQLQuery.php b/model/SQLQuery.php index d655ce3e1..0c63f8adc 100755 --- a/model/SQLQuery.php +++ b/model/SQLQuery.php @@ -480,7 +480,11 @@ class SQLQuery { * @return string */ function __toString() { - return $this->sql(); + try { + return $this->sql(); + } catch(Exception $e) { + return ""; + } } /** From c615c4eb917586b7011f3922baeb5efea69e5ac1 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Mon, 21 Mar 2011 21:37:55 +1300 Subject: [PATCH 15/64] ENHANCEMENT: First cut of running SearchContext through DataList/DataQuery. Note that the eventual goal is probably to ditch SearchContext entirely. --- core/model/DataQuery.php | 81 ++++++++++++++++++++- search/SearchContext.php | 46 ++++-------- search/filters/EndsWithFilter.php | 6 +- search/filters/ExactMatchFilter.php | 6 +- search/filters/ExactMatchMultiFilter.php | 7 +- search/filters/FulltextFilter.php | 2 +- search/filters/GreaterThanFilter.php | 6 +- search/filters/LessThanFilter.php | 6 +- search/filters/NegationFilter.php | 4 +- search/filters/PartialMatchFilter.php | 6 +- search/filters/SearchFilter.php | 93 +----------------------- search/filters/StartsWithFilter.php | 6 +- search/filters/StartsWithMultiFilter.php | 6 +- search/filters/SubstringFilter.php | 4 +- tests/model/SQLQueryTest.php | 2 + 15 files changed, 129 insertions(+), 152 deletions(-) diff --git a/core/model/DataQuery.php b/core/model/DataQuery.php index 534db3568..bcea0d23e 100644 --- a/core/model/DataQuery.php +++ b/core/model/DataQuery.php @@ -352,7 +352,86 @@ class DataQuery { return $this; } } - + + /** + * Traverse the relationship fields, and add the table + * mappings to the query object state. This has to be called + * in any overloaded {@link SearchFilter->apply()} methods manually. + * + * @param $relation The array/dot-syntax relation to follow + * @return The model class of the related item + */ + function applyRelation($relation) { + // NO-OP + if(!$relation) return $this->dataClass; + + if(is_string($relation)) $relation = explode(".", $relation); + + $modelClass = $this->dataClass; + + foreach($relation as $rel) { + $model = singleton($modelClass); + if ($component = $model->has_one($rel)) { + if(!$this->query->isJoinedTo($component)) { + $foreignKey = $model->getReverseAssociation($component); + $this->query->leftJoin($component, "\"$component\".\"ID\" = \"{$modelClass}\".\"{$foreignKey}ID\""); + + /** + * add join clause to the component's ancestry classes so that the search filter could search on its + * ancester fields. + */ + $ancestry = ClassInfo::ancestry($component, true); + if(!empty($ancestry)){ + $ancestry = array_reverse($ancestry); + foreach($ancestry as $ancestor){ + if($ancestor != $component){ + $this->query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\""); + $component=$ancestor; + } + } + } + } + $modelClass = $component; + + } elseif ($component = $model->has_many($rel)) { + if(!$this->query->isJoinedTo($component)) { + $ancestry = $model->getClassAncestry(); + $foreignKey = $model->getRemoteJoinField($rel); + $this->query->leftJoin($component, "\"$component\".\"{$foreignKey}\" = \"{$ancestry[0]}\".\"ID\""); + /** + * add join clause to the component's ancestry classes so that the search filter could search on its + * ancestor fields. + */ + $ancestry = ClassInfo::ancestry($component, true); + if(!empty($ancestry)){ + $ancestry = array_reverse($ancestry); + foreach($ancestry as $ancestor){ + if($ancestor != $component){ + $this->query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\""); + $component=$ancestor; + } + } + } + } + $modelClass = $component; + + } elseif ($component = $model->many_many($rel)) { + list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component; + $parentBaseClass = ClassInfo::baseDataClass($parentClass); + $componentBaseClass = ClassInfo::baseDataClass($componentClass); + $this->query->innerJoin($relationTable, "\"$relationTable\".\"$parentField\" = \"$parentBaseClass\".\"ID\""); + $this->query->leftJoin($componentBaseClass, "\"$relationTable\".\"$componentField\" = \"$componentBaseClass\".\"ID\""); + if(ClassInfo::hasTable($componentClass)) { + $this->query->leftJoin($componentClass, "\"$relationTable\".\"$componentField\" = \"$componentClass\".\"ID\""); + } + $modelClass = $componentClass; + + } + } + + return $modelClass; + } + /** * Select the given fields from the given table */ diff --git a/search/SearchContext.php b/search/SearchContext.php index 54a4e8a73..ca4be07c2 100644 --- a/search/SearchContext.php +++ b/search/SearchContext.php @@ -115,19 +115,17 @@ class SearchContext extends Object { * @return SQLQuery */ public function getQuery($searchParams, $sort = false, $limit = false, $existingQuery = null) { - $model = singleton($this->modelClass); - - if($existingQuery) { - $query = $existingQuery; - } else { - $query = $model->extendedSQL(); - } - - $SQL_limit = Convert::raw2sql($limit); - $query->limit($SQL_limit); - - $SQL_sort = (!empty($sort)) ? Convert::raw2sql($sort) : singleton($this->modelClass)->stat('default_sort'); - $query->orderby($SQL_sort); + if($existingQuery) { + if(!($existingQuery instanceof DataList)) throw new InvalidArgumentException("existingQuery must be DataList"); + if($existingQuery->dataClass() != $this->modelClass) throw new InvalidArgumentException("existingQuery's dataClass is " . $existingQuery->dataClass() . ", $this->modelClass expected."); + $query = $existingQuery; + + } else { + $query = DataList::create($this->modelClass); + } + + $query->limit($limit); + $query->sort($sort); // hack to work with $searchParems when it's an Object $searchParamArray = array(); @@ -143,15 +141,12 @@ class SearchContext extends Object { $filter->setModel($this->modelClass); $filter->setValue($value); if(! $filter->isEmpty()) { - $filter->apply($query); + $filter->apply($query->dataQuery()); } } } - $query->connective = $this->connective; - $query->distinct = true; - - $model->extend('augmentSQL', $query); + if($this->connective != "AND") throw new Exception("SearchContext connective '$this->connective' not supported after ORM-rewrite."); return $query; } @@ -168,18 +163,9 @@ class SearchContext extends Object { */ public function getResults($searchParams, $sort = false, $limit = false) { $searchParams = array_filter($searchParams, array($this,'clearEmptySearchFields')); - - $query = $this->getQuery($searchParams, $sort, $limit); - - // use if a raw SQL query is needed - $results = new DataObjectSet(); - foreach($query->execute() as $row) { - $className = $row['RecordClassName']; - $results->push(new $className($row)); - } - return $results; - // - //return DataObject::get($this->modelClass, $query->getFilter(), "", "", $limit); + + // getQuery actually returns a DataList + return $this->getQuery($searchParams, $sort, $limit); } /** diff --git a/search/filters/EndsWithFilter.php b/search/filters/EndsWithFilter.php index 056353ede..3cc8eb360 100644 --- a/search/filters/EndsWithFilter.php +++ b/search/filters/EndsWithFilter.php @@ -23,9 +23,9 @@ class EndsWithFilter extends SearchFilter { * * @return unknown */ - public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); - $query->where($this->getDbName() . " LIKE '%" . Convert::raw2sql($this->getValue()) . "'"); + public function apply(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + $query->filter($this->getDbName() . " LIKE '%" . Convert::raw2sql($this->getValue()) . "'"); } public function isEmpty() { diff --git a/search/filters/ExactMatchFilter.php b/search/filters/ExactMatchFilter.php index 161c016aa..9dcd28ba2 100644 --- a/search/filters/ExactMatchFilter.php +++ b/search/filters/ExactMatchFilter.php @@ -20,9 +20,9 @@ class ExactMatchFilter extends SearchFilter { * * @return unknown */ - public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); - return $query->where(sprintf( + public function apply(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + return $query->filter(sprintf( "%s = '%s'", $this->getDbName(), Convert::raw2sql($this->getValue()) diff --git a/search/filters/ExactMatchMultiFilter.php b/search/filters/ExactMatchMultiFilter.php index e85d410ea..9b35f9106 100644 --- a/search/filters/ExactMatchMultiFilter.php +++ b/search/filters/ExactMatchMultiFilter.php @@ -14,9 +14,8 @@ */ class ExactMatchMultiFilter extends SearchFilter { - public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); - + public function apply(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); // hack // PREVIOUS $values = explode(',',$this->getValue()); $values = array(); @@ -40,7 +39,7 @@ class ExactMatchMultiFilter extends SearchFilter { } $SQL_valueStr = "'" . implode("','", $values) . "'"; - return $query->where(sprintf( + return $query->filter(sprintf( "%s IN (%s)", $this->getDbName(), $SQL_valueStr diff --git a/search/filters/FulltextFilter.php b/search/filters/FulltextFilter.php index 83e4538c1..0401331f0 100644 --- a/search/filters/FulltextFilter.php +++ b/search/filters/FulltextFilter.php @@ -27,7 +27,7 @@ */ class FulltextFilter extends SearchFilter { - public function apply(SQLQuery $query) { + public function apply(DataQuery $query) { $query->where(sprintf( "MATCH (%s) AGAINST ('%s')", $this->getDbName(), diff --git a/search/filters/GreaterThanFilter.php b/search/filters/GreaterThanFilter.php index 20c95e24d..0556c211d 100644 --- a/search/filters/GreaterThanFilter.php +++ b/search/filters/GreaterThanFilter.php @@ -12,9 +12,9 @@ class GreaterThanFilter extends SearchFilter { /** * @return $query */ - public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); - return $query->where(sprintf( + public function apply(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + return $query->filter(sprintf( "%s > '%s'", $this->getDbName(), Convert::raw2sql($this->getDbFormattedValue()) diff --git a/search/filters/LessThanFilter.php b/search/filters/LessThanFilter.php index 9b522ef51..b79d29a65 100644 --- a/search/filters/LessThanFilter.php +++ b/search/filters/LessThanFilter.php @@ -12,9 +12,9 @@ class LessThanFilter extends SearchFilter { /** * @return $query */ - public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); - return $query->where(sprintf( + public function apply(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + return $query->filter(sprintf( "%s < '%s'", $this->getDbName(), Convert::raw2sql($this->getDbFormattedValue()) diff --git a/search/filters/NegationFilter.php b/search/filters/NegationFilter.php index 73ad02ecd..9d092936a 100644 --- a/search/filters/NegationFilter.php +++ b/search/filters/NegationFilter.php @@ -7,8 +7,8 @@ */ class NegationFilter extends SearchFilter { - public function apply(SQLQuery $query) { - return $query->where(sprintf( + public function apply(DataQuery $query) { + return $query->filter(sprintf( "%s != '%s'", $this->getDbName(), Convert::raw2sql($this->getValue()) diff --git a/search/filters/PartialMatchFilter.php b/search/filters/PartialMatchFilter.php index bad845f5c..cf97d48fb 100644 --- a/search/filters/PartialMatchFilter.php +++ b/search/filters/PartialMatchFilter.php @@ -12,9 +12,9 @@ */ class PartialMatchFilter extends SearchFilter { - public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); - return $query->where(sprintf( + public function apply(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + return $query->filter(sprintf( "%s LIKE '%%%s%%'", $this->getDbName(), Convert::raw2sql($this->getValue()) diff --git a/search/filters/SearchFilter.php b/search/filters/SearchFilter.php index d6f4989c6..615ab7c1b 100755 --- a/search/filters/SearchFilter.php +++ b/search/filters/SearchFilter.php @@ -149,96 +149,7 @@ abstract class SearchFilter extends Object { $dbField->setValue($this->value); return $dbField->RAW(); } - - /** - * Traverse the relationship fields, and add the table - * mappings to the query object state. This has to be called - * in any overloaded {@link SearchFilter->apply()} methods manually. - * - * @todo try to make this implicitly triggered so it doesn't have to be manually called in child filters - * @param SQLQuery $query - * @return SQLQuery - */ - function applyRelation($query) { - if (is_array($this->relation)) { - foreach($this->relation as $rel) { - $model = singleton($this->model); - if ($component = $model->has_one($rel)) { - if(!$query->isJoinedTo($component)) { - $foreignKey = $model->getReverseAssociation($component); - $query->leftJoin($component, "\"$component\".\"ID\" = \"{$this->model}\".\"{$foreignKey}ID\""); - - /** - * add join clause to the component's ancestry classes so that the search filter could search on its - * ancester fields. - */ - $ancestry = ClassInfo::ancestry($component, true); - if(!empty($ancestry)){ - $ancestry = array_reverse($ancestry); - foreach($ancestry as $ancestor){ - if($ancestor != $component){ - $query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\""); - $component=$ancestor; - } - } - } - } - $this->model = $component; - } elseif ($component = $model->has_many($rel)) { - if(!$query->isJoinedTo($component)) { - $ancestry = $model->getClassAncestry(); - $foreignKey = $model->getRemoteJoinField($rel); - $query->leftJoin($component, "\"$component\".\"{$foreignKey}\" = \"{$ancestry[0]}\".\"ID\""); - /** - * add join clause to the component's ancestry classes so that the search filter could search on its - * ancestor fields. - */ - $ancestry = ClassInfo::ancestry($component, true); - if(!empty($ancestry)){ - $ancestry = array_reverse($ancestry); - foreach($ancestry as $ancestor){ - if($ancestor != $component){ - $query->innerJoin($ancestor, "\"$component\".\"ID\" = \"$ancestor\".\"ID\""); - $component=$ancestor; - } - } - } - } - $this->model = $component; - } elseif ($component = $model->many_many($rel)) { - list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component; - $parentBaseClass = ClassInfo::baseDataClass($parentClass); - $componentBaseClass = ClassInfo::baseDataClass($componentClass); - $query->innerJoin($relationTable, "\"$relationTable\".\"$parentField\" = \"$parentBaseClass\".\"ID\""); - $query->leftJoin($componentBaseClass, "\"$relationTable\".\"$componentField\" = \"$componentBaseClass\".\"ID\""); - if(ClassInfo::hasTable($componentClass)) { - $query->leftJoin($componentClass, "\"$relationTable\".\"$componentField\" = \"$componentClass\".\"ID\""); - } - $this->model = $componentClass; - - // Experimental support for user-defined relationships via a "(relName)Query" method - // This will likely be dropped in 2.4 for a system that makes use of Lazy Data Lists. - } elseif($model->hasMethod($rel.'Query')) { - // Get the query representing the join - it should have "$ID" in the filter - $newQuery = $model->{"{$rel}Query"}(); - if($newQuery) { - // Get the table to join to - //DATABASE ABSTRACTION: I don't think we need this line anymore: - $newModel = str_replace('`','',array_shift($newQuery->from)); - // Get the filter to use on the join - $ancestry = $model->getClassAncestry(); - $newFilter = "(" . str_replace('$ID', "\"{$ancestry[0]}\".\"ID\"" , implode(") AND (", $newQuery->where) ) . ")"; - $query->leftJoin($newModel, $newFilter); - $this->model = $newModel; - } else { - $this->name = "NULL"; - return; - } - } - } - } - return $query; - } + /** * Apply filter criteria to a SQL query. @@ -246,7 +157,7 @@ abstract class SearchFilter extends Object { * @param SQLQuery $query * @return SQLQuery */ - abstract public function apply(SQLQuery $query); + abstract public function apply(DataQuery $query); /** * Determines if a field has a value, diff --git a/search/filters/StartsWithFilter.php b/search/filters/StartsWithFilter.php index c6ada3c7c..fda662316 100644 --- a/search/filters/StartsWithFilter.php +++ b/search/filters/StartsWithFilter.php @@ -23,9 +23,9 @@ class StartsWithFilter extends SearchFilter { * * @return unknown */ - public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); - $query->where($this->getDbName() . " LIKE '" . Convert::raw2sql($this->getValue()) . "%'"); + public function apply(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); + $query->filter($this->getDbName() . " LIKE '" . Convert::raw2sql($this->getValue()) . "%'"); } public function isEmpty() { diff --git a/search/filters/StartsWithMultiFilter.php b/search/filters/StartsWithMultiFilter.php index ed105a6cf..22f4f4dcf 100644 --- a/search/filters/StartsWithMultiFilter.php +++ b/search/filters/StartsWithMultiFilter.php @@ -14,8 +14,8 @@ */ class StartsWithMultiFilter extends SearchFilter { - public function apply(SQLQuery $query) { - $query = $this->applyRelation($query); + public function apply(DataQuery $query) { + $this->model = $query->applyRelation($this->relation); $values = explode(',', $this->getValue()); foreach($values as $value) { @@ -25,7 +25,7 @@ class StartsWithMultiFilter extends SearchFilter { ); } - return $query->where(implode(" OR ", $matches)); + return $query->filter(implode(" OR ", $matches)); } public function isEmpty() { diff --git a/search/filters/SubstringFilter.php b/search/filters/SubstringFilter.php index 7b72b46b4..a8c345d64 100644 --- a/search/filters/SubstringFilter.php +++ b/search/filters/SubstringFilter.php @@ -12,8 +12,8 @@ */ class SubstringFilter extends SearchFilter { - public function apply(SQLQuery $query) { - return $query->where(sprintf( + public function apply(DataQuery $query) { + return $query->filter(sprintf( "LOCATE('%s', %s) != 0", Convert::raw2sql($this->getValue()), $this->getDbName() diff --git a/tests/model/SQLQueryTest.php b/tests/model/SQLQueryTest.php index 44ae1a4a1..89e16fcbb 100644 --- a/tests/model/SQLQueryTest.php +++ b/tests/model/SQLQueryTest.php @@ -65,6 +65,7 @@ class SQLQueryTest extends SapphireTest { } function testSelectWithPredicateFilters() { + /* this is no longer part of this $query = new SQLQuery(); $query->select(array("Name"))->from("SQLQueryTest_DO"); @@ -77,6 +78,7 @@ class SQLQueryTest extends SapphireTest { $match->apply($query); $this->assertEquals("SELECT Name FROM SQLQueryTest_DO WHERE (\"SQLQueryTest_DO\".\"Name\" = 'Value') AND (\"SQLQueryTest_DO\".\"Meta\" LIKE '%Value%')", $query->sql()); + */ } function testSelectWithLimitClause() { From b8f736d66557d527969737365df3a13a014179bd Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Mon, 21 Mar 2011 21:41:18 +1300 Subject: [PATCH 16/64] ENHANCEMENT: Updated restfulserver to make use of refactored searchquery. note that searchquery will probably be bypasssed entirely in the near future. --- api/RestfulServer.php | 52 ++++++++++++------------------------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/api/RestfulServer.php b/api/RestfulServer.php index fde7742d0..d297b1cfd 100644 --- a/api/RestfulServer.php +++ b/api/RestfulServer.php @@ -221,26 +221,19 @@ class RestfulServer extends Controller { // depending on the request if($id) { // Format: /api/v1// - $query = $this->getObjectQuery($className, $id, $params); - $obj = singleton($className)->buildDataObjectSet($query->execute()); + $obj = $this->getObjectQuery($className, $id, $params)->First(); if(!$obj) return $this->notFound(); - $obj = $obj->First(); if(!$obj->canView()) return $this->permissionFailure(); // Format: /api/v1/// if($relationName) { - $query = $this->getObjectRelationQuery($obj, $params, $sort, $limit, $relationName); - if($query === false) return $this->notFound(); - $obj = singleton($className)->buildDataObjectSet($query->execute()); + $obj = $this->getObjectRelationQuery($obj, $params, $sort, $limit, $relationName); + if(!$obj) return $this->notFound(); } } else { // Format: /api/v1/ - $query = $this->getObjectsQuery($className, $params, $sort, $limit); - $obj = singleton($className)->buildDataObjectSet($query->execute()); - - // show empty serialized result when no records are present - if(!$obj) $obj = new DataObjectSet(); + $obj = $this->getObjectsQuery($className, $params, $sort, $limit); } $this->getResponse()->addHeader('Content-Type', $responseFormatter->getOutputContentType()); @@ -249,7 +242,7 @@ class RestfulServer extends Controller { $fields = $rawFields ? explode(',', $rawFields) : null; if($obj instanceof DataObjectSet) { - $responseFormatter->setTotalSize($query->unlimitedRowCount()); + $responseFormatter->setTotalSize($obj->dataQuery()->query()->unlimitedRowCount()); return $responseFormatter->convertDataObjectSet($obj, $fields); } else if(!$obj) { $responseFormatter->setTotalSize(0); @@ -277,9 +270,8 @@ class RestfulServer extends Controller { } else { $searchContext = singleton($className)->getDefaultSearchContext(); } - $query = $searchContext->getQuery($params, $sort, $limit, $existingQuery); - - return $query; + + return $searchContext->getQuery($params, $sort, $limit, $existingQuery); } /** @@ -499,13 +491,10 @@ class RestfulServer extends Controller { * @param string $className * @param int $id * @param array $params - * @return SQLQuery + * @return DataList */ protected function getObjectQuery($className, $id, $params) { - $baseClass = ClassInfo::baseDataClass($className); - return singleton($className)->extendedSQL( - "\"$baseClass\".\"ID\" = {$id}" - ); + return DataList::create($className)->byIDs(array($id)); } /** @@ -529,24 +518,11 @@ class RestfulServer extends Controller { * @return SQLQuery|boolean */ protected function getObjectRelationQuery($obj, $params, $sort, $limit, $relationName) { - if($obj->hasMethod("{$relationName}Query")) { - // @todo HACK Switch to ComponentSet->getQuery() once we implement it (and lazy loading) - $query = $obj->{"{$relationName}Query"}(null, $sort, null, $limit); - $relationClass = $obj->{"{$relationName}Class"}(); - } elseif($relationClass = $obj->many_many($relationName)) { - // many_many() returns different notation - $relationClass = $relationClass[1]; - $query = $obj->getManyManyComponentsQuery($relationName); - } elseif($relationClass = $obj->has_many($relationName)) { - $query = $obj->getComponentsQuery($relationName); - } elseif($relationClass = $obj->has_one($relationName)) { - $query = null; - } else { - return false; - } - - // get all results - return $this->getSearchQuery($relationClass, $params, $sort, $limit, $query); + // The relation method will return a DataList, that getSearchQuery subsequently manipulates + if($obj->hasMethod($relationName)) { + $query = $obj->$relationName(); + return $this->getSearchQuery($query->dataClass(), $params, $sort, $limit, $query); + } } protected function permissionFailure() { From 3a17d5c42747aaeece1d81a71af368473f7d6f9e Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Wed, 30 Mar 2011 14:19:27 +1300 Subject: [PATCH 17/64] API CHANGE: Add removeByID(), canSortBy(), and byID() to DataList and its subclasses. --- core/model/DataList.php | 25 ++++++++++++++++++++++- core/model/HasManyList.php | 15 +++++++++++--- core/model/ManyManyList.php | 19 ++++++++++++----- tests/model/DataExtensionTest.php | 2 +- tests/model/DataObjectTest.php | 34 ++++++++++++++++++++++++++++++- 5 files changed, 84 insertions(+), 11 deletions(-) diff --git a/core/model/DataList.php b/core/model/DataList.php index 51f491c8a..6d3dd7c67 100644 --- a/core/model/DataList.php +++ b/core/model/DataList.php @@ -76,6 +76,13 @@ class DataList extends DataObjectSet { $this->dataQuery->sort($sort); return $this; } + + /** + * Returns true if this DataList can be sorted by the given field. + */ + public function canSortBy($field) { + return $this->dataQuery()->query()->canSortBy($field); + } /** * Add an join clause to this data list's query. @@ -242,6 +249,14 @@ class DataList extends DataObjectSet { return $this; } + + /** + * Return the item of the given ID + */ + public function byID($id) { + $baseClass = ClassInfo::baseDataClass($this->dataClass); + return $this->filter("\"$baseClass\".\"ID\" = " . (int)$id)->First(); + } /** * Return a single column from this DataList. @@ -318,7 +333,7 @@ class DataList extends DataObjectSet { */ function removeMany($idList) { foreach($idList as $id) { - $this->remove($id); + $this->removeByID($id); } } @@ -357,6 +372,14 @@ class DataList extends DataObjectSet { } + /** + * Remove an item from this DataList by ID + */ + function removeByID($itemID) { + $item = $this->byID($itemID); + if($item) return $item->delete(); + } + // Methods that won't function on DataLists function push($item) { diff --git a/core/model/HasManyList.php b/core/model/HasManyList.php index 194d49443..87ec54842 100644 --- a/core/model/HasManyList.php +++ b/core/model/HasManyList.php @@ -60,12 +60,21 @@ class HasManyList extends RelationList { /** * Remove an item from this relation. * Doesn't actually remove the item, it just clears the foreign key value. - * @param $item The DataObject to be removed, or its ID + * @param $itemID The ID of the item to be removed + */ + function removeByID($itemID) { + $item = $this->byID($item); + return $this->remove($item); + } + + /** + * Remove an item from this relation. + * Doesn't actually remove the item, it just clears the foreign key value. + * @param $item The DataObject to be removed * @todo Maybe we should delete the object instead? */ function remove($item) { - if(is_numeric($item)) $item = DataObject::get_by_id($this->dataClass, $item); - else if(!($item instanceof $this->dataClass)) user_eror("HasManyList::remove() expecting a $this->dataClass object, or ID value", E_USER_ERROR); + if(!($item instanceof $this->dataClass)) throw new InvalidArgumentException("HasManyList::remove() expecting a $this->dataClass object, or ID value", E_USER_ERROR); $fk = $this->foreignKey; $item->$fk = null; diff --git a/core/model/ManyManyList.php b/core/model/ManyManyList.php index 5814644d2..60666bba4 100644 --- a/core/model/ManyManyList.php +++ b/core/model/ManyManyList.php @@ -78,7 +78,7 @@ class ManyManyList extends RelationList { } // Delete old entries, to prevent duplication - $this->remove($itemID); + $this->removeById($itemID); // Insert new entry $manipulation = array(); @@ -97,12 +97,21 @@ class ManyManyList extends RelationList { /** * Remove the given item from this list. * Note that for a ManyManyList, the item is never actually deleted, only the join table is affected - * @param $item The data object or its ID + * @param $itemID The ID of the item to remove. */ function remove($item) { - if(is_numeric($item)) $itemID = $item; - else if($item instanceof $this->dataClass) $itemID = $item->ID; - else user_eror("ManyManyList::remove() expecting a $this->dataClass object, or ID value", E_USER_ERROR); + if(!($item instanceof $this->dataClass)) throw new InvalidArgumentException("ManyManyList::remove() expecting a $this->dataClass object"); + + return $this->removeByID($item->ID); + } + + /** + * Remove the given item from this list. + * Note that for a ManyManyList, the item is never actually deleted, only the join table is affected + * @param $itemID The item it + */ + function removeByID($itemID) { + if(!is_numeric($itemID)) throw new InvalidArgumentException("ManyManyList::removeById() expecting an ID"); $query = new SQLQuery("*", array($this->joinTable)); $query->delete = true; diff --git a/tests/model/DataExtensionTest.php b/tests/model/DataExtensionTest.php index 0a8c7f4a8..d8f6fb0d6 100644 --- a/tests/model/DataExtensionTest.php +++ b/tests/model/DataExtensionTest.php @@ -66,7 +66,7 @@ class DataExtensionTest extends SapphireTest { $parent->Faves()->add($obj2->ID); $this->assertEquals(2, $parent->Faves()->Count()); - $parent->Faves()->remove($obj2->ID); + $parent->Faves()->removeByID($obj2->ID); $this->assertEquals(1, $parent->Faves()->Count()); } diff --git a/tests/model/DataObjectTest.php b/tests/model/DataObjectTest.php index 3f028108f..49a1bd400 100755 --- a/tests/model/DataObjectTest.php +++ b/tests/model/DataObjectTest.php @@ -310,7 +310,7 @@ class DataObjectTest extends SapphireTest { ); // test removing single DataObject by ID - $player1->Teams()->remove($team1->ID); + $player1->Teams()->removeByID($team1->ID); $player1->flushCache(); $compareTeams = new ComponentSet(); $this->assertEquals( @@ -1067,6 +1067,38 @@ class DataObjectTest extends SapphireTest { $objEmpty->Title = '0'; // $this->assertFalse($objEmpty->isEmpty(), 'Zero value in attribute considered non-empty'); } + + /** + * Test DataList->byID() + */ + function testByID() { + $id = $this->idFromFixture('DataObjectTest_Team','team2'); + $this->assertEquals('Team 2', DataObject::get("DataObjectTest_Team")->byID($id)->Title); + } + + /** + * Test DataList->removeByID() + */ + function testRemoveByID() { + $id = $this->idFromFixture('DataObjectTest_Team','team2'); + DataObject::get("DataObjectTest_Team")->removeByID($id); + + $this->assertNull(DataObject::get("DataObjectTest_Team")->byID($id)); + } + + /** + * Test DataList->canSortBy() + */ + function testCanSortBy() { + // Basic check + $this->assertTrue(DataObject::get("DataObjectTest_Team")->canSortBy("Title")); + $this->assertFalse(DataObject::get("DataObjectTest_Team")->canSortBy("SomethingElse")); + + // Subclasses + $this->assertTrue(DataObject::get("DataObjectTest_SubTeam")->canSortBy("Title")); + $this->assertTrue(DataObject::get("DataObjectTest_SubTeam")->canSortBy("SubclassDatabaseField")); + } + } class DataObjectTest_Player extends Member implements TestOnly { From 4a061fd071200fd57a2c1c3aa257d0232d314f2a Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Wed, 30 Mar 2011 14:28:50 +1300 Subject: [PATCH 18/64] ENHANCEMENT: Refactored MemberTableList field to make better use of DataList and ManyManyList. Refactored ComplexTableField and TableListField to, stripping out as much model logic as possible. --- admin/code/MemberTableField.php | 239 +++----------------------- admin/javascript/MemberTableField.js | 1 - forms/ComplexTableField.php | 168 ++---------------- forms/TableListField.php | 195 +++++++++------------ security/Group.php | 1 - tests/forms/ComplexTableFieldTest.php | 8 +- tests/forms/TableListFieldTest.php | 32 ++++ 7 files changed, 156 insertions(+), 488 deletions(-) diff --git a/admin/code/MemberTableField.php b/admin/code/MemberTableField.php index 934daf13e..8a69c638a 100755 --- a/admin/code/MemberTableField.php +++ b/admin/code/MemberTableField.php @@ -33,8 +33,6 @@ class MemberTableField extends ComplexTableField { public $itemClass = 'MemberTableField_Item'; - static $data_class = 'Member'; - /** * Set the page size for this table. * @var int @@ -60,8 +58,24 @@ class MemberTableField extends ComplexTableField { * @param boolean $hidePassword Hide the password field or not in the summary? */ function __construct($controller, $name, $group = null, $members = null, $hidePassword = true) { - $sourceClass = self::$data_class; - $SNG_member = singleton($sourceClass); + + if(!$members) { + if($group) { + if(is_numeric($group)) $group = DataObject::get_by_id('Group', $group); + $this->group = $group; + $members = $group->Members(); + + } elseif(isset($_REQUEST['ctf'][$this->Name()]["ID"]) && is_numeric($_REQUEST['ctf'][$this->Name()]["ID"])) { + throw new Exception("Is this still being used? It's a hack and we should remove it."); + $group = DataObject::get_by_id('Group', $_REQUEST['ctf'][$this->Name()]["ID"]); + $this->group = $group; + $members = $group->Members(); + } else { + $members = DataObject::get("Member"); + } + } + + $SNG_member = singleton('Member'); $fieldList = $SNG_member->summaryFields(); $memberDbFields = $SNG_member->db(); $csvFieldList = array(); @@ -70,52 +84,24 @@ class MemberTableField extends ComplexTableField { $csvFieldList[$field] = $field; } - if($group) { - if(is_object($group)) { - $this->group = $group; - } elseif(is_numeric($group)) { - $this->group = DataObject::get_by_id('Group', $group); - } - } else if(isset($_REQUEST['ctf'][$this->Name()]["ID"]) && is_numeric($_REQUEST['ctf'][$this->Name()]["ID"])) { - $this->group = DataObject::get_by_id('Group', $_REQUEST['ctf'][$this->Name()]["ID"]); - } - if(!$hidePassword) { $fieldList["SetPassword"] = "Password"; } $this->hidePassword = $hidePassword; - - // @todo shouldn't this use $this->group? It's unclear exactly - // what group it should be customising the custom Member set with. - if($members && $group) { - $this->setCustomSourceItems($this->memberListWithGroupID($members, $group)); - } - parent::__construct($controller, $name, $sourceClass, $fieldList); - + // Add a search filter $SQL_search = isset($_REQUEST['MemberSearch']) ? Convert::raw2sql($_REQUEST['MemberSearch']) : null; if(!empty($_REQUEST['MemberSearch'])) { $searchFilters = array(); foreach($SNG_member->searchableFields() as $fieldName => $fieldSpec) { if(strpos($fieldName, '.') === false) $searchFilters[] = "\"$fieldName\" LIKE '%{$SQL_search}%'"; } - $this->sourceFilter[] = '(' . implode(' OR ', $searchFilters) . ')'; + $members = $members->filter('(' . implode(' OR ', $searchFilters) . ')'); } - if($this->group) { - user_error("MemberTableField's group setting doesn't yet work in the new-orm branch", E_USER_WARNING); - /* - $groupIDs = array($this->group->ID); - if($this->group->AllChildren()) $groupIDs = array_merge($groupIDs, $this->group->AllChildren()->column('ID')); - $this->sourceFilter[] = sprintf( - '"Group_Members"."GroupID" IN (%s)', - implode(',', $groupIDs) - ); - */ - } - - $this->sourceJoin = " INNER JOIN \"Group_Members\" ON \"MemberID\"=\"Member\".\"ID\""; + parent::__construct($controller, $name, $members, $fieldList); + $this->setFieldListCsv($csvFieldList); $this->setPageSize($this->stat('page_size')); } @@ -130,14 +116,6 @@ class MemberTableField extends ComplexTableField { return $ret; } - function sourceID() { - return ($this->group) ? $this->group->ID : 0; - } - - function AddLink() { - return Controller::join_links($this->Link(), 'add'); - } - function SearchForm() { $groupID = (isset($this->group)) ? $this->group->ID : 0; $query = isset($_GET['MemberSearch']) ? $_GET['MemberSearch'] : null; @@ -168,6 +146,7 @@ class MemberTableField extends ComplexTableField { if(!$token->checkRequest($this->controller->getRequest())) return $this->httpError(400); $data = $_REQUEST; + $groupID = (isset($data['ctf']['ID'])) ? $data['ctf']['ID'] : null; if(!is_numeric($groupID)) { @@ -177,7 +156,7 @@ class MemberTableField extends ComplexTableField { // Get existing record either by ID or unique identifier. $identifierField = Member::get_unique_identifier_field(); - $className = self::$data_class; + $className = 'Member'; $record = null; if(isset($data[$identifierField])) { $record = DataObject::get_one( @@ -204,7 +183,7 @@ class MemberTableField extends ComplexTableField { $valid = $record->validate(); if($valid->valid()) { $record->write(); - $record->Groups()->add($groupID); + $this->getDataList()->add($record); $this->sourceItems(); @@ -232,82 +211,18 @@ class MemberTableField extends ComplexTableField { return FormResponse::respond(); } - /** - * Custom delete implementation: - * Remove member from group rather than from the database - */ - function delete() { - // Protect against CSRF on destructive action - $token = $this->getForm()->getSecurityToken(); - // TODO Not sure how this is called, using $_REQUEST to be on the safe side - if(!$token->check($_REQUEST['SecurityID'])) return $this->httpError(400); - - $groupID = Convert::raw2sql($_REQUEST['ctf']['ID']); - $memberID = Convert::raw2sql($_REQUEST['ctf']['childID']); - if(is_numeric($groupID) && is_numeric($memberID)) { - $member = DataObject::get_by_id('Member', $memberID); - $member->Groups()->remove($groupID); - } else { - user_error("MemberTableField::delete: Bad parameters: Group=$groupID, Member=$memberID", E_USER_ERROR); - } - - return FormResponse::respond(); - - } - - /** - * ################################# - * Utility Functions - * ################################# - */ - function getParentClass() { - return 'Group'; - } - - function getParentIdName($childClass, $parentClass) { - return 'GroupID'; - } - /** * ################################# * Custom Functions * ################################# */ - /** - * Customise an existing DataObjectSet of Member - * objects with a GroupID. - * - * @param DataObjectSet $members Set of Member objects to customise - * @param Group $group Group object to customise with - * @return DataObjectSet Customised set of Member objects - */ - function memberListWithGroupID($members, $group) { - $newMembers = new DataObjectSet(); - foreach($members as $member) { - $newMembers->push($member->customise(array('GroupID' => $group->ID))); - } - return $newMembers; - } - - function setGroup($group) { - $this->group = $group; - } - /** * @return Group */ function getGroup() { return $this->group; } - - function setController($controller) { - $this->controller = $controller; - } - - function GetControllerName() { - return $this->controller->class; - } /** * Add existing member to group by name (with JS-autocompletion) @@ -361,7 +276,7 @@ class MemberTableField extends ComplexTableField { * * @return string */ - function saveComplexTableField($data, $form, $params) { + function saveComplexTableField($data, $form, $params) { $className = $this->sourceClass(); $childData = new $className(); @@ -389,66 +304,6 @@ class MemberTableField extends ComplexTableField { $form->sessionMessage($message, 'good'); $this->controller->redirectBack(); - } - - /** - * Cached version for getting the appropraite members for this particular group. - * - * This includes getting inherited groups, such as groups under groups. - */ - function sourceItems() { - // Caching. - if($this->sourceItems) { - return $this->sourceItems; - } - - // Setup limits - $limitClause = ''; - if(isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) { - $limitClause = ($_REQUEST['ctf'][$this->Name()]['start']) . ", {$this->pageSize}"; - } else { - $limitClause = "0, {$this->pageSize}"; - } - - // We use the group to get the members, as they already have the bulk of the look up functions - $start = isset($_REQUEST['ctf'][$this->Name()]['start']) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0; - - $this->sourceItems = false; - - if($this->group) { - $this->sourceItems = $this->group->Members( - $this->pageSize, // limit - $start, // offset - $this->sourceFilter, - $this->sourceSort - ); - } else { - $this->sourceItems = DataObject::get(self::$data_class, - $this->sourceFilter, - $this->sourceSort, - null, - array('limit' => $this->pageSize, 'start' => $start) - ); - } - // Because we are not used $this->upagedSourceItems any more, and the DataObjectSet is usually the source - // that a large member set runs out of memory. we disable it here. - //$this->unpagedSourceItems = $this->group->Members('', '', $this->sourceFilter, $this->sourceSort); - $this->totalCount = ($this->sourceItems) ? $this->sourceItems->TotalItems() : 0; - - return $this->sourceItems; - } - - function TotalCount() { - $this->sourceItems(); // Called for its side-effect of setting total count - return $this->totalCount; - } - - /** - * Handles item requests - * MemberTableField needs its own item request class so that it can overload the delete method - */ - function handleItem($request) { - return new MemberTableField_ItemRequest($this, $request->param('ID')); } } @@ -460,7 +315,7 @@ class MemberTableField extends ComplexTableField { class MemberTableField_Popup extends ComplexTableField_Popup { function __construct($controller, $name, $fields, $validator, $readonly, $dataObject) { - $group = ($controller instanceof MemberTableField) ? $controller->getGroup() : $controller->getParent()->getGroup(); + $group = ($controller instanceof MemberTableField) ? $controller->getGroup() : $controller->getParentController()->getGroup(); // Set default groups - also implemented in AddForm() if($group) { $groupsField = $fields->dataFieldByName('Groups'); @@ -513,44 +368,4 @@ class MemberTableField_Item extends ComplexTableField_Item { } } -/** -* @package cms -* @subpackage security -*/ - -class MemberTableField_ItemRequest extends ComplexTableField_ItemRequest { - - /** - * Deleting an item from a member table field should just remove that member from the group - */ - function delete($request) { - // Protect against CSRF on destructive action - $token = $this->ctf->getForm()->getSecurityToken(); - if(!$token->checkRequest($request)) return $this->httpError('400'); - - if($this->ctf->Can('delete') !== true) { - return false; - } - - // if a group limitation is set on the table, remove relation. - // otherwise remove the record from the database - if($this->ctf->getGroup()) { - $groupID = $this->ctf->sourceID(); - $group = DataObject::get_by_id('Group', $groupID); - - // Remove from group and all child groups - foreach($group->getAllChildren() as $subGroup) { - $this->dataObj()->Groups()->remove($subGroup); - } - $this->dataObj()->Groups()->remove($groupID); - } else { - $this->dataObj()->delete(); - } - } - - function getParent() { - return $this->ctf; - } -} - ?> \ No newline at end of file diff --git a/admin/javascript/MemberTableField.js b/admin/javascript/MemberTableField.js index c275468fb..1d3516353 100755 --- a/admin/javascript/MemberTableField.js +++ b/admin/javascript/MemberTableField.js @@ -305,7 +305,6 @@ MemberFilterButton.prototype = { updateURL += '&' + this.inputFields[index].name + '=' + encodeURIComponent( this.inputFields[index].value ); } } - updateURL += ($('SecurityID') ? '&SecurityID=' + $('SecurityID').value : ''); jQuery($(fieldID)).get(updateURL, null, function() {Behaviour.apply($(fieldID), true);}); } catch(er) { diff --git a/forms/ComplexTableField.php b/forms/ComplexTableField.php index 0839daf3a..13f18cb4e 100644 --- a/forms/ComplexTableField.php +++ b/forms/ComplexTableField.php @@ -255,7 +255,7 @@ JS; } $pageStart = (isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0; - $sourceItems->setPageLimits($pageStart, $this->pageSize, $this->totalCount); + $sourceItems->setPageLimits($pageStart, $this->pageSize, $this->TotalCount()); $output = new DataObjectSet(); foreach($sourceItems as $pageIndex=>$item) { @@ -343,85 +343,6 @@ JS; $this->controller = $controller; } - /** - * Determines on which relation-class the DetailForm is saved - * by looking at the surrounding form-record. - * - * @return String - */ - function getParentClass() { - if($this->parentClass === false) { - // purposely set parent-relation to false - return false; - } elseif(!empty($this->parentClass)) { - return $this->parentClass; - } elseif($this->form && $this->form->getRecord()) { - return $this->form->getRecord()->ClassName; - } - } - - /** - * Return the record in which the CTF resides, if it exists. - */ - function getParentRecord() { - if($this->form && $record = $this->form->getRecord()) { - return $record; - } else { - $parentID = (int)$this->sourceID(); - $parentClass = $this->getParentClass(); - - if($parentClass) { - if($parentID) return DataObject::get_by_id($parentClass, $parentID); - else return singleton($parentClass); - } - } - } - - /** - * (Optional) Setter for a correct parent-relation-class. - * Defaults to the record loaded into the surrounding form as a fallback. - * Caution: Please use the classname, not the actual column-name in the database. - * - * @param $className string - */ - function setParentClass($className) { - $this->parentClass = $className; - } - - /** - * Returns the db-fieldname of the currently used has_one-relationship. - */ - function getParentIdName($parentClass, $childClass) { - return $this->getParentIdNameRelation($childClass, $parentClass, 'has_one'); - } - - /** - * Manually overwrites the parent-ID relations. - * @see setParentClass() - * - * @param String $str Example: FamilyID (when one Individual has_one Family) - */ - function setParentIdName($str) { - $this->parentIdName = $str; - } - - /** - * Returns the db-fieldname of the currently used relationship. - * Note: constructed resolve ambiguous cases in the same manner as - * DataObject::getComponentJoinField() - */ - function getParentIdNameRelation($parentClass, $childClass, $relation) { - if($this->parentIdName) return $this->parentIdName; - - $relations = array_flip(singleton($parentClass)->$relation()); - - $classes = array_reverse(ClassInfo::ancestry($childClass)); - foreach($classes as $class) { - if(isset($relations[$class])) return $relations[$class] . 'ID'; - } - return false; - } - function setTemplatePopup($template) { $this->templatePopup = $template; } @@ -458,45 +379,8 @@ JS; } function getFieldsFor($childData) { - $hasManyRelationName = null; - $manyManyRelationName = null; - - // See if our parent class has any many_many relations by this source class - if($parentClass = $this->getParentRecord()) { - $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()); - $childData->$parentIDName = $this->sourceID(); - } - $detailFields = $this->getCustomFieldsFor($childData); - if($this->getParentClass() && $hasManyRelationName && $childData->ID) { - $hasManyComponentSet = $parentClass->getComponents($hasManyRelationName); - } - // the ID field confuses the Controller-logic in finding the right view for ReferencedField $detailFields->removeByName('ID'); @@ -505,9 +389,7 @@ JS; $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())); - + /* TODO: Figure out how to implement this if($this->getParentClass()) { $detailFields->push(new HiddenField('ctf[parentClass]', '', $this->getParentClass())); @@ -518,6 +400,7 @@ JS; $detailFields->push(new HiddenField($parentIdName, '', $this->sourceID())); } } + */ return $detailFields; } @@ -688,8 +571,8 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest { if($this->ctf->Can('delete') !== true) { return false; } - - $this->dataObj()->delete(); + + $this->ctf->getDataList()->removeByID($this->itemID); } /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -761,7 +644,7 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest { } // Save this item into the given relationship - $this->ctf->getDataList()->add($childData); + $this->ctf->getDataList()->add($dataObject); $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null; @@ -798,16 +681,16 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest { } function PopupLastLink() { - if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->totalCount-1) { + if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->TotalCount()-1) { return null; } - $start = $this->totalCount - 1; + $start = $this->TotalCount - 1; return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}"); } function PopupNextLink() { - if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->totalCount-1) { + if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == $this->TotalCount()-1) { return null; } @@ -836,18 +719,18 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest { $result = new DataObjectSet(); if($currentItem < 6) { $offset = 1; - } elseif($this->totalCount - $currentItem <= 4) { - $offset = $currentItem - (10 - ($this->totalCount - $currentItem)); + } elseif($this->TotalCount() - $currentItem <= 4) { + $offset = $currentItem - (10 - ($this->TotalCount() - $currentItem)); $offset = $offset <= 0 ? 1 : $offset; } else { $offset = $currentItem - 5; } - for($i = $offset;$i <= $offset + $this->pageSize && $i <= $this->totalCount;$i++) { + for($i = $offset;$i <= $offset + $this->pageSize && $i <= $this->TotalCount();$i++) { $start = $i - 1; $links['link'] = Controller::join_links($this->Link() . "$this->methodName?ctf[start]={$start}"); $links['number'] = $i; $links['active'] = $i == $currentItem ? false : true; - $result->push(new ArrayData($links)); + $result->push(new ArrayData($links)); } return $result; } @@ -863,13 +746,6 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest { * ################################# */ - /** - * Returns the db-fieldname of the currently used has_one-relationship. - */ - function getParentIdName($parentClass, $childClass) { - return $this->getParentIdNameRelation($childClass, $parentClass, 'has_one'); - } - /** * Manually overwrites the parent-ID relations. * @see setParentClass() @@ -877,25 +753,9 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest { * @param String $str Example: FamilyID (when one Individual has_one Family) */ function setParentIdName($str) { - $this->parentIdName = $str; + throw new Exception("setParentIdName is no longer necessary"); } - /** - * Returns the db-fieldname of the currently used relationship. - */ - function getParentIdNameRelation($parentClass, $childClass, $relation) { - if($this->parentIdName) return $this->parentIdName; - - $relations = singleton($parentClass)->$relation(); - $classes = ClassInfo::ancestry($childClass); - if($relations) { - foreach($relations as $k => $v) { - if(array_key_exists($v, $classes)) return $k . 'ID'; - } - } - return false; - } - function setTemplatePopup($template) { $this->templatePopup = $template; } diff --git a/forms/TableListField.php b/forms/TableListField.php index 5d99d4585..ad850e0c7 100755 --- a/forms/TableListField.php +++ b/forms/TableListField.php @@ -134,14 +134,6 @@ class TableListField extends FormField { */ protected $customCsvQuery; - /** - * @var $customSourceItems DataObjectSet Use the manual setting of a result-set only as a last-resort - * for sets which can't be resolved in a single query. - * - * @todo Add pagination support for customSourceItems. - */ - protected $customSourceItems; - /** * Character to seperate exported columns in the CSV file */ @@ -166,12 +158,6 @@ class TableListField extends FormField { "\n"=>"", ); - - /** - * @var int Shows total count regardless or pagination - */ - protected $totalCount; - /** * @var boolean Trigger pagination */ @@ -240,22 +226,29 @@ class TableListField extends FormField { protected $__cachedQuery; + /** + * This is a flag that enables some backward-compatibility helpers. + */ + private $getDataListFromForm; + function __construct($name, $sourceClass = null, $fieldList = null, $sourceFilter = null, $sourceSort = null, $sourceJoin = null) { - $this->fieldList = ($fieldList) ? $fieldList : singleton($sourceClass)->summaryFields(); - if($sourceClass) { - // You can optionally pass a DataList as the 2nd argument to the constructor - if($sourceClass instanceof DataList) { + // You can optionally pass a DataList/DataObjectSet + if($sourceClass instanceof DataObjectSet) { $this->dataList = $sourceClass; } else { $this->dataList = DataObject::get($sourceClass)->filter($sourceFilter) ->sort($sourceSort)->join($sourceJoin); + // Grab it from the form relation, if available. + $this->getDataListFromForm = true; } } + $this->fieldList = ($fieldList) ? $fieldList : singleton($this->sourceClass())->summaryFields(); + $this->readOnly = false; parent::__construct($name); @@ -271,7 +264,10 @@ class TableListField extends FormField { ); function sourceClass() { - return $this->getDataList()->dataClass(); + $list = $this->getDataList(); + if(method_exists($list, 'dataClass')) return $list->dataClass(); + // Failover for DataObjectSet + else return get_class($list->First()); } function handleItem($request) { @@ -358,13 +354,10 @@ JS * @return bool */ function isFieldSortable($fieldName) { - if($this->customSourceItems || $this->disableSorting) { - return false; - } - - if(!$this->__cachedQuery) $this->__cachedQuery = $this->getQuery(); - - return $this->__cachedQuery->canSortBy($fieldName); + if($this->disableSorting) return false; + $list = $this->getDataList(); + if(method_exists($list,'canSortBy')) return $list->canSortBy($fieldName); + else return false; } /** @@ -400,45 +393,42 @@ JS } function setCustomSourceItems(DataObjectSet $items) { + user_error('TableList::setCustomSourceItems() deprecated, just pass the items into the constructor', E_USER_WARNING); + // The type-hinting above doesn't seem to work consistently if($items instanceof DataObjectSet) { - $this->customSourceItems = $items; + $this->dataList = $items; } else { user_error('TableList::setCustomSourceItems() should be passed a DataObjectSet', E_USER_WARNING); } } + /** + * Get items, with sort & limit applied + */ function sourceItems() { + // get items (this may actually be a DataObjectSet) + $items = clone $this->getDataList(); + + // TODO: Sorting could be implemented on regular DataObjectSets. + if(method_exists($items,'canSortBy') && isset($_REQUEST['ctf'][$this->Name()]['sort'])) { + $sort = $_REQUEST['ctf'][$this->Name()]['sort']; + // TODO: sort direction + if($items->canSortBy($sort)) $items = $items->sort($sort); + } + // Determine pagination limit, offset - $SQL_limit = ($this->showPagination && $this->pageSize) ? "{$this->pageSize}" : null; - if(isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) { - $SQL_start = (isset($_REQUEST['ctf'][$this->Name()]['start'])) ? intval($_REQUEST['ctf'][$this->Name()]['start']) : "0"; - } else { - $SQL_start = 0; - } - - // Custom source items can be explicitly passed - if(isset($this->customSourceItems)) { - if($this->showPagination && $this->pageSize) { - $items = $this->customSourceItems->getRange($SQL_start, $SQL_limit); - } else { - $items = $this->customSourceItems; - } - - // Otherwise we use the internal data list - } else { - // get the DataList of items - $items = $this->getDataList(); - - // we don't limit when doing certain actions T - $methodName = isset($_REQUEST['url']) ? array_pop(explode('/', $_REQUEST['url'])) : null; - if(!$methodName || !in_array($methodName,array('printall','export'))) { - $items->limit(array( - 'limit' => $SQL_limit, - 'start' => (isset($SQL_start)) ? $SQL_start : null - )); - } - } + // To disable pagination, set $this->showPagination to false. + if($this->showPagination && $this->pageSize) { + $SQL_limit = (int)$this->pageSize; + if(isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) { + $SQL_start = (isset($_REQUEST['ctf'][$this->Name()]['start'])) ? intval($_REQUEST['ctf'][$this->Name()]['start']) : "0"; + } else { + $SQL_start = 0; + } + + $items = $items->getRange($SQL_start, $SQL_limit); + } return $items; } @@ -458,12 +448,9 @@ JS * Returns the DataList for this field. */ function getDataList() { - // Load the data from the form - // Note that this will override any specific. This is so that explicitly-passed sets of - // parameters that represent a relation can be replaced with the relation itself. This is - // a little clumsy and won't work if people have used a field name that is the same as a - // relation but have specified alternative parameters. - if($this->form) { + // If we weren't passed in a DataList to begin with, try and get the datalist from the form + if($this->form && $this->getDataListFromForm) { + $this->getDataListFromForm = false; $relation = $this->name; if($record = $this->form->getRecord()) { if($record->hasMethod($relation)) $this->dataList = $record->$relation(); @@ -474,20 +461,7 @@ JS user_error(get_class($this). ' is missing a DataList', E_USER_ERROR); } - $dl = clone $this->dataList; - - if(isset($_REQUEST['ctf'][$this->Name()]['sort'])) { - $query = $this->dataList->dataQuery()->query(); - $SQL_sort = Convert::raw2sql($_REQUEST['ctf'][$this->Name()]['sort']); - $sql = $query->sql(); - // see {isFieldSortable} - if(in_array($SQL_sort,$query->select) || stripos($sql,"AS {$SQL_sort}")) { - $dl->sort($SQL_sort); - } - if($query->canSortBy($column)) $query->orderby = $column.' '.$dir; - } - - return $dl; + return $this->dataList; } function getCsvDataList() { @@ -499,14 +473,20 @@ JS * @deprecated Use getDataList() instead. */ function getQuery() { - return $this->getDataList()->dataQuery()->query(); + $list = $this->getDataList(); + if(method_exists($list,'dataQuery')) { + return $this->getDataList()->dataQuery()->query(); + } } /** * @deprecated Use getCsvDataList() instead. */ function getCsvQuery() { - return $this->getCsvDataList()->dataQuery()->query(); + $list = $this->getCsvDataList(); + if(method_exists($list,'dataQuery')) { + return $list->dataQuery()->query(); + } } function FieldList() { @@ -562,8 +542,7 @@ JS $childId = Convert::raw2sql($_REQUEST['ctf']['childID']); if (is_numeric($childId)) { - $childObject = DataObject::get_by_id($this->sourceClass(), $childId); - if($childObject) $childObject->delete(); + $this->getDataList()->removeById($childId); } // TODO return status in JSON etc. @@ -883,9 +862,20 @@ JS return min($this->pageSize, $this->TotalCount()); } } - + + /** + * @ignore + */ + private $_cache_TotalCount; + + /** + * Return the total number of items in the source DataList + */ function TotalCount() { - return $this->getDataList()->Count(); + if($this->_cache_TotalCount === null) { + $this->_cache_TotalCount = $this->getDataList()->Count(); + } + return $this->_cache_TotalCount; } @@ -950,6 +940,14 @@ JS function export() { $now = Date("d-m-Y-H-i"); $fileName = "export-$now.csv"; + + // No pagination for export + $oldShowPagination = $this->showPagination; + $this->showPagination = false; + + $result = $this->renderWith(array($this->template . '_printable', 'TableListField_printable')); + + $this->showPagination = $oldShowPagination; if($fileData = $this->generateExportFileData($numColumns, $numRows)){ return SS_HTTPRequest::send_file($fileData, $fileName); @@ -1562,42 +1560,11 @@ class TableListField_ItemRequest extends RequestHandler { // used to discover fields if requested and for population of field if(is_numeric($this->itemID)) { // we have to use the basedataclass, otherwise we might exclude other subclasses - return DataObject::get_by_id(ClassInfo::baseDataClass(Object::getCustomClass($this->ctf->sourceClass())), $this->itemID); + return $this->ctf->getDataList()->byId($this->itemID); } } - /** - * Returns the db-fieldname of the currently used has_one-relationship. - */ - function getParentIdName( $parentClass, $childClass ) { - return $this->getParentIdNameRelation( $childClass, $parentClass, 'has_one' ); - } - - /** - * Manually overwrites the parent-ID relations. - * @see setParentClass() - * - * @param String $str Example: FamilyID (when one Individual has_one Family) - */ - function setParentIdName($str) { - $this->parentIdName = $str; - } - - /** - * Returns the db-fieldname of the currently used relationship. - */ - function getParentIdNameRelation($parentClass, $childClass, $relation) { - if($this->parentIdName) return $this->parentIdName; - - $relations = singleton($parentClass)->$relation(); - $classes = ClassInfo::ancestry($childClass); - foreach($relations as $k => $v) { - if(array_key_exists($v, $classes)) return $k . 'ID'; - } - return false; - } - /** * @return TableListField */ diff --git a/security/Group.php b/security/Group.php index 6ceceb8b5..e340d72f9 100644 --- a/security/Group.php +++ b/security/Group.php @@ -153,7 +153,6 @@ class Group extends DataObject { } $memberList->setPermissions(array('edit', 'delete', 'export', 'add', 'inlineadd')); - $memberList->setParentClass('Group'); $memberList->setPopupCaption(_t('SecurityAdmin.VIEWUSER', 'View User')); $fields->push($idField = new HiddenField("ID")); diff --git a/tests/forms/ComplexTableFieldTest.php b/tests/forms/ComplexTableFieldTest.php index d49f664ff..a4620cefd 100644 --- a/tests/forms/ComplexTableFieldTest.php +++ b/tests/forms/ComplexTableFieldTest.php @@ -100,13 +100,11 @@ class ComplexTableFieldTest_Controller extends Controller { $playersField = new ComplexTableField( $this, 'Players', - 'ComplexTableFieldTest_Player', + $team->Players(), ComplexTableFieldTest_Player::$summary_fields, 'getCMSFields' ); - $playersField->setParentClass('ComplexTableFieldTest_Team'); - $form = new Form( $this, 'ManyManyForm', @@ -131,13 +129,11 @@ class ComplexTableFieldTest_Controller extends Controller { $sponsorsField = new ComplexTableField( $this, 'Sponsors', - 'ComplexTableFieldTest_Sponsor', + $team->Sponsors(), ComplexTableFieldTest_Sponsor::$summary_fields, 'getCMSFields' ); - $sponsorsField->setParentClass('ComplexTableFieldTest_Team'); - $form = new Form( $this, 'HasManyForm', diff --git a/tests/forms/TableListFieldTest.php b/tests/forms/TableListFieldTest.php index 61fb00308..d1ed184c8 100755 --- a/tests/forms/TableListFieldTest.php +++ b/tests/forms/TableListFieldTest.php @@ -292,6 +292,38 @@ class TableListFieldTest extends SapphireTest { unset($_REQUEST['ctf']); } + + /** + * Check that a DataObjectSet can be passed to TableListField + */ + function testDataObjectSet() { + $one = new TableListFieldTest_Obj; + $one->A = "A-one"; + $two = new TableListFieldTest_Obj; + $two->A = "A-two"; + $three = new TableListFieldTest_Obj; + $three->A = "A-three"; + + $list = new DataObjectSet($one, $two, $three); + + // A TableListField must be inside a form for its links to be generated + $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet( + new TableListField("Tester", $list, array( + "A" => "Col A", + "B" => "Col B", + "C" => "Col C", + "D" => "Col D", + "E" => "Col E", + )) + ), new FieldSet()); + + $table = $form->dataFieldByName('Tester'); + $rendered = $table->FieldHolder(); + + $this->assertContains('A-one', $rendered); + $this->assertContains('A-two', $rendered); + $this->assertContains('A-three', $rendered); + } } class TableListFieldTest_Obj extends DataObject implements TestOnly { From 25018180c511efd5f425aa840113942c5df63f0f Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Wed, 30 Mar 2011 14:29:22 +1300 Subject: [PATCH 19/64] BUGFIX: Edge-case handler for jquery.ondemand. NOTE - this is clumsy, it would be better to work out what's being passed as the xhr argument and why. --- javascript/jquery-ondemand/jquery.ondemand.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/javascript/jquery-ondemand/jquery.ondemand.js b/javascript/jquery-ondemand/jquery.ondemand.js index fab4bce2a..29a891bed 100644 --- a/javascript/jquery-ondemand/jquery.ondemand.js +++ b/javascript/jquery-ondemand/jquery.ondemand.js @@ -84,7 +84,7 @@ var self = this, processDfd = new $.Deferred(); // CSS - if(xhr.getResponseHeader('X-Include-CSS')) { + if(xhr.getResponseHeader && xhr.getResponseHeader('X-Include-CSS')) { var cssIncludes = xhr.getResponseHeader('X-Include-CSS').split(','); for(var i=0;i Date: Wed, 30 Mar 2011 16:51:26 +1300 Subject: [PATCH 20/64] BUGFIX: Fixed DataList::relation(). --- core/model/HasManyList.php | 2 +- core/model/ManyManyList.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/model/HasManyList.php b/core/model/HasManyList.php index 87ec54842..0626eaeaa 100644 --- a/core/model/HasManyList.php +++ b/core/model/HasManyList.php @@ -25,7 +25,7 @@ class HasManyList extends RelationList { // Apply relation filter if(is_array($this->foreignID)) { return "\"$this->foreignKey\" IN ('" . - implode(', ', array_map('Convert::raw2sql', $this->foreignID)) . "')"; + implode("', '", array_map('Convert::raw2sql', $this->foreignID)) . "')"; } else if($this->foreignID){ return "\"$this->foreignKey\" = '" . Convert::raw2sql($this->foreignID) . "'"; diff --git a/core/model/ManyManyList.php b/core/model/ManyManyList.php index 60666bba4..5faee849d 100644 --- a/core/model/ManyManyList.php +++ b/core/model/ManyManyList.php @@ -52,7 +52,7 @@ class ManyManyList extends RelationList { // Apply relation filter if(is_array($this->foreignID)) { return "\"$this->joinTable\".\"$this->foreignKey\" IN ('" . - implode(', ', array_map('Convert::raw2sql', $this->foreignID)) . "')"; + implode("', '", array_map('Convert::raw2sql', $this->foreignID)) . "')"; } else if($this->foreignID){ return "\"$this->joinTable\".\"$this->foreignKey\" = '" . Convert::raw2sql($this->foreignID) . "'"; From 7efd19e7cbfb62dd65ce98cc8564a1005e96c8c1 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Wed, 30 Mar 2011 16:51:45 +1300 Subject: [PATCH 21/64] BUGFIX: Fixed DataList arrayaccess. --- core/model/DataList.php | 35 ++++++++++++++++++++++++++++++++++ tests/model/DataObjectTest.php | 9 +++++++++ 2 files changed, 44 insertions(+) diff --git a/core/model/DataList.php b/core/model/DataList.php index 6d3dd7c67..6f69b1a54 100644 --- a/core/model/DataList.php +++ b/core/model/DataList.php @@ -400,6 +400,41 @@ class DataList extends DataObjectSet { function removeDuplicates() { user_error("Can't call DataList::removeDuplicates() because its data comes from a specific query.", E_USER_ERROR); } + + /** + * Necessary for interface ArrayAccess. Returns whether an item with $key exists + * @param mixed $key + * @return bool + */ + public function offsetExists($key) { + return ($this->getRange($key, 1)->First() != null); + } + + /** + * Necessary for interface ArrayAccess. Returns item stored in array with index $key + * @param mixed $key + * @return DataObject + */ + public function offsetGet($key) { + return $this->getRange($key, 1)->First(); + } + + /** + * Necessary for interface ArrayAccess. Set an item with the key in $key + * @param mixed $key + * @param mixed $value + */ + public function offsetSet($key, $value) { + throw new Exception("Can't alter items in a DataList using array-access"); + } + + /** + * Necessary for interface ArrayAccess. Unset an item with the key in $key + * @param mixed $key + */ + public function offsetUnset($key) { + throw new Exception("Can't alter items in a DataList using array-access"); + } } diff --git a/tests/model/DataObjectTest.php b/tests/model/DataObjectTest.php index 49a1bd400..428fc1a30 100755 --- a/tests/model/DataObjectTest.php +++ b/tests/model/DataObjectTest.php @@ -1098,6 +1098,15 @@ class DataObjectTest extends SapphireTest { $this->assertTrue(DataObject::get("DataObjectTest_SubTeam")->canSortBy("Title")); $this->assertTrue(DataObject::get("DataObjectTest_SubTeam")->canSortBy("SubclassDatabaseField")); } + + function testDataListArrayAccess() { + $list = DataObject::get("DataObjectTest_Team")->sort("Title"); + + $this->assertEquals("Subteam 1", $list[0]->Title); + $this->assertEquals("Subteam 3", $list[2]->Title); + $this->assertEquals("Team 2", $list[4]->Title); + + } } From 0de6dbc848cd7188611b52a09890b23800346337 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Wed, 30 Mar 2011 17:31:24 +1300 Subject: [PATCH 22/64] BUGFIX: Fixed Permission::add_to_hidden_permissions() and Permission::remove_from_hidden_permissions() --- security/PermissionCheckboxSetField.php | 1 + tests/security/PermissionTest.php | 1 + 2 files changed, 2 insertions(+) diff --git a/security/PermissionCheckboxSetField.php b/security/PermissionCheckboxSetField.php index 1e93eb1e4..cac263e03 100644 --- a/security/PermissionCheckboxSetField.php +++ b/security/PermissionCheckboxSetField.php @@ -173,6 +173,7 @@ class PermissionCheckboxSetField extends FormField { $options .= "
  • $categoryName
  • "; foreach($permissions as $code => $permission) { if(in_array($code, $this->hiddenPermissions)) continue; + if(in_array($code, Permission::$hidden_permissions)) continue; $value = $permission['name']; diff --git a/tests/security/PermissionTest.php b/tests/security/PermissionTest.php index 85455b33f..03cb09340 100644 --- a/tests/security/PermissionTest.php +++ b/tests/security/PermissionTest.php @@ -78,5 +78,6 @@ class PermissionTest extends SapphireTest { Permission::remove_from_hidden_permissions('CMS_ACCESS_AssetAdmin'); $this->assertContains('CMS_ACCESS_AssetAdmin', $permissionCheckboxSet->Field()); + Permission::remove_from_hidden_permissions('CMS_ACCESS_CMSMain'); } } \ No newline at end of file From 397d19f23b98dfd56157adb42e629db127e0f8a9 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Wed, 30 Mar 2011 18:06:15 +1300 Subject: [PATCH 23/64] BUGFIX: Fixed bug in relation to new ORM(DataObject::get() doesn't return a null anymore) --- security/Group.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/security/Group.php b/security/Group.php index e340d72f9..299ba83d7 100644 --- a/security/Group.php +++ b/security/Group.php @@ -441,7 +441,7 @@ class Group extends DataObject { // Add default author group if no other group exists $allGroups = DataObject::get('Group'); - if(!$allGroups) { + if(!$allGroups->count()) { $authorGroup = new Group(); $authorGroup->Code = 'content-authors'; $authorGroup->Title = _t('Group.DefaultGroupTitleContentAuthors', 'Content Authors'); @@ -456,7 +456,7 @@ class Group extends DataObject { // Add default admin group if none with permission code ADMIN exists $adminGroups = Permission::get_groups_by_permission('ADMIN'); - if(!$adminGroups) { + if(!$adminGroups->count()) { $adminGroup = new Group(); $adminGroup->Code = 'administrators'; $adminGroup->Title = _t('Group.DefaultGroupTitleAdministrators', 'Administrators'); From 6915e58a39daa6792dd8aaf0210b4dcea03b483c Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Wed, 30 Mar 2011 18:06:33 +1300 Subject: [PATCH 24/64] BUGFIX: Fix SQLQuery::queriedTables() and added test. --- model/SQLQuery.php | 9 +++++++-- tests/model/VersionedTest.php | 12 ++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/model/SQLQuery.php b/model/SQLQuery.php index 0c63f8adc..b25832811 100755 --- a/model/SQLQuery.php +++ b/model/SQLQuery.php @@ -230,7 +230,7 @@ class SQLQuery { public function queriedTables() { $tables = array(); foreach($this->from as $key => $tableClause) { - if(is_array($tableClause)) $table = $tableClause['table']; + if(is_array($tableClause)) $table = '"'.$tableClause['table'].'"'; else if(is_string($tableClause) && preg_match('/JOIN +("[^"]+") +(AS|ON) +/i', $tableClause, $matches)) $table = $matches[1]; else $table = $tableClause; @@ -455,6 +455,9 @@ class SQLQuery { * @return string */ function sql() { + // TODO: Don't require this internal-state manipulate-and-preserve - let sqlQueryToString() handle the new syntax + $origFrom = $this->from; + // Build from clauses foreach($this->from as $alias => $join) { // $join can be something like this array structure @@ -468,9 +471,11 @@ class SQLQuery { } } - $sql = DB::getConn()->sqlQueryToString($this); if($this->replacementsOld) $sql = str_replace($this->replacementsOld, $this->replacementsNew, $sql); + + $this->from = $origFrom; + return $sql; } diff --git a/tests/model/VersionedTest.php b/tests/model/VersionedTest.php index 1b60aa824..d88e97ec8 100644 --- a/tests/model/VersionedTest.php +++ b/tests/model/VersionedTest.php @@ -230,6 +230,18 @@ class VersionedTest extends SapphireTest { 'Models w/o Versioned can have their own Version field.' ); } + + /** + * Test that SQLQuery::queriedTables() applies the version-suffixes properly. + */ + public function testQueriedTables() { + Versioned::reading_stage('Live'); + + $this->assertEquals(array( + 'VersionedTest_DataObject_Live', + 'VersionedTest_Subclass_Live', + ), DataObject::get('VersionedTest_DataObject')->dataQuery()->query()->queriedTables()); + } } class VersionedTest_DataObject extends DataObject implements TestOnly { From f5d2e43636488447b8dde8c1c26ffd5159f10ccd Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Mon, 4 Apr 2011 12:18:50 +1200 Subject: [PATCH 25/64] ENHANCEMENT: Added documentation for the new ORM. --- docs/en/topics/datamodel.md | 189 +++++++++++++++++++++++++++++++----- 1 file changed, 165 insertions(+), 24 deletions(-) diff --git a/docs/en/topics/datamodel.md b/docs/en/topics/datamodel.md index 4217fc4bc..479ca6815 100644 --- a/docs/en/topics/datamodel.md +++ b/docs/en/topics/datamodel.md @@ -28,40 +28,181 @@ Note: You need to be logged in as an administrator to perform this command. ## Querying Data -There are static methods available for querying data. They automatically compile the necessary SQL to query the database -so they are very helpful. In case you need to fall back to plain-jane SQL, have a look at `[api:SQLQuery]`. +Every query to data starts with a `DataList::create($class)` call. For example, this query would return all of the Member objects: :::php - $records = DataObject::get($obj, $filter, $sort, $join, $limit); + $members = DataList::create('Member'); + +The ORM uses a "fluent" syntax, where you specify a query by chaining together different methods. Two common methods +are filter() and sort(): :::php - $record = DataObject::get_one($obj, $filter); + $members = DataList::create('Member')->filter(array('FirstName' => 'Sam'))->sort('Surname'); + +Those of you who know a bit about SQL might be thinking "it looks like you're querying all members, and then filtering +to those with a first name of 'Sam'. Isn't this very slow?" Is isn't, because the ORM doesn't actually execute the +query until you iterate on the result with a `foreach()` or `<% control %>`. :::php - $record = DataObject::get_by_id($obj, $id); + // The SQL query isn't executed here... + $members = DataList::create('Member'); + // ...or here + $members = $members->filter(array('FirstName' => 'Sam')); + // ...or even here + $members = $members->sort('Surname'); + + // *This* is where the query is executed + foreach($members as $member) { + echo "

    $member->FirstName $member->Surname

    "; + } -**CAUTION: Please make sure to properly escape your SQL-snippets (see [security](/topics/security).** +This also means that getting the count of a list of objects will be done with a single, efficient query. -## Joining + :::php + $members = DataList::create('Member')->filter(array('FirstName' => 'Sam'))->sort('Surname'); + // This will create an single SELECT COUNT query. + echo $members->Count(); + +All of this lets you focus on writing your application, and not worrying too much about whether or not your queries are efficient. +### Returning a single DataObject + +There are a couple of ways of getting a single DataObject from the ORM. If you know the ID number of the object, you can use `byID($id)`: + + :::php + $member = DataList::create('Member')->byID(5); + +If you have constructed a query that you know should return a single record, you can call `First()`: + + :::php + $member = DataList::create('Member')->filter(array('FirstName' => 'Sam', 'Surname' => 'Minnee'))->First(); + + +### Filters + +**FUN FACT:** This isn't implemented in the code yet, but will be shortly. + +As you might expect, the `filter()` method filters the list of objects that gets returned. The previous example +included this filter, which returns all Members with a first name of "Sam". + + :::php + $members = DataList::create('Member')->filter(array('FirstName' => 'Sam')); + +In SilverStripe 2, we would have passed `"\"FirstName\" = 'Sam'` to make this query. Now, we pass an array, +`array('FirstName' => 'Sam')`, to minimise the risk of SQL injection bugs. The format of this array follows a few +rules: + + * Each element of the array specifies a filter. You can specify as many filters as you like, and they **all** must be +true. + * The key in the filter corresponds to the field that you want to filter by. + * The value in the filter corresponds to the value that you want to filter to. + +So, this would return only those members called "Sam Minnée". + + :::php + $members = DataList::create('Member')->filter(array( + 'FirstName' => 'Sam', + 'Surname' => 'Minnée', + )); + +By default, these filters specify case-insensitive exact matches. There are a number of suffixes that you can put on +field names to change this: `":StartsWith"`, `":EndsWith"`, `":Contains"`, `":GreaterThan"`, `":LessThan"`, `":Not"`, +and `":MatchCase"`. `":Not"` and `":MatchCase"` are special in that you can add it to any of the other filters. + +This query will return everyone whose first name doesn't start with S, who have logged on since 1/1/2011. + + :::php + $members = DataList::create('Member')->filter(array( + 'FirstName:StartsWith:Not' => 'S' + 'LastVisited:GreaterThan' => '2011-01-01' + )); + +If you wish to match against any of a number of columns, you can list several field names, separated by commas. This +will return all members whose first name or surname contain the string 'sam'. + + :::php + $members = DataList::create('Member')->filter(array( + 'FirstName,Surname:Contains' => 'sam' + )); + +If you wish to match against any of a number of values, you can pass an array as the value. This will return all +members whose first name is either Sam or Ingo. + + :::php + $members = DataList::create('Member')->filter(array( + 'FirstName' => array('sam', 'ingo'), + )); + +### Relation filters + +So far we have only filtered a data list by fields on the object that you're requesting. For simple cases, this might +be okay, but often, a data model is made up of a number of related objects. For example, in SilverStripe each member +can be placed in a number of groups, and each group has a number of permissions. + +For this, Sapphire ORM supports **Relation Filters**. Any ORM request can be filtered by fields on a related object by +specifying the filter key as `.`. You can chain relations together as many +times as is necessary. + +For example, this will return all members assigned ot a group that has a permission record with the code "ADMIN". In other words, it will return all administrators. + + :::php + $members = DataList::create('Member')->filter(array( + 'Groups.Permissions.Code' => 'ADMIN', + )); + +Note that we are just joining to these tables to filter the records. Even if a member is in more than 1 administrator group, unique members will still be returned by this query. + +The other features of filters can be applied to relation filters as well. This will return all members in groups whose +names start with 'A' or 'B'. + + :::php + $members = DataList::create('Member')->filter(array( + 'Groups.Title:StartsWith' => array('A', 'B'), + )); + +You can even follow a relation back to the original model class! This will return all members are in at least 1 group that also has a member called Sam. + + :::php + $members = DataList::create('Member')->filter(array( + 'Groups.Members.FirstName' => 'Sam' + )); + +### Raw SQL options for advanced users + +Occassionally, the system described above won't let you do exactly what you need to do. In these situtations, we have +methods that manipulate the SQL query at a lower level. When using these, please ensure that all table & field names +are escaped with double quotes, otherwise some DB back-ends (e.g. PostgreSQL) won't work. + +In general, we advise against using these methods unless it's absolutely necessary. If the ORM doesn't do quite what +you need it to, you may also consider extending the ORM with new data types or filter modifiers (that documentation still needs to be written) + +#### Where clauses + +You can specify a WHERE clause fragment (that will be combined with other filters using AND) with the `where()` method: + + :: php + $members = DataList::create('Member')->where("\"FirstName\" = 'Sam'") + +#### Joining + +You can specify a join with the innerJoin and leftJoin methods. Both of these methods have the same arguments: + + * The name of the table to join to + * The filter clause for the join + * An optional alias + +For example: + + :: php + // Without an alias + $members = DataList::create('Member')->leftJoin("Group_Members", "\"Group_Members\".\"MemberID\" = \"Member\".\"ID\""); + + $members = DataList::create('Member')->innerJoin("Group_Members", "\"Rel\".\"MemberID\" = \"Member\".\"ID\"", "REl"); + Passing a *$join* statement to DataObject::get will filter results further by the JOINs performed against the foreign table. **It will NOT return the additionally joined data.** The returned *$records* will always be a `[api:DataObject]`. -When using *$join* statements be sure the string is in the proper format for the respective database engine. In MySQL -the use of back-ticks may be necessary when referring Table Names and potentially Columns. (see [MySQL -Identifiers](http://dev.mysql.com/doc/refman/5.0/en/identifiers.html)): - - :::php - // Example from the forums: http://www.silverstripe.org/archive/show/79865#post79865 - // Note the use of backticks on table names - $links = DataObject::get("SiteTree", - "ShowInMenus = 1 AND ParentID = 23", - "", - "LEFT JOIN `ConsultationPaperHolder` ON `ConsultationPaperHolder`.ID = `SiteTree`.ID", - "0, 10"); - - ## Properties @@ -312,7 +453,7 @@ Inside sapphire it doesn't matter if you're editing a *has_many*- or a *many_man } -### Custom Relation Getters +### Custom Relations You can use the flexible datamodel to get a filtered result-list without writing any SQL. For example, this snippet gets you the "Players"-relation on a team, but only containing active players. (See `[api:DataObject::$has_many]` for more info on @@ -324,8 +465,8 @@ the described relations). "Players" => "Player" ); - // can be accessed by $myTeam->ActivePlayers - function getActivePlayers() { + // can be accessed by $myTeam->ActivePlayers() + function ActivePlayers() { return $this->Players("Status='Active'"); } } From 79cde9df25f820de6e59c4e93990cc4f596caf6e Mon Sep 17 00:00:00 2001 From: ajshort Date: Sun, 27 Mar 2011 21:37:06 +1100 Subject: [PATCH 26/64] API CHANGE: Renamed ArrayData::getArray() to toMap() to make it consistent with DataObject. This also makes it work with SapphireTest assertation methods. --- view/ArrayData.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/view/ArrayData.php b/view/ArrayData.php index bb32c5dc5..fa4916740 100755 --- a/view/ArrayData.php +++ b/view/ArrayData.php @@ -43,7 +43,7 @@ class ArrayData extends ViewableData { * * @return array */ - public function getArray() { + public function toMap() { return $this->array; } @@ -107,4 +107,12 @@ class ArrayData extends ViewableData { function forTemplate() { return var_export($this->array, true); } + + /** + * @deprecated 3.0 Use {@link ArrayData::toMap()}. + */ + public function getArray() { + return $this->toMap(); + } + } \ No newline at end of file From 3fbb29a6c5ad48e1f7cff74c40cbf9f9d312b089 Mon Sep 17 00:00:00 2001 From: ajshort Date: Thu, 31 Mar 2011 12:01:04 +1100 Subject: [PATCH 27/64] FEATURE: Added PaginatedList, which wraps around a data list or set to provide pagination functionality. This replaces the pagination functionality baked into DataObjectSet. API CHANGE: Removed pagination related methods from DataObjectSet and implemented them on PaginatedList. API CHANGE: Removed DataObjectSet::parseQueryLimit(), this is now implemented as PaginatedList::setPaginationFromQuery(). API CHANGE: Deprecated DataObjectSet::TotalItems in favour of Count(). ENHANCEMENT: Added FirstLink and LastLink to PaginatedList. MINOR: Updated documentation, and added a how-to on paginating items. --- core/PaginatedList.php | 416 ++++++++++++++++++++ core/model/DataList.php | 1 - docs/en/howto/pagination.md | 66 ++++ docs/en/reference/built-in-page-controls.md | 4 +- docs/en/reference/searchcontext.md | 20 +- filesystem/File.php | 1 - model/DataObjectSet.php | 295 +------------- tests/PaginatedListTest.php | 247 ++++++++++++ tests/model/DataObjectSetTest.php | 46 --- 9 files changed, 745 insertions(+), 351 deletions(-) create mode 100644 core/PaginatedList.php create mode 100644 docs/en/howto/pagination.md create mode 100644 tests/PaginatedListTest.php diff --git a/core/PaginatedList.php b/core/PaginatedList.php new file mode 100644 index 000000000..11227495d --- /dev/null +++ b/core/PaginatedList.php @@ -0,0 +1,416 @@ +list = $list; + $this->failover = $list; + $this->request = $request; + } + + /** + * @return DataObjectSet + */ + public function getList() { + return $this->list; + } + + /** + * Returns the GET var that is used to set the page start. This defaults + * to "start". + * + * If there is more than one paginated list on a page, it is neccesary to + * set a different get var for each using {@link setPaginationGetVar()}. + * + * @return string + */ + public function getPaginationGetVar() { + return $this->getVar; + } + + /** + * Sets the GET var used to set the page start. + * + * @param string $var + */ + public function setPaginationGetVar($var) { + $this->getVar = $var; + } + + /** + * Returns the number of items displayed per page. This defaults to 10. + * + * @return int. + */ + public function getPageLength() { + return $this->pageLength; + } + + /** + * Set the number of items displayed per page. + * + * @param int $length + */ + public function setPageLength($length) { + $this->pageLength = $length; + } + + /** + * Sets the current page. + * + * @param int $page + */ + public function setCurrentPage($page) { + $this->pageStart = ($page - 1) * $this->pageLength; + } + + /** + * Returns the offset of the item the current page starts at. + * + * @return int + */ + public function getPageStart() { + if ($this->pageStart === null) { + if ($this->request && isset($this->request[$this->getVar])) { + $this->pageStart = (int) $this->request[$this->getVar]; + } else { + $this->pageStart = 0; + } + } + + return $this->pageStart; + } + + /** + * Sets the offset of the item that current page starts at. This should be + * a multiple of the page length. + * + * @param int $start + */ + public function setPageStart($start) { + $this->pageStart = $start; + } + + /** + * Returns the total number of items in the unpaginated list. + * + * @return int + */ + public function getTotalItems() { + if ($this->totalItems === null) { + $this->totalItems = count($this->list); + } + + return $this->totalItems; + } + + /** + * Sets the total number of items in the list. This is useful when doing + * custom pagination. + * + * @param int $items + */ + public function setTotalItems($items) { + $this->totalItems = $items; + } + + /** + * Sets the page length, page start and total items from a query object's + * limit, offset and unlimited count. The query MUST have a limit clause. + * + * @param SQLQuery $query + */ + public function setPaginationFromQuery(SQLQuery $query) { + if ($query->limit) { + $this->setPageLength($query->limit['limit']); + $this->setPageStart($query->limit['start']); + $this->setTotalItems($query->unlimitedRowCount()); + } + } + + /** + * @return IteratorIterator + */ + public function getIterator() { + return new IteratorIterator( + $this->list->getRange($this->getPageStart(), $this->pageLength) + ); + } + + /** + * Returns a set of links to all the pages in the list. This is useful for + * basic pagination. + * + * By default it returns links to every page, but if you pass the $max + * parameter the number of pages will be limited to that number, centered + * around the current page. + * + * @param int $max + * @return DataObjectSet + */ + public function Pages($max = null) { + $result = new DataObjectSet(); + + if ($max) { + $start = ($this->CurrentPage() - floor($max / 2)) - 1; + $end = $this->CurrentPage() + floor($max / 2); + + if ($start < 0) { + $start = 0; + $end = $max; + } + + if ($end > $this->TotalPages()) { + $end = $this->TotalPages(); + $start = max(0, $end - $max); + } + } else { + $start = 0; + $end = $this->TotalPages(); + } + + for ($i = $start; $i < $end; $i++) { + $result->push(new ArrayData(array( + 'PageNum' => $i + 1, + 'Link' => HTTP::setGetVar($this->getVar, $i * $this->pageLength), + 'CurrentBool' => $this->CurrentPage() == ($i + 1) + ))); + } + + return $result; + } + + /** + * Returns a summarised pagination which limits the number of pages shown + * around the current page for visually balanced. + * + * Example: 25 pages total, currently on page 6, context of 4 pages + * [prev] [1] ... [4] [5] [[6]] [7] [8] ... [25] [next] + * + * Example template usage: + * + * <% if MyPages.MoreThanOnePage %> + * <% if MyPages.NotFirstPage %> + * + * <% end_if %> + * <% control MyPages.PaginationSummary(4) %> + * <% if CurrentBool %> + * $PageNum + * <% else %> + * <% if Link %> + * $PageNum + * <% else %> + * ... + * <% end_if %> + * <% end_if %> + * <% end_control %> + * <% if MyPages.NotLastPage %> + * + * <% end_if %> + * <% end_if %> + * + * + * @param int $context The number of pages to display around the current + * page. The number should be event, as half the number of each pages + * are displayed on either side of the current one. + * @return DataObjectSet + */ + public function PaginationSummary($context = 4) { + $result = new DataObjectSet(); + $current = $this->CurrentPage(); + $total = $this->TotalPages(); + + // Make the number even for offset calculations. + if ($context % 2) { + $context--; + } + + // If the first or last page is current, then show all context on one + // side of it - otherwise show half on both sides. + if ($current == 1 || $current == $total) { + $offset = $context; + } else { + $offset = floor($context / 2); + } + + $left = max($current - $offset, 1); + $range = range($current - $offset, $current + $offset); + + if ($left + $context > $total) { + $left = $total - $context; + } + + for ($i = 0; $i < $total; $i++) { + $link = HTTP::setGetVar($this->getVar, $i * $this->pageLength); + $num = $i + 1; + + $emptyRange = $num != 1 && $num != $total && ( + $num == $left - 1 || $num == $left + $context + 1 + ); + + if ($emptyRange) { + $result->push(new ArrayData(array( + 'PageNum' => null, + 'Link' => null, + 'CurrentBool' => false + ))); + } elseif ($num == 1 || $num == $total || in_array($num, $range)) { + $result->push(new ArrayData(array( + 'PageNum' => $num, + 'Link' => $link, + 'CurrentBool' => $current == $num + ))); + } + } + + return $result; + } + + /** + * @return int + */ + public function CurrentPage() { + return floor($this->getPageStart() / $this->pageLength) + 1; + } + + /** + * @return int + */ + public function TotalPages() { + return ceil($this->getTotalItems() / $this->pageLength); + } + + /** + * @return bool + */ + public function MoreThanOnePage() { + return $this->TotalPages() > 1; + } + + /** + * @return bool + */ + public function NotFirstPage() { + return $this->CurrentPage() != 1; + } + + /** + * @return bool + */ + public function NotLastPage() { + return $this->CurrentPage() != $this->TotalPages(); + } + + /** + * Returns the number of the first item being displayed on the current + * page. This is useful for things like "displaying 10-20". + * + * @return int + */ + public function FirstItem() { + return ($start = $this->getPageStart()) ? $start + 1 : 1; + } + + /** + * Returns the number of the last item being displayed on this page. + * + * @return int + */ + public function LastItem() { + if ($start = $this->getPageStart()) { + return min($start + $this->pageLength, $this->getTotalItems()); + } else { + return min($this->pageLength, $this->getTotalItems()); + } + } + + /** + * Returns a link to the first page. + * + * @return string + */ + public function FirstLink() { + return HTTP::setGetVar($this->getVar, 0); + } + + /** + * Returns a link to the last page. + * + * @return string + */ + public function LastLink() { + return HTTP::setGetVar($this->getVar, ($this->TotalPages() - 1) * $this->pageLength); + } + + /** + * Returns a link to the next page, if there is another page after the + * current one. + * + * @return string + */ + public function NextLink() { + if ($this->NotLastPage()) { + return HTTP::setGetVar($this->getVar, $this->getPageStart() + $this->pageLength); + } + } + + /** + * Returns a link to the previous page, if the first page is not currently + * active. + * + * @return string + */ + public function PrevLink() { + if ($this->NotFirstPage()) { + return HTTP::setGetVar($this->getVar, $this->getPageStart() - $this->pageLength); + } + } + + // DEPRECATED -------------------------------------------------------------- + + /** + * @deprecated 3.0 Use individual getter methods. + */ + public function getPageLimits() { + return array( + 'pageStart' => $this->getPageStart(), + 'pageLength' => $this->pageLength, + 'totalSize' => $this->getTotalItems(), + ); + } + + /** + * @deprecated 3.0 Use individual setter methods. + */ + public function setPageLimits($pageStart, $pageLength, $totalSize) { + $this->setPageStart($pageStart); + $this->setPageLength($pageLength); + $this->setTotalSize($totalSize); + } + +} \ No newline at end of file diff --git a/core/model/DataList.php b/core/model/DataList.php index 6f69b1a54..950e6fe2c 100644 --- a/core/model/DataList.php +++ b/core/model/DataList.php @@ -122,7 +122,6 @@ class DataList extends DataObjectSet { */ protected function generateItems() { $query = $this->dataQuery->query(); - $this->parseQueryLimit($query); $rows = $query->execute(); $results = array(); foreach($rows as $row) { diff --git a/docs/en/howto/pagination.md b/docs/en/howto/pagination.md new file mode 100644 index 000000000..195b6adee --- /dev/null +++ b/docs/en/howto/pagination.md @@ -0,0 +1,66 @@ +# Paginating A List + +Adding pagination to a `[api:DataList]` or `[DataObjectSet]` is quite simple. All +you need to do is wrap the object in a `[api:PaginatedList]` decorator, which takes +care of fetching a sub-set of the total list and presenting it to the template. + +In order to create a paginated list, you can create a method on your controller +that first creates a `DataList` that will return all pages, and then wraps it +in a `[api:PaginatedSet]` object. The `PaginatedList` object is also passed the +HTTP request object so it can read the current page information from the +"?start=" GET var. + +The paginator will automatically set up query limits and read the request for +information. + + :::php + /** + * Returns a paginated list of all pages in the site. + */ + public function PaginatedPages() { + $pages = DataList::create('Page'); + return new PaginatedList($pages, $this->request); + } + +## Setting Up The Template + +Now all that remains is to render this list into a template, along with pagination +controls. There are two ways to generate pagination controls: +`[api:PaginatedSet->Pages()]` and `[api:PaginatedSet->PaginationSummary()]`. In +this example we will use `PaginationSummary()`. + +The first step is to simply list the objects in the template: + + :::ss +
      + <% control PaginatedPages %> +
    • $Title
    • + <% end_control %> +
    + +By default this will display 10 pages at a time. The next step is to add pagination +controls below this so the user can switch between pages: + + :::ss + <% if PaginatedPages.MoreThanOnePage %> + <% if PaginatedPages.NotFirstPage %> + + <% end_if %> + <% control PaginatedPages.Pages %> + <% if CurrentBool %> + $PageNum + <% else %> + <% if Link %> + $PageNum + <% else %> + ... + <% end_if %> + <% end_if %> + <% end_control %> + <% if PaginatedPages.NotLastPage %> + + <% end_if %> + <% end_if %> + +If there is more than one page, this block will render a set of pagination +controls in the form `[1] ... [3] [4] [[5]] [6] [7] ... [10]`. \ No newline at end of file diff --git a/docs/en/reference/built-in-page-controls.md b/docs/en/reference/built-in-page-controls.md index 7f03dcb5b..e41e17d5d 100644 --- a/docs/en/reference/built-in-page-controls.md +++ b/docs/en/reference/built-in-page-controls.md @@ -319,7 +319,7 @@ a quick reference (not all of them are described above): $NexPageLink, $Link, $RelativeLink, $ChildrenOf, $Page, $Level, $Menu, $Section2, $LoginForm, $SilverStripeNavigator, $PageComments, $Now, $LinkTo, $AbsoluteLink, $CurrentMember, $PastVisitor, $PastMember, $XML_val, $RAW_val, $SQL_val, $JS_val, $ATT_val, $First, $Last, $FirstLast, $MiddleString, $Middle, $Even, $Odd, $EvenOdd, $Pos, $TotalItems, -$BaseHref, $Debug, $CurrentPage, $Top +$BaseHref, $Debug, $Top ### All fields available in Page_Controller @@ -334,7 +334,7 @@ $LinkToID, $VersionID, $CopyContentFromID, $RecordClassName $Link, $LinkOrCurrent, $LinkOrSection, $LinkingMode, $ElementName, $InSection, $Comments, $Breadcrumbs, $NestedTitle, $MetaTags, $ContentSource, $MultipleParents, $TreeTitle, $CMSTreeClasses, $Now, $LinkTo, $AbsoluteLink, $CurrentMember, $PastVisitor, $PastMember, $XML_val, $RAW_val, $SQL_val, $JS_val, $ATT_val, $First, $Last, $FirstLast, $MiddleString, -$Middle, $Even, $Odd, $EvenOdd, $Pos, $TotalItems, $BaseHref, $CurrentPage, $Top +$Middle, $Even, $Odd, $EvenOdd, $Pos, $TotalItems, $BaseHref, $Top ### All fields available in Page diff --git a/docs/en/reference/searchcontext.md b/docs/en/reference/searchcontext.md index 8f4f88130..e005a68ed 100644 --- a/docs/en/reference/searchcontext.md +++ b/docs/en/reference/searchcontext.md @@ -89,23 +89,27 @@ method, we're building our own `getCustomSearchContext()` variant. ### Pagination -For paginating records on multiple pages, you need to get the generated `SQLQuery` before firing off the actual -search. This way we can set the "page limits" on the result through `setPageLimits()`, and only retrieve a fraction of -the whole result set. - +For pagination records on multiple pages, you need to wrap the results in a +`PaginatedList` object. This object is also passed the generated `SQLQuery` +in order to read page limit information. It is also passed the current +`SS_HTTPRequest` object so it can read the current page from a GET var. :::php - function getResults($searchCriteria = array()) { + public function getResults($searchCriteria = array()) { $start = ($this->request->getVar('start')) ? (int)$this->request->getVar('start') : 0; $limit = 10; $context = singleton('MyDataObject')->getCustomSearchContext(); $query = $context->getQuery($searchCriteria, null, array('start'=>$start,'limit'=>$limit)); $records = $context->getResults($searchCriteria, null, array('start'=>$start,'limit'=>$limit)); + if($records) { - $records->setPageLimits($start, $limit, $query->unlimitedRowCount()); + $records = new PaginatedList($records, $this->request); + $records->setPageStart($start); + $records->setPageSize($limit); + $records->setTotalSize($query->unlimitedRowCount()); } - + return $records; } @@ -135,7 +139,7 @@ For more information on how to paginate your results within the template, see [T to show the results of your custom search you need at least this content in your template, notice that Results.PaginationSummary(4) defines how many pages the search will show in the search results. something like: -**Next 1 2 *3* 4 5 … 558** +**Next 1 2 *3* 4 5 ��� 558** :::ss diff --git a/filesystem/File.php b/filesystem/File.php index 34640ff88..c07a267b3 100755 --- a/filesystem/File.php +++ b/filesystem/File.php @@ -714,7 +714,6 @@ class File extends DataObject { $records = $query->execute(); $ret = $this->buildDataObjectSet($records, $containerClass); - if($ret) $ret->parseQueryLimit($query); return $ret; } diff --git a/model/DataObjectSet.php b/model/DataObjectSet.php index 854947c57..eb70f796c 100644 --- a/model/DataObjectSet.php +++ b/model/DataObjectSet.php @@ -32,30 +32,6 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable */ protected $current = null; - /** - * The number object the current page starts at. - * @var int - */ - protected $pageStart; - - /** - * The number of objects per page. - * @var int - */ - protected $pageLength; - - /** - * Total number of DataObjects in this set. - * @var int - */ - protected $totalSize; - - /** - * The pagination GET variable that controls the start of this set. - * @var string - */ - protected $paginationGetVar = "start"; - /** * Create a new DataObjectSet. If you pass one or more arguments, it will try to convert them into {@link ArrayData} objects. * @todo Does NOT automatically convert objects with complex datatypes (e.g. converting arrays within an objects to its own DataObjectSet) @@ -188,272 +164,6 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable public function toDropDownMap($index = 'ID', $titleField = 'Title', $emptyString = null, $sort = false) { return $this->map($index, $titleField, $emptyString, $sort); } - - /** - * Set number of objects on each page. - * @param int $length Number of objects per page - */ - public function setPageLength($length) { - $this->pageLength = $length; - } - - /** - * Set the page limits. - * @param int $pageStart The start of this page. - * @param int $pageLength Number of objects per page - * @param int $totalSize Total number of objects. - */ - public function setPageLimits($pageStart, $pageLength, $totalSize) { - $this->pageStart = $pageStart; - $this->pageLength = $pageLength; - $this->totalSize = $totalSize; - } - - /** - * Get the page limits - * @return array - */ - public function getPageLimits() { - return array( - 'pageStart' => $this->pageStart, - 'pageLength' => $this->pageLength, - 'totalSize' => $this->totalSize, - ); - } - - /** - * Use the limit from the given query to add prev/next buttons to this DataObjectSet. - * @param SQLQuery $query The query used to generate this DataObjectSet - */ - public function parseQueryLimit(SQLQuery $query) { - if($query->limit) { - if(is_array($query->limit)) { - $length = $query->limit['limit']; - $start = $query->limit['start']; - } else if(stripos($query->limit, 'OFFSET')) { - list($length, $start) = preg_split("/ +OFFSET +/i", trim($query->limit)); - } else { - $result = preg_split("/ *, */", trim($query->limit)); - $start = $result[0]; - $length = isset($result[1]) ? $result[1] : null; - } - - if(!$length) { - $length = $start; - $start = 0; - } - $this->setPageLimits($start, $length, $query->unlimitedRowCount()); - } - } - - /** - * Returns the number of the current page. - * @return int - */ - public function CurrentPage() { - return floor($this->pageStart / $this->pageLength) + 1; - } - - /** - * Returns the total number of pages. - * @return int - */ - public function TotalPages() { - if($this->totalSize == 0) { - $this->totalSize = $this->Count(); - } - if($this->pageLength == 0) { - $this->pageLength = 10; - } - - return ceil($this->totalSize / $this->pageLength); - } - - /** - * Return a datafeed of page-links, good for use in search results, etc. - * $maxPages will put an upper limit on the number of pages to return. It will - * show the pages surrounding the current page, so you can still get to the deeper pages. - * @param int $maxPages The maximum number of pages to return - * @return DataObjectSet - */ - public function Pages($maxPages = 0){ - $ret = new DataObjectSet(); - - if($maxPages) { - $startPage = ($this->CurrentPage() - floor($maxPages / 2)) - 1; - $endPage = $this->CurrentPage() + floor($maxPages / 2); - - if($startPage < 0) { - $startPage = 0; - $endPage = $maxPages; - } - if($endPage > $this->TotalPages()) { - $endPage = $this->TotalPages(); - $startPage = max(0, $endPage - $maxPages); - } - - } else { - $startPage = 0; - $endPage = $this->TotalPages(); - } - - for($i=$startPage; $i < $endPage; $i++){ - $link = HTTP::setGetVar($this->paginationGetVar, $i*$this->pageLength); - $thePage = new ArrayData(array( - "PageNum" => $i+1, - "Link" => $link, - "CurrentBool" => ($this->CurrentPage() == $i+1)?true:false, - ) - ); - $ret->push($thePage); - } - - return $ret; - } - - /* - * Display a summarized pagination which limits the number of pages shown - * "around" the currently active page for visual balance. - * In case more paginated pages have to be displayed, only - * - * Example: 25 pages total, currently on page 6, context of 4 pages - * [prev] [1] ... [4] [5] [[6]] [7] [8] ... [25] [next] - * - * Example template usage: - * - * <% if MyPages.MoreThanOnePage %> - * <% if MyPages.NotFirstPage %> - * - * <% end_if %> - * <% control MyPages.PaginationSummary(4) %> - * <% if CurrentBool %> - * $PageNum - * <% else %> - * <% if Link %> - * $PageNum - * <% else %> - * ... - * <% end_if %> - * <% end_if %> - * <% end_control %> - * <% if MyPages.NotLastPage %> - * - * <% end_if %> - * <% end_if %> - * - * - * @param integer $context Number of pages to display "around" the current page. Number should be even, - * because its halved to either side of the current page. - * @return DataObjectSet - */ - public function PaginationSummary($context = 4) { - $ret = new DataObjectSet(); - - // convert number of pages to even number for offset calculation - if($context % 2) $context--; - - // find out the offset - $current = $this->CurrentPage(); - $totalPages = $this->TotalPages(); - - // if the first or last page is shown, use all content on one side (either left or right of current page) - // otherwise half the number for usage "around" the current page - $offset = ($current == 1 || $current == $totalPages) ? $context : floor($context/2); - - $leftOffset = $current - ($offset); - if($leftOffset < 1) $leftOffset = 1; - if($leftOffset + $context > $totalPages) $leftOffset = $totalPages - $context; - - for($i=0; $i < $totalPages; $i++) { - $link = HTTP::setGetVar($this->paginationGetVar, $i*$this->pageLength); - $num = $i+1; - $currentBool = ($current == $i+1) ? true:false; - if( - ($num == $leftOffset-1 && $num != 1 && $num != $totalPages) - || ($num == $leftOffset+$context+1 && $num != 1 && $num != $totalPages) - ) { - $ret->push(new ArrayData(array( - "PageNum" => null, - "Link" => null, - "CurrentBool" => $currentBool, - ) - )); - } else if($num == 1 || $num == $totalPages || in_array($num, range($current-$offset,$current+$offset))) { - $ret->push(new ArrayData(array( - "PageNum" => $num, - "Link" => $link, - "CurrentBool" => $currentBool, - ) - )); - } - } - return $ret; - } - - /** - * Returns true if the current page is not the first page. - * @return boolean - */ - public function NotFirstPage(){ - return $this->CurrentPage() != 1; - } - - /** - * Returns true if the current page is not the last page. - * @return boolean - */ - public function NotLastPage(){ - return $this->CurrentPage() != $this->TotalPages(); - } - - /** - * Returns true if there is more than one page. - * @return boolean - */ - public function MoreThanOnePage(){ - return $this->TotalPages() > 1; - } - - function FirstItem() { - return isset($this->pageStart) ? $this->pageStart + 1 : 1; - } - - function LastItem() { - if(isset($this->pageStart)) { - return min($this->pageStart + $this->pageLength, $this->totalSize); - } else { - return min($this->pageLength, $this->totalSize); - } - } - - /** - * Returns the URL of the previous page. - * @return string - */ - public function PrevLink() { - if($this->pageStart - $this->pageLength >= 0) { - return HTTP::setGetVar($this->paginationGetVar, $this->pageStart - $this->pageLength); - } - } - - /** - * Returns the URL of the next page. - * @return string - */ - public function NextLink() { - if($this->pageStart + $this->pageLength < $this->totalSize) { - return HTTP::setGetVar($this->paginationGetVar, $this->pageStart + $this->pageLength); - } - } - - /** - * Allows us to use multiple pagination GET variables on the same page (eg. if you have search results and page comments on a single page) - * - * @param string $var The variable to go in the GET string (Defaults to 'start') - */ - public function setPaginationGetVar($var) { - $this->paginationGetVar = $var; - } /** * Add an item to the DataObject Set. @@ -608,11 +318,10 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable } /** - * Return the total number of items in this dataset. - * @return int + * @deprecated 3.0 Use {@link DataObjectSet::Count()}. */ public function TotalItems() { - return $this->totalSize ? $this->totalSize : $this->Count(); + return $this->Count(); } /** diff --git a/tests/PaginatedListTest.php b/tests/PaginatedListTest.php new file mode 100644 index 000000000..e6384c8bf --- /dev/null +++ b/tests/PaginatedListTest.php @@ -0,0 +1,247 @@ +assertEquals(0, $list->getPageStart(), 'The start defaults to 0.'); + + $list->setPageStart(10); + $this->assertEquals(10, $list->getPageStart(), 'You can set the page start.'); + + $list = new PaginatedList(new DataObjectSet(), array('start' => 50)); + $this->assertEquals(50, $list->getPageStart(), 'The page start can be read from the request.'); + } + + public function testGetTotalItems() { + $list = new PaginatedList(new DataObjectSet()); + $this->assertEquals(0, $list->getTotalItems()); + + $list->setTotalItems(10); + $this->assertEquals(10, $list->getTotalItems()); + + $list = new PaginatedList(new DataObjectSet( + new ArrayData(array()), + new ArrayData(array()) + )); + $this->assertEquals(2, $list->getTotalItems()); + } + + public function testSetPaginationFromQuery() { + $query = $this->getMock('SQLQuery'); + $query->limit = array('limit' => 15, 'start' => 30); + $query->expects($this->once()) + ->method('unlimitedRowCount') + ->will($this->returnValue(100)); + + $list = new PaginatedList(new DataObjectSet()); + $list->setPaginationFromQuery($query); + + $this->assertEquals(15, $list->getPageLength()); + $this->assertEquals(30, $list->getPageStart()); + $this->assertEquals(100, $list->getTotalItems()); + } + + public function testSetCurrentPage() { + $list = new PaginatedList(new DataObjectSet()); + $list->setPageLength(10); + $list->setCurrentPage(10); + + $this->assertEquals(10, $list->CurrentPage()); + $this->assertEquals(90, $list->getPageStart()); + } + + public function testGetIterator() { + $list = new PaginatedList(new DataObjectSet(array( + new DataObject(array('Num' => 1)), + new DataObject(array('Num' => 2)), + new DataObject(array('Num' => 3)), + new DataObject(array('Num' => 4)), + new DataObject(array('Num' => 5)), + ))); + $list->setPageLength(2); + + $this->assertDOSEquals( + array(array('Num' => 1), array('Num' => 2)), $list->getIterator() + ); + + $list->setCurrentPage(2); + $this->assertDOSEquals( + array(array('Num' => 3), array('Num' => 4)), $list->getIterator() + ); + + $list->setCurrentPage(3); + $this->assertDOSEquals( + array(array('Num' => 5)), $list->getIterator() + ); + + $list->setCurrentPage(999); + $this->assertDOSEquals(array(), $list->getIterator()); + } + + public function testPages() { + $list = new PaginatedList(new DataObjectSet()); + $list->setPageLength(10); + $list->setTotalItems(50); + + $this->assertEquals(5, count($list->Pages())); + $this->assertEquals(3, count($list->Pages(3))); + $this->assertEquals(5, count($list->Pages(15))); + + $list->setCurrentPage(3); + + $expectAll = array( + array('PageNum' => 1), + array('PageNum' => 2), + array('PageNum' => 3, 'CurrentBool' => true), + array('PageNum' => 4), + array('PageNum' => 5), + ); + $this->assertDOSEquals($expectAll, $list->Pages()); + + $expectLimited = array( + array('PageNum' => 2), + array('PageNum' => 3, 'CurrentBool' => true), + array('PageNum' => 4), + ); + $this->assertDOSEquals($expectLimited, $list->Pages(3)); + } + + public function testPaginationSummary() { + $list = new PaginatedList(new DataObjectSet()); + + $list->setPageLength(10); + $list->setTotalItems(250); + $list->setCurrentPage(6); + + $expect = array( + array('PageNum' => 1), + array('PageNum' => null), + array('PageNum' => 4), + array('PageNum' => 5), + array('PageNum' => 6, 'CurrentBool' => true), + array('PageNum' => 7), + array('PageNum' => 8), + array('PageNum' => null), + array('PageNum' => 25), + ); + $this->assertDOSEquals($expect, $list->PaginationSummary(4)); + } + + public function testCurrentPage() { + $list = new PaginatedList(new DataObjectSet()); + $list->setTotalItems(50); + + $this->assertEquals(1, $list->CurrentPage()); + $list->setPageStart(10); + $this->assertEquals(2, $list->CurrentPage()); + $list->setPageStart(40); + $this->assertEquals(5, $list->CurrentPage()); + } + + public function testTotalPages() { + $list = new PaginatedList(new DataObjectSet()); + + $list->setPageLength(1); + $this->assertEquals(0, $list->TotalPages()); + + $list->setTotalItems(1); + $this->assertEquals(1, $list->TotalPages()); + + $list->setTotalItems(5); + $this->assertEquals(5, $list->TotalPages()); + } + + public function testMoreThanOnePage() { + $list = new PaginatedList(new DataObjectSet()); + + $list->setPageLength(1); + $list->setTotalItems(1); + $this->assertFalse($list->MoreThanOnePage()); + + $list->setTotalItems(2); + $this->assertTrue($list->MoreThanOnePage()); + } + + public function testNotFirstPage() { + $list = new PaginatedList(new DataObjectSet()); + $this->assertFalse($list->NotFirstPage()); + $list->setCurrentPage(2); + $this->assertTrue($list->NotFirstPage()); + } + + public function testNotLastPage() { + $list = new PaginatedList(new DataObjectSet()); + $list->setTotalItems(50); + + $this->assertTrue($list->NotLastPage()); + $list->setCurrentPage(5); + $this->assertFalse($list->NotLastPage()); + } + + public function testFirstItem() { + $list = new PaginatedList(new DataObjectSet()); + $this->assertEquals(1, $list->FirstItem()); + $list->setPageStart(10); + $this->assertEquals(11, $list->FirstItem()); + } + + public function testLastItem() { + $list = new PaginatedList(new DataObjectSet()); + $list->setPageLength(10); + $list->setTotalItems(25); + + $list->setCurrentPage(1); + $this->assertEquals(10, $list->LastItem()); + $list->setCurrentPage(2); + $this->assertEquals(20, $list->LastItem()); + $list->setCurrentPage(3); + $this->assertEquals(25, $list->LastItem()); + } + + public function testFirstLink() { + $list = new PaginatedList(new DataObjectSet()); + $this->assertContains('start=0', $list->FirstLink()); + } + + public function testLastLink() { + $list = new PaginatedList(new DataObjectSet()); + $list->setPageLength(10); + $list->setTotalItems(100); + $this->assertContains('start=90', $list->LastLink()); + } + + public function testNextLink() { + $list = new PaginatedList(new DataObjectSet()); + $list->setTotalItems(50); + + $this->assertContains('start=10', $list->NextLink()); + $list->setCurrentPage(2); + $this->assertContains('start=20', $list->NextLink()); + $list->setCurrentPage(3); + $this->assertContains('start=30', $list->NextLink()); + $list->setCurrentPage(4); + $this->assertContains('start=40', $list->NextLink()); + $list->setCurrentPage(5); + $this->assertNull($list->NextLink()); + } + + public function testPrevLink() { + $list = new PaginatedList(new DataObjectSet()); + $list->setTotalItems(50); + + $this->assertNull($list->PrevLink()); + $list->setCurrentPage(2); + $this->assertContains('start=0', $list->PrevLink()); + $list->setCurrentPage(3); + $this->assertContains('start=10', $list->PrevLink()); + $list->setCurrentPage(5); + $this->assertContains('start=30', $list->PrevLink()); + } + +} \ No newline at end of file diff --git a/tests/model/DataObjectSetTest.php b/tests/model/DataObjectSetTest.php index 54d6c687c..c5d73c325 100644 --- a/tests/model/DataObjectSetTest.php +++ b/tests/model/DataObjectSetTest.php @@ -245,52 +245,6 @@ class DataObjectSetTest extends SapphireTest { $this->assertEquals($mixedSet->Count(), 2, 'There are 3 unique data objects in a very mixed set'); } - /** - * Test {@link DataObjectSet->parseQueryLimit()} - */ - function testParseQueryLimit() { - // Create empty objects, because they don't need to have contents - $sql = new SQLQuery('*', '"Member"'); - $max = $sql->unlimitedRowCount(); - $set = new DataObjectSet(); - - // Test handling an array - $set->parseQueryLimit($sql->limit(array('limit'=>5, 'start'=>2))); - $expected = array( - 'pageStart' => 2, - 'pageLength' => 5, - 'totalSize' => $max, - ); - $this->assertEquals($expected, $set->getPageLimits(), 'The page limits match expected values.'); - - // Test handling OFFSET string - // uppercase - $set->parseQueryLimit($sql->limit('3 OFFSET 1')); - $expected = array( - 'pageStart' => 1, - 'pageLength' => 3, - 'totalSize' => $max, - ); - $this->assertEquals($expected, $set->getPageLimits(), 'The page limits match expected values.'); - // and lowercase - $set->parseQueryLimit($sql->limit('32 offset 3')); - $expected = array( - 'pageStart' => 3, - 'pageLength' => 32, - 'totalSize' => $max, - ); - $this->assertEquals($expected, $set->getPageLimits(), 'The page limits match expected values.'); - - // Finally check MySQL LIMIT syntax - $set->parseQueryLimit($sql->limit('7, 7')); - $expected = array( - 'pageStart' => 7, - 'pageLength' => 7, - 'totalSize' => $max, - ); - $this->assertEquals($expected, $set->getPageLimits(), 'The page limits match expected values.'); - } - /** * Test {@link DataObjectSet->insertFirst()} */ From ff3d3816ca5ecb58ff047e89dab292c75708237b Mon Sep 17 00:00:00 2001 From: ajshort Date: Mon, 28 Mar 2011 00:48:27 +1100 Subject: [PATCH 28/64] MINOR: Updated SapphireTest::dataObjectArrayMatch() to use array_key_exists() rather than isset(). This allows tempting for NULL values in DOS assertion methods. --- dev/SapphireTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/SapphireTest.php b/dev/SapphireTest.php index 6f55b7739..3cabf2590 100755 --- a/dev/SapphireTest.php +++ b/dev/SapphireTest.php @@ -659,7 +659,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase { */ private function dataObjectArrayMatch($item, $match) { foreach($match as $k => $v) { - if(!isset($item[$k]) || $item[$k] != $v) return false; + if(!array_key_exists($k, $item) || $item[$k] != $v) return false; } return true; } From 45a8866fabaaa0c262220bbbff42002268c1a1c0 Mon Sep 17 00:00:00 2001 From: ajshort Date: Fri, 1 Apr 2011 15:09:48 +1100 Subject: [PATCH 29/64] FEATURE: Added SS_ListDecorator, which is a utility class used to create decorators which wrap around a list instance, but are stll recognised as a list. --- core/model/ListDecorator.php | 197 +++++++++++++++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 core/model/ListDecorator.php diff --git a/core/model/ListDecorator.php b/core/model/ListDecorator.php new file mode 100644 index 000000000..7ca89de91 --- /dev/null +++ b/core/model/ListDecorator.php @@ -0,0 +1,197 @@ +list = $list; + parent::__construct(); + } + + /** + * Returns the list this decorator wraps around. + * + * @return DataObjectSet + */ + public function getList() { + return $this->list; + } + + // PROXIED METHODS --------------------------------------------------------- + + public function offsetExists($key) { + return $this->list->offsetExists($key); + } + + public function offsetGet($key) { + return $this->list->offsetGet($key); + } + + public function offsetSet($key, $value) { + $this->list->offsetSet($key, $value); + } + + public function offsetUnset($key) { + $this->list->offsetUnset($key); + } + + public function destroy() { + $this->list->destroy(); + } + + public function emptyItems() { + $this->list->emptyItems(); + } + + public function toArray($index = null) { + return $this->list->toArray($index); + } + + public function toNestedArray($index = null){ + return $this->list->toNestedArray($index); + } + + public function push($item, $key = null) { + $this->list->push($item, $key); + } + + public function insertFirst($item, $key = null) { + $this->list->insertFirst($item, $key); + } + + public function unshift($item) { + $this->list->unshift($item); + } + + public function shift() { + return $this->list->shift(); + } + + public function pop() { + return $this->list->pop(); + } + + public function remove($itemObject) { + $this->list->remove($itemObject); + } + + public function replace($itemOld, $itemNew) { + $this->list->replace($itemOld, $itemNew); + } + + public function merge($anotherSet){ + $this->list->merge($anotherSet); + } + + public function getRange($offset, $length) { + return $this->list->getRange($offset, $length); + } + + public function getIterator() { + return $this->list->getIterator(); + } + + public function exists() { + return $this->list->exists(); + } + + public function First() { + return $this->list->First(); + } + + public function Last() { + return $this->list->Last(); + } + + public function TotalItems() { + return $this->list->TotalItems(); + } + + public function Count() { + return $this->list->Count(); + } + + public function UL() { + return $this->list->UL(); + } + + public function forTemplate() { + return $this->list->forTemplate(); + } + + public function map($index = 'ID', $titleField = 'Title', $emptyString = null, $sort = false) { + return $this->list->map($index, $titleField, $emptyString, $sort); + } + + public function find($key, $value) { + return $this->list->find($key, $value); + } + + public function column($value = 'ID') { + return $this->list->column($value); + } + + public function groupBy($index){ + return $this->list->groupBy($index); + } + + public function GroupedBy($index, $childControl = "Children") { + return $this->list->GroupedBy($index, $childControl); + } + + public function buildNestedUL($nestingLevels, $ulExtraAttributes = '') { + return $this->list->buildNestedUL($nestingLevels, $ulExtraAttributes); + } + + public function getChildrenAsUL($nestingLevels, $level = 0, $template = "
  • \$Title", $ulExtraAttributes = null, &$itemCount = 0) { + return $this->list->getChildrenAsUL( + $nestingLevels, + $level, + $template, + $ulExtraAttributes, + $itemCount); + } + + public function sort($fieldname, $direction = "ASC") { + $this->list->sort($fieldname, $direction); + } + + public function removeDuplicates($field = 'ID') { + $this->list->removeDuplicates($field); + } + + public function debug() { + return $this->list->debug(); + } + + public function groupWithParents($groupField, $groupClassName, $sortParents = null, $parentField = 'ID', $collapse = false, $requiredParents = null) { + return $this->list->groupWithParents( + $groupField, + $groupClassName, + $sortParents, + $parentField, + $collapse, + $requiredParents); + } + + public function addWithoutWrite($field) { + $this->list->addWithoutWrite($field); + } + + public function containsIDs($idList) { + return $this->list->condaintsIDs($idList); + } + + public function onlyContainsIDs($idList) { + return $this->list->onlyContainsIDs($idList); + } + +} \ No newline at end of file From fc8d442505cf2d6e4120b08a2b94b96987413fc3 Mon Sep 17 00:00:00 2001 From: ajshort Date: Fri, 1 Apr 2011 15:10:07 +1100 Subject: [PATCH 30/64] MINOR: Removed useless setPageLimits call from ComplexTableField::Items(). --- forms/ComplexTableField.php | 1 - 1 file changed, 1 deletion(-) diff --git a/forms/ComplexTableField.php b/forms/ComplexTableField.php index 13f18cb4e..6537aef57 100644 --- a/forms/ComplexTableField.php +++ b/forms/ComplexTableField.php @@ -255,7 +255,6 @@ JS; } $pageStart = (isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0; - $sourceItems->setPageLimits($pageStart, $this->pageSize, $this->TotalCount()); $output = new DataObjectSet(); foreach($sourceItems as $pageIndex=>$item) { From 8fb7b531cfc1b4725d9c095c1a6bd2f22a8ecd71 Mon Sep 17 00:00:00 2001 From: ajshort Date: Fri, 1 Apr 2011 15:10:27 +1100 Subject: [PATCH 31/64] MINOR: Updated MySQLDatabase::searchEngine() to use the new PaginatedList API. --- model/MySQLDatabase.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) mode change 100755 => 100644 model/MySQLDatabase.php diff --git a/model/MySQLDatabase.php b/model/MySQLDatabase.php old mode 100755 new mode 100644 index 9f5e70ca5..67d969b13 --- a/model/MySQLDatabase.php +++ b/model/MySQLDatabase.php @@ -832,8 +832,12 @@ class MySQLDatabase extends SS_Database { if(isset($objects)) $doSet = new DataObjectSet($objects); else $doSet = new DataObjectSet(); - $doSet->setPageLimits($start, $pageLength, $totalCount); - return $doSet; + $list = new PaginatedList($doSet); + $list->setPageStart($start); + $list->setPageLEngth($pageLength); + $list->setTotalItems($totalCount); + + return $list; } /** From 8ec412e4d9568ad2561f7c4b7410ad1faf693749 Mon Sep 17 00:00:00 2001 From: ajshort Date: Fri, 1 Apr 2011 15:10:34 +1100 Subject: [PATCH 32/64] MINOR: Updated PaginatedList to be a list decorator. --- core/PaginatedList.php | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/core/PaginatedList.php b/core/PaginatedList.php index 11227495d..2e123b5c7 100644 --- a/core/PaginatedList.php +++ b/core/PaginatedList.php @@ -5,9 +5,8 @@ * @package sapphire * @subpackage view */ -class PaginatedList extends ViewableData implements IteratorAggregate { +class PaginatedList extends SS_ListDecorator { - protected $list; protected $request; protected $getVar = 'start'; @@ -28,16 +27,8 @@ class PaginatedList extends ViewableData implements IteratorAggregate { throw new Exception('The request must be readable as an array.'); } - $this->list = $list; - $this->failover = $list; - $this->request = $request; - } - - /** - * @return DataObjectSet - */ - public function getList() { - return $this->list; + $this->request = $request; + parent::__construct($list); } /** From 81c0caaddba8d86788da91bd11647a08a31720c6 Mon Sep 17 00:00:00 2001 From: ajshort Date: Tue, 5 Apr 2011 21:01:57 +1000 Subject: [PATCH 33/64] API CHANGE: Renamed DataList::filter() and DataQuery::filter() to ::where(). --- admin/code/MemberTableField.php | 2 +- core/model/DataList.php | 17 +++++++++-------- core/model/DataQuery.php | 2 +- core/model/RelationList.php | 2 +- forms/TableListField.php | 2 +- model/DataObject.php | 10 +++++----- model/Hierarchy.php | 8 ++++---- model/Versioned.php | 8 ++++---- search/filters/EndsWithFilter.php | 2 +- search/filters/ExactMatchFilter.php | 2 +- search/filters/ExactMatchMultiFilter.php | 2 +- search/filters/GreaterThanFilter.php | 2 +- search/filters/LessThanFilter.php | 2 +- search/filters/NegationFilter.php | 2 +- search/filters/PartialMatchFilter.php | 2 +- search/filters/StartsWithFilter.php | 2 +- search/filters/StartsWithMultiFilter.php | 2 +- search/filters/SubstringFilter.php | 2 +- security/Group.php | 2 +- security/Member.php | 2 +- 20 files changed, 38 insertions(+), 37 deletions(-) mode change 100755 => 100644 admin/code/MemberTableField.php mode change 100755 => 100644 forms/TableListField.php mode change 100755 => 100644 model/DataObject.php mode change 100755 => 100644 model/Hierarchy.php mode change 100755 => 100644 security/Member.php diff --git a/admin/code/MemberTableField.php b/admin/code/MemberTableField.php old mode 100755 new mode 100644 index 8a69c638a..863b81999 --- a/admin/code/MemberTableField.php +++ b/admin/code/MemberTableField.php @@ -97,7 +97,7 @@ class MemberTableField extends ComplexTableField { foreach($SNG_member->searchableFields() as $fieldName => $fieldSpec) { if(strpos($fieldName, '.') === false) $searchFilters[] = "\"$fieldName\" LIKE '%{$SQL_search}%'"; } - $members = $members->filter('(' . implode(' OR ', $searchFilters) . ')'); + $members = $members->where('(' . implode(' OR ', $searchFilters) . ')'); } parent::__construct($controller, $name, $members, $fieldList); diff --git a/core/model/DataList.php b/core/model/DataList.php index 950e6fe2c..c94250add 100644 --- a/core/model/DataList.php +++ b/core/model/DataList.php @@ -60,11 +60,12 @@ class DataList extends DataObjectSet { } /** - * Filter this data list by a WHERE clause - * @todo Implement array syntax for this. Perhaps the WHERE clause should be $this->where()? + * Add a WHERE clause to the query. + * + * @param string $filter */ - public function filter($filter) { - $this->dataQuery->filter($filter); + public function where($filter) { + $this->dataQuery->where($filter); return $this; } @@ -235,7 +236,7 @@ class DataList extends DataObjectSet { * Find an element of this DataList where the given key = value */ public function find($key, $value) { - return $this->filter("\"$key\" = '" . Convert::raw2sql($value) . "'")->First(); + return $this->where("\"$key\" = '" . Convert::raw2sql($value) . "'")->First(); } @@ -244,7 +245,7 @@ class DataList extends DataObjectSet { */ public function byIDs(array $ids) { $baseClass = ClassInfo::baseDataClass($this->dataClass); - $this->filter("\"$baseClass\".\"ID\" IN (" . implode(',', $ids) .")"); + $this->where("\"$baseClass\".\"ID\" IN (" . implode(',', $ids) .")"); return $this; } @@ -254,7 +255,7 @@ class DataList extends DataObjectSet { */ public function byID($id) { $baseClass = ClassInfo::baseDataClass($this->dataClass); - return $this->filter("\"$baseClass\".\"ID\" = " . (int)$id)->First(); + return $this->where("\"$baseClass\".\"ID\" = " . (int)$id)->First(); } /** @@ -340,7 +341,7 @@ class DataList extends DataObjectSet { * Remove every element in this DataList matching the given $filter. */ function removeByFilter($filter) { - foreach($this->filter($filter) as $item) { + foreach($this->where($filter) as $item) { $this->remove($item); } } diff --git a/core/model/DataQuery.php b/core/model/DataQuery.php index bcea0d23e..3494ab2b6 100644 --- a/core/model/DataQuery.php +++ b/core/model/DataQuery.php @@ -269,7 +269,7 @@ class DataQuery { /** * Set the WHERE clause of this query */ - function filter($filter) { + function where($filter) { if($filter) { $clone = $this; $clone->query->where($filter); diff --git a/core/model/RelationList.php b/core/model/RelationList.php index 05321f2f1..27f72f6d0 100644 --- a/core/model/RelationList.php +++ b/core/model/RelationList.php @@ -18,7 +18,7 @@ abstract class RelationList extends DataList { if(is_array($id) && sizeof($id) == 1) $id = reset($id); $this->foreignID = $id; - $this->dataQuery->filter($this->foreignIDFilter()); + $this->dataQuery->where($this->foreignIDFilter()); } /** diff --git a/forms/TableListField.php b/forms/TableListField.php old mode 100755 new mode 100644 index ad850e0c7..27ab91d62 --- a/forms/TableListField.php +++ b/forms/TableListField.php @@ -240,7 +240,7 @@ class TableListField extends FormField { $this->dataList = $sourceClass; } else { - $this->dataList = DataObject::get($sourceClass)->filter($sourceFilter) + $this->dataList = DataObject::get($sourceClass)->where($sourceFilter) ->sort($sourceSort)->join($sourceJoin); // Grab it from the form relation, if available. $this->getDataListFromForm = true; diff --git a/model/DataObject.php b/model/DataObject.php old mode 100755 new mode 100644 index da301ecb8..8da2175fd --- a/model/DataObject.php +++ b/model/DataObject.php @@ -1199,7 +1199,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // - move the details of the delete code in the DataQuery system // - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest // obviously, that means getting requireTable() to configure cascading deletes ;-) - $srcQuery = DataList::create($this->class)->filter("ID = $this->ID")->dataQuery()->query(); + $srcQuery = DataList::create($this->class)->where("ID = $this->ID")->dataQuery()->query(); foreach($srcQuery->queriedTables() as $table) { $query = new SQLQuery("*", array('"'.$table.'"')); $query->where("\"ID\" = $this->ID"); @@ -1329,7 +1329,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $result = new HasManyList($componentClass, $joinField); if($this->ID) $result->setForeignID($this->ID); - $result = $result->filter($filter)->limit($limit)->sort($sort)->join($join); + $result = $result->where($filter)->limit($limit)->sort($sort)->join($join); return $result; } @@ -1419,7 +1419,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // foreignID set elsewhere. if($this->ID) $result->setForeignID($this->ID); - return $result->filter($filter)->sort($sort)->limit($limit); + return $result->where($filter)->sort($sort)->limit($limit); } /** @@ -2487,7 +2487,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // Deprecated 2.5? // Todo: Make the $containerClass method redundant if($containerClass != "DataList") user_error("The DataObject::get() \$containerClass argument has been deprecated", E_USER_NOTICE); - $result = DataList::create($callerClass)->filter($filter)->sort($sort)->join($join)->limit($limit); + $result = DataList::create($callerClass)->where($filter)->sort($sort)->join($join)->limit($limit); return $result; } @@ -2595,7 +2595,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity ] = false; } if(!$cache || !isset(DataObject::$cache_get_one[$callerClass][$cacheKey])) { - $dl = DataList::create($callerClass)->filter($filter)->sort($orderby); + $dl = DataList::create($callerClass)->where($filter)->sort($orderby); $item = $dl->First(); if($cache) { diff --git a/model/Hierarchy.php b/model/Hierarchy.php old mode 100755 new mode 100644 index 1ddc08bf4..b98a28b86 --- a/model/Hierarchy.php +++ b/model/Hierarchy.php @@ -548,8 +548,8 @@ class Hierarchy extends DataExtension { $baseClass = ClassInfo::baseDataClass($this->owner->class); $id = $this->owner->ID; - $children = DataObject::get($baseClass)->filter("\"{$baseClass}\".\"ParentID\" = $id AND \"{$baseClass}\".\"ID\" != $id"); - if(!$showAll) $children = $children->filter('"ShowInMenus" = 1'); + $children = DataObject::get($baseClass)->where("\"{$baseClass}\".\"ParentID\" = $id AND \"{$baseClass}\".\"ID\" != $id"); + if(!$showAll) $children = $children->where('"ShowInMenus" = 1'); // Query the live site $children->dataQuery()->setQueryParam('Versioned.mode', 'stage'); @@ -558,13 +558,13 @@ class Hierarchy extends DataExtension { if($onlyDeletedFromStage) { // Note that this makes a second query, and could be optimised to be a joi; $stageChildren = DataObject::get($baseClass) - ->filter("\"{$baseClass}\".\"ParentID\" = $id AND \"{$baseClass}\".\"ID\" != $id"); + ->where("\"{$baseClass}\".\"ParentID\" = $id AND \"{$baseClass}\".\"ID\" != $id"); $stageChildren->dataQuery()->setQueryParam('Versioned.mode', 'stage'); $stageChildren->dataQuery()->setQueryParam('Versioned.stage', ''); $ids = $stageChildren->column("ID"); if($ids) { - $children->filter("\"$baseClass\".\"ID\" NOT IN (" . implode(',',$ids) . ")"); + $children->where("\"$baseClass\".\"ID\" NOT IN (" . implode(',',$ids) . ")"); } } diff --git a/model/Versioned.php b/model/Versioned.php index 4444bd5c1..e3ab086c7 100644 --- a/model/Versioned.php +++ b/model/Versioned.php @@ -955,7 +955,7 @@ class Versioned extends DataExtension { */ static function get_latest_version($class, $id) { $baseClass = ClassInfo::baseDataClass($class); - $list = DataList::create($class)->filter("\"$baseClass\".\"RecordID\" = $id"); + $list = DataList::create($class)->where("\"$baseClass\".\"RecordID\" = $id"); $list->dataQuery()->setQueryParam("Versioned.mode", "latest_versions"); return $list->First(); } @@ -967,7 +967,7 @@ class Versioned extends DataExtension { * In particular, this will query deleted records as well as active ones. */ static function get_including_deleted($class, $filter = "", $sort = "") { - $list = DataList::create($class)->filter($filter)->sort($sort); + $list = DataList::create($class)->where($filter)->sort($sort); $list->dataQuery()->setQueryParam("Versioned.mode", "latest_versions"); return $list; } @@ -978,7 +978,7 @@ class Versioned extends DataExtension { */ static function get_version($class, $id, $version) { $baseClass = ClassInfo::baseDataClass($class); - $list = DataList::create($class)->filter("\"$baseClass\".\"RecordID\" = $id")->filter("\"$baseClass\".\"Version\" = " . (int)$version); + $list = DataList::create($class)->where("\"$baseClass\".\"RecordID\" = $id")->where("\"$baseClass\".\"Version\" = " . (int)$version); $list->dataQuery()->setQueryParam('Versioned.mode', 'all_versions'); return $list->First(); } @@ -989,7 +989,7 @@ class Versioned extends DataExtension { */ static function get_all_versions($class, $id) { $baseClass = ClassInfo::baseDataClass($class); - $list = DataList::create($class)->filter("\"$baseClass\".\"RecordID\" = $id"); + $list = DataList::create($class)->where("\"$baseClass\".\"RecordID\" = $id"); $list->dataQuery()->setQueryParam('Versioned.mode', 'all_versions'); return $list; } diff --git a/search/filters/EndsWithFilter.php b/search/filters/EndsWithFilter.php index 3cc8eb360..89aa14ee5 100644 --- a/search/filters/EndsWithFilter.php +++ b/search/filters/EndsWithFilter.php @@ -25,7 +25,7 @@ class EndsWithFilter extends SearchFilter { */ public function apply(DataQuery $query) { $this->model = $query->applyRelation($this->relation); - $query->filter($this->getDbName() . " LIKE '%" . Convert::raw2sql($this->getValue()) . "'"); + $query->where($this->getDbName() . " LIKE '%" . Convert::raw2sql($this->getValue()) . "'"); } public function isEmpty() { diff --git a/search/filters/ExactMatchFilter.php b/search/filters/ExactMatchFilter.php index 9dcd28ba2..73cb7dc63 100644 --- a/search/filters/ExactMatchFilter.php +++ b/search/filters/ExactMatchFilter.php @@ -22,7 +22,7 @@ class ExactMatchFilter extends SearchFilter { */ public function apply(DataQuery $query) { $this->model = $query->applyRelation($this->relation); - return $query->filter(sprintf( + return $query->where(sprintf( "%s = '%s'", $this->getDbName(), Convert::raw2sql($this->getValue()) diff --git a/search/filters/ExactMatchMultiFilter.php b/search/filters/ExactMatchMultiFilter.php index 9b35f9106..7c4c80b38 100644 --- a/search/filters/ExactMatchMultiFilter.php +++ b/search/filters/ExactMatchMultiFilter.php @@ -39,7 +39,7 @@ class ExactMatchMultiFilter extends SearchFilter { } $SQL_valueStr = "'" . implode("','", $values) . "'"; - return $query->filter(sprintf( + return $query->where(sprintf( "%s IN (%s)", $this->getDbName(), $SQL_valueStr diff --git a/search/filters/GreaterThanFilter.php b/search/filters/GreaterThanFilter.php index 0556c211d..86ca0d05f 100644 --- a/search/filters/GreaterThanFilter.php +++ b/search/filters/GreaterThanFilter.php @@ -14,7 +14,7 @@ class GreaterThanFilter extends SearchFilter { */ public function apply(DataQuery $query) { $this->model = $query->applyRelation($this->relation); - return $query->filter(sprintf( + return $query->where(sprintf( "%s > '%s'", $this->getDbName(), Convert::raw2sql($this->getDbFormattedValue()) diff --git a/search/filters/LessThanFilter.php b/search/filters/LessThanFilter.php index b79d29a65..84b7f34ad 100644 --- a/search/filters/LessThanFilter.php +++ b/search/filters/LessThanFilter.php @@ -14,7 +14,7 @@ class LessThanFilter extends SearchFilter { */ public function apply(DataQuery $query) { $this->model = $query->applyRelation($this->relation); - return $query->filter(sprintf( + return $query->where(sprintf( "%s < '%s'", $this->getDbName(), Convert::raw2sql($this->getDbFormattedValue()) diff --git a/search/filters/NegationFilter.php b/search/filters/NegationFilter.php index 9d092936a..110093e3e 100644 --- a/search/filters/NegationFilter.php +++ b/search/filters/NegationFilter.php @@ -8,7 +8,7 @@ class NegationFilter extends SearchFilter { public function apply(DataQuery $query) { - return $query->filter(sprintf( + return $query->where(sprintf( "%s != '%s'", $this->getDbName(), Convert::raw2sql($this->getValue()) diff --git a/search/filters/PartialMatchFilter.php b/search/filters/PartialMatchFilter.php index cf97d48fb..4756bd5d9 100644 --- a/search/filters/PartialMatchFilter.php +++ b/search/filters/PartialMatchFilter.php @@ -14,7 +14,7 @@ class PartialMatchFilter extends SearchFilter { public function apply(DataQuery $query) { $this->model = $query->applyRelation($this->relation); - return $query->filter(sprintf( + return $query->where(sprintf( "%s LIKE '%%%s%%'", $this->getDbName(), Convert::raw2sql($this->getValue()) diff --git a/search/filters/StartsWithFilter.php b/search/filters/StartsWithFilter.php index fda662316..35fb33ed6 100644 --- a/search/filters/StartsWithFilter.php +++ b/search/filters/StartsWithFilter.php @@ -25,7 +25,7 @@ class StartsWithFilter extends SearchFilter { */ public function apply(DataQuery $query) { $this->model = $query->applyRelation($this->relation); - $query->filter($this->getDbName() . " LIKE '" . Convert::raw2sql($this->getValue()) . "%'"); + $query->where($this->getDbName() . " LIKE '" . Convert::raw2sql($this->getValue()) . "%'"); } public function isEmpty() { diff --git a/search/filters/StartsWithMultiFilter.php b/search/filters/StartsWithMultiFilter.php index 22f4f4dcf..7bf6c5094 100644 --- a/search/filters/StartsWithMultiFilter.php +++ b/search/filters/StartsWithMultiFilter.php @@ -25,7 +25,7 @@ class StartsWithMultiFilter extends SearchFilter { ); } - return $query->filter(implode(" OR ", $matches)); + return $query->where(implode(" OR ", $matches)); } public function isEmpty() { diff --git a/search/filters/SubstringFilter.php b/search/filters/SubstringFilter.php index a8c345d64..965413270 100644 --- a/search/filters/SubstringFilter.php +++ b/search/filters/SubstringFilter.php @@ -13,7 +13,7 @@ class SubstringFilter extends SearchFilter { public function apply(DataQuery $query) { - return $query->filter(sprintf( + return $query->where(sprintf( "LOCATE('%s', %s) != 0", Convert::raw2sql($this->getValue()), $this->getDbName() diff --git a/security/Group.php b/security/Group.php index 299ba83d7..af30c5f99 100644 --- a/security/Group.php +++ b/security/Group.php @@ -211,7 +211,7 @@ class Group extends DataObject { // Call the relation method on the DataList to get the members from all the groups return $groups->relation('DirectMembers') - ->filter($filter)->sort($sort)->join($join)->limit($limit); + ->where($filter)->sort($sort)->join($join)->limit($limit); } /** diff --git a/security/Member.php b/security/Member.php old mode 100755 new mode 100644 index 33b1b5bd4..d646a0fca --- a/security/Member.php +++ b/security/Member.php @@ -953,7 +953,7 @@ class Member extends DataObject { foreach($groups as $group) { if(!$group->allowedIPAddress($ip)) $disallowedGroups[] = $groupID; } - if($disallowedGroups) $group->filter("\"Group\".\"ID\" NOT IN (" . + if($disallowedGroups) $group->where("\"Group\".\"ID\" NOT IN (" . implode(',',$disallowedGroups) . ")"); return $groups; From 34e9ddfcc343a90adc8249f70cc2a7bb0d5f9ec4 Mon Sep 17 00:00:00 2001 From: ajshort Date: Tue, 19 Apr 2011 13:44:25 +1000 Subject: [PATCH 34/64] MINOR: Moved files from /core/model into /model. --- {core/model => model}/DataList.php | 0 {core/model => model}/DataQuery.php | 0 {core/model => model}/HasManyList.php | 0 {core/model => model}/ListDecorator.php | 0 {core/model => model}/ManyManyList.php | 0 {core/model => model}/RelationList.php | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename {core/model => model}/DataList.php (100%) rename {core/model => model}/DataQuery.php (100%) rename {core/model => model}/HasManyList.php (100%) rename {core/model => model}/ListDecorator.php (100%) rename {core/model => model}/ManyManyList.php (100%) rename {core/model => model}/RelationList.php (100%) diff --git a/core/model/DataList.php b/model/DataList.php similarity index 100% rename from core/model/DataList.php rename to model/DataList.php diff --git a/core/model/DataQuery.php b/model/DataQuery.php similarity index 100% rename from core/model/DataQuery.php rename to model/DataQuery.php diff --git a/core/model/HasManyList.php b/model/HasManyList.php similarity index 100% rename from core/model/HasManyList.php rename to model/HasManyList.php diff --git a/core/model/ListDecorator.php b/model/ListDecorator.php similarity index 100% rename from core/model/ListDecorator.php rename to model/ListDecorator.php diff --git a/core/model/ManyManyList.php b/model/ManyManyList.php similarity index 100% rename from core/model/ManyManyList.php rename to model/ManyManyList.php diff --git a/core/model/RelationList.php b/model/RelationList.php similarity index 100% rename from core/model/RelationList.php rename to model/RelationList.php From 2365bfc652225b2197b3ef79398c2a49d817ea61 Mon Sep 17 00:00:00 2001 From: ajshort Date: Tue, 19 Apr 2011 16:21:33 +1000 Subject: [PATCH 35/64] MINOR: Moved model tests into the tests/model directory. --- tests/{ => model}/DataQueryTest.php | 0 tests/{ => model}/PaginatedListTest.php | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/{ => model}/DataQueryTest.php (100%) rename tests/{ => model}/PaginatedListTest.php (100%) diff --git a/tests/DataQueryTest.php b/tests/model/DataQueryTest.php similarity index 100% rename from tests/DataQueryTest.php rename to tests/model/DataQueryTest.php diff --git a/tests/PaginatedListTest.php b/tests/model/PaginatedListTest.php similarity index 100% rename from tests/PaginatedListTest.php rename to tests/model/PaginatedListTest.php From a15b6941a61f3e8901ed565e4beb5ffd7b997688 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Sun, 1 May 2011 17:12:26 +1200 Subject: [PATCH 36/64] BUGFIX: Replaced ManifestBuilder::has_been_included() with SS_ClassLoader::hasManifest(). --- core/manifest/ClassLoader.php | 7 +++++++ model/DataQuery.php | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/core/manifest/ClassLoader.php b/core/manifest/ClassLoader.php index 1c1513d70..0009a2ed0 100644 --- a/core/manifest/ClassLoader.php +++ b/core/manifest/ClassLoader.php @@ -34,6 +34,13 @@ class SS_ClassLoader { public function getManifest() { return $this->manifests[count($this->manifests) - 1]; } + + /** + * Returns true if this class loader has a manifest. + */ + public function hasManifest() { + return (bool)$this->manifests; + } /** * Pushes a class manifest instance onto the top of the stack. This will diff --git a/model/DataQuery.php b/model/DataQuery.php index 3494ab2b6..eb1bfbe46 100644 --- a/model/DataQuery.php +++ b/model/DataQuery.php @@ -76,7 +76,7 @@ class DataQuery { // Error checking if(!$tableClasses) { - if(!ManifestBuilder::has_been_included()) { + if(!SS_ClassLoader::instance()->hasManifest()) { user_error("DataObjects have been requested before the manifest is loaded. Please ensure you are not querying the database in _config.php.", E_USER_ERROR); } else { user_error("DataObject::buildSQL: Can't find data classes (classes linked to tables) for $this->dataClass. Please ensure you run dev/build after creating a new DataObject.", E_USER_ERROR); From 2f799619885fe28276a0102eb15a519f4894f879 Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Sun, 1 May 2011 17:12:50 +1200 Subject: [PATCH 37/64] MINOR: Moved tests for DataList into DataListTest. --- tests/model/DataListTest.php | 107 +++++++++++++++++++++++++++++++++ tests/model/DataObjectTest.php | 39 ------------ 2 files changed, 107 insertions(+), 39 deletions(-) create mode 100644 tests/model/DataListTest.php diff --git a/tests/model/DataListTest.php b/tests/model/DataListTest.php new file mode 100644 index 000000000..1240453df --- /dev/null +++ b/tests/model/DataListTest.php @@ -0,0 +1,107 @@ +Name; + } + $this->assertEquals(array('Joe', 'Bob', 'Phil'), $names); + + // If we don't want to iterate, we can extract a single column from the list with column() + $this->assertEquals(array('Joe', 'Bob', 'Phil'), $list->column('Name')); + + // We can sort a list + $list = $list->sort('Name'); + $this->assertEquals(array('Bob', 'Joe', 'Phil'), $list->column('Name')); + + // We can also restrict the output to a range + $this->assertEquals(array('Joe', 'Phil'), $list->getRange(1,2)->column('Name')); + } + + function testFilter() { + // coming soon! + } + + function testWhere() { + // We can use raw SQL queries with where. This is only recommended for advanced uses; + // if you can, you should use filter(). + $list = DataList::create('DataObjectTest_TeamComment'); + + // where() returns a new DataList, like all the other modifiers, so it can be chained. + $list2 = $list->where('"Name" = \'Joe\''); + $this->assertEquals(array('This is a team comment by Joe'), $list2->column('Comment')); + + // The where() clauses are chained together with AND + $list3 = $list2->where('"Name" = \'Bob\''); + $this->assertEquals(array(), $list3->column('Comment')); + } + + /** + * Test DataList->byID() + */ + function testByID() { + // We can get a single item by ID. + $id = $this->idFromFixture('DataObjectTest_Team','team2'); + $team = DataList::create("DataObjectTest_Team")->byID($id); + + // byID() returns a DataObject, rather than a DataList + $this->assertInstanceOf('DataObjectTest_Team', $team); + $this->assertEquals('Team 2', $team->Title); + } + + /** + * Test DataList->removeByID() + */ + function testRemoveByID() { + $list = DataList::create("DataObjectTest_Team"); + $id = $this->idFromFixture('DataObjectTest_Team','team2'); + + $this->assertNotNull($list->byID($id)); + $list->removeByID($id); + $this->assertNull($list->byID($id)); + } + + /** + * Test DataList->canSortBy() + */ + function testCanSortBy() { + // Basic check + $team = DataList::create("DataObjectTest_Team"); + $this->assertTrue($team->canSortBy("Title")); + $this->assertFalse($team->canSortBy("SomethingElse")); + + // Subclasses + $subteam = DataList::create("DataObjectTest_SubTeam"); + $this->assertTrue($subteam->canSortBy("Title")); + $this->assertTrue($subteam->canSortBy("SubclassDatabaseField")); + } + + function testDataListArrayAccess() { + $list = DataList::create("DataObjectTest_Team")->sort("Title"); + + // We can use array access to refer to single items in the DataList, as if it were an array + $this->assertEquals("Subteam 1", $list[0]->Title); + $this->assertEquals("Subteam 3", $list[2]->Title); + $this->assertEquals("Team 2", $list[4]->Title); + } +} \ No newline at end of file diff --git a/tests/model/DataObjectTest.php b/tests/model/DataObjectTest.php index 428fc1a30..fa1f0b8b8 100755 --- a/tests/model/DataObjectTest.php +++ b/tests/model/DataObjectTest.php @@ -1068,45 +1068,6 @@ class DataObjectTest extends SapphireTest { $this->assertFalse($objEmpty->isEmpty(), 'Zero value in attribute considered non-empty'); } - /** - * Test DataList->byID() - */ - function testByID() { - $id = $this->idFromFixture('DataObjectTest_Team','team2'); - $this->assertEquals('Team 2', DataObject::get("DataObjectTest_Team")->byID($id)->Title); - } - - /** - * Test DataList->removeByID() - */ - function testRemoveByID() { - $id = $this->idFromFixture('DataObjectTest_Team','team2'); - DataObject::get("DataObjectTest_Team")->removeByID($id); - - $this->assertNull(DataObject::get("DataObjectTest_Team")->byID($id)); - } - - /** - * Test DataList->canSortBy() - */ - function testCanSortBy() { - // Basic check - $this->assertTrue(DataObject::get("DataObjectTest_Team")->canSortBy("Title")); - $this->assertFalse(DataObject::get("DataObjectTest_Team")->canSortBy("SomethingElse")); - - // Subclasses - $this->assertTrue(DataObject::get("DataObjectTest_SubTeam")->canSortBy("Title")); - $this->assertTrue(DataObject::get("DataObjectTest_SubTeam")->canSortBy("SubclassDatabaseField")); - } - - function testDataListArrayAccess() { - $list = DataObject::get("DataObjectTest_Team")->sort("Title"); - - $this->assertEquals("Subteam 1", $list[0]->Title); - $this->assertEquals("Subteam 3", $list[2]->Title); - $this->assertEquals("Team 2", $list[4]->Title); - - } } From 7fbb919ce824ba3b80e0c294c90c9b42d048bdca Mon Sep 17 00:00:00 2001 From: Sam Minnee Date: Sun, 1 May 2011 17:33:02 +1200 Subject: [PATCH 38/64] API CHANGE: Introduce DataModel object, as a representation of the project's entire data model, and tie it to $this->model an all DataObjects, Controllers, and RequestHandlers for easy non-static access. API CHANGE: Add DataList::newObject(), which creates a new object on that DataList. API CHANGE: RequestHandler::handleRequest() now needs to handle a $model argument, if you override it. --- api/VersionedRestfulServer.php | 5 ++-- cli-script.php | 3 ++- control/Controller.php | 5 ++-- control/Director.php | 11 ++++---- control/RequestHandler.php | 20 ++++++++++++-- dev/DevelopmentAdmin.php | 4 +-- dev/SapphireTest.php | 7 ++++- dev/YamlFixture.php | 8 +++--- main.php | 4 ++- model/DataList.php | 24 +++++++++++++++-- model/DataModel.php | 49 ++++++++++++++++++++++++++++++++++ model/DataObject.php | 44 +++++++++++++++++++++++------- security/Security.php | 1 + 13 files changed, 153 insertions(+), 32 deletions(-) create mode 100644 model/DataModel.php diff --git a/api/VersionedRestfulServer.php b/api/VersionedRestfulServer.php index b40413f7c..a2fbf6584 100644 --- a/api/VersionedRestfulServer.php +++ b/api/VersionedRestfulServer.php @@ -11,10 +11,11 @@ class VersionedRestfulServer extends Controller { 'index' ); - function handleRequest($request) { + function handleRequest($request, $model) { + $this->setModel($model); Versioned::reading_stage('Live'); $restfulserver = new RestfulServer(); - $response = $restfulserver->handleRequest($request); + $response = $restfulserver->handleRequest($request, $model); return $response; } } diff --git a/cli-script.php b/cli-script.php index 74dd97b46..a65baaa90 100755 --- a/cli-script.php +++ b/cli-script.php @@ -80,6 +80,7 @@ if(!$url) { $_SERVER['REQUEST_URI'] = BASE_URL . '/' . $url; // Direct away - this is the "main" function, that hands control to the apporopriate controller -Director::direct($url); +DataModel::set_inst(new DataModel()); +Director::direct($url, DataModel::inst()); ?> diff --git a/control/Controller.php b/control/Controller.php index 4f959bd87..eff8c624f 100755 --- a/control/Controller.php +++ b/control/Controller.php @@ -115,13 +115,14 @@ class Controller extends RequestHandler { * @return SS_HTTPResponse The response that this controller produces, * including HTTP headers such as redirection info */ - function handleRequest(SS_HTTPRequest $request) { + function handleRequest(SS_HTTPRequest $request, DataModel $model) { if(!$request) user_error("Controller::handleRequest() not passed a request!", E_USER_ERROR); $this->pushCurrent(); $this->urlParams = $request->allParams(); $this->request = $request; $this->response = new SS_HTTPResponse(); + $this->setModel($model); $this->extend('onBeforeInit'); @@ -138,7 +139,7 @@ class Controller extends RequestHandler { return $this->response; } - $body = parent::handleRequest($request); + $body = parent::handleRequest($request, $model); if($body instanceof SS_HTTPResponse) { if(isset($_REQUEST['debug_request'])) Debug::message("Request handler returned SS_HTTPResponse object to $this->class controller; returning it without modification."); $this->response = $body; diff --git a/control/Director.php b/control/Director.php index 472152feb..6c8c8f593 100755 --- a/control/Director.php +++ b/control/Director.php @@ -74,7 +74,7 @@ class Director { * @uses handleRequest() rule-lookup logic is handled by this. * @uses Controller::run() Controller::run() handles the page logic for a Director::direct() call. */ - static function direct($url) { + static function direct($url, DataModel $model) { // Validate $_FILES array before merging it with $_POST foreach($_FILES as $k => $v) { if(is_array($v['tmp_name'])) { @@ -107,7 +107,7 @@ class Director { // Load the session into the controller $session = new Session(isset($_SESSION) ? $_SESSION : null); - $result = Director::handleRequest($req, $session); + $result = Director::handleRequest($req, $session, $model); $session->inst_save(); // Return code for a redirection request @@ -206,7 +206,8 @@ class Director { $req = new SS_HTTPRequest($httpMethod, $url, $getVars, $postVars, $body); if($headers) foreach($headers as $k => $v) $req->addHeader($k, $v); - $result = Director::handleRequest($req, $session); + // TODO: Pass in the DataModel + $result = Director::handleRequest($req, $session, DataModel::inst()); // Restore the superglobals $_REQUEST = $existingRequestVars; @@ -231,7 +232,7 @@ class Director { * * @return SS_HTTPResponse|string */ - protected static function handleRequest(SS_HTTPRequest $request, Session $session) { + protected static function handleRequest(SS_HTTPRequest $request, Session $session, DataModel $model) { krsort(Director::$rules); if(isset($_REQUEST['debug'])) Debug::show(Director::$rules); @@ -264,7 +265,7 @@ class Director { $controllerObj->setSession($session); try { - $result = $controllerObj->handleRequest($request); + $result = $controllerObj->handleRequest($request, $model); } catch(SS_HTTPResponse_Exception $responseException) { $result = $responseException->getResponse(); } diff --git a/control/RequestHandler.php b/control/RequestHandler.php index c8af381c2..2309a04d4 100755 --- a/control/RequestHandler.php +++ b/control/RequestHandler.php @@ -36,6 +36,11 @@ class RequestHandler extends ViewableData { */ protected $request = null; + /** + * The DataModel for this request + */ + protected $model = null; + /** * This variable records whether RequestHandler::__construct() * was called or not. Useful for checking if subclasses have @@ -86,9 +91,19 @@ class RequestHandler extends ViewableData { // Check necessary to avoid class conflicts before manifest is rebuilt if(class_exists('NullHTTPRequest')) $this->request = new NullHTTPRequest(); + // This will prevent bugs if setModel() isn't called. + $this->model = DataModel::inst(); + parent::__construct(); } + /** + * Set the DataModel for this request. + */ + public function setModel($model) { + $this->model = $model; + } + /** * Handles URL requests. * @@ -110,7 +125,7 @@ class RequestHandler extends ViewableData { * @uses SS_HTTPRequest->match() * @return SS_HTTPResponse|RequestHandler|string|array */ - function handleRequest(SS_HTTPRequest $request) { + function handleRequest(SS_HTTPRequest $request, DataModel $model) { // $handlerClass is used to step up the class hierarchy to implement url_handlers inheritance $handlerClass = ($this->class) ? $this->class : get_class($this); @@ -119,6 +134,7 @@ class RequestHandler extends ViewableData { } $this->request = $request; + $this->setModel($model); // We stop after RequestHandler; in other words, at ViewableData while($handlerClass && $handlerClass != 'ViewableData') { @@ -164,7 +180,7 @@ class RequestHandler extends ViewableData { // to prevent infinite loops. Also prevent further handling of controller actions which return themselves // to avoid infinite loops. if($this !== $result && !$request->isEmptyPattern($rule) && is_object($result) && $result instanceof RequestHandler) { - $returnValue = $result->handleRequest($request); + $returnValue = $result->handleRequest($request, $model); // Array results can be used to handle if(is_array($returnValue)) $returnValue = $this->customise($returnValue); diff --git a/dev/DevelopmentAdmin.php b/dev/DevelopmentAdmin.php index f40b8a5c5..e3edecb76 100644 --- a/dev/DevelopmentAdmin.php +++ b/dev/DevelopmentAdmin.php @@ -134,7 +134,7 @@ class DevelopmentAdmin extends Controller { function build($request) { if(Director::is_cli()) { $da = Object::create('DatabaseAdmin'); - return $da->handleRequest($request); + return $da->handleRequest($request, $this->model); } else { $renderer = Object::create('DebugView'); $renderer->writeHeader(); @@ -143,7 +143,7 @@ class DevelopmentAdmin extends Controller { echo "

    Database is building.... Check below for any errors

    Database has been built successfully

    "; $da = Object::create('DatabaseAdmin'); - return $da->handleRequest($request); + return $da->handleRequest($request, $this->model); echo ""; $renderer->writeFooter(); diff --git a/dev/SapphireTest.php b/dev/SapphireTest.php index 3cabf2590..c35a12161 100755 --- a/dev/SapphireTest.php +++ b/dev/SapphireTest.php @@ -112,6 +112,8 @@ class SapphireTest extends PHPUnit_Framework_TestCase { */ protected $fixtures; + protected $model; + function setUp() { // Mark test as being run $this->originalIsRunningTest = self::$is_running_test; @@ -146,6 +148,9 @@ class SapphireTest extends PHPUnit_Framework_TestCase { $className = get_class($this); $fixtureFile = eval("return {$className}::\$fixture_file;"); $prefix = defined('SS_DATABASE_PREFIX') ? SS_DATABASE_PREFIX : 'ss_'; + + // Todo: this could be a special test model + $this->model = DataModel::inst(); // Set up fixture if($fixtureFile || $this->usesDatabase || !self::using_temp_db()) { @@ -180,7 +185,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase { } $fixture = new YamlFixture($fixtureFilePath); - $fixture->saveIntoDatabase(); + $fixture->saveIntoDatabase($this->model); $this->fixtures[] = $fixture; // backwards compatibility: Load first fixture into $this->fixture diff --git a/dev/YamlFixture.php b/dev/YamlFixture.php index 0a969e56f..abd207dbf 100644 --- a/dev/YamlFixture.php +++ b/dev/YamlFixture.php @@ -154,7 +154,7 @@ class YamlFixture extends Object { * the record is written twice: first after populating all non-relational fields, * then again after populating all relations (has_one, has_many, many_many). */ - public function saveIntoDatabase() { + public function saveIntoDatabase(DataModel $model) { // We have to disable validation while we import the fixtures, as the order in // which they are imported doesnt guarantee valid relations until after the // import is complete. @@ -167,7 +167,7 @@ class YamlFixture extends Object { $this->fixtureDictionary = array(); foreach($fixtureContent as $dataClass => $items) { if(ClassInfo::exists($dataClass)) { - $this->writeDataObject($dataClass, $items); + $this->writeDataObject($model, $dataClass, $items); } else { $this->writeSQL($dataClass, $items); } @@ -182,9 +182,9 @@ class YamlFixture extends Object { * @param string $dataClass * @param array $items */ - protected function writeDataObject($dataClass, $items) { + protected function writeDataObject($model, $dataClass, $items) { foreach($items as $identifier => $fields) { - $obj = new $dataClass(); + $obj = $model->$dataClass->newObject(); // If an ID is explicitly passed, then we'll sort out the initial write straight away // This is just in case field setters triggered by the population code in the next block diff --git a/main.php b/main.php index ac6f9809f..a5bd3801b 100644 --- a/main.php +++ b/main.php @@ -123,8 +123,10 @@ if (isset($_GET['debug_profile'])) Profiler::unmark('DB::connect'); if (isset($_GET['debug_profile'])) Profiler::unmark('main.php init'); + // Direct away - this is the "main" function, that hands control to the appropriate controller -Director::direct($url); +DataModel::set_inst(new DataModel()); +Director::direct($url, DataModel::inst()); if (isset($_GET['debug_profile'])) { Profiler::unmark('all_execution'); diff --git a/model/DataList.php b/model/DataList.php index c94250add..3f06c29b6 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -15,6 +15,11 @@ class DataList extends DataObjectSet { */ protected $dataQuery; + /** + * The DataModel from which this DataList comes. + */ + protected $model; + /** * Synonym of the constructor. Can be chained with literate methods. * DataList::create("SiteTree")->sort("Title") is legal, but @@ -35,6 +40,10 @@ class DataList extends DataObjectSet { parent::__construct(); } + public function setModel(DataModel $model) { + $this->model = $model; + } + public function dataClass() { return $this->dataClass; } @@ -141,8 +150,10 @@ class DataList extends DataObjectSet { if(empty($row['RecordClassName'])) $row['RecordClassName'] = $row['ClassName']; // Instantiate the class mentioned in RecordClassName only if it exists, otherwise default to $this->dataClass - if(class_exists($row['RecordClassName'])) return new $row['RecordClassName']($row); - else return new $defaultClass($row); + if(class_exists($row['RecordClassName'])) $item = new $row['RecordClassName']($row, false, $this->model); + else $item = new $defaultClass($row, false, $this->model); + + return $item; } /** @@ -362,6 +373,15 @@ class DataList extends DataObjectSet { // Nothing needs to happen by default // TO DO: If a filter is given to this data list then } + + /** + * Return a new item to add to this DataList. + * @todo This doesn't factor in filters. + */ + function newObject($initialFields = null) { + $class = $this->dataClass; + return new $class($initialFields, false, $this->model); + } function remove($item) { // TO DO: Allow for amendment of this behaviour - for exmaple, we can remove an item from diff --git a/model/DataModel.php b/model/DataModel.php new file mode 100644 index 000000000..55bf74c15 --- /dev/null +++ b/model/DataModel.php @@ -0,0 +1,49 @@ +SiteTree->where('"ParentID" = 0 AND "ShowInMenus" = 1'); + */ +class DataModel { + protected static $inst; + + /** + * Get the global DataModel. + */ + static function inst() { + if(!self::$inst) self::$inst = new self; + return self::$inst; + } + + /** + * Set the global DataModel, used when data is requested from static methods. + */ + static function set_inst(DataModel $inst) { + self::$inst = $inst; + } + + //////////////////////////////////////////////////////////////////////// + + protected $customDataLists = array(); + + function __get($class) { + if(isset($this->customDataLists[$class])) { + return clone $this->customDataLists[$class]; + } else { + $list = DataList::create($class); + $list->setModel($this); + return $list; + } + } + + function __set($class, $item) { + $item = clone $item; + $item->setModel($this); + $this->customDataLists[$class] = $item; + } + +} \ No newline at end of file diff --git a/model/DataObject.php b/model/DataObject.php index 8da2175fd..e1b9aff55 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -96,6 +96,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity */ public $destroyed = false; + /** + * The DataModel from this this object comes + */ + protected $model; + /** * Data stored in this objects database record. An array indexed by fieldname. * @@ -289,7 +294,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @param boolean $isSingleton This this to true if this is a singleton() object, a stub for calling methods. Singletons * don't have their defaults set. */ - function __construct($record = null, $isSingleton = false) { + function __construct($record = null, $isSingleton = false, $model = null) { // Set the fields data. if(!$record) { $record = array( @@ -374,6 +379,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // prevent populateDefaults() and setField() from marking overwritten defaults as changed $this->changed = array(); + + $this->model = $model ? $model : DataModel::inst(); + } + + /** + * Set the DataModel + */ + function setModel(DataModel $model) { + $this->model = $model; } /** @@ -399,7 +413,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity */ function duplicate($doWrite = true) { $className = $this->class; - $clone = new $className( $this->record ); + $clone = new $className( $this->record, false, $this->model ); $clone->ID = 0; if($doWrite) { @@ -497,7 +511,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity 'ClassName' => $originalClass, 'RecordClassName' => $originalClass, ) - )); + ), false, $this->model); if($newClassName != $originalClass) { $newInstance->setClassName($newClassName); @@ -1199,7 +1213,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // - move the details of the delete code in the DataQuery system // - update the code to just delete the base table, and rely on cascading deletes in the DB to do the rest // obviously, that means getting requireTable() to configure cascading deletes ;-) - $srcQuery = DataList::create($this->class)->where("ID = $this->ID")->dataQuery()->query(); + $srcQuery = DataList::create($this->class, $this->model)->where("ID = $this->ID")->dataQuery()->query(); foreach($srcQuery->queriedTables() as $table) { $query = new SQLQuery("*", array('"'.$table.'"')); $query->where("\"ID\" = $this->ID"); @@ -1274,11 +1288,11 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $joinID = $this->getField($joinField); if($joinID) { - $component = DataObject::get_by_id($class, $joinID); + $component = $this->model->$class->byID($joinID); } if(!isset($component) || !$component) { - $component = new $class(); + $component = $this->model->$class->newObject(); } } elseif($class = $this->belongs_to($componentName)) { $joinField = $this->getRemoteJoinField($componentName, 'belongs_to'); @@ -1289,7 +1303,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } if(!isset($component) || !$component) { - $component = new $class(); + $component = $this->model->$class->newObject(); $component->$joinField = $this->ID; } } else { @@ -1327,6 +1341,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $joinField = $this->getRemoteJoinField($componentName, 'has_many'); $result = new HasManyList($componentClass, $joinField); + if($this->model) $result->setModel($this->model); if($this->ID) $result->setForeignID($this->ID); $result = $result->where($filter)->limit($limit)->sort($sort)->join($join); @@ -1414,6 +1429,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity $result = new ManyManyList($componentClass, $table, $componentField, $parentField, $this->many_many_extraFields($componentName)); + if($this->model) $result->setModel($this->model); // If this is called on a singleton, then we return an 'orphaned relation' that can have the // foreignID set elsewhere. @@ -2488,6 +2504,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity // Todo: Make the $containerClass method redundant if($containerClass != "DataList") user_error("The DataObject::get() \$containerClass argument has been deprecated", E_USER_NOTICE); $result = DataList::create($callerClass)->where($filter)->sort($sort)->join($join)->limit($limit); + $result->setModel(DataModel::inst()); return $result; } @@ -2495,9 +2512,15 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @deprecated */ public function Aggregate($class = null) { - if($class) return new DataList($class); - else if(isset($this)) return new DataList(get_class($this)); + if($class) { + $list = new DataList($class); + $list->setModel(DataModel::inst()); + } else if(isset($this)) { + $list = new DataList(get_class($this)); + $list->setModel($this->model); + } else throw new InvalidArgumentException("DataObject::aggregate() must be called as an instance method or passed a classname"); + return $list; } /** @@ -2596,6 +2619,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity } if(!$cache || !isset(DataObject::$cache_get_one[$callerClass][$cacheKey])) { $dl = DataList::create($callerClass)->where($filter)->sort($orderby); + $dl->setModel(DataModel::inst()); $item = $dl->First(); if($cache) { @@ -2794,7 +2818,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity if(!$hasData) { $className = $this->class; foreach($defaultRecords as $record) { - $obj = new $className($record); + $obj = $this->model->$className->newObject($record); $obj->write(); } DB::alteration_message("Added default records to $className table","created"); diff --git a/security/Security.php b/security/Security.php index b0019d8ef..afc8662be 100644 --- a/security/Security.php +++ b/security/Security.php @@ -349,6 +349,7 @@ class Security extends Controller { $tmpPage->ID = -1 * rand(1,10000000); $controller = new Page_Controller($tmpPage); + $controller->setModel($this->model); $controller->init(); //Controller::$currentController = $controller; } else { From a940fb2888a44ffea4a15321d6d90cd25cf2d42e Mon Sep 17 00:00:00 2001 From: ajshort Date: Tue, 19 Apr 2011 17:03:23 +1000 Subject: [PATCH 39/64] FEATURE: Added the SS_List interface which classes can implement to be recognised as a list. --- model/List.php | 106 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 model/List.php diff --git a/model/List.php b/model/List.php new file mode 100644 index 000000000..96c7e2048 --- /dev/null +++ b/model/List.php @@ -0,0 +1,106 @@ + Date: Mon, 2 May 2011 17:14:05 +1000 Subject: [PATCH 40/64] MINOR: Updated places that expect a DataObjectSet to accept an SS_List instance. --- admin/code/CMSBatchAction.php | 4 ++-- admin/code/SecurityAdmin.php | 2 +- api/DataFormatter.php | 2 +- api/JSONDataFormatter.php | 2 +- api/RSSFeed.php | 2 +- api/XMLDataFormatter.php | 2 +- core/PaginatedList.php | 4 ++-- forms/TableListField.php | 2 +- security/PermissionCheckboxSetField.php | 4 ++-- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/admin/code/CMSBatchAction.php b/admin/code/CMSBatchAction.php index fcd4d6437..436002108 100644 --- a/admin/code/CMSBatchAction.php +++ b/admin/code/CMSBatchAction.php @@ -26,7 +26,7 @@ abstract class CMSBatchAction extends Object { * Run this action for the given set of pages. * Return a set of status-updated JavaScript to return to the CMS. */ - abstract function run(DataObjectSet $objs); + abstract function run(SS_List $objs); /** * Helper method for processing batch actions. @@ -46,7 +46,7 @@ abstract class CMSBatchAction extends Object { * } * } */ - public function batchaction(DataObjectSet $objs, $helperMethod, $successMessage, $arguments = array()) { + public function batchaction(SS_List $objs, $helperMethod, $successMessage, $arguments = array()) { $status = array('modified' => array(), 'error' => array()); foreach($objs as $obj) { diff --git a/admin/code/SecurityAdmin.php b/admin/code/SecurityAdmin.php index f13e3b556..0e68649ab 100644 --- a/admin/code/SecurityAdmin.php +++ b/admin/code/SecurityAdmin.php @@ -358,7 +358,7 @@ class SecurityAdmin_DeleteBatchAction extends CMSBatchAction { return _t('AssetAdmin_DeleteBatchAction.TITLE', 'Delete groups'); } - function run(DataObjectSet $records) { + function run(SS_List $records) { $status = array( 'modified'=>array(), 'deleted'=>array() diff --git a/api/DataFormatter.php b/api/DataFormatter.php index 9e87196da..a988fb4a9 100644 --- a/api/DataFormatter.php +++ b/api/DataFormatter.php @@ -288,7 +288,7 @@ abstract class DataFormatter extends Object { /** * Convert a data object set to this format. Return a string. */ - abstract function convertDataObjectSet(DataObjectSet $set); + abstract function convertDataObjectSet(SS_List $set); /** * @param string $strData HTTP Payload as string diff --git a/api/JSONDataFormatter.php b/api/JSONDataFormatter.php index eeebc1bc3..5557a74f2 100644 --- a/api/JSONDataFormatter.php +++ b/api/JSONDataFormatter.php @@ -121,7 +121,7 @@ class JSONDataFormatter extends DataFormatter { * @param DataObjectSet $set * @return String XML */ - public function convertDataObjectSet(DataObjectSet $set, $fields = null) { + public function convertDataObjectSet(SS_List $set, $fields = null) { $items = array(); foreach ($set as $do) $items[] = $this->convertDataObjectToJSONObject($do, $fields); diff --git a/api/RSSFeed.php b/api/RSSFeed.php index d146c6ae9..0cb34c236 100755 --- a/api/RSSFeed.php +++ b/api/RSSFeed.php @@ -99,7 +99,7 @@ class RSSFeed extends ViewableData { * @param string $etag The ETag is an unique identifier that is changed * every time the representation does */ - function __construct(DataObjectSet $entries, $link, $title, + function __construct(SS_List $entries, $link, $title, $description = null, $titleField = "Title", $descriptionField = "Content", $authorField = null, $lastModified = null, $etag = null) { diff --git a/api/XMLDataFormatter.php b/api/XMLDataFormatter.php index 63d882211..dd49e930a 100644 --- a/api/XMLDataFormatter.php +++ b/api/XMLDataFormatter.php @@ -132,7 +132,7 @@ class XMLDataFormatter extends DataFormatter { * @param DataObjectSet $set * @return String XML */ - public function convertDataObjectSet(DataObjectSet $set, $fields = null) { + public function convertDataObjectSet(SS_List $set, $fields = null) { Controller::curr()->getResponse()->addHeader("Content-Type", "text/xml"); $className = $set->class; diff --git a/core/PaginatedList.php b/core/PaginatedList.php index 2e123b5c7..95112d90c 100644 --- a/core/PaginatedList.php +++ b/core/PaginatedList.php @@ -17,12 +17,12 @@ class PaginatedList extends SS_ListDecorator { /** * Constructs a new paginated list instance around a list. * - * @param DataObjectSet $list The list to paginate. The getRange method will + * @param SS_List $list The list to paginate. The getRange method will * be used to get the subset of objects to show. * @param array|ArrayAccess Either a map of request parameters or * request object that the pagination offset is read from. */ - public function __construct(DataObjectSet $list, $request = array()) { + public function __construct(SS_List $list, $request = array()) { if (!is_array($request) && !$request instanceof ArrayAccess) { throw new Exception('The request must be readable as an array.'); } diff --git a/forms/TableListField.php b/forms/TableListField.php index 27ab91d62..0e93cd4a5 100644 --- a/forms/TableListField.php +++ b/forms/TableListField.php @@ -392,7 +392,7 @@ JS $this->customCsvQuery = $query; } - function setCustomSourceItems(DataObjectSet $items) { + function setCustomSourceItems(SS_List $items) { user_error('TableList::setCustomSourceItems() deprecated, just pass the items into the constructor', E_USER_WARNING); // The type-hinting above doesn't seem to work consistently diff --git a/security/PermissionCheckboxSetField.php b/security/PermissionCheckboxSetField.php index cac263e03..14467d1d8 100644 --- a/security/PermissionCheckboxSetField.php +++ b/security/PermissionCheckboxSetField.php @@ -42,9 +42,9 @@ class PermissionCheckboxSetField extends FormField { $this->filterField = $filterField; $this->managedClass = $managedClass; - if(is_a($records, 'DataObjectSet')) { + if($records instanceof SS_List) { $this->records = $records; - } elseif(is_a($records, 'DataObject')) { + } elseif($records instanceof Group) { $this->records = new DataObjectSet($records); } elseif($records) { throw new InvalidArgumentException('$record should be either a Group record, or a DataObjectSet of Group records'); From df87fd5a1190f3c69e2b79c406905495fbb226f4 Mon Sep 17 00:00:00 2001 From: ajshort Date: Mon, 2 May 2011 18:12:39 +1000 Subject: [PATCH 41/64] API CHANGE: Updated SS_ListDecorator to implement the list inteface and extend ViewableData, rather than DataObjectSet. --- model/ListDecorator.php | 101 +++++----------------------------------- 1 file changed, 12 insertions(+), 89 deletions(-) diff --git a/model/ListDecorator.php b/model/ListDecorator.php index 7ca89de91..ccea22054 100644 --- a/model/ListDecorator.php +++ b/model/ListDecorator.php @@ -1,25 +1,27 @@ list = $list; + public function __construct(SS_List $list) { + $this->list = $list; + $this->failover = $this->list; + parent::__construct(); } /** * Returns the list this decorator wraps around. * - * @return DataObjectSet + * @return SS_List */ public function getList() { return $this->list; @@ -43,14 +45,6 @@ abstract class SS_ListDecorator extends DataObjectSet { $this->list->offsetUnset($key); } - public function destroy() { - $this->list->destroy(); - } - - public function emptyItems() { - $this->list->emptyItems(); - } - public function toArray($index = null) { return $this->list->toArray($index); } @@ -59,38 +53,14 @@ abstract class SS_ListDecorator extends DataObjectSet { return $this->list->toNestedArray($index); } - public function push($item, $key = null) { - $this->list->push($item, $key); - } - - public function insertFirst($item, $key = null) { - $this->list->insertFirst($item, $key); - } - - public function unshift($item) { - $this->list->unshift($item); - } - - public function shift() { - return $this->list->shift(); - } - - public function pop() { - return $this->list->pop(); + public function add($item) { + $this->list->add($item); } public function remove($itemObject) { $this->list->remove($itemObject); } - public function replace($itemOld, $itemNew) { - $this->list->replace($itemOld, $itemNew); - } - - public function merge($anotherSet){ - $this->list->merge($anotherSet); - } - public function getRange($offset, $length) { return $this->list->getRange($offset, $length); } @@ -119,10 +89,6 @@ abstract class SS_ListDecorator extends DataObjectSet { return $this->list->Count(); } - public function UL() { - return $this->list->UL(); - } - public function forTemplate() { return $this->list->forTemplate(); } @@ -139,59 +105,16 @@ abstract class SS_ListDecorator extends DataObjectSet { return $this->list->column($value); } - public function groupBy($index){ - return $this->list->groupBy($index); - } - - public function GroupedBy($index, $childControl = "Children") { - return $this->list->GroupedBy($index, $childControl); - } - - public function buildNestedUL($nestingLevels, $ulExtraAttributes = '') { - return $this->list->buildNestedUL($nestingLevels, $ulExtraAttributes); - } - - public function getChildrenAsUL($nestingLevels, $level = 0, $template = "
  • \$Title", $ulExtraAttributes = null, &$itemCount = 0) { - return $this->list->getChildrenAsUL( - $nestingLevels, - $level, - $template, - $ulExtraAttributes, - $itemCount); + public function canSortBy($by) { + return $this->list->canSortBy($by); } public function sort($fieldname, $direction = "ASC") { $this->list->sort($fieldname, $direction); } - public function removeDuplicates($field = 'ID') { - $this->list->removeDuplicates($field); - } - public function debug() { return $this->list->debug(); } - public function groupWithParents($groupField, $groupClassName, $sortParents = null, $parentField = 'ID', $collapse = false, $requiredParents = null) { - return $this->list->groupWithParents( - $groupField, - $groupClassName, - $sortParents, - $parentField, - $collapse, - $requiredParents); - } - - public function addWithoutWrite($field) { - $this->list->addWithoutWrite($field); - } - - public function containsIDs($idList) { - return $this->list->condaintsIDs($idList); - } - - public function onlyContainsIDs($idList) { - return $this->list->onlyContainsIDs($idList); - } - } \ No newline at end of file From 50506659d176422e0701044fdd1d322ffadf7b87 Mon Sep 17 00:00:00 2001 From: ajshort Date: Mon, 2 May 2011 18:14:55 +1000 Subject: [PATCH 42/64] API ChANGE: Updated DataList to not extend from DataObjectSet, but instead implement the list iterator. MINOR: Return an ArrayIterator from DataList rather than a DataObjectSet_Iterator. --- model/DataList.php | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/model/DataList.php b/model/DataList.php index 3f06c29b6..73e1d7127 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -1,10 +1,9 @@ dataQuery->query(); $rows = $query->execute(); $results = array(); @@ -139,7 +140,27 @@ class DataList extends DataObjectSet { } return $results; } - + + public function toNestedArray() { + $result = array(); + + foreach ($this as $item) { + $result[] = $item->toMap(); + } + + return $result; + } + + public function map($keyfield, $titlefield) { + $map = array(); + + foreach ($this as $item) { + $map[$item->$keyfield] = $item->$titlefield; + } + + return $map; + } + /** * Create a data object from the given SQL row */ @@ -162,23 +183,13 @@ class DataList extends DataObjectSet { * @return DataObjectSet_Iterator */ public function getIterator() { - return new DataObjectSet_Iterator($this->generateItems()); - } - - /** - * Convert this DataList to a DataObjectSet. - * Useful if you want to push additional records onto the list. - */ - public function toDataObjectSet() { - $array = array(); - foreach($this as $item) $array[] = $item; - return new DataObjectSet($array); + return new ArrayIterator($this->toArray()); } /** * Return the number of items in this DataList */ - function Count() { + function count() { return $this->dataQuery->count(); } From b872ad7bb832ad9b0066a319be22cb8b1f6630b0 Mon Sep 17 00:00:00 2001 From: ajshort Date: Tue, 3 May 2011 12:15:49 +1000 Subject: [PATCH 43/64] MINOR: Added default parameters to DataList->map(). --- model/DataList.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/DataList.php b/model/DataList.php index 73e1d7127..8b4df8834 100644 --- a/model/DataList.php +++ b/model/DataList.php @@ -151,7 +151,7 @@ class DataList extends ViewableData implements SS_List { return $result; } - public function map($keyfield, $titlefield) { + public function map($keyfield = 'ID', $titlefield = 'Title') { $map = array(); foreach ($this as $item) { From 26faead3e5f9090630ee3beb80b1f0967bd6dbdd Mon Sep 17 00:00:00 2001 From: ajshort Date: Tue, 3 May 2011 12:16:15 +1000 Subject: [PATCH 44/64] MINOR: Made DataObjectSet implement the SS_List interface. --- model/DataObjectSet.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/model/DataObjectSet.php b/model/DataObjectSet.php index eb70f796c..decc1fcb2 100644 --- a/model/DataObjectSet.php +++ b/model/DataObjectSet.php @@ -5,7 +5,7 @@ * @package sapphire * @subpackage model */ -class DataObjectSet extends ViewableData implements IteratorAggregate, Countable, ArrayAccess { +class DataObjectSet extends ViewableData implements SS_List { /** * The DataObjects in this set. * @var array @@ -165,6 +165,10 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable return $this->map($index, $titleField, $emptyString, $sort); } + public function add($item) { + $this->push($item); + } + /** * Add an item to the DataObject Set. * @param DataObject $item Item to add. @@ -529,6 +533,10 @@ class DataObjectSet extends ViewableData implements IteratorAggregate, Countable return $output; } + public function canSortBy($by) { + return true; + } + /** * Sorts the current DataObjectSet instance. * @param string $fieldname The name of the field on the DataObject that you wish to sort the set by. From 04e30243d0f4b5ce07b43df5f32d14633ddf0174 Mon Sep 17 00:00:00 2001 From: ajshort Date: Tue, 3 May 2011 12:16:40 +1000 Subject: [PATCH 45/64] MINOR: Updated MemberTest to work with a standard array iterator. --- tests/security/MemberTest.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/security/MemberTest.php b/tests/security/MemberTest.php index 36e07d18c..dd8ba8e6a 100644 --- a/tests/security/MemberTest.php +++ b/tests/security/MemberTest.php @@ -140,18 +140,18 @@ class MemberTest extends FunctionalTest { $passwords = DataObject::get("MemberPassword", "\"MemberID\" = $member->ID", "\"Created\" DESC, \"ID\" DESC")->getIterator(); $this->assertNotNull($passwords); - $record = $passwords->rewind(); - $this->assertTrue($record->checkPassword('test3'), "Password test3 not found in MemberRecord"); + $passwords->rewind(); + $this->assertTrue($passwords->current()->checkPassword('test3'), "Password test3 not found in MemberRecord"); - $record = $passwords->next(); - $this->assertTrue($record->checkPassword('test2'), "Password test2 not found in MemberRecord"); + $passwords->next(); + $this->assertTrue($passwords->current()->checkPassword('test2'), "Password test2 not found in MemberRecord"); - $record = $passwords->next(); - $this->assertTrue($record->checkPassword('test1'), "Password test1 not found in MemberRecord"); + $passwords->next(); + $this->assertTrue($passwords->current()->checkPassword('test1'), "Password test1 not found in MemberRecord"); - $record = $passwords->next(); - $this->assertType('DataObject', $record); - $this->assertTrue($record->checkPassword('1nitialPassword'), "Password 1nitialPassword not found in MemberRecord"); + $passwords->next(); + $this->assertType('DataObject', $passwords->current()); + $this->assertTrue($passwords->current()->checkPassword('1nitialPassword'), "Password 1nitialPassword not found in MemberRecord"); } /** From 14c57ae082fbca1ddbe2b5aebf1c895dda2af293 Mon Sep 17 00:00:00 2001 From: ajshort Date: Tue, 3 May 2011 12:22:18 +1000 Subject: [PATCH 46/64] MINOR: Updated TableListField to accept any list type, not just a DataObjectSet. --- forms/TableListField.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/forms/TableListField.php b/forms/TableListField.php index 0e93cd4a5..0dc26ab2d 100644 --- a/forms/TableListField.php +++ b/forms/TableListField.php @@ -235,8 +235,8 @@ class TableListField extends FormField { $sourceSort = null, $sourceJoin = null) { if($sourceClass) { - // You can optionally pass a DataList/DataObjectSet - if($sourceClass instanceof DataObjectSet) { + // You can optionally pass a list + if($sourceClass instanceof SS_List) { $this->dataList = $sourceClass; } else { From c025ce7a4a2aaf2f3384390cd8766a78310bce85 Mon Sep 17 00:00:00 2001 From: ajshort Date: Tue, 3 May 2011 13:05:27 +1000 Subject: [PATCH 47/64] MINOR: Replaced usage of deprecated toDropdownMap() with map(). --- tests/forms/TableFieldTest.php | 6 +++--- tests/forms/TableListFieldTest.php | 6 +++--- tests/model/DataObjectSetTest.php | 13 ++----------- tests/security/GroupTest.php | 2 +- 4 files changed, 9 insertions(+), 18 deletions(-) diff --git a/tests/forms/TableFieldTest.php b/tests/forms/TableFieldTest.php index d5922ac16..e18391f5d 100755 --- a/tests/forms/TableFieldTest.php +++ b/tests/forms/TableFieldTest.php @@ -51,7 +51,7 @@ class TableFieldTest extends SapphireTest { $tableField->saveInto($group); // Let's check that the 2 permissions entries have been saved - $permissions = $group->Permissions()->toDropdownMap('Arg', 'Code'); + $permissions = $group->Permissions()->map('Arg', 'Code'); $this->assertEquals(array( 1 => 'CustomPerm1', 2 => 'CustomPerm2', @@ -75,7 +75,7 @@ class TableFieldTest extends SapphireTest { $tableField->saveInto($group); // Let's check that the 2 existing permissions entries, and the 1 new one, have been saved - $permissions = $group->Permissions()->toDropdownMap('Arg', 'Code'); + $permissions = $group->Permissions()->map('Arg', 'Code'); $this->assertEquals(array( 1 => 'CustomPerm1', 2 => 'CustomPerm2', @@ -126,7 +126,7 @@ class TableFieldTest extends SapphireTest { $tableField->saveInto($group); // Let's check that the 2 permissions entries have been saved - $permissions = $group->Permissions()->toDropdownMap('Arg', 'Code'); + $permissions = $group->Permissions()->map('Arg', 'Code'); $this->assertEquals(array( 101 => 'Perm1 Modified', 102 => 'Perm2 Modified', diff --git a/tests/forms/TableListFieldTest.php b/tests/forms/TableListFieldTest.php index d1ed184c8..4b3d8964c 100755 --- a/tests/forms/TableListFieldTest.php +++ b/tests/forms/TableListFieldTest.php @@ -52,7 +52,7 @@ class TableListFieldTest extends SapphireTest { $items = $table->sourceItems(); $this->assertNotNull($items); - $itemMap = $items->toDropdownMap("ID", "A") ; + $itemMap = $items->map("ID", "A") ; $this->assertEquals(array( $item1->ID => "a1", $item2->ID => "a2", @@ -88,7 +88,7 @@ class TableListFieldTest extends SapphireTest { $items = $table->sourceItems(); $this->assertNotNull($items); - $itemMap = $items->toDropdownMap("ID", "A") ; + $itemMap = $items->map("ID", "A") ; $this->assertEquals(array( $item1->ID => "a1", $item2->ID => "a2" @@ -122,7 +122,7 @@ class TableListFieldTest extends SapphireTest { $items = $table->sourceItems(); $this->assertNotNull($items); - $itemMap = $items->toDropdownMap("ID", "A") ; + $itemMap = $items->map("ID", "A") ; $this->assertEquals(array($item3->ID => "a3", $item4->ID => "a4"), $itemMap); } diff --git a/tests/model/DataObjectSetTest.php b/tests/model/DataObjectSetTest.php index c5d73c325..331a11756 100644 --- a/tests/model/DataObjectSetTest.php +++ b/tests/model/DataObjectSetTest.php @@ -177,25 +177,16 @@ class DataObjectSetTest extends SapphireTest { $comments = DataObject::get('DataObjectSetTest_TeamComment', '', "\"ID\" ASC"); /* Now we get a map of all the PageComment records */ - $map = $comments->map('ID', 'Title', '(Select one)'); + $map = $comments->map('ID', 'Title'); $expectedMap = array( - '' => '(Select one)', 1 => 'Joe', 2 => 'Bob', 3 => 'Phil' ); /* There are 9 items in the map. 3 are records. 1 is the empty value */ - $this->assertEquals(count($map), 4, 'There are 4 items in the map. 3 are records. 1 is the empty value'); - - /* We have the same map as our expected map, asserted above */ - - /* toDropDownMap() is an alias of map() - let's make a map from that */ - $map2 = $comments->toDropDownMap('ID', 'Title', '(Select one)'); - - /* There are 4 items in the map. 3 are records. 1 is the empty value */ - $this->assertEquals(count($map), 4, 'There are 4 items in the map. 3 are records. 1 is the empty value.'); + $this->assertEquals(count($map), 3, 'There are 3 items in the map.'); } function testRemoveDuplicates() { diff --git a/tests/security/GroupTest.php b/tests/security/GroupTest.php index 4f378426a..37d1f395e 100644 --- a/tests/security/GroupTest.php +++ b/tests/security/GroupTest.php @@ -128,7 +128,7 @@ class GroupTest_Member extends Member implements TestOnly { function getCMSFields() { $groups = DataObject::get('Group'); - $groupsMap = ($groups) ? $groups->toDropDownMap() : false; + $groupsMap = ($groups) ? $groups->map() : false; $fields = new FieldSet( new HiddenField('ID', 'ID'), new CheckboxSetField( From 43c39bed8f5786feff4f0a9a1dbb9bc686cc7352 Mon Sep 17 00:00:00 2001 From: ajshort Date: Tue, 3 May 2011 13:11:09 +1000 Subject: [PATCH 48/64] MINOR: Updated RestfulServer to properly convert list instances. --- api/RestfulServer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/RestfulServer.php b/api/RestfulServer.php index d297b1cfd..46186e224 100644 --- a/api/RestfulServer.php +++ b/api/RestfulServer.php @@ -241,7 +241,7 @@ class RestfulServer extends Controller { $rawFields = $this->request->getVar('fields'); $fields = $rawFields ? explode(',', $rawFields) : null; - if($obj instanceof DataObjectSet) { + if($obj instanceof SS_List) { $responseFormatter->setTotalSize($obj->dataQuery()->query()->unlimitedRowCount()); return $responseFormatter->convertDataObjectSet($obj, $fields); } else if(!$obj) { From 577be1e6fc0c18849aff791c766a5cdf61894752 Mon Sep 17 00:00:00 2001 From: ajshort Date: Tue, 3 May 2011 21:59:51 +1000 Subject: [PATCH 49/64] MINOR: Explicitly sort objects in DataListTest to prevent test failures across different DB's. --- tests/model/DataListTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/model/DataListTest.php b/tests/model/DataListTest.php index 1240453df..9e94e7dbb 100644 --- a/tests/model/DataListTest.php +++ b/tests/model/DataListTest.php @@ -18,7 +18,7 @@ class DataListTest extends SapphireTest { function testListCreationSortAndLimit() { // By default, a DataList will contain all items of that class - $list = DataList::create('DataObjectTest_TeamComment'); + $list = DataList::create('DataObjectTest_TeamComment')->sort('ID'); // We can iterate on the DataList $names = array(); From 7e7677bbba83465a2e04710b16739629363309a9 Mon Sep 17 00:00:00 2001 From: ajshort Date: Wed, 4 May 2011 00:06:42 +1000 Subject: [PATCH 50/64] FEATURE: Added ArrayList to allow an array of arrays or objects to be handled as a list. --- model/ArrayList.php | 141 ++++++++++++++++++++++++++++++++++ tests/model/ArrayListTest.php | 138 +++++++++++++++++++++++++++++++++ 2 files changed, 279 insertions(+) create mode 100644 model/ArrayList.php create mode 100644 tests/model/ArrayListTest.php diff --git a/model/ArrayList.php b/model/ArrayList.php new file mode 100644 index 000000000..e05e3dff3 --- /dev/null +++ b/model/ArrayList.php @@ -0,0 +1,141 @@ +array = $array; + parent::__construct(); + } + + public function count() { + return count($this->array); + } + + public function getIterator() { + return new ArrayIterator($this->array); + } + + public function toArray() { + return $this->array; + } + + public function toNestedArray() { + $result = array(); + + foreach ($this->array as $item) { + if (is_object($item)) { + if (method_exists($item, 'toMap')) { + $result[] = $item->toMap(); + } else { + $result[] = (array) $item; + } + } else { + $result[] = $item; + } + } + + return $result; + } + + public function getRange($offset, $length) { + return array_slice($this->array, $offset, $length); + } + + public function add($item) { + $this->array[] = $item; + } + + public function remove($item) { + foreach ($this->array as $key => $value) { + if ($item === $value) unset($this->array[$key]); + } + } + + public function first() { + return reset($this->array); + } + + public function last() { + return end($this->array); + } + + public function map($keyfield, $titlefield) { + $map = array(); + foreach ($this->array as $item) { + $map[$this->extract($item, $keyfield)] = $this->extract($item, $titlefield); + } + return $map; + } + + public function find($key, $value) { + foreach ($this->array as $item) { + if ($this->extract($item, $key) == $value) return $item; + } + } + + public function column($field) { + $result = array(); + foreach ($this->array as $item) { + $result[] = $this->extract($item, $field); + } + return $result; + } + + public function canSortBy($by) { + return true; + } + + public function sort($by, $dir = 'ASC') { + $sorts = array(); + $dir = strtoupper($dir) == 'DESC' ? SORT_DESC : SORT_ASC; + + foreach ($this->array as $item) { + $sorts[] = $this->extract($item, $by); + } + + array_multisort($sorts, $dir, $this->array); + } + + public function offsetExists($offset) { + return array_key_exists($offset, $this->array); + } + + public function offsetGet($offset) { + if ($this->offsetExists($offset)) return $this->array[$offset]; + } + + public function offsetSet($offset, $value) { + $this->array[$offset] = $value; + } + + public function offsetUnset($offset) { + unset($this->array[$offset]); + } + + /** + * Extracts a value from an item in the list, where the item is either an + * object or array. + * + * @param array|object $item + * @param string $key + * @return mixed + */ + protected function extract($item, $key) { + if (is_object($item)) { + return $item->$key; + } else { + if (array_key_exists($key, $item)) return $item[$key]; + } + } + +} \ No newline at end of file diff --git a/tests/model/ArrayListTest.php b/tests/model/ArrayListTest.php new file mode 100644 index 000000000..c8ba216f2 --- /dev/null +++ b/tests/model/ArrayListTest.php @@ -0,0 +1,138 @@ +assertEquals(0, $list->count()); + $list = new ArrayList(array(1, 2, 3)); + $this->assertEquals(3, $list->count()); + } + + public function testToNestedArray() { + $list = new ArrayList(array( + array('First' => 'FirstFirst', 'Second' => 'FirstSecond'), + (object) array('First' => 'SecondFirst', 'Second' => 'SecondSecond'), + new ArrayListTest_Object('ThirdFirst', 'ThirdSecond') + )); + + $this->assertEquals($list->toNestedArray(), array( + array('First' => 'FirstFirst', 'Second' => 'FirstSecond'), + array('First' => 'SecondFirst', 'Second' => 'SecondSecond'), + array('First' => 'ThirdFirst', 'Second' => 'ThirdSecond') + )); + } + + public function testGetRange() { + $list = new ArrayList(array( + array('Key' => 1), array('Key' => 2), array('Key' => 3) + )); + $this->assertEquals($list->getRange(1, 2), array( + array('Key' => 2), array('Key' => 3) + )); + } + + public function testAddRemove() { + $list = new ArrayList(array( + array('Key' => 1), array('Key' => 2) + )); + + $list->add(array('Key' => 3)); + $this->assertEquals($list->toArray(), array( + array('Key' => 1), array('Key' => 2), array('Key' => 3) + )); + + $list->remove(array('Key' => 2)); + $this->assertEquals(array_values($list->toArray()), array( + array('Key' => 1), array('Key' => 3) + )); + } + + public function testFirstLast() { + $list = new ArrayList(array( + array('Key' => 1), array('Key' => 2), array('Key' => 3) + )); + $this->assertEquals($list->first(), array('Key' => 1)); + $this->assertEquals($list->last(), array('Key' => 3)); + } + + public function testMap() { + $list = new ArrayList(array( + array('ID' => 1, 'Name' => 'Steve',), + (object) array('ID' => 3, 'Name' => 'Bob'), + array('ID' => 5, 'Name' => 'John') + )); + $this->assertEquals($list->map('ID', 'Name'), array( + 1 => 'Steve', + 3 => 'Bob', + 5 => 'John' + )); + } + + public function testFind() { + $list = new ArrayList(array( + array('Name' => 'Steve'), + (object) array('Name' => 'Bob'), + array('Name' => 'John') + )); + $this->assertEquals($list->find('Name', 'Bob'), (object) array( + 'Name' => 'Bob' + )); + } + + public function testColumn() { + $list = new ArrayList(array( + array('Name' => 'Steve'), + (object) array('Name' => 'Bob'), + array('Name' => 'John') + )); + $this->assertEquals($list->column('Name'), array( + 'Steve', 'Bob', 'John' + )); + } + + public function testSort() { + $list = new ArrayList(array( + array('Name' => 'Steve'), + (object) array('Name' => 'Bob'), + array('Name' => 'John') + )); + + $list->sort('Name'); + $this->assertEquals($list->toArray(), array( + (object) array('Name' => 'Bob'), + array('Name' => 'John'), + array('Name' => 'Steve') + )); + + $list->sort('Name', 'DESC'); + $this->assertEquals($list->toArray(), array( + array('Name' => 'Steve'), + array('Name' => 'John'), + (object) array('Name' => 'Bob') + )); + } + +} + +/** + * @ignore + */ +class ArrayListTest_Object { + + public $First; + public $Second; + + public function __construct($first, $second) { + $this->First = $first; + $this->Second = $second; + } + + public function toMap() { + return array('First' => $this->First, 'Second' => $this->Second); + } + +} \ No newline at end of file From bdfff5bee3c470c720dc27de6633fad50cbca963 Mon Sep 17 00:00:00 2001 From: ajshort Date: Wed, 4 May 2011 11:40:00 +1000 Subject: [PATCH 51/64] MINOR: Updated TableField to use ArrayList rather than DataObjectSet when using a DataList. --- forms/TableField.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/forms/TableField.php b/forms/TableField.php index 5c7aedf5f..2e33eed1b 100644 --- a/forms/TableField.php +++ b/forms/TableField.php @@ -164,9 +164,10 @@ class TableField extends TableListField { $rows = $this->sortData(ArrayLib::invert($this->value)); // ignore all rows which are already saved if(isset($rows['new'])) { - if($sourceItems instanceof DataList) $sourceItems = $sourceItems->toDataObjectSet(); - - + if($sourceItems instanceof DataList) { + $sourceItems = new ArrayList($sourceItems->toArray()); + } + $newRows = $this->sortData($rows['new']); // iterate over each value (not each row) $i = 0; @@ -183,7 +184,7 @@ class TableField extends TableListField { // generate a temporary DataObject container (not saved in the database) $sourceClass = $this->sourceClass(); - $sourceItems->push(new $sourceClass($newRow)); + $sourceItems->add(new $sourceClass($newRow)); $i++; } From 9f8729442798b17b477e9e34a1e839f2f2e29f3b Mon Sep 17 00:00:00 2001 From: ajshort Date: Thu, 5 May 2011 17:48:12 +1000 Subject: [PATCH 52/64] ENHANCEMENT: Added exists(), replace(), merge(), push(), pop(), unshift() and shift() methods to ArrayList. --- model/ArrayList.php | 67 +++++++++++++++++++++++++++++++- tests/model/ArrayListTest.php | 73 +++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) diff --git a/model/ArrayList.php b/model/ArrayList.php index e05e3dff3..f62bac282 100644 --- a/model/ArrayList.php +++ b/model/ArrayList.php @@ -21,6 +21,10 @@ class ArrayList extends ViewableData implements SS_List { return count($this->array); } + public function exists() { + return (bool) count($this); + } + public function getIterator() { return new ArrayIterator($this->array); } @@ -52,7 +56,7 @@ class ArrayList extends ViewableData implements SS_List { } public function add($item) { - $this->array[] = $item; + $this->push($item); } public function remove($item) { @@ -61,6 +65,67 @@ class ArrayList extends ViewableData implements SS_List { } } + /** + * Replaces an item in this list with another item. + * + * @param array|object $item + * @param array|object $with + */ + public function replace($item, $with) { + foreach ($this->array as $key => $candidate) { + if ($candidate === $item) { + $this->array[$key] = $with; + return; + } + } + } + + /** + * Merges with another array or list by pushing all the items in it onto the + * end of this list. + * + * @param array|object $with + */ + public function merge($with) { + foreach ($with as $item) $this->push($item); + } + + /** + * Pushes an item onto the end of this list. + * + * @param array|object $item + */ + public function push($item) { + $this->array[] = $item; + } + + /** + * Pops the last element off the end of the list and returns it. + * + * @return array|object + */ + public function pop() { + return array_pop($this->array); + } + + /** + * Unshifts an item onto the beginning of the list. + * + * @param array|object $item + */ + public function unshift($item) { + array_unshift($this->array, $item); + } + + /** + * Shifts the item off the beginning of the list and returns it. + * + * @return array|object + */ + public function shift() { + return array_shift($this->array); + } + public function first() { return reset($this->array); } diff --git a/tests/model/ArrayListTest.php b/tests/model/ArrayListTest.php index c8ba216f2..bb13dc0ed 100644 --- a/tests/model/ArrayListTest.php +++ b/tests/model/ArrayListTest.php @@ -12,6 +12,13 @@ class ArrayListTest extends SapphireTest { $this->assertEquals(3, $list->count()); } + public function testExists() { + $list = new ArrayList(); + $this->assertFalse($list->exists()); + $list = new ArrayList(array(1, 2, 3)); + $this->assertTrue($list->exists()); + } + public function testToNestedArray() { $list = new ArrayList(array( array('First' => 'FirstFirst', 'Second' => 'FirstSecond'), @@ -51,6 +58,72 @@ class ArrayListTest extends SapphireTest { )); } + public function testReplace() { + $list = new ArrayList(array( + array('Key' => 1), + $two = (object) array('Key' => 2), + (object) array('Key' => 3) + )); + + $this->assertEquals(array('Key' => 1), $list[0]); + $list->replace(array('Key' => 1), array('Replaced' => 1)); + $this->assertEquals(3, count($list)); + $this->assertEquals(array('Replaced' => 1), $list[0]); + + $this->assertEquals($two, $list[1]); + $list->replace($two, array('Replaced' => 2)); + $this->assertEquals(3, count($list)); + $this->assertEquals(array('Replaced' => 2), $list[1]); + } + + public function testMerge() { + $list = new ArrayList(array( + array('Num' => 1), array('Num' => 2) + )); + $list->merge(array( + array('Num' => 3), array('Num' => 4) + )); + + $this->assertEquals(4, count($list)); + $this->assertEquals($list->toArray(), array( + array('Num' => 1), array('Num' => 2), array('Num' => 3), array('Num' => 4) + )); + } + + public function testPushPop() { + $list = new ArrayList(array('Num' => 1)); + $this->assertEquals(1, count($list)); + + $list->push(array('Num' => 2)); + $this->assertEquals(2, count($list)); + $this->assertEquals(array('Num' => 2), $list->last()); + + $list->push(array('Num' => 3)); + $this->assertEquals(3, count($list)); + $this->assertEquals(array('Num' => 3), $list->last()); + + $this->assertEquals(array('Num' => 3), $list->pop()); + $this->assertEquals(2, count($list)); + $this->assertEquals(array('Num' => 2), $list->last()); + } + + public function testShiftUnshift() { + $list = new ArrayList(array('Num' => 1)); + $this->assertEquals(1, count($list)); + + $list->unshift(array('Num' => 2)); + $this->assertEquals(2, count($list)); + $this->assertEquals(array('Num' => 2), $list->first()); + + $list->unshift(array('Num' => 3)); + $this->assertEquals(3, count($list)); + $this->assertEquals(array('Num' => 3), $list->first()); + + $this->assertEquals(array('Num' => 3), $list->shift()); + $this->assertEquals(2, count($list)); + $this->assertEquals(array('Num' => 2), $list->first()); + } + public function testFirstLast() { $list = new ArrayList(array( array('Key' => 1), array('Key' => 2), array('Key' => 3) From 3f132a105bd3e0c3f930a642bff8ce35ca0fd393 Mon Sep 17 00:00:00 2001 From: ajshort Date: Thu, 5 May 2011 20:40:24 +1000 Subject: [PATCH 53/64] API CHANGE: Replaced DataObjectSet instances with ArrayList. --- admin/code/CMSBatchActionHandler.php | 4 +-- admin/code/LeftAndMain.php | 4 +-- admin/code/ModelAdmin.php | 6 ++-- api/RSSFeed.php | 2 +- api/RestfulServer.php | 2 +- api/RestfulService.php | 6 ++-- api/SapphireSoapServer.php | 4 +-- core/PaginatedList.php | 4 +-- dev/BulkLoader.php | 2 +- dev/ModelViewer.php | 10 +++--- forms/ComplexTableField.php | 6 ++-- forms/OptionsetField.php | 2 +- forms/SelectionGroup.php | 2 +- forms/TableField.php | 8 ++--- forms/TableListField.php | 22 ++++++------- forms/TreeMultiselectField.php | 2 +- model/DataDifferencer.php | 2 +- model/Hierarchy.php | 6 ++-- model/MySQLDatabase.php | 4 +-- model/SQLMap.php | 2 +- model/Versioned.php | 2 +- model/fieldtypes/Int.php | 2 +- parsers/BBCodeParser.php | 2 +- security/Group.php | 6 ++-- security/Permission.php | 4 +-- security/PermissionCheckboxSetField.php | 4 +-- tests/api/RSSFeedTest.php | 2 +- tests/forms/TableListFieldTest.php | 2 +- tests/model/PaginatedListTest.php | 42 ++++++++++++------------- tests/view/SSViewerTest.php | 8 ++--- 30 files changed, 87 insertions(+), 87 deletions(-) diff --git a/admin/code/CMSBatchActionHandler.php b/admin/code/CMSBatchActionHandler.php index 6a273924a..f26f833fe 100644 --- a/admin/code/CMSBatchActionHandler.php +++ b/admin/code/CMSBatchActionHandler.php @@ -116,7 +116,7 @@ class CMSBatchActionHandler extends RequestHandler { } } } else { - $pages = new DataObjectSet(); + $pages = new ArrayList(); } return $actionHandler->run($pages); @@ -173,7 +173,7 @@ class CMSBatchActionHandler extends RequestHandler { */ function batchActionList() { $actions = $this->batchActions(); - $actionList = new DataObjectSet(); + $actionList = new ArrayList(); foreach($actions as $urlSegment => $action) { $actionClass = $action['class']; diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index 73d3c317c..d07b0eb25 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -382,10 +382,10 @@ class LeftAndMain extends Controller { */ public function MainMenu() { // Don't accidentally return a menu if you're not logged in - it's used to determine access. - if(!Member::currentUser()) return new DataObjectSet(); + if(!Member::currentUser()) return new ArrayList(); // Encode into DO set - $menu = new DataObjectSet(); + $menu = new ArrayList(); $menuItems = CMSMenu::get_viewable_menu_items(); if($menuItems) foreach($menuItems as $code => $menuItem) { // alternate permission checks (in addition to LeftAndMain->canView()) diff --git a/admin/code/ModelAdmin.php b/admin/code/ModelAdmin.php index dae73578b..2eab7a7e9 100755 --- a/admin/code/ModelAdmin.php +++ b/admin/code/ModelAdmin.php @@ -248,7 +248,7 @@ abstract class ModelAdmin extends LeftAndMain { */ protected function getModelForms() { $models = $this->getManagedModels(); - $forms = new DataObjectSet(); + $forms = new ArrayList(); foreach($models as $class => $options) { if(is_numeric($class)) $class = $options; @@ -476,11 +476,11 @@ class ModelAdmin_CollectionController extends Controller { $importerClass = $importers[$modelName]; $importer = new $importerClass($modelName); $spec = $importer->getImportSpec(); - $specFields = new DataObjectSet(); + $specFields = new ArrayList(); foreach($spec['fields'] as $name => $desc) { $specFields->push(new ArrayData(array('Name' => $name, 'Description' => $desc))); } - $specRelations = new DataObjectSet(); + $specRelations = new ArrayList(); foreach($spec['relations'] as $name => $desc) { $specRelations->push(new ArrayData(array('Name' => $name, 'Description' => $desc))); } diff --git a/api/RSSFeed.php b/api/RSSFeed.php index 0cb34c236..5e3d2911b 100755 --- a/api/RSSFeed.php +++ b/api/RSSFeed.php @@ -137,7 +137,7 @@ class RSSFeed extends ViewableData { * @return DataObjectSet Returns the {@link RSSFeed_Entry} objects. */ function Entries() { - $output = new DataObjectSet(); + $output = new ArrayList(); if(isset($this->entries)) { foreach($this->entries as $entry) { $output->push(new RSSFeed_Entry($entry, $this->titleField, $this->descriptionField, $this->authorField)); diff --git a/api/RestfulServer.php b/api/RestfulServer.php index 46186e224..343b9bb30 100644 --- a/api/RestfulServer.php +++ b/api/RestfulServer.php @@ -246,7 +246,7 @@ class RestfulServer extends Controller { return $responseFormatter->convertDataObjectSet($obj, $fields); } else if(!$obj) { $responseFormatter->setTotalSize(0); - return $responseFormatter->convertDataObjectSet(new DataObjectSet(), $fields); + return $responseFormatter->convertDataObjectSet(new ArrayList(), $fields); } else { return $responseFormatter->convertDataObject($obj, $fields); } diff --git a/api/RestfulService.php b/api/RestfulService.php index e3f19217d..56d1982df 100644 --- a/api/RestfulService.php +++ b/api/RestfulService.php @@ -250,7 +250,7 @@ class RestfulService extends ViewableData { public function getAttributes($xml, $collection=NULL, $element=NULL){ $xml = new SimpleXMLElement($xml); - $output = new DataObjectSet(); + $output = new ArrayList(); if($collection) $childElements = $xml->{$collection}; @@ -305,7 +305,7 @@ class RestfulService extends ViewableData { public function getValues($xml, $collection=NULL, $element=NULL){ $xml = new SimpleXMLElement($xml); - $output = new DataObjectSet(); + $output = new ArrayList(); $childElements = $xml; if($collection) @@ -386,7 +386,7 @@ class RestfulService extends ViewableData { */ function searchAttributes($xml, $node=NULL){ $xml = new SimpleXMLElement($xml); - $output = new DataObjectSet(); + $output = new ArrayList(); $childElements = $xml->xpath($node); diff --git a/api/SapphireSoapServer.php b/api/SapphireSoapServer.php index 48124aef6..df6945b78 100755 --- a/api/SapphireSoapServer.php +++ b/api/SapphireSoapServer.php @@ -65,12 +65,12 @@ class SapphireSoapServer extends Controller { } $methods[] = new ArrayData(array( "Name" => $methodName, - "Arguments" => new DataObjectSet($processedArguments), + "Arguments" => new ArrayList($processedArguments), "ReturnType" => self::$xsd_types[$returnType], )); } - return new DataObjectSet($methods); + return new ArrayList($methods); } /** diff --git a/core/PaginatedList.php b/core/PaginatedList.php index 95112d90c..6d85822e5 100644 --- a/core/PaginatedList.php +++ b/core/PaginatedList.php @@ -165,7 +165,7 @@ class PaginatedList extends SS_ListDecorator { * @return DataObjectSet */ public function Pages($max = null) { - $result = new DataObjectSet(); + $result = new ArrayList(); if ($max) { $start = ($this->CurrentPage() - floor($max / 2)) - 1; @@ -232,7 +232,7 @@ class PaginatedList extends SS_ListDecorator { * @return DataObjectSet */ public function PaginationSummary($context = 4) { - $result = new DataObjectSet(); + $result = new ArrayList(); $current = $this->CurrentPage(); $total = $this->TotalPages(); diff --git a/dev/BulkLoader.php b/dev/BulkLoader.php index 20d82743b..f89ccbf91 100644 --- a/dev/BulkLoader.php +++ b/dev/BulkLoader.php @@ -399,7 +399,7 @@ class BulkLoader_Result extends Object { * @return DataObjectSet */ protected function mapToDataObjectSet($arr) { - $set = new DataObjectSet(); + $set = new ArrayList(); foreach($arr as $arrItem) { $obj = DataObject::get_by_id($arrItem['ClassName'], $arrItem['ID']); $obj->_BulkLoaderMessage = $arrItem['Message']; diff --git a/dev/ModelViewer.php b/dev/ModelViewer.php index e23857ee8..74dbebba7 100644 --- a/dev/ModelViewer.php +++ b/dev/ModelViewer.php @@ -43,7 +43,7 @@ class ModelViewer extends Controller { function Models() { $classes = ClassInfo::subclassesFor('DataObject'); array_shift($classes); - $output = new DataObjectSet(); + $output = new ArrayList(); foreach($classes as $class) { $output->push(new ModelViewer_Model($class)); } @@ -60,7 +60,7 @@ class ModelViewer extends Controller { $modules = array(); foreach($classes as $class) { $model = new ModelViewer_Model($class); - if(!isset($modules[$model->Module])) $modules[$model->Module] = new DataObjectSet(); + if(!isset($modules[$model->Module])) $modules[$model->Module] = new ArrayList(); $modules[$model->Module]->push($model); } ksort($modules); @@ -70,7 +70,7 @@ class ModelViewer extends Controller { $modules = array($this->module => $modules[$this->module]); } - $output = new DataObjectSet(); + $output = new ArrayList(); foreach($modules as $moduleName => $models) { $output->push(new ArrayData(array( 'Link' => 'dev/viewmodel/' . $moduleName, @@ -149,7 +149,7 @@ class ModelViewer_Model extends ViewableData { } function Fields() { - $output = new DataObjectSet(); + $output = new ArrayList(); $output->push(new ModelViewer_Field($this,'ID', 'PrimaryKey')); if(!$this->ParentModel) { @@ -165,7 +165,7 @@ class ModelViewer_Model extends ViewableData { } function Relations() { - $output = new DataObjectSet(); + $output = new ArrayList(); foreach(array('has_one','has_many','many_many') as $relType) { $items = singleton($this->className)->uninherited($relType,true); diff --git a/forms/ComplexTableField.php b/forms/ComplexTableField.php index 6537aef57..aba398a5e 100644 --- a/forms/ComplexTableField.php +++ b/forms/ComplexTableField.php @@ -256,7 +256,7 @@ JS; $pageStart = (isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) ? $_REQUEST['ctf'][$this->Name()]['start'] : 0; - $output = new DataObjectSet(); + $output = new ArrayList(); foreach($sourceItems as $pageIndex=>$item) { $output->push(Object::create($this->itemClass,$item, $this, $pageStart+$pageIndex)); } @@ -540,7 +540,7 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest { */ /* this doesn't actually work :-( function Paginator() { - $paginatingSet = new DataObjectSet(array($this->dataObj())); + $paginatingSet = new ArrayList(array($this->dataObj())); $start = isset($_REQUEST['ctf']['start']) ? $_REQUEST['ctf']['start'] : 0; $paginatingSet->setPageLimits($start, 1, $this->ctf->TotalCount()); return $paginatingSet; @@ -715,7 +715,7 @@ class ComplexTableField_ItemRequest extends TableListField_ItemRequest { function Pagination() { $this->pageSize = 9; $currentItem = $this->PopupCurrentItem(); - $result = new DataObjectSet(); + $result = new ArrayList(); if($currentItem < 6) { $offset = 1; } elseif($this->TotalCount() - $currentItem <= 4) { diff --git a/forms/OptionsetField.php b/forms/OptionsetField.php index 8c80eb048..4b32ae151 100755 --- a/forms/OptionsetField.php +++ b/forms/OptionsetField.php @@ -136,7 +136,7 @@ class OptionsetField extends DropdownField { } function ExtraOptions() { - return new DataObjectSet(); + return new ArrayList(); } } ?> \ No newline at end of file diff --git a/forms/SelectionGroup.php b/forms/SelectionGroup.php index 672e9aa2e..5a8ca5af2 100755 --- a/forms/SelectionGroup.php +++ b/forms/SelectionGroup.php @@ -74,7 +74,7 @@ class SelectionGroup extends CompositeField { $firstSelected = $checked =""; } - return new DataObjectSet($newItems); + return new ArrayList($newItems); } function hasData() { diff --git a/forms/TableField.php b/forms/TableField.php index 2e33eed1b..7c1e36fd6 100644 --- a/forms/TableField.php +++ b/forms/TableField.php @@ -128,7 +128,7 @@ class TableField extends TableListField { $headings[] = new ArrayData(array("Name" => $fieldName, "Title" => $fieldTitle, "Class" => $class)); $i++; } - return new DataObjectSet($headings); + return new ArrayList($headings); } /** @@ -151,14 +151,14 @@ class TableField extends TableListField { */ function Items() { // holds TableField_Item instances - $items = new DataObjectSet(); + $items = new ArrayList(); $sourceItems = $this->sourceItems(); // either load all rows from the field value, // (e.g. when validation failed), or from sourceItems() if($this->value) { - if(!$sourceItems) $sourceItems = new DataObjectSet(); + if(!$sourceItems) $sourceItems = new ArrayList(); // get an array keyed by rows, rather than values $rows = $this->sortData(ArrayLib::invert($this->value)); @@ -221,7 +221,7 @@ class TableField extends TableListField { ); $form->loadDataFrom($dataObj); - // Add the item to our new DataObjectSet, with a wrapper class. + // Add the item to our new ArrayList, with a wrapper class. return new TableField_Item($dataObj, $this, $form, $this->fieldTypes); } diff --git a/forms/TableListField.php b/forms/TableListField.php index 0dc26ab2d..c4d730a0a 100644 --- a/forms/TableListField.php +++ b/forms/TableListField.php @@ -338,7 +338,7 @@ JS "SortDirection" => (isset($_REQUEST['ctf'][$this->Name()]['dir'])) ? $_REQUEST['ctf'][$this->Name()]['dir'] : null )); } - return new DataObjectSet($headings); + return new ArrayList($headings); } function disableSorting($to = true) { @@ -367,7 +367,7 @@ JS * @return DataObjectSet */ function Actions() { - $allowedActions = new DataObjectSet(); + $allowedActions = new ArrayList(); foreach($this->actions as $actionName => $actionSettings) { if($this->Can($actionName)) { $allowedActions->push(new ViewableData()); @@ -437,7 +437,7 @@ JS * Return a DataObjectSet of TableListField_Item objects, suitable for display in the template. */ function Items() { - $fieldItems = new DataObjectSet(); + $fieldItems = new ArrayList(); if($items = $this->sourceItems()) foreach($items as $item) { if($item) $fieldItems->push(new $this->itemClass($item, $this)); } @@ -630,7 +630,7 @@ JS 'Title' => DBField::create('Varchar', $fieldTitle), )); } - return new DataObjectSet($summaryFields); + return new ArrayList($summaryFields); } function HasGroupedItems() { @@ -648,9 +648,9 @@ JS } $groupedItems = $items->groupBy($this->groupByField); - $groupedArrItems = new DataObjectSet(); + $groupedArrItems = new ArrayList(); foreach($groupedItems as $key => $group) { - $fieldItems = new DataObjectSet(); + $fieldItems = new ArrayList(); foreach($group as $item) { if($item) $fieldItems->push(new $this->itemClass($item, $this)); } @@ -961,7 +961,7 @@ JS $csvColumns = ($this->fieldListCsv) ? $this->fieldListCsv : $this->fieldList; $fileData = ''; $columnData = array(); - $fieldItems = new DataObjectSet(); + $fieldItems = new ArrayList(); if($this->csvHasHeader) { $fileData .= "\"" . implode("\"{$separator}\"", array_values($csvColumns)) . "\""; @@ -1064,7 +1064,7 @@ JS * ################################# */ function Utility() { - $links = new DataObjectSet(); + $links = new ArrayList(); if($this->can('export')) { $links->push(new ArrayData(array( 'Title' => _t('TableListField.CSVEXPORT', 'Export to CSV'), @@ -1280,7 +1280,7 @@ JS function SelectOptions(){ if(!$this->selectOptions) return; - $selectOptionsSet = new DataObjectSet(); + $selectOptionsSet = new ArrayList(); foreach($this->selectOptions as $k => $v) { $selectOptionsSet->push(new ArrayData(array( 'Key' => $k, @@ -1377,7 +1377,7 @@ class TableListField_Item extends ViewableData { "CsvSeparator" => $this->parent->getCsvSeparator(), )); } - return new DataObjectSet($fields); + return new ArrayList($fields); } function Markable() { @@ -1430,7 +1430,7 @@ class TableListField_Item extends ViewableData { * @return DataObjectSet */ function Actions() { - $allowedActions = new DataObjectSet(); + $allowedActions = new ArrayList(); foreach($this->parent->actions as $actionName => $actionSettings) { if($this->parent->Can($actionName)) { $allowedActions->push(new ArrayData(array( diff --git a/forms/TreeMultiselectField.php b/forms/TreeMultiselectField.php index 80cdeb81d..e53ca1397 100755 --- a/forms/TreeMultiselectField.php +++ b/forms/TreeMultiselectField.php @@ -59,7 +59,7 @@ class TreeMultiselectField extends TreeDropdownField { // Otherwise, look data up from the linked relation } if($this->value != 'unchanged' && is_string($this->value)) { - $items = new DataObjectSet(); + $items = new ArrayList(); $ids = explode(',', $this->value); foreach($ids as $id) { if(!is_numeric($id)) continue; diff --git a/model/DataDifferencer.php b/model/DataDifferencer.php index 9bb1c564f..1278b1252 100644 --- a/model/DataDifferencer.php +++ b/model/DataDifferencer.php @@ -96,7 +96,7 @@ class DataDifferencer extends ViewableData { * - To: The newer version of the field */ function ChangedFields() { - $changedFields = new DataObjectSet(); + $changedFields = new ArrayList(); if($this->fromRecord) { $base = $this->fromRecord; diff --git a/model/Hierarchy.php b/model/Hierarchy.php index b98a28b86..5180d2944 100644 --- a/model/Hierarchy.php +++ b/model/Hierarchy.php @@ -400,7 +400,7 @@ class Hierarchy extends DataExtension { if(!(isset($this->_cache_children) && $this->_cache_children)) { $result = $this->owner->stageChildren(false); if(isset($result)) { - $this->_cache_children = new DataObjectSet(); + $this->_cache_children = new ArrayList(); foreach($result as $child) { if($child->canView()) { $this->_cache_children->push($child); @@ -452,7 +452,7 @@ class Hierarchy extends DataExtension { // Next, go through the live children. Only some of these will be listed $liveChildren = $this->owner->liveChildren(true, true); if($liveChildren) { - $merged = new DataObjectSet(); + $merged = new ArrayList(); $merged->merge($stageChildren); $merged->merge($liveChildren); $stageChildren = $merged; @@ -590,7 +590,7 @@ class Hierarchy extends DataExtension { * @return DataObjectSet */ public function getAncestors() { - $ancestors = new DataObjectSet(); + $ancestors = new ArrayList(); $object = $this->owner; while($object = $object->getParent()) { diff --git a/model/MySQLDatabase.php b/model/MySQLDatabase.php index 67d969b13..fc1a7bd06 100644 --- a/model/MySQLDatabase.php +++ b/model/MySQLDatabase.php @@ -829,8 +829,8 @@ class MySQLDatabase extends SS_Database { $objects[] = new $record['ClassName']($record); - if(isset($objects)) $doSet = new DataObjectSet($objects); - else $doSet = new DataObjectSet(); + if(isset($objects)) $doSet = new ArrayList($objects); + else $doSet = new ArrayList(); $list = new PaginatedList($doSet); $list->setPageStart($start); diff --git a/model/SQLMap.php b/model/SQLMap.php index 933591ef3..af6d7323c 100755 --- a/model/SQLMap.php +++ b/model/SQLMap.php @@ -68,7 +68,7 @@ class SQLMap extends Object implements IteratorAggregate { */ protected function genItems() { if(!isset($this->items)) { - $this->items = new DataObjectSet(); + $this->items = new ArrayList(); $items = $this->query->execute(); foreach($items as $item) { diff --git a/model/Versioned.php b/model/Versioned.php index e3ab086c7..e845995cb 100644 --- a/model/Versioned.php +++ b/model/Versioned.php @@ -700,7 +700,7 @@ class Versioned extends DataExtension { $query->orderby = ($sort) ? $sort : "\"{$baseTable}_versions\".\"LastEdited\" DESC, \"{$baseTable}_versions\".\"Version\" DESC"; $records = $query->execute(); - $versions = new DataObjectSet(); + $versions = new ArrayList(); foreach($records as $record) { $versions->push(new Versioned_Version($record)); diff --git a/model/fieldtypes/Int.php b/model/fieldtypes/Int.php index 2f14a725e..c31459e13 100644 --- a/model/fieldtypes/Int.php +++ b/model/fieldtypes/Int.php @@ -31,7 +31,7 @@ class Int extends DBField { } function Times() { - $output = new DataObjectSet(); + $output = new ArrayList(); for( $i = 0; $i < $this->value; $i++ ) $output->push( new ArrayData( array( 'Number' => $i + 1 ) ) ); diff --git a/parsers/BBCodeParser.php b/parsers/BBCodeParser.php index 1f7ba19a1..ae798d478 100644 --- a/parsers/BBCodeParser.php +++ b/parsers/BBCodeParser.php @@ -60,7 +60,7 @@ class BBCodeParser extends TextParser { static function usable_tags() { - return new DataObjectSet( + return new ArrayList( new ArrayData(array( "Title" => _t('BBCodeParser.BOLD', 'Bold Text'), "Example" => '[b]'._t('BBCodeParser.BOLDEXAMPLE', 'Bold').'[/b]' diff --git a/security/Group.php b/security/Group.php index af30c5f99..2f5416406 100644 --- a/security/Group.php +++ b/security/Group.php @@ -42,7 +42,7 @@ class Group extends DataObject { } function getAllChildren() { - $doSet = new DataObjectSet(); + $doSet = new ArrayList(); if ($children = DataObject::get('Group', '"ParentID" = '.$this->ID)) { foreach($children as $child) { @@ -137,7 +137,7 @@ class Group extends DataObject { // Add roles (and disable all checkboxes for inherited roles) $allRoles = Permission::check('ADMIN') ? DataObject::get('PermissionRole') : DataObject::get('PermissionRole', 'OnlyAdminCanApply = 0'); $groupRoles = $this->Roles(); - $inheritedRoles = new DataObjectSet(); + $inheritedRoles = new ArrayList(); $ancestors = $this->getAncestors(); foreach($ancestors as $ancestor) { $ancestorRoles = $ancestor->Roles(); @@ -399,7 +399,7 @@ class Group extends DataObject { $children = $extInstance->AllChildrenIncludingDeleted(); $extInstance->clearOwner(); - $filteredChildren = new DataObjectSet(); + $filteredChildren = new ArrayList(); if($children) foreach($children as $child) { if($child->canView()) $filteredChildren->push($child); diff --git a/security/Permission.php b/security/Permission.php index 3d9ede9ab..f8b01f35b 100755 --- a/security/Permission.php +++ b/security/Permission.php @@ -379,7 +379,7 @@ class Permission extends DataObject { */ public static function get_members_by_permission($code) { $toplevelGroups = self::get_groups_by_permission($code); - if (!$toplevelGroups) return new DataObjectSet(); + if (!$toplevelGroups) return new ArrayList(); $groupIDs = array(); foreach($toplevelGroups as $group) { @@ -389,7 +389,7 @@ class Permission extends DataObject { } } - if(!count($groupIDs)) return new DataObjectSet(); + if(!count($groupIDs)) return new ArrayList(); $members = DataObject::get( Object::getCustomClass('Member'), diff --git a/security/PermissionCheckboxSetField.php b/security/PermissionCheckboxSetField.php index 14467d1d8..334fe14e6 100644 --- a/security/PermissionCheckboxSetField.php +++ b/security/PermissionCheckboxSetField.php @@ -45,7 +45,7 @@ class PermissionCheckboxSetField extends FormField { if($records instanceof SS_List) { $this->records = $records; } elseif($records instanceof Group) { - $this->records = new DataObjectSet($records); + $this->records = new ArrayList(array($records)); } elseif($records) { throw new InvalidArgumentException('$record should be either a Group record, or a DataObjectSet of Group records'); } @@ -76,7 +76,7 @@ class PermissionCheckboxSetField extends FormField { $uninheritedCodes = array(); $inheritedCodes = array(); - $records = ($this->records) ? $this->records : new DataObjectSet(); + $records = ($this->records) ? $this->records : new ArrayList(); // Get existing values from the form record (assuming the formfield name is a join field on the record) if(is_object($this->form)) { diff --git a/tests/api/RSSFeedTest.php b/tests/api/RSSFeedTest.php index 55b235892..a33137006 100755 --- a/tests/api/RSSFeedTest.php +++ b/tests/api/RSSFeedTest.php @@ -8,7 +8,7 @@ class RSSFeedTest extends SapphireTest { protected static $original_host; function testRSSFeed() { - $list = new DataObjectSet(); + $list = new ArrayList(); $list->push(new RSSFeedTest_ItemA()); $list->push(new RSSFeedTest_ItemB()); $list->push(new RSSFeedTest_ItemC()); diff --git a/tests/forms/TableListFieldTest.php b/tests/forms/TableListFieldTest.php index 4b3d8964c..8be886f49 100755 --- a/tests/forms/TableListFieldTest.php +++ b/tests/forms/TableListFieldTest.php @@ -304,7 +304,7 @@ class TableListFieldTest extends SapphireTest { $three = new TableListFieldTest_Obj; $three->A = "A-three"; - $list = new DataObjectSet($one, $two, $three); + $list = new ArrayList(array($one, $two, $three)); // A TableListField must be inside a form for its links to be generated $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet( diff --git a/tests/model/PaginatedListTest.php b/tests/model/PaginatedListTest.php index e6384c8bf..fc367034c 100644 --- a/tests/model/PaginatedListTest.php +++ b/tests/model/PaginatedListTest.php @@ -8,27 +8,27 @@ class PaginatedListTest extends SapphireTest { public function testPageStart() { - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $this->assertEquals(0, $list->getPageStart(), 'The start defaults to 0.'); $list->setPageStart(10); $this->assertEquals(10, $list->getPageStart(), 'You can set the page start.'); - $list = new PaginatedList(new DataObjectSet(), array('start' => 50)); + $list = new PaginatedList(new ArrayList(), array('start' => 50)); $this->assertEquals(50, $list->getPageStart(), 'The page start can be read from the request.'); } public function testGetTotalItems() { - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $this->assertEquals(0, $list->getTotalItems()); $list->setTotalItems(10); $this->assertEquals(10, $list->getTotalItems()); - $list = new PaginatedList(new DataObjectSet( + $list = new PaginatedList(new ArrayList(array( new ArrayData(array()), new ArrayData(array()) - )); + ))); $this->assertEquals(2, $list->getTotalItems()); } @@ -39,7 +39,7 @@ class PaginatedListTest extends SapphireTest { ->method('unlimitedRowCount') ->will($this->returnValue(100)); - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $list->setPaginationFromQuery($query); $this->assertEquals(15, $list->getPageLength()); @@ -48,7 +48,7 @@ class PaginatedListTest extends SapphireTest { } public function testSetCurrentPage() { - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $list->setPageLength(10); $list->setCurrentPage(10); @@ -57,7 +57,7 @@ class PaginatedListTest extends SapphireTest { } public function testGetIterator() { - $list = new PaginatedList(new DataObjectSet(array( + $list = new PaginatedList(new ArrayList(array( new DataObject(array('Num' => 1)), new DataObject(array('Num' => 2)), new DataObject(array('Num' => 3)), @@ -85,7 +85,7 @@ class PaginatedListTest extends SapphireTest { } public function testPages() { - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $list->setPageLength(10); $list->setTotalItems(50); @@ -113,7 +113,7 @@ class PaginatedListTest extends SapphireTest { } public function testPaginationSummary() { - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $list->setPageLength(10); $list->setTotalItems(250); @@ -134,7 +134,7 @@ class PaginatedListTest extends SapphireTest { } public function testCurrentPage() { - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $list->setTotalItems(50); $this->assertEquals(1, $list->CurrentPage()); @@ -145,7 +145,7 @@ class PaginatedListTest extends SapphireTest { } public function testTotalPages() { - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $list->setPageLength(1); $this->assertEquals(0, $list->TotalPages()); @@ -158,7 +158,7 @@ class PaginatedListTest extends SapphireTest { } public function testMoreThanOnePage() { - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $list->setPageLength(1); $list->setTotalItems(1); @@ -169,14 +169,14 @@ class PaginatedListTest extends SapphireTest { } public function testNotFirstPage() { - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $this->assertFalse($list->NotFirstPage()); $list->setCurrentPage(2); $this->assertTrue($list->NotFirstPage()); } public function testNotLastPage() { - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $list->setTotalItems(50); $this->assertTrue($list->NotLastPage()); @@ -185,14 +185,14 @@ class PaginatedListTest extends SapphireTest { } public function testFirstItem() { - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $this->assertEquals(1, $list->FirstItem()); $list->setPageStart(10); $this->assertEquals(11, $list->FirstItem()); } public function testLastItem() { - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $list->setPageLength(10); $list->setTotalItems(25); @@ -205,19 +205,19 @@ class PaginatedListTest extends SapphireTest { } public function testFirstLink() { - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $this->assertContains('start=0', $list->FirstLink()); } public function testLastLink() { - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $list->setPageLength(10); $list->setTotalItems(100); $this->assertContains('start=90', $list->LastLink()); } public function testNextLink() { - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $list->setTotalItems(50); $this->assertContains('start=10', $list->NextLink()); @@ -232,7 +232,7 @@ class PaginatedListTest extends SapphireTest { } public function testPrevLink() { - $list = new PaginatedList(new DataObjectSet()); + $list = new PaginatedList(new ArrayList()); $list->setTotalItems(50); $this->assertNull($list->PrevLink()); diff --git a/tests/view/SSViewerTest.php b/tests/view/SSViewerTest.php index 3fe08bbcf..5230a6acb 100644 --- a/tests/view/SSViewerTest.php +++ b/tests/view/SSViewerTest.php @@ -332,10 +332,10 @@ after') $data = new ArrayData(array( 'Title' => 'A', - 'Children' => new DataObjectSet(array( + 'Children' => new ArrayList(array( new ArrayData(array( 'Title' => 'A1', - 'Children' => new DataObjectSet(array( + 'Children' => new ArrayList(array( new ArrayData(array( 'Title' => 'A1 i', )), new ArrayData(array( 'Title' => 'A1 ii', )), )), @@ -415,7 +415,7 @@ after') // Data to run the loop tests on - one sequence of three items, each with a subitem $data = new ArrayData(array( 'Name' => 'Top', - 'Foo' => new DataObjectSet(array( + 'Foo' => new ArrayList(array( new ArrayData(array( 'Name' => '1', 'Sub' => new ArrayData(array( @@ -538,7 +538,7 @@ class SSViewerTestFixture extends ViewableData { // Special field name Loop### to create a list if(preg_match('/^Loop([0-9]+)$/', $fieldName, $matches)) { - $output = new DataObjectSet(); + $output = new ArrayList(); for($i=0;$i<$matches[1];$i++) $output->push(new SSViewerTestFixture($childName)); return $output; From 2a3fdd16a4aa938e9cdaad65f3380a3a91b92aee Mon Sep 17 00:00:00 2001 From: ajshort Date: Thu, 5 May 2011 20:40:51 +1000 Subject: [PATCH 54/64] MINOR: Added a default "ID" parameter to ArrayList->column(). --- model/ArrayList.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/ArrayList.php b/model/ArrayList.php index f62bac282..19d8a909a 100644 --- a/model/ArrayList.php +++ b/model/ArrayList.php @@ -148,7 +148,7 @@ class ArrayList extends ViewableData implements SS_List { } } - public function column($field) { + public function column($field = 'ID') { $result = array(); foreach ($this->array as $item) { $result[] = $this->extract($item, $field); From 99a2baf3f81914ff85b85fd2008720159ef14795 Mon Sep 17 00:00:00 2001 From: ajshort Date: Thu, 5 May 2011 20:41:06 +1000 Subject: [PATCH 55/64] MINOR: Updated ArrayList->getRange() to return an ArrayList instance. --- model/ArrayList.php | 2 +- tests/model/ArrayListTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/model/ArrayList.php b/model/ArrayList.php index 19d8a909a..c8c76d821 100644 --- a/model/ArrayList.php +++ b/model/ArrayList.php @@ -52,7 +52,7 @@ class ArrayList extends ViewableData implements SS_List { } public function getRange($offset, $length) { - return array_slice($this->array, $offset, $length); + return new ArrayList(array_slice($this->array, $offset, $length)); } public function add($item) { diff --git a/tests/model/ArrayListTest.php b/tests/model/ArrayListTest.php index bb13dc0ed..a733a2b0b 100644 --- a/tests/model/ArrayListTest.php +++ b/tests/model/ArrayListTest.php @@ -37,7 +37,7 @@ class ArrayListTest extends SapphireTest { $list = new ArrayList(array( array('Key' => 1), array('Key' => 2), array('Key' => 3) )); - $this->assertEquals($list->getRange(1, 2), array( + $this->assertEquals($list->getRange(1, 2)->toArray(), array( array('Key' => 2), array('Key' => 3) )); } From 462689a4e664a7636528cba9928fbbc1df28715e Mon Sep 17 00:00:00 2001 From: ajshort Date: Thu, 5 May 2011 20:53:07 +1000 Subject: [PATCH 56/64] ENHANCEMENT: Added ArrayList->removeDuplicates(). --- model/ArrayList.php | 20 ++++++++++++++++++++ tests/model/ArrayListTest.php | 19 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/model/ArrayList.php b/model/ArrayList.php index c8c76d821..aa1c7432e 100644 --- a/model/ArrayList.php +++ b/model/ArrayList.php @@ -90,6 +90,26 @@ class ArrayList extends ViewableData implements SS_List { foreach ($with as $item) $this->push($item); } + /** + * Removes items from this list which have a duplicate value for a certain + * field. This is especially useful when combining lists. + * + * @param string $field + */ + public function removeDuplicates($field = 'ID') { + $seen = array(); + + foreach ($this->array as $key => $item) { + $value = $this->extract($item, $field); + + if (array_key_exists($value, $seen)) { + unset($this->array[$key]); + } + + $seen[$value] = true; + } + } + /** * Pushes an item onto the end of this list. * diff --git a/tests/model/ArrayListTest.php b/tests/model/ArrayListTest.php index a733a2b0b..9fae2f641 100644 --- a/tests/model/ArrayListTest.php +++ b/tests/model/ArrayListTest.php @@ -90,6 +90,25 @@ class ArrayListTest extends SapphireTest { )); } + public function testRemoveDuplicates() { + $list = new ArrayList(array( + array('ID' => 1, 'Field' => 1), + array('ID' => 2, 'Field' => 2), + array('ID' => 3, 'Field' => 3), + array('ID' => 4, 'Field' => 1), + (object) array('ID' => 5, 'Field' => 2) + )); + + $this->assertEquals(5, count($list)); + $list->removeDuplicates(); + $this->assertEquals(5, count($list)); + + $list->removeDuplicates('Field'); + $this->assertEquals(3, count($list)); + $this->assertEquals(array(1, 2, 3), $list->column('Field')); + $this->assertEquals(array(1, 2, 3), $list->column('ID')); + } + public function testPushPop() { $list = new ArrayList(array('Num' => 1)); $this->assertEquals(1, count($list)); From b3fc458101db2e1363b2471654bfe1bc23fcb50c Mon Sep 17 00:00:00 2001 From: ajshort Date: Fri, 6 May 2011 00:24:33 +1000 Subject: [PATCH 57/64] ENHANCEMENT: Made it possible to sort by multiple fields in ArrayList::sort(). --- model/ArrayList.php | 28 ++++++++++++++++++++++++---- tests/model/ArrayListTest.php | 20 ++++++++++++++++++++ 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/model/ArrayList.php b/model/ArrayList.php index aa1c7432e..b1a5a929b 100644 --- a/model/ArrayList.php +++ b/model/ArrayList.php @@ -180,15 +180,35 @@ class ArrayList extends ViewableData implements SS_List { return true; } + /** + * Sorts this list by one or more fields. You can either pass in a single + * field name and direction, or a map of field names to sort directions. + * + * @param string|array $by + * @param string $dir + * @see SS_List::sort() + */ public function sort($by, $dir = 'ASC') { $sorts = array(); - $dir = strtoupper($dir) == 'DESC' ? SORT_DESC : SORT_ASC; - foreach ($this->array as $item) { - $sorts[] = $this->extract($item, $by); + if (!is_array($by)) { + $by = array($by => $dir); } - array_multisort($sorts, $dir, $this->array); + foreach ($by as $field => $dir) { + $dir = strtoupper($dir) == 'DESC' ? SORT_DESC : SORT_ASC; + $vals = array(); + + foreach ($this->array as $item) { + $vals[] = $this->extract($item, $field); + } + + $sorts[] = $vals; + $sorts[] = $dir; + } + + $sorts[] = &$this->array; + call_user_func_array('array_multisort', $sorts); } public function offsetExists($offset) { diff --git a/tests/model/ArrayListTest.php b/tests/model/ArrayListTest.php index 9fae2f641..630ab7aee 100644 --- a/tests/model/ArrayListTest.php +++ b/tests/model/ArrayListTest.php @@ -208,6 +208,26 @@ class ArrayListTest extends SapphireTest { )); } + public function testMultiSort() { + $list = new ArrayList(array( + (object) array('Name'=>'Object1', 'F1'=>1, 'F2'=>2, 'F3'=>3), + (object) array('Name'=>'Object2', 'F1'=>2, 'F2'=>1, 'F3'=>4), + (object) array('Name'=>'Object3', 'F1'=>5, 'F2'=>2, 'F3'=>2), + )); + + $list->sort('F3', 'ASC'); + $this->assertEquals($list->first()->Name, 'Object3', 'Object3 should be first in the list'); + + $list->sort('F3', 'DESC'); + $this->assertEquals($list->first()->Name, 'Object2', 'Object2 should be first in the list'); + + $list->sort(array('F2'=>'ASC', 'F1'=>'ASC')); + $this->assertEquals($list->last()->Name, 'Object3', 'Object3 should be last in the list'); + + $list->sort(array('F2'=>'ASC', 'F1'=>'DESC')); + $this->assertEquals($list->last()->Name, 'Object1', 'Object1 should be last in the list'); + } + } /** From c954ae5aaae1d1b44f87a85f42f4420f847856d5 Mon Sep 17 00:00:00 2001 From: ajshort Date: Fri, 6 May 2011 00:30:45 +1000 Subject: [PATCH 58/64] MINOR: Renamed ArrayList->array to ArrayList->items; --- model/ArrayList.php | 58 ++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/model/ArrayList.php b/model/ArrayList.php index b1a5a929b..823fd7f86 100644 --- a/model/ArrayList.php +++ b/model/ArrayList.php @@ -10,15 +10,15 @@ class ArrayList extends ViewableData implements SS_List { /** * @var array */ - protected $array; + protected $items; - public function __construct(array $array = array()) { - $this->array = $array; + public function __construct(array $items = array()) { + $this->items = $items; parent::__construct(); } public function count() { - return count($this->array); + return count($this->items); } public function exists() { @@ -26,17 +26,17 @@ class ArrayList extends ViewableData implements SS_List { } public function getIterator() { - return new ArrayIterator($this->array); + return new ArrayIterator($this->items); } public function toArray() { - return $this->array; + return $this->items; } public function toNestedArray() { $result = array(); - foreach ($this->array as $item) { + foreach ($this->items as $item) { if (is_object($item)) { if (method_exists($item, 'toMap')) { $result[] = $item->toMap(); @@ -52,7 +52,7 @@ class ArrayList extends ViewableData implements SS_List { } public function getRange($offset, $length) { - return new ArrayList(array_slice($this->array, $offset, $length)); + return new ArrayList(array_slice($this->items, $offset, $length)); } public function add($item) { @@ -60,8 +60,8 @@ class ArrayList extends ViewableData implements SS_List { } public function remove($item) { - foreach ($this->array as $key => $value) { - if ($item === $value) unset($this->array[$key]); + foreach ($this->items as $key => $value) { + if ($item === $value) unset($this->items[$key]); } } @@ -72,9 +72,9 @@ class ArrayList extends ViewableData implements SS_List { * @param array|object $with */ public function replace($item, $with) { - foreach ($this->array as $key => $candidate) { + foreach ($this->items as $key => $candidate) { if ($candidate === $item) { - $this->array[$key] = $with; + $this->items[$key] = $with; return; } } @@ -99,11 +99,11 @@ class ArrayList extends ViewableData implements SS_List { public function removeDuplicates($field = 'ID') { $seen = array(); - foreach ($this->array as $key => $item) { + foreach ($this->items as $key => $item) { $value = $this->extract($item, $field); if (array_key_exists($value, $seen)) { - unset($this->array[$key]); + unset($this->items[$key]); } $seen[$value] = true; @@ -116,7 +116,7 @@ class ArrayList extends ViewableData implements SS_List { * @param array|object $item */ public function push($item) { - $this->array[] = $item; + $this->items[] = $item; } /** @@ -125,7 +125,7 @@ class ArrayList extends ViewableData implements SS_List { * @return array|object */ public function pop() { - return array_pop($this->array); + return array_pop($this->items); } /** @@ -134,7 +134,7 @@ class ArrayList extends ViewableData implements SS_List { * @param array|object $item */ public function unshift($item) { - array_unshift($this->array, $item); + array_unshift($this->items, $item); } /** @@ -143,34 +143,34 @@ class ArrayList extends ViewableData implements SS_List { * @return array|object */ public function shift() { - return array_shift($this->array); + return array_shift($this->items); } public function first() { - return reset($this->array); + return reset($this->items); } public function last() { - return end($this->array); + return end($this->items); } public function map($keyfield, $titlefield) { $map = array(); - foreach ($this->array as $item) { + foreach ($this->items as $item) { $map[$this->extract($item, $keyfield)] = $this->extract($item, $titlefield); } return $map; } public function find($key, $value) { - foreach ($this->array as $item) { + foreach ($this->items as $item) { if ($this->extract($item, $key) == $value) return $item; } } public function column($field = 'ID') { $result = array(); - foreach ($this->array as $item) { + foreach ($this->items as $item) { $result[] = $this->extract($item, $field); } return $result; @@ -199,7 +199,7 @@ class ArrayList extends ViewableData implements SS_List { $dir = strtoupper($dir) == 'DESC' ? SORT_DESC : SORT_ASC; $vals = array(); - foreach ($this->array as $item) { + foreach ($this->items as $item) { $vals[] = $this->extract($item, $field); } @@ -207,24 +207,24 @@ class ArrayList extends ViewableData implements SS_List { $sorts[] = $dir; } - $sorts[] = &$this->array; + $sorts[] = &$this->items; call_user_func_array('array_multisort', $sorts); } public function offsetExists($offset) { - return array_key_exists($offset, $this->array); + return array_key_exists($offset, $this->items); } public function offsetGet($offset) { - if ($this->offsetExists($offset)) return $this->array[$offset]; + if ($this->offsetExists($offset)) return $this->items[$offset]; } public function offsetSet($offset, $value) { - $this->array[$offset] = $value; + $this->items[$offset] = $value; } public function offsetUnset($offset) { - unset($this->array[$offset]); + unset($this->items[$offset]); } /** From 16a016b9dcd7865e95032b33519fc50fed55fcc7 Mon Sep 17 00:00:00 2001 From: ajshort Date: Fri, 6 May 2011 00:48:45 +1000 Subject: [PATCH 59/64] API CHANGE: Deprecated FormField->Name() in favour of FormField->getName(). --- forms/FormField.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/forms/FormField.php b/forms/FormField.php index 417eb483f..afb3c7493 100755 --- a/forms/FormField.php +++ b/forms/FormField.php @@ -119,10 +119,17 @@ class FormField extends RequestHandler { * * @return string */ - function Name() { + function getName() { return $this->name; } - + + /** + * @deprecated 3.0 Use {@link getName()}. + */ + public function Name() { + return $this->getName(); + } + function attrName() { return $this->name; } From 2294ef582058bc640ebdc7ac51565069e008a3e5 Mon Sep 17 00:00:00 2001 From: ajshort Date: Fri, 6 May 2011 00:52:50 +1000 Subject: [PATCH 60/64] API CHANGE: Deprecated DataObjectSet in favour of DataList or ArrayList. MINOR: Moved DataObjectSet tests to ArrayListTest. --- model/DataObjectSet.php | 905 +----------------------------- tests/model/ArrayListTest.php | 28 + tests/model/DataObjectSetTest.php | 384 ------------- tests/model/DataObjectSetTest.yml | 45 -- 4 files changed, 56 insertions(+), 1306 deletions(-) delete mode 100644 tests/model/DataObjectSetTest.php delete mode 100644 tests/model/DataObjectSetTest.yml diff --git a/model/DataObjectSet.php b/model/DataObjectSet.php index decc1fcb2..fbffe2dd2 100644 --- a/model/DataObjectSet.php +++ b/model/DataObjectSet.php @@ -1,889 +1,40 @@ 1) ? func_get_args() : $items; - - // We now have support for using the key of a data object set - foreach($itemsArr as $i => $item) { - if(is_subclass_of($item, 'ViewableData')) { - $this->items[$i] = $item; - } elseif(is_object($item) || ArrayLib::is_associative($item)) { - $this->items[$i] = new ArrayData($item); + public function __construct($items = array()) { + user_error( + 'DataObjectSet is deprecated, please use DataList or ArrayList instead.', + E_USER_NOTICE + ); + + if ($items) { + if (!is_array($items) || func_num_args() > 1) { + $items = func_get_args(); + } + + foreach ($items as $i => $item) { + if ($item instanceof ViewableData) { + continue; + } + + if (is_object($item) || ArrayLib::is_associative($item)) { + $items[$i] = new ArrayData($item); } else { user_error( - "DataObjectSet::__construct: Passed item #{$i} is not an object or associative array, - can't be properly iterated on in templates", - E_USER_WARNING - ); - $this->items[$i] = $item; + "DataObjectSet::__construct: Passed item #{$i} is not an" + . ' and object or associative array, can\'t be properly' + . ' iterated on in templates', E_USER_WARNING + ); } } } - parent::__construct(); - } - - /** - * Necessary for interface ArrayAccess. Returns whether an item with $key exists - * @param mixed $key - * @return bool - */ - public function offsetExists($key) { - return isset($this->items[$key]); + + parent::__construct($items); } - /** - * Necessary for interface ArrayAccess. Returns item stored in array with index $key - * @param mixed $key - * @return DataObject - */ - public function offsetGet($key) { - return $this->items[$key]; - } - - /** - * Necessary for interface ArrayAccess. Set an item with the key in $key - * @param mixed $key - * @param mixed $value - */ - public function offsetSet($key, $value) { - $this->items[$key] = $value; - } - - /** - * Necessary for interface ArrayAccess. Unset an item with the key in $key - * @param mixed $key - */ - public function offsetUnset($key) { - unset($this->items[$key]); - } - - /** - * Destory all of the DataObjects in this set. - */ - public function destroy() { - foreach($this as $item) { - $item->destroy(); - } - } - - /** - * Removes all the items in this set. - */ - public function emptyItems() { - $this->items = array(); - } - - /** - * Convert this DataObjectSet to an array of DataObjects. - * @param string $index Index the array by this field. - * @return array - */ - public function toArray($index = null) { - $map = array(); - foreach($this as $item) { - if($index) $map[$item->$index] = $item; - else $map[] = $item; - } - - return $map; - } - - /** - * Convert this DataObjectSet to an array of maps. - * @param string $index Index the array by this field. - * @return array - */ - public function toNestedArray($index = null){ - if(!$index) { - $index = "ID"; - } - - $map = array(); - - foreach( $this as $item ) { - $map[$item->$index] = $item->getAllFields(); - } - - return $map; - } - - /** - * Returns an array of ID => Title for the items in this set. - * - * This is an alias of {@link DataObjectSet->map()} - * - * @deprecated 2.5 Please use map() instead - * - * @param string $index The field to use as a key for the array - * @param string $titleField The field (or method) to get values for the map - * @param string $emptyString Empty option text e.g "(Select one)" - * @param bool $sort Sort the map alphabetically based on the $titleField value - * @return array - */ - public function toDropDownMap($index = 'ID', $titleField = 'Title', $emptyString = null, $sort = false) { - return $this->map($index, $titleField, $emptyString, $sort); - } - - public function add($item) { - $this->push($item); - } - - /** - * Add an item to the DataObject Set. - * @param DataObject $item Item to add. - * @param string $key Key to index this DataObject by. - */ - public function push($item, $key = null) { - if($key != null) { - unset($this->items[$key]); - $this->items[$key] = $item; - } else { - $this->items[] = $item; - } - } - - /** - * Add an item to the beginning of the DataObjectSet - * @param DataObject $item Item to add - * @param string $key Key to index this DataObject by. - */ - public function insertFirst($item, $key = null) { - if($key == null) { - array_unshift($this->items, $item); - } else { - $this->items = array_merge(array($key=>$item), $this->items); - } - } - - /** - * Insert a DataObject at the beginning of this set. - * @param DataObject $item Item to insert. - */ - public function unshift($item) { - $this->insertFirst($item); - } - - /** - * Remove a DataObject from the beginning of this set and return it. - * This is the equivalent of pop() but acts on the head of the set. - * Opposite of unshift(). - * - * @return DataObject (or null if there are no items in the set) - */ - public function shift() { - return array_shift($this->items); - } - - /** - * Remove a DataObject from the end of this set and return it. - * This is the equivalent of shift() but acts on the tail of the set. - * Opposite of push(). - * - * @return DataObject (or null if there are no items in the set) - */ - public function pop() { - return array_pop($this->items); - } - - /** - * Remove a DataObject from this set. - * @param DataObject $itemObject Item to remove. - */ - public function remove($itemObject) { - foreach($this->items as $key=>$item){ - if($item === $itemObject){ - unset($this->items[$key]); - } - } - } - - /** - * Replaces $itemOld with $itemNew - * - * @param DataObject $itemOld - * @param DataObject $itemNew - */ - public function replace($itemOld, $itemNew) { - foreach($this->items as $key => $item) { - if($item === $itemOld) { - $this->items[$key] = $itemNew; - return; - } - } - } - - /** - * Merge another set onto the end of this set. - * To merge without causing duplicates, consider calling - * {@link removeDuplicates()} after this method on the new set. - * - * @param DataObjectSet $anotherSet Set to mege onto this set. - */ - public function merge($anotherSet){ - if($anotherSet) { - foreach($anotherSet as $item){ - $this->push($item); - } - } - } - - /** - * Gets a specific slice of an existing set. - * - * @param int $offset - * @param int $length - * @return DataObjectSet - */ - public function getRange($offset, $length) { - $set = array_slice($this->items, (int)$offset, (int)$length); - return new DataObjectSet($set); - } - - /** - * Returns an Iterator for this DataObjectSet. - * This function allows you to use DataObjectSets in foreach loops - * @return DataObjectSet_Iterator - */ - public function getIterator() { - return new DataObjectSet_Iterator($this->items); - } - - /** - * Returns false if the set is empty. - * @return boolean - */ - public function exists() { - return $this->count() > 0; - } - - /** - * Return the first item in the set. - * @return DataObject - */ - public function First() { - if(count($this->items) < 1) - return null; - - $keys = array_keys($this->items); - return $this->items[$keys[0]]; - } - - /** - * Return the last item in the set. - * @return DataObject - */ - public function Last() { - if(count($this->items) < 1) - return null; - - $keys = array_keys($this->items); - return $this->items[$keys[sizeof($keys)-1]]; - } - - /** - * @deprecated 3.0 Use {@link DataObjectSet::Count()}. - */ - public function TotalItems() { - return $this->Count(); - } - - /** - * Returns the actual number of items in this dataset. - * @return int - */ - public function Count() { - return sizeof($this->items); - } - - /** - * Returns this set as a XHTML unordered list. - * @return string - */ - public function UL() { - if($this->exists()) { - $result = "\n"; - - return $result; - } - } - - /** - * Returns this set as a XHTML unordered list. - * @return string - */ - public function forTemplate() { - return $this->UL(); - } - - /** - * Returns an array of ID => Title for the items in this set. - * - * @param string $index The field to use as a key for the array - * @param string $titleField The field (or method) to get values for the map - * @param string $emptyString Empty option text e.g "(Select one)" - * @param bool $sort Sort the map alphabetically based on the $titleField value - * @return array - */ - public function map($index = 'ID', $titleField = 'Title', $emptyString = null, $sort = false) { - $map = array(); - foreach($this as $item) { - $map[$item->$index] = ($item->hasMethod($titleField)) - ? $item->$titleField() : $item->$titleField; - } - if($emptyString) $map = array('' => $emptyString) + $map; - - if($sort) asort($map); - - return $map; - } - - /** - * Find an item in this list where the field $key is equal to $value - * Eg: $doSet->find('ID', 4); - * @return ViewableData The first matching item. - */ - public function find($key, $value) { - foreach($this as $item) { - if($item->$key == $value) return $item; - } - } - - /** - * Return a column of the given field - * @param string $value The field name - * @return array - */ - public function column($value = "ID") { - $list = array(); - foreach($this as $item ){ - $list[] = ($item->hasMethod($value)) ? $item->$value() : $item->$value; - } - return $list; - } - - /** - * Returns an array of DataObjectSets. The array is keyed by index. - * - * @param string $index The field name to index the array by. - * @return array - */ - public function groupBy($index){ - foreach($this as $item ){ - $key = ($item->hasMethod($index)) ? $item->$index() : $item->$index; - if(!isset($result[$key])) { - $result[$key] = new DataObjectSet(); - } - $result[$key]->push($item); - } - return $result; - } - - /** - * Groups the items by a given field. - * Returns a DataObjectSet suitable for use in a nested template. - * @param string $index The field to group by - * @param string $childControl The name of the nested page control - * @return DataObjectSet - */ - public function GroupedBy($index, $childControl = "Children") { - $grouped = $this->groupBy($index); - $groupedAsSet = new DataObjectSet(); - foreach($grouped as $group) { - $groupedAsSet->push($group->First()->customise(array( - $childControl => $group - ))); - } - return $groupedAsSet; - } - - /** - * Returns a nested unordered list out of a "chain" of DataObject-relations, - * using the automagic ComponentSet-relation-methods to find subsequent DataObjectSets. - * The formatting of the list can be different for each level, and is evaluated as an SS-template - * with access to the current DataObjects attributes and methods. - * - * Example: Groups (Level 0, the "calling" DataObjectSet, needs to be queried externally) - * and their Members (Level 1, determined by the Group->Members()-relation). - * - * @param array $nestingLevels - * Defines relation-methods on DataObjects as a string, plus custom - * SS-template-code for the list-output. Use "Root" for the current DataObjectSet (is will not evaluate into - * a function). - * Caution: Don't close the list-elements (determined programatically). - * You need to escape dollar-signs that need to be evaluated as SS-template-code. - * Use $EvenOdd to get appropriate classes for CSS-styling. - * Format: - * array( - * array( - * "dataclass" => "Root", - * "template" => "
  • \$AccountName" - * ), - * array( - * "dataclass" => "GrantObjects", - * "template" => "
  • #\$GrantNumber: \$TotalAmount.Nice, \$ApplicationDate.ShortMonth \$ApplicationDate.Year" - * ) - * ); - * @param string $ulExtraAttributes Extra attributes - * - * @return string Unordered List (HTML) - */ - public function buildNestedUL($nestingLevels, $ulExtraAttributes = "") { - return $this->getChildrenAsUL($nestingLevels, 0, "", $ulExtraAttributes); - } - - /** - * Gets called recursively on the child-objects of the chain. - * - * @param array $nestingLevels see {@buildNestedUL} - * @param int $level Current nesting level - * @param string $template Template for list item - * @param string $ulExtraAttributes Extra attributes - * @return string - */ - public function getChildrenAsUL($nestingLevels, $level = 0, $template = "
  • \$Title", $ulExtraAttributes = null, &$itemCount = 0) { - $output = ""; - $hasNextLevel = false; - $ulExtraAttributes = " $ulExtraAttributes"; - $output = "\n"; - - $currentNestingLevel = $nestingLevels[$level]; - - // either current or default template - $currentTemplate = (!empty($currentNestingLevel)) ? $currentNestingLevel['template'] : $template; - $myViewer = SSViewer::fromString($currentTemplate); - - if(isset($nestingLevels[$level+1]['dataclass'])){ - $childrenMethod = $nestingLevels[$level+1]['dataclass']; - } - // sql-parts - - $filter = (isset($nestingLevels[$level+1]['filter'])) ? $nestingLevels[$level+1]['filter'] : null; - $sort = (isset($nestingLevels[$level+1]['sort'])) ? $nestingLevels[$level+1]['sort'] : null; - $join = (isset($nestingLevels[$level+1]['join'])) ? $nestingLevels[$level+1]['join'] : null; - $limit = (isset($nestingLevels[$level+1]['limit'])) ? $nestingLevels[$level+1]['limit'] : null; - $having = (isset($nestingLevels[$level+1]['having'])) ? $nestingLevels[$level+1]['having'] : null; - - foreach($this as $parent) { - $evenOdd = ($itemCount % 2 == 0) ? "even" : "odd"; - $parent->setField('EvenOdd', $evenOdd); - $template = $myViewer->process($parent); - - // if no output is selected, fall back to the id to keep the item "clickable" - $output .= $template . "\n"; - - if(isset($childrenMethod)) { - // workaround for missing groupby/having-parameters in instance_get - // get the dataobjects for the next level - $children = $parent->$childrenMethod($filter, $sort, $join, $limit, $having); - if($children) { - $output .= $children->getChildrenAsUL($nestingLevels, $level+1, $currentTemplate, $ulExtraAttributes); - } - } - $output .= "
  • \n"; - $itemCount++; - } - - $output .= "\n"; - - return $output; - } - - public function canSortBy($by) { - return true; - } - - /** - * Sorts the current DataObjectSet instance. - * @param string $fieldname The name of the field on the DataObject that you wish to sort the set by. - * @param string $direction Direction to sort by, either "ASC" or "DESC". - */ - public function sort($fieldname, $direction = "ASC") { - if($this->items) { - if (is_string($fieldname) && preg_match('/(.+?)(\s+?)(A|DE)SC$/', $fieldname, $matches)) { - $fieldname = $matches[1]; - $direction = $matches[3].'SC'; - } - column_sort($this->items, $fieldname, $direction, false); - } - } - - /** - * Remove duplicates from this set based on the dataobjects field. - * Assumes all items contained in the set all have that field. - * Useful after merging to sets via {@link merge()}. - * - * @param string $field the field to check for duplicates - */ - public function removeDuplicates($field = 'ID') { - $exists = array(); - foreach($this->items as $key => $item) { - if(isset($exists[$fullkey = ClassInfo::baseDataClass($item) . ":" . $item->$field])) { - unset($this->items[$key]); - } - $exists[$fullkey] = true; - } - } - - /** - * Returns information about this set in HTML format for debugging. - * @return string - */ - public function debug() { - $val = "

    " . $this->class . "

      "; - foreach($this as $item) { - $val .= "
    • " . Debug::text($item) . "
    • "; - } - $val .= "
    "; - return $val; - } - - /** - * Groups the set by $groupField and returns the parent of each group whose class - * is $groupClassName. If $collapse is true, the group will be collapsed up until an ancestor with the - * given class is found. - * @param string $groupField The field to group by. - * @param string $groupClassName Classname. - * @param string $sortParents SORT clause to insert into the parents SQL. - * @param string $parentField Parent field. - * @param boolean $collapse Collapse up until an ancestor with the given class is found. - * @param string $requiredParents Required parents - * @return DataObjectSet - */ - public function groupWithParents($groupField, $groupClassName, $sortParents = null, $parentField = 'ID', $collapse = false, $requiredParents = null) { - $groupTable = ClassInfo::baseDataClass($groupClassName); - - // Each item in this DataObjectSet is grouped into a multidimensional array - // indexed by it's parent. The parent IDs are later used to find the parents - // that make up the returned set. - $groupedSet = array(); - - // Array to store the subgroups matching the requirements - $resultsArray = array(); - - // Put this item into the array indexed by $groupField. - // the keys are later used to retrieve the top-level records - foreach( $this as $item ) { - $groupedSet[$item->$groupField][] = $item; - } - - $parentSet = null; - - // retrieve parents for this set - - // TODO How will we collapse the hierarchy to bridge the gap? - - // if collapse is specified, then find the most direct ancestor of type - // $groupClassName - if($collapse) { - // The most direct ancestors with the type $groupClassName - $parentSet = array(); - - // get direct parents - $parents = DataObject::get($groupClassName, "\"$groupTable\".\"$parentField\" IN( " . implode( ",", array_keys( $groupedSet ) ) . ")", $sortParents ); - - // for each of these parents... - foreach($parents as $parent) { - // store the old parent ID. This is required to change the grouped items - // in the $groupSet array - $oldParentID = $parent->ID; - - // get the parental stack - $parentObjects= $parent->parentStack(); - $parentStack = array(); - - foreach( $parentObjects as $parentObj ) - $parentStack[] = $parentObj->ID; - - // is some particular IDs are required, then get the intersection - if($requiredParents && count($requiredParents)) { - $parentStack = array_intersect($requiredParents, $parentStack); - } - - $newParent = null; - - // If there are no parents, the group can be omitted - if(empty($parentStack)) { - $newParent = new DataObjectSet(); - } else { - $newParent = DataObject::get_one( $groupClassName, "\"$groupTable\".\"$parentField\" IN( " . implode( ",", $parentStack ) . ")" ); - } - - // change each of the descendant's association from the old parent to - // the new parent. This effectively collapses the hierarchy - foreach( $groupedSet[$oldParentID] as $descendant ) { - $groupedSet[$newParent->ID][] = $descendant; - } - - // Add the most direct ancestor of type $groupClassName - $parentSet[] = $newParent; - } - // otherwise get the parents of these items - } else { - - $requiredIDs = array_keys( $groupedSet ); - - if( $requiredParents && cont($requiredParents)) { - $requiredIDs = array_intersect($requiredParents, $requiredIDs); - } - - if(empty($requiredIDs)) { - $parentSet = new DataObjectSet(); - } else { - $parentSet = DataObject::get( $groupClassName, "\"$groupTable\".\"$parentField\" IN( " . implode( ",", $requiredIDs ) . ")", $sortParents ); - } - - $parentSet = $parentSet->toArray(); - } - - foreach($parentSet as $parent) { - $resultsArray[] = $parent->customise(array( - "GroupItems" => new DataObjectSet($groupedSet[$parent->$parentField]) - )); - } - - return new DataObjectSet($resultsArray); - } - - /** - * Add a field to this set without writing it to the database - * @param DataObject $field Field to add - */ - function addWithoutWrite($field) { - $this->items[] = $field; - } - - /** - * Returns true if the DataObjectSet contains all of the IDs givem - * @param $idList An array of object IDs - */ - function containsIDs($idList) { - foreach($idList as $item) $wants[$item] = true; - foreach($this as $item) if($item) unset($wants[$item->ID]); - return !$wants; - } - - /** - * Returns true if the DataObjectSet contains all of and *only* the IDs given. - * Note that it won't like duplicates very much. - * @param $idList An array of object IDs - */ - function onlyContainsIDs($idList) { - return $this->containsIDs($idList) && sizeof($idList) == $this->count(); - } - -} - -/** - * Sort a 2D array by particular column. - * @param array $data The array to sort. - * @param mixed $column The name of the column you wish to sort by, or an array of column=>directions to sort by. - * @param string $direction Direction to sort by, either "ASC" or "DESC". - * @param boolean $preserveIndexes Preserve indexes - */ -function column_sort(&$data, $column, $direction = "ASC", $preserveIndexes = true) { - global $column_sort_field; - - // if we were only given a string for column, move it into an array - if (is_string($column)) $column = array($column => $direction); - - // convert directions to integers - foreach ($column as $k => $v) { - if ($v == 'ASC') { - $column[$k] = 1; - } - elseif ($v == 'DESC') { - $column[$k] = -1; - } - elseif (!is_numeric($v)) { - $column[$k] = 0; - } - } - - $column_sort_field = $column; - - if($preserveIndexes) { - uasort($data, "column_sort_callback_basic"); - } else { - usort($data, "column_sort_callback_basic"); - } -} - -/** - * Callback used by column_sort - */ -function column_sort_callback_basic($a, $b) { - global $column_sort_field; - $result = 0; - // loop through each sort field - foreach ($column_sort_field as $field => $multiplier) { - // if A < B then no further examination is necessary - if ($a->$field < $b->$field) { - $result = -1 * $multiplier; - break; - } - // if A > B then no further examination is necessary - elseif ($a->$field > $b->$field) { - $result = $multiplier; - break; - } - // A == B means we need to compare the two using the next field - // if this was the last field, then function returns that objects - // are equivalent - } - - return $result; -} - -/** - * An Iterator for a DataObjectSet - * - * @package sapphire - * @subpackage model - */ -class DataObjectSet_Iterator implements Iterator { - function __construct($items) { - $this->items = $items; - - $this->current = $this->prepareItem(current($this->items)); - } - - /** - * Prepare an item taken from the internal array for - * output by this iterator. Ensures that it is an object. - * @param DataObject $item Item to prepare - * @return DataObject - */ - protected function prepareItem($item) { - if(is_object($item)) { - $item->iteratorProperties(key($this->items), sizeof($this->items)); - } - // This gives some reliablity but it patches over the root cause of the bug... - // else if(key($this->items) !== null) $item = new ViewableData(); - return $item; - } - - - /** - * Return the current object of the iterator. - * @return DataObject - */ - public function current() { - return $this->current; - } - - /** - * Return the key of the current object of the iterator. - * @return mixed - */ - public function key() { - return key($this->items); - } - - /** - * Return the next item in this set. - * @return DataObject - */ - public function next() { - $this->current = $this->prepareItem(next($this->items)); - return $this->current; - } - - /** - * Rewind the iterator to the beginning of the set. - * @return DataObject The first item in the set. - */ - public function rewind() { - $this->current = $this->prepareItem(reset($this->items)); - return $this->current; - } - - /** - * Check the iterator is pointing to a valid item in the set. - * @return boolean - */ - public function valid() { - return $this->current !== false; - } - - /** - * Return the next item in this set without progressing the iterator. - * @return DataObject - */ - public function peekNext() { - return $this->getOffset(1); - } - - /** - * Return the prvious item in this set, without affecting the iterator. - * @return DataObject - */ - public function peekPrev() { - return $this->getOffset(-1); - } - - /** - * Return the object in this set offset by $offset from the iterator pointer. - * @param int $offset The offset. - * @return DataObject|boolean DataObject of offset item, or boolean FALSE if not found - */ - public function getOffset($offset) { - $keys = array_keys($this->items); - foreach($keys as $i => $key) { - if($key == key($this->items)) break; - } - - if(isset($keys[$i + $offset])) { - $requiredKey = $keys[$i + $offset]; - return $this->items[$requiredKey]; - } - - return false; - } -} - -?> \ No newline at end of file +} \ No newline at end of file diff --git a/tests/model/ArrayListTest.php b/tests/model/ArrayListTest.php index 630ab7aee..b7f44c7f2 100644 --- a/tests/model/ArrayListTest.php +++ b/tests/model/ArrayListTest.php @@ -5,6 +5,34 @@ */ class ArrayListTest extends SapphireTest { + public function testArrayAccessExists() { + $list = new ArrayList(array( + $one = new DataObject(array('Title' => 'one')), + $two = new DataObject(array('Title' => 'two')), + $three = new DataObject(array('Title' => 'three')) + )); + $this->assertEquals(count($list), 3); + $this->assertTrue(isset($list[0]), 'First item in the set is set'); + $this->assertEquals($one, $list[0], 'First item in the set is accessible by array notation'); + } + + public function testArrayAccessUnset() { + $list = new ArrayList(array( + $one = new DataObject(array('Title' => 'one')), + $two = new DataObject(array('Title' => 'two')), + $three = new DataObject(array('Title' => 'three')) + )); + unset($list[0]); + $this->assertEquals(count($list), 2); + } + + public function testArrayAccessSet() { + $list = new ArrayList(); + $this->assertEquals(0, count($list)); + $list['testing!'] = $test = new DataObject(array('Title' => 'I\'m testing!')); + $this->assertEquals($test, $list['testing!'], 'Set item is accessible by the key we set it as'); + } + public function testCount() { $list = new ArrayList(); $this->assertEquals(0, $list->count()); diff --git a/tests/model/DataObjectSetTest.php b/tests/model/DataObjectSetTest.php deleted file mode 100644 index 331a11756..000000000 --- a/tests/model/DataObjectSetTest.php +++ /dev/null @@ -1,384 +0,0 @@ - 'one')), - $two = new DataObject(array('Title' => 'two')), - $three = new DataObject(array('Title' => 'three')) - )); - $this->assertEquals(count($set), 3); - $this->assertTrue(isset($set[0]), 'First item in the set is set'); - $this->assertEquals($one, $set[0], 'First item in the set is accessible by array notation'); - } - - function testArrayAccessUnset() { - $set = new DataObjectSet(array( - $one = new DataObject(array('Title' => 'one')), - $two = new DataObject(array('Title' => 'two')), - $three = new DataObject(array('Title' => 'three')) - )); - unset($set[0]); - $this->assertEquals(count($set), 2); - } - - function testArrayAccessSet() { - $set = new DataObjectSet(); - $this->assertEquals(0, count($set)); - $set['testing!'] = $test = new DataObject(array('Title' => 'I\'m testing!')); - $this->assertEquals($test, $set['testing!'], 'Set item is accessible by the key we set it as'); - } - - function testIterator() { - $set = new DataObjectSet(array( - $one = new DataObject(array('Title'=>'one')), - $two = new DataObject(array('Title'=>'two')), - $three = new DataObject(array('Title'=>'three')), - $four = new DataObject(array('Title'=>'four')) - )); - - // test Pos() with foreach() - $i = 0; - foreach($set as $item) { - $i++; - $this->assertEquals($i, $item->Pos(), "Iterator position is set correctly on ViewableData when iterated with foreach()"); - } - - // test Pos() manually - $this->assertEquals(1, $one->Pos()); - $this->assertEquals(2, $two->Pos()); - $this->assertEquals(3, $three->Pos()); - $this->assertEquals(4, $four->Pos()); - - // test DataObjectSet->Count() - $this->assertEquals(4, $set->Count()); - - // test DataObjectSet->First() - $this->assertSame($one, $set->First()); - - // test DataObjectSet->Last() - $this->assertSame($four, $set->Last()); - - // test ViewableData->First() - $this->assertTrue($one->First()); - $this->assertFalse($two->First()); - $this->assertFalse($three->First()); - $this->assertFalse($four->First()); - - // test ViewableData->Last() - $this->assertFalse($one->Last()); - $this->assertFalse($two->Last()); - $this->assertFalse($three->Last()); - $this->assertTrue($four->Last()); - - // test ViewableData->Middle() - $this->assertFalse($one->Middle()); - $this->assertTrue($two->Middle()); - $this->assertTrue($three->Middle()); - $this->assertFalse($four->Middle()); - - // test ViewableData->Even() - $this->assertFalse($one->Even()); - $this->assertTrue($two->Even()); - $this->assertFalse($three->Even()); - $this->assertTrue($four->Even()); - - // test ViewableData->Odd() - $this->assertTrue($one->Odd()); - $this->assertFalse($two->Odd()); - $this->assertTrue($three->Odd()); - $this->assertFalse($four->Odd()); - } - - public function testMultipleOf() { - $comments = DataObject::get('DataObjectSetTest_TeamComment', '', "\"ID\" ASC"); - $commArr = $comments->toArray(); - $multiplesOf3 = 0; - - foreach($commArr as $comment) { - if($comment->MultipleOf(3)) { - $comment->IsMultipleOf3 = true; - $multiplesOf3++; - } else { - $comment->IsMultipleOf3 = false; - } - } - - $this->assertEquals(1, $multiplesOf3); - - $this->assertFalse($commArr[0]->IsMultipleOf3); - $this->assertFalse($commArr[1]->IsMultipleOf3); - $this->assertTrue($commArr[2]->IsMultipleOf3); - - foreach($comments as $comment) { - if($comment->MultipleOf(3, 1)) { - $comment->IsMultipleOf3 = true; - } else { - $comment->IsMultipleOf3 = false; - } - } - - $this->assertFalse($commArr[0]->IsMultipleOf3); - $this->assertFalse($commArr[1]->IsMultipleOf3); - $this->assertTrue($commArr[2]->IsMultipleOf3); - } - - /** - * Test {@link DataObjectSet->Count()} - */ - function testCount() { - $comments = DataObject::get('DataObjectSetTest_TeamComment', '', "\"ID\" ASC"); - - /* There are a total of 8 items in the set */ - $this->assertEquals($comments->Count(), 3, 'There are a total of 3 items in the set'); - } - - /** - * Test {@link DataObjectSet->First()} - */ - function testFirst() { - $comments = DataObject::get('DataObjectSetTest_TeamComment', '', "\"ID\" ASC"); - - /* The first object is Joe's comment */ - $this->assertEquals($comments->First()->Name, 'Joe', 'The first object has a Name field value of "Joe"'); - } - - /** - * Test {@link DataObjectSet->Last()} - */ - function testLast() { - $comments = DataObject::get('DataObjectSetTest_TeamComment', '', "\"ID\" ASC"); - - /* The last object is Dean's comment */ - $this->assertEquals($comments->Last()->Name, 'Phil', 'The last object has a Name field value of "Phil"'); - } - - /** - * Test {@link DataObjectSet->map()} - */ - function testMap() { - $comments = DataObject::get('DataObjectSetTest_TeamComment', '', "\"ID\" ASC"); - - /* Now we get a map of all the PageComment records */ - $map = $comments->map('ID', 'Title'); - - $expectedMap = array( - 1 => 'Joe', - 2 => 'Bob', - 3 => 'Phil' - ); - - /* There are 9 items in the map. 3 are records. 1 is the empty value */ - $this->assertEquals(count($map), 3, 'There are 3 items in the map.'); - } - - function testRemoveDuplicates() { - // Note that PageComment and DataObjectSetTest_TeamComment are both descendants of DataObject, and don't - // share an inheritance relationship below that. - $pageComments = DataObject::get('DataObjectSetTest_TeamComment'); - $teamComments = DataObject::get('DataObjectSetTest_TeamComment'); - - /* Test default functionality (remove by ID). We'd expect to loose all our - * team comments as they have the same IDs as the first three page comments */ - - $allComments = new DataObjectSet(); - $allComments->merge($pageComments); - $allComments->merge($teamComments); - - $this->assertEquals($allComments->Count(), 6); - - $allComments->removeDuplicates(); - - $this->assertEquals($allComments->Count(), 3, 'Standard functionality is to remove duplicate base class/IDs'); - - /* Now test removing duplicates based on a common field. In this case we shall - * use 'Name', so we can get all the unique commentators */ - - - $comment = new DataObjectSetTest_TeamComment(); - $comment->Name = "Bob"; - - $allComments->push($comment); - - $this->assertEquals($allComments->Count(), 4); - - $allComments->removeDuplicates('Name'); - - $this->assertEquals($allComments->Count(), 3, 'There are 3 uniquely named commentators'); - - // Ensure that duplicates are removed where the base data class is the same. - $mixedSet = new DataObjectSet(); - $mixedSet->push(new DataObjectSetTest_Base(array('ID' => 1))); - $mixedSet->push(new DataObjectSetTest_ChildClass(array('ID' => 1))); // dup: same base class and ID - $mixedSet->push(new DataObjectSetTest_ChildClass(array('ID' => 1))); // dup: more than one dup of the same object - $mixedSet->push(new DataObjectSetTest_ChildClass(array('ID' => 2))); // not dup: same type again, but different - $mixedSet->push(new DataObjectSetTest_Base(array('ID' => 1))); // dup: another dup, not consequetive. - - $mixedSet->removeDuplicates('ID'); - - $this->assertEquals($mixedSet->Count(), 2, 'There are 3 unique data objects in a very mixed set'); - } - - /** - * Test {@link DataObjectSet->insertFirst()} - */ - function testInsertFirst() { - // inserFirst doesn't work with DataLists any more, because of new ORM. - /* - // Get one comment - $comment = DataObject::get_one('DataObjectSetTest_TeamComment', "\"Name\" = 'Joe'"); - - // Get all other comments - $set = DataObject::get('DataObjectSetTest_TeamComment', '"Name" != \'Joe\''); - - // Duplicate so we can use it later without another lookup - $otherSet = clone $set; - // insert without a key - $otherSet->insertFirst($comment); - $this->assertEquals($comment, $otherSet->First(), 'Comment should be first'); - - // Give us another copy - $otherSet = clone $set; - // insert with a numeric key - $otherSet->insertFirst($comment, 2); - $this->assertEquals($comment, $otherSet->First(), 'Comment should be first'); - - // insert with a non-numeric key - $set->insertFirst($comment, 'SomeRandomKey'); - $this->assertEquals($comment, $set->First(), 'Comment should be first'); - */ - } - - /** - * Test {@link DataObjectSet->getRange()} - */ - function testGetRange() { - $comments = DataObject::get('DataObjectSetTest_TeamComment', '', "\"ID\" ASC"); - - // Make sure we got all 8 comments - $this->assertEquals($comments->Count(), 3, 'Three comments in the database.'); - - // Grab a range - $range = $comments->getRange(1, 2); - $this->assertEquals($range->Count(), 2, 'Two comment in the range.'); - - // And now grab a range that shouldn't be full. Remember counting starts at 0. - $range = $comments->getRange(2, 1); - $this->assertEquals($range->Count(), 1, 'One comment in the range.'); - - // Make sure it's the last one - $this->assertEquals($range->First(), $comments->Last(), 'The only item in the range should be the last one.'); - } - - /** - * Test {@link DataObjectSet->exists()} - */ - function testExists() { - // Test an empty set - $set = new DataObjectSet(); - $this->assertFalse($set->exists(), 'Empty set doesn\'t exist.'); - - // Test a non-empty set - $set = DataObject::get('DataObjectSetTest_TeamComment', '', "\"ID\" ASC"); - $this->assertTrue($set->exists(), 'Non-empty set does exist.'); - } - - /** - * Test {@link DataObjectSet->shift()} - */ - function testShift() { - $set = new DataObjectSet(); - $set->push(new ArrayData(array('Name' => 'Joe'))); - $set->push(new ArrayData(array('Name' => 'Bob'))); - $set->push(new ArrayData(array('Name' => 'Ted'))); - $this->assertEquals('Joe', $set->shift()->Name); - } - - /** - * Test {@link DataObjectSet->unshift()} - */ - function testUnshift() { - $set = new DataObjectSet(); - $set->push(new ArrayData(array('Name' => 'Joe'))); - $set->push(new ArrayData(array('Name' => 'Bob'))); - $set->push(new ArrayData(array('Name' => 'Ted'))); - $set->unshift(new ArrayData(array('Name' => 'Steve'))); - $this->assertEquals('Steve', $set->First()->Name); - } - - /** - * Test {@link DataObjectSet->pop()} - */ - function testPop() { - $set = new DataObjectSet(); - $set->push(new ArrayData(array('Name' => 'Joe'))); - $set->push(new ArrayData(array('Name' => 'Bob'))); - $set->push(new ArrayData(array('Name' => 'Ted'))); - $this->assertEquals('Ted', $set->pop()->Name); - } - - /** - * Test {@link DataObjectSet->sort()} - */ - function testSort() { - $set = new DataObjectSet(array( - array('Name'=>'Object1', 'F1'=>1, 'F2'=>2, 'F3'=>3), - array('Name'=>'Object2', 'F1'=>2, 'F2'=>1, 'F3'=>4), - array('Name'=>'Object3', 'F1'=>5, 'F2'=>2, 'F3'=>2), - )); - // test a single sort ASC - $set->sort('F3', 'ASC'); - $this->assertEquals($set->First()->Name, 'Object3', 'Object3 should be first in the set'); - // test a single sort DESC - $set->sort('F3', 'DESC'); - $this->assertEquals($set->First()->Name, 'Object2', 'Object2 should be first in the set'); - // test a multi sort - $set->sort(array('F2'=>'ASC', 'F1'=>'ASC')); - $this->assertEquals($set->Last()->Name, 'Object3', 'Object3 should be last in the set'); - // test a multi sort - $set->sort(array('F2'=>'ASC', 'F1'=>'DESC')); - $this->assertEquals($set->Last()->Name, 'Object1', 'Object1 should be last in the set'); - } -} - -/** - * @package sapphire - * @subpackage tests - */ -class DataObjectSetTest_TeamComment extends DataObject implements TestOnly { - - static $db = array( - 'Name' => 'Varchar', - 'Comment' => 'Text', - ); - - static $has_one = array( - 'Team' => 'DataObjectTest_Team', - ); -} - -class DataObjectSetTest_Base extends DataObject implements TestOnly { - static $db = array( - 'Name' => 'Varchar' - ); -} - -class DataObjectSetTest_ChildClass extends DataObjectSetTest_Base implements TestOnly { -} \ No newline at end of file diff --git a/tests/model/DataObjectSetTest.yml b/tests/model/DataObjectSetTest.yml deleted file mode 100644 index 1a251eb6c..000000000 --- a/tests/model/DataObjectSetTest.yml +++ /dev/null @@ -1,45 +0,0 @@ -DataObjectTest_Team: - team1: - Title: Team 1 - team2: - Title: Team 2 - -DataObjectTest_Player: - captain1: - FirstName: Captain 1 - FavouriteTeam: =>DataObjectTest_Team.team1 - Teams: =>DataObjectTest_Team.team1 - captain2: - FirstName: Captain 2 - Teams: =>DataObjectTest_Team.team2 - player1: - FirstName: Player 1 - player2: - FirstName: Player 2 - Teams: =>DataObjectTest_Team.team1,=>DataObjectTest_Team.team2 - -DataObjectTest_SubTeam: - subteam1: - Title: Subteam 1 - SubclassDatabaseField: Subclassed 1 - ExtendedDatabaseField: Extended 1 - subteam2_with_player_relation: - Title: Subteam 2 - SubclassDatabaseField: Subclassed 2 - ExtendeHasOneRelationship: =>DataObjectTest_Player.player1 - subteam3_with_empty_fields: - Title: Subteam 3 - -DataObjectSetTest_TeamComment: - comment1: - Name: Joe - Comment: This is a team comment by Joe - Team: =>DataObjectTest_Team.team1 - comment2: - Name: Bob - Comment: This is a team comment by Bob - Team: =>DataObjectTest_Team.team1 - comment3: - Name: Phil - Comment: Phil is a unique guy, and comments on team2 - Team: =>DataObjectTest_Team.team2 \ No newline at end of file From a75abd5cd701750ed0efd1650b03c254e55b17e4 Mon Sep 17 00:00:00 2001 From: ajshort Date: Fri, 6 May 2011 07:08:33 +1000 Subject: [PATCH 61/64] MINOR: Replaced DataObjectSet with ArrayList in MySQLDatabase->searchEngine(). --- model/MySQLDatabase.php | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/model/MySQLDatabase.php b/model/MySQLDatabase.php index fc1a7bd06..db50372cf 100644 --- a/model/MySQLDatabase.php +++ b/model/MySQLDatabase.php @@ -825,14 +825,13 @@ class MySQLDatabase extends SS_Database { // Get records $records = DB::query($fullQuery); - foreach($records as $record) + $objects = array(); + + foreach($records as $record) { $objects[] = new $record['ClassName']($record); + } - - if(isset($objects)) $doSet = new ArrayList($objects); - else $doSet = new ArrayList(); - - $list = new PaginatedList($doSet); + $list = new PaginatedList(new ArrayList($objects)); $list->setPageStart($start); $list->setPageLEngth($pageLength); $list->setTotalItems($totalCount); From f0676c7d56dfa1e82bd416a6ce0f5323d195f0e9 Mon Sep 17 00:00:00 2001 From: ajshort Date: Fri, 6 May 2011 08:30:54 +1000 Subject: [PATCH 62/64] API CHANGE: Updated FieldSet to use ArrayList rather then DataObjectSet. MINOR: Updated FieldSetTest since position information is no longer available on iterated objects. --- forms/FieldSet.php | 30 +++++++++++++----------------- tests/forms/FieldSetTest.php | 4 ++-- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/forms/FieldSet.php b/forms/FieldSet.php index 9cfc3896d..53d0049e8 100755 --- a/forms/FieldSet.php +++ b/forms/FieldSet.php @@ -1,11 +1,11 @@ 1) ? func_get_args() : $items; - parent::__construct($itemsArr); - - if(isset($this->items) && count($this->items)) { - foreach($this->items as $item) { - if(isset($item) && is_a($item, 'FormField')) { - $item->setContainerFieldSet($this); - } - } + public function __construct($items = array()) { + if (!is_array($items) || func_num_args() > 1) { + $items = func_get_args(); + } + + parent::__construct($items); + + foreach ($items as $item) { + if ($item instanceof FormField) $item->setContainerFieldSet($this); } - } /** diff --git a/tests/forms/FieldSetTest.php b/tests/forms/FieldSetTest.php index c01bf7372..d1e0a4e21 100644 --- a/tests/forms/FieldSetTest.php +++ b/tests/forms/FieldSetTest.php @@ -333,7 +333,7 @@ class FieldSetTest extends SapphireTest { $this->assertEquals(4, $fields->Count()); /* The position of the Title field is at number 3 */ - $this->assertEquals(3, $fields->fieldByName('Title')->Pos()); + $this->assertEquals('Title', $fields[2]->Name()); } function testInsertBeforeMultipleFields() { @@ -383,7 +383,7 @@ class FieldSetTest extends SapphireTest { $this->assertEquals(4, $fields->Count()); /* The position of the Title field should be at number 2 */ - $this->assertEquals(2, $fields->fieldByName('Title')->Pos()); + $this->assertEquals('Title', $fields[1]->Name()); } function testRootFieldSet() { From def001566c22fd4a123e138c2df4886bbc72d78a Mon Sep 17 00:00:00 2001 From: ajshort Date: Fri, 6 May 2011 08:51:02 +1000 Subject: [PATCH 63/64] API CHANGE: Renamed FieldSet to FieldList. API CHANGE: Renamed HiddenFieldSet to HiddenFieldList. --- forms/FieldList.php | 556 +++++++++++++++++++++++++++++++++++++++++++ forms/FieldSet.php | 557 +------------------------------------------- 2 files changed, 564 insertions(+), 549 deletions(-) create mode 100755 forms/FieldList.php mode change 100755 => 100644 forms/FieldSet.php diff --git a/forms/FieldList.php b/forms/FieldList.php new file mode 100755 index 000000000..4b6af6065 --- /dev/null +++ b/forms/FieldList.php @@ -0,0 +1,556 @@ + 1) { + $items = func_get_args(); + } + + parent::__construct($items); + + foreach ($items as $item) { + if ($item instanceof FormField) $item->setContainerFieldSet($this); + } + } + + /** + * Return a sequential set of all fields that have data. This excludes wrapper composite fields + * as well as heading / help text fields. + */ + public function dataFields() { + if(!$this->sequentialSet) $this->collateDataFields($this->sequentialSet); + return $this->sequentialSet; + } + + public function saveableFields() { + if(!$this->sequentialSaveableSet) $this->collateDataFields($this->sequentialSaveableSet, true); + return $this->sequentialSaveableSet; + } + + protected function flushFieldsCache() { + $this->sequentialSet = null; + $this->sequentialSaveableSet = null; + } + + protected function collateDataFields(&$list, $saveableOnly = false) { + foreach($this as $field) { + if($field->isComposite()) $field->collateDataFields($list, $saveableOnly); + + if($saveableOnly) { + $isIncluded = ($field->hasData() && !$field->isReadonly() && !$field->isDisabled()); + } else { + $isIncluded = ($field->hasData()); + } + if($isIncluded) { + $name = $field->Name(); + if(isset($list[$name])) { + $errSuffix = ""; + if($this->form) $errSuffix = " in your '{$this->form->class}' form called '" . $this->form->Name() . "'"; + else $errSuffix = ''; + user_error("collateDataFields() I noticed that a field called '$name' appears twice$errSuffix.", E_USER_ERROR); + } + $list[$name] = $field; + } + } + } + + /** + * Add an extra field to a tab within this fieldset. + * This is most commonly used when overloading getCMSFields() + * + * @param string $tabName The name of the tab or tabset. Subtabs can be referred to as TabSet.Tab or TabSet.Tab.Subtab. + * This function will create any missing tabs. + * @param FormField $field The {@link FormField} object to add to the end of that tab. + * @param string $insertBefore The name of the field to insert before. Optional. + */ + public function addFieldToTab($tabName, $field, $insertBefore = null) { + // This is a cache that must be flushed + $this->flushFieldsCache(); + + // Find the tab + $tab = $this->findOrMakeTab($tabName); + + // Add the field to the end of this set + if($insertBefore) $tab->insertBefore($field, $insertBefore); + else $tab->push($field); + } + + /** + * Add a number of extra fields to a tab within this fieldset. + * This is most commonly used when overloading getCMSFields() + * + * @param string $tabName The name of the tab or tabset. Subtabs can be referred to as TabSet.Tab or TabSet.Tab.Subtab. + * This function will create any missing tabs. + * @param array $fields An array of {@link FormField} objects. + */ + public function addFieldsToTab($tabName, $fields, $insertBefore = null) { + $this->flushFieldsCache(); + + // Find the tab + $tab = $this->findOrMakeTab($tabName); + + // Add the fields to the end of this set + foreach($fields as $field) { + // Check if a field by the same name exists in this tab + if($insertBefore) { + $tab->insertBefore($field, $insertBefore); + } elseif($tab->fieldByName($field->Name())) { + // It exists, so we need to replace the old one + $this->replaceField($field->Name(), $field); + } else { + $tab->push($field); + } + } + } + + /** + * Remove the given field from the given tab in the field. + * + * @param string $tabName The name of the tab + * @param string $fieldName The name of the field + */ + public function removeFieldFromTab($tabName, $fieldName) { + $this->flushFieldsCache(); + + // Find the tab + $tab = $this->findOrMakeTab($tabName); + $tab->removeByName($fieldName); + } + + /** + * Removes a number of fields from a Tab/TabSet within this FieldSet. + * + * @param string $tabName The name of the Tab or TabSet field + * @param array $fields A list of fields, e.g. array('Name', 'Email') + */ + public function removeFieldsFromTab($tabName, $fields) { + $this->flushFieldsCache(); + + // Find the tab + $tab = $this->findOrMakeTab($tabName); + + // Add the fields to the end of this set + foreach($fields as $field) $tab->removeByName($field); + } + + /** + * Remove a field from this FieldSet by Name. + * The field could also be inside a CompositeField. + * + * @param string $fieldName The name of the field or tab + * @param boolean $dataFieldOnly If this is true, then a field will only + * be removed if it's a data field. Dataless fields, such as tabs, will + * be left as-is. + */ + public function removeByName($fieldName, $dataFieldOnly = false) { + if(!$fieldName) { + user_error('FieldSet::removeByName() was called with a blank field name.', E_USER_WARNING); + } + $this->flushFieldsCache(); + + foreach($this->items as $i => $child) { + if(is_object($child)){ + $childName = $child->Name(); + if(!$childName) $childName = $child->Title(); + + if(($childName == $fieldName) && (!$dataFieldOnly || $child->hasData())) { + array_splice( $this->items, $i, 1 ); + break; + } else if($child->isComposite()) { + $child->removeByName($fieldName, $dataFieldOnly); + } + } + } + } + + /** + * Replace a single field with another. Ignores dataless fields such as Tabs and TabSets + * + * @param string $fieldName The name of the field to replace + * @param FormField $newField The field object to replace with + * @return boolean TRUE field was successfully replaced + * FALSE field wasn't found, nothing changed + */ + public function replaceField($fieldName, $newField) { + $this->flushFieldsCache(); + foreach($this->items as $i => $field) { + if(is_object($field)) { + if($field->Name() == $fieldName && $field->hasData()) { + $this->items[$i] = $newField; + return true; + + } else if($field->isComposite()) { + if($field->replaceField($fieldName, $newField)) return true; + } + } + } + return false; + } + + /** + * Rename the title of a particular field name in this set. + * + * @param string $fieldName Name of field to rename title of + * @param string $newFieldTitle New title of field + * @return boolean + */ + function renameField($fieldName, $newFieldTitle) { + $field = $this->dataFieldByName($fieldName); + if(!$field) return false; + + $field->setTitle($newFieldTitle); + + return $field->Title() == $newFieldTitle; + } + + /** + * @return boolean + */ + public function hasTabSet() { + foreach($this->items as $i => $field) { + if(is_object($field) && $field instanceof TabSet) { + return true; + } + } + + return false; + } + + /** + * Returns the specified tab object, creating it if necessary. + * + * @todo Support recursive creation of TabSets + * + * @param string $tabName The tab to return, in the form "Tab.Subtab.Subsubtab". + * Caution: Does not recursively create TabSet instances, you need to make sure everything + * up until the last tab in the chain exists. + * @param string $title Natural language title of the tab. If {@link $tabName} is passed in dot notation, + * the title parameter will only apply to the innermost referenced tab. + * The title is only changed if the tab doesn't exist already. + * @return Tab The found or newly created Tab instance + */ + public function findOrMakeTab($tabName, $title = null) { + $parts = explode('.',$tabName); + + // We could have made this recursive, but I've chosen to keep all the logic code within FieldSet rather than add it to TabSet and Tab too. + $currentPointer = $this; + foreach($parts as $k => $part) { + $parentPointer = $currentPointer; + $currentPointer = $currentPointer->fieldByName($part); + // Create any missing tabs + if(!$currentPointer) { + if(is_a($parentPointer, 'TabSet')) { + // use $title on the innermost tab only + if($title && $k == count($parts)-1) { + $currentPointer = new Tab($part, $title); + } else { + $currentPointer = new Tab($part); + } + $parentPointer->push($currentPointer); + } else { + $withName = ($parentPointer->hasMethod('Name')) ? " named '{$parentPointer->Name()}'" : null; + user_error("FieldSet::addFieldToTab() Tried to add a tab to object '{$parentPointer->class}'{$withName} - '$part' didn't exist.", E_USER_ERROR); + } + } + } + + return $currentPointer; + } + + /** + * Returns a named field. + * You can use dot syntax to get fields from child composite fields + * + * @todo Implement similiarly to dataFieldByName() to support nested sets - or merge with dataFields() + */ + public function fieldByName($name) { + if(strpos($name,'.') !== false) list($name, $remainder) = explode('.',$name,2); + else $remainder = null; + + foreach($this->items as $child) { + if(trim($name) == trim($child->Name()) || $name == $child->id) { + if($remainder) { + if($child->isComposite()) { + return $child->fieldByName($remainder); + } else { + user_error("Trying to get field '$remainder' from non-composite field $child->class.$name", E_USER_WARNING); + return null; + } + } else { + return $child; + } + } + } + } + + /** + * Returns a named field in a sequential set. + * Use this if you're using nested FormFields. + * + * @param string $name The name of the field to return + * @return FormField instance + */ + public function dataFieldByName($name) { + if($dataFields = $this->dataFields()) { + foreach($dataFields as $child) { + if(trim($name) == trim($child->Name()) || $name == $child->id) return $child; + } + } + } + + /** + * Inserts a field before a particular field in a FieldSet. + * + * @param FormField $item The form field to insert + * @param string $name Name of the field to insert before + */ + public function insertBefore($item, $name) { + $this->onBeforeInsert($item); + $item->setContainerFieldSet($this); + + $i = 0; + foreach($this->items as $child) { + if($name == $child->Name() || $name == $child->id) { + array_splice($this->items, $i, 0, array($item)); + return $item; + } elseif($child->isComposite()) { + $ret = $child->insertBefore($item, $name); + if($ret) return $ret; + } + $i++; + } + + return false; + } + + /** + * Inserts a field after a particular field in a FieldSet. + * + * @param FormField $item The form field to insert + * @param string $name Name of the field to insert after + */ + public function insertAfter($item, $name) { + $this->onBeforeInsert($item); + $item->setContainerFieldSet($this); + + $i = 0; + foreach($this->items as $child) { + if($name == $child->Name() || $name == $child->id) { + array_splice($this->items, $i+1, 0, array($item)); + return $item; + } elseif($child->isComposite()) { + $ret = $child->insertAfter($item, $name); + if($ret) return $ret; + } + $i++; + } + + return false; + } + + /** + * Push a single field into this FieldSet instance. + * + * @param FormField $item The FormField to add + * @param string $key An option array key (field name) + */ + public function push($item, $key = null) { + $this->onBeforeInsert($item); + $item->setContainerFieldSet($this); + return parent::push($item, $key = null); + } + + /** + * Handler method called before the FieldSet is going to be manipulated. + */ + protected function onBeforeInsert($item) { + $this->flushFieldsCache(); + if($item->Name()) $this->rootFieldSet()->removeByName($item->Name(), true); + } + + + /** + * Set the Form instance for this FieldSet. + * + * @param Form $form The form to set this FieldSet to + */ + public function setForm($form) { + foreach($this as $field) $field->setForm($form); + } + + /** + * Load the given data into this form. + * + * @param data An map of data to load into the FieldSet + */ + public function setValues($data) { + foreach($this->dataFields() as $field) { + $fieldName = $field->Name(); + if(isset($data[$fieldName])) $field->setValue($data[$fieldName]); + } + } + + /** + * Return all fields + * in a form - including fields nested in {@link CompositeFields}. + * Useful when doing custom field layouts. + * + * @return FieldSet + */ + function HiddenFields() { + $hiddenFields = new HiddenFieldSet(); + $dataFields = $this->dataFields(); + + if($dataFields) foreach($dataFields as $field) { + if($field instanceof HiddenField) $hiddenFields->push($field); + } + + return $hiddenFields; + } + + /** + * Transform this FieldSet with a given tranform method, + * e.g. $this->transform(new ReadonlyTransformation()) + * + * @return FieldSet + */ + function transform($trans) { + $this->flushFieldsCache(); + $newFields = new FieldSet(); + foreach($this as $field) { + $newFields->push($field->transform($trans)); + } + return $newFields; + } + + /** + * Returns the root field set that this belongs to + */ + function rootFieldSet() { + if($this->containerField) return $this->containerField->rootFieldSet(); + else return $this; + } + + function setContainerField($field) { + $this->containerField = $field; + } + + /** + * Transforms this FieldSet instance to readonly. + * + * @return FieldSet + */ + function makeReadonly() { + return $this->transform(new ReadonlyTransformation()); + } + + /** + * Transform the named field into a readonly feld. + * + * @param string|FormField + */ + function makeFieldReadonly($field) { + $fieldName = ($field instanceof FormField) ? $field->Name() : $field; + $srcField = $this->dataFieldByName($fieldName); + $this->replaceField($fieldName, $srcField->performReadonlyTransformation()); + } + + /** + * Change the order of fields in this FieldSet by specifying an ordered list of field names. + * This works well in conjunction with SilverStripe's scaffolding functions: take the scaffold, and + * shuffle the fields around to the order that you want. + * + * Please note that any tabs or other dataless fields will be clobbered by this operation. + * + * @param array $fieldNames Field names can be given as an array, or just as a list of arguments. + */ + function changeFieldOrder($fieldNames) { + // Field names can be given as an array, or just as a list of arguments. + if(!is_array($fieldNames)) $fieldNames = func_get_args(); + + // Build a map of fields indexed by their name. This will make the 2nd step much easier. + $fieldMap = array(); + foreach($this->dataFields() as $field) $fieldMap[$field->Name()] = $field; + + // Iterate through the ordered list of names, building a new array to be put into $this->items. + // While we're doing this, empty out $fieldMap so that we can keep track of leftovers. + // Unrecognised field names are okay; just ignore them + $fields = array(); + foreach($fieldNames as $fieldName) { + if(isset($fieldMap[$fieldName])) { + $fields[] = $fieldMap[$fieldName]; + unset($fieldMap[$fieldName]); + } + } + + // Add the leftover fields to the end of the list. + $fields = $fields + array_values($fieldMap); + + // Update our internal $this->items parameter. + $this->items = $fields; + + $this->flushFieldsCache(); + } + + /** + * Find the numerical position of a field within + * the children collection. Doesn't work recursively. + * + * @param string|FormField + * @return Position in children collection (first position starts with 0). Returns FALSE if the field can't be found. + */ + function fieldPosition($field) { + if(is_object($field)) $field = $field->Name(); + + $i = 0; + foreach($this->dataFields() as $child) { + if($child->Name() == $field) return $i; + $i++; + } + + return false; + } + +} + +/** + * A field list designed to store a list of hidden fields. When inserted into a template, only the + * input tags will be included + * + * @package forms + * @subpackage fields-structural + */ +class HiddenFieldList extends FieldList { + function forTemplate() { + $output = ""; + foreach($this as $field) { + $output .= $field->Field(); + } + return $output; + } +} \ No newline at end of file diff --git a/forms/FieldSet.php b/forms/FieldSet.php old mode 100755 new mode 100644 index 53d0049e8..e3ae4b8eb --- a/forms/FieldSet.php +++ b/forms/FieldSet.php @@ -1,558 +1,17 @@ 1) { - $items = func_get_args(); - } + user_error( + 'FieldSet is deprecated, please use FieldList instead.', E_USER_NOTICE + ); - parent::__construct($items); - - foreach ($items as $item) { - if ($item instanceof FormField) $item->setContainerFieldSet($this); - } - } - - /** - * Return a sequential set of all fields that have data. This excludes wrapper composite fields - * as well as heading / help text fields. - */ - public function dataFields() { - if(!$this->sequentialSet) $this->collateDataFields($this->sequentialSet); - return $this->sequentialSet; - } - - public function saveableFields() { - if(!$this->sequentialSaveableSet) $this->collateDataFields($this->sequentialSaveableSet, true); - return $this->sequentialSaveableSet; - } - - protected function flushFieldsCache() { - $this->sequentialSet = null; - $this->sequentialSaveableSet = null; - } - - protected function collateDataFields(&$list, $saveableOnly = false) { - foreach($this as $field) { - if($field->isComposite()) $field->collateDataFields($list, $saveableOnly); - - if($saveableOnly) { - $isIncluded = ($field->hasData() && !$field->isReadonly() && !$field->isDisabled()); - } else { - $isIncluded = ($field->hasData()); - } - if($isIncluded) { - $name = $field->Name(); - if(isset($list[$name])) { - $errSuffix = ""; - if($this->form) $errSuffix = " in your '{$this->form->class}' form called '" . $this->form->Name() . "'"; - else $errSuffix = ''; - user_error("collateDataFields() I noticed that a field called '$name' appears twice$errSuffix.", E_USER_ERROR); - } - $list[$name] = $field; - } - } - } - - /** - * Add an extra field to a tab within this fieldset. - * This is most commonly used when overloading getCMSFields() - * - * @param string $tabName The name of the tab or tabset. Subtabs can be referred to as TabSet.Tab or TabSet.Tab.Subtab. - * This function will create any missing tabs. - * @param FormField $field The {@link FormField} object to add to the end of that tab. - * @param string $insertBefore The name of the field to insert before. Optional. - */ - public function addFieldToTab($tabName, $field, $insertBefore = null) { - // This is a cache that must be flushed - $this->flushFieldsCache(); - - // Find the tab - $tab = $this->findOrMakeTab($tabName); - - // Add the field to the end of this set - if($insertBefore) $tab->insertBefore($field, $insertBefore); - else $tab->push($field); - } - - /** - * Add a number of extra fields to a tab within this fieldset. - * This is most commonly used when overloading getCMSFields() - * - * @param string $tabName The name of the tab or tabset. Subtabs can be referred to as TabSet.Tab or TabSet.Tab.Subtab. - * This function will create any missing tabs. - * @param array $fields An array of {@link FormField} objects. - */ - public function addFieldsToTab($tabName, $fields, $insertBefore = null) { - $this->flushFieldsCache(); - - // Find the tab - $tab = $this->findOrMakeTab($tabName); - - // Add the fields to the end of this set - foreach($fields as $field) { - // Check if a field by the same name exists in this tab - if($insertBefore) { - $tab->insertBefore($field, $insertBefore); - } elseif($tab->fieldByName($field->Name())) { - // It exists, so we need to replace the old one - $this->replaceField($field->Name(), $field); - } else { - $tab->push($field); - } - } + parent::__construct(!is_array($items) || func_num_args() > 1 ? func_get_args(): $items); } - /** - * Remove the given field from the given tab in the field. - * - * @param string $tabName The name of the tab - * @param string $fieldName The name of the field - */ - public function removeFieldFromTab($tabName, $fieldName) { - $this->flushFieldsCache(); - - // Find the tab - $tab = $this->findOrMakeTab($tabName); - $tab->removeByName($fieldName); - } - - /** - * Removes a number of fields from a Tab/TabSet within this FieldSet. - * - * @param string $tabName The name of the Tab or TabSet field - * @param array $fields A list of fields, e.g. array('Name', 'Email') - */ - public function removeFieldsFromTab($tabName, $fields) { - $this->flushFieldsCache(); - - // Find the tab - $tab = $this->findOrMakeTab($tabName); - - // Add the fields to the end of this set - foreach($fields as $field) $tab->removeByName($field); - } - - /** - * Remove a field from this FieldSet by Name. - * The field could also be inside a CompositeField. - * - * @param string $fieldName The name of the field or tab - * @param boolean $dataFieldOnly If this is true, then a field will only - * be removed if it's a data field. Dataless fields, such as tabs, will - * be left as-is. - */ - public function removeByName($fieldName, $dataFieldOnly = false) { - if(!$fieldName) { - user_error('FieldSet::removeByName() was called with a blank field name.', E_USER_WARNING); - } - $this->flushFieldsCache(); - - foreach($this->items as $i => $child) { - if(is_object($child)){ - $childName = $child->Name(); - if(!$childName) $childName = $child->Title(); - - if(($childName == $fieldName) && (!$dataFieldOnly || $child->hasData())) { - array_splice( $this->items, $i, 1 ); - break; - } else if($child->isComposite()) { - $child->removeByName($fieldName, $dataFieldOnly); - } - } - } - } - - /** - * Replace a single field with another. Ignores dataless fields such as Tabs and TabSets - * - * @param string $fieldName The name of the field to replace - * @param FormField $newField The field object to replace with - * @return boolean TRUE field was successfully replaced - * FALSE field wasn't found, nothing changed - */ - public function replaceField($fieldName, $newField) { - $this->flushFieldsCache(); - foreach($this->items as $i => $field) { - if(is_object($field)) { - if($field->Name() == $fieldName && $field->hasData()) { - $this->items[$i] = $newField; - return true; - - } else if($field->isComposite()) { - if($field->replaceField($fieldName, $newField)) return true; - } - } - } - return false; - } - - /** - * Rename the title of a particular field name in this set. - * - * @param string $fieldName Name of field to rename title of - * @param string $newFieldTitle New title of field - * @return boolean - */ - function renameField($fieldName, $newFieldTitle) { - $field = $this->dataFieldByName($fieldName); - if(!$field) return false; - - $field->setTitle($newFieldTitle); - - return $field->Title() == $newFieldTitle; - } - - /** - * @return boolean - */ - public function hasTabSet() { - foreach($this->items as $i => $field) { - if(is_object($field) && $field instanceof TabSet) { - return true; - } - } - - return false; - } - - /** - * Returns the specified tab object, creating it if necessary. - * - * @todo Support recursive creation of TabSets - * - * @param string $tabName The tab to return, in the form "Tab.Subtab.Subsubtab". - * Caution: Does not recursively create TabSet instances, you need to make sure everything - * up until the last tab in the chain exists. - * @param string $title Natural language title of the tab. If {@link $tabName} is passed in dot notation, - * the title parameter will only apply to the innermost referenced tab. - * The title is only changed if the tab doesn't exist already. - * @return Tab The found or newly created Tab instance - */ - public function findOrMakeTab($tabName, $title = null) { - $parts = explode('.',$tabName); - - // We could have made this recursive, but I've chosen to keep all the logic code within FieldSet rather than add it to TabSet and Tab too. - $currentPointer = $this; - foreach($parts as $k => $part) { - $parentPointer = $currentPointer; - $currentPointer = $currentPointer->fieldByName($part); - // Create any missing tabs - if(!$currentPointer) { - if(is_a($parentPointer, 'TabSet')) { - // use $title on the innermost tab only - if($title && $k == count($parts)-1) { - $currentPointer = new Tab($part, $title); - } else { - $currentPointer = new Tab($part); - } - $parentPointer->push($currentPointer); - } else { - $withName = ($parentPointer->hasMethod('Name')) ? " named '{$parentPointer->Name()}'" : null; - user_error("FieldSet::addFieldToTab() Tried to add a tab to object '{$parentPointer->class}'{$withName} - '$part' didn't exist.", E_USER_ERROR); - } - } - } - - return $currentPointer; - } - - /** - * Returns a named field. - * You can use dot syntax to get fields from child composite fields - * - * @todo Implement similiarly to dataFieldByName() to support nested sets - or merge with dataFields() - */ - public function fieldByName($name) { - if(strpos($name,'.') !== false) list($name, $remainder) = explode('.',$name,2); - else $remainder = null; - - foreach($this->items as $child) { - if(trim($name) == trim($child->Name()) || $name == $child->id) { - if($remainder) { - if($child->isComposite()) { - return $child->fieldByName($remainder); - } else { - user_error("Trying to get field '$remainder' from non-composite field $child->class.$name", E_USER_WARNING); - return null; - } - } else { - return $child; - } - } - } - } - - /** - * Returns a named field in a sequential set. - * Use this if you're using nested FormFields. - * - * @param string $name The name of the field to return - * @return FormField instance - */ - public function dataFieldByName($name) { - if($dataFields = $this->dataFields()) { - foreach($dataFields as $child) { - if(trim($name) == trim($child->Name()) || $name == $child->id) return $child; - } - } - } - - /** - * Inserts a field before a particular field in a FieldSet. - * - * @param FormField $item The form field to insert - * @param string $name Name of the field to insert before - */ - public function insertBefore($item, $name) { - $this->onBeforeInsert($item); - $item->setContainerFieldSet($this); - - $i = 0; - foreach($this->items as $child) { - if($name == $child->Name() || $name == $child->id) { - array_splice($this->items, $i, 0, array($item)); - return $item; - } elseif($child->isComposite()) { - $ret = $child->insertBefore($item, $name); - if($ret) return $ret; - } - $i++; - } - - return false; - } - - /** - * Inserts a field after a particular field in a FieldSet. - * - * @param FormField $item The form field to insert - * @param string $name Name of the field to insert after - */ - public function insertAfter($item, $name) { - $this->onBeforeInsert($item); - $item->setContainerFieldSet($this); - - $i = 0; - foreach($this->items as $child) { - if($name == $child->Name() || $name == $child->id) { - array_splice($this->items, $i+1, 0, array($item)); - return $item; - } elseif($child->isComposite()) { - $ret = $child->insertAfter($item, $name); - if($ret) return $ret; - } - $i++; - } - - return false; - } - - /** - * Push a single field into this FieldSet instance. - * - * @param FormField $item The FormField to add - * @param string $key An option array key (field name) - */ - public function push($item, $key = null) { - $this->onBeforeInsert($item); - $item->setContainerFieldSet($this); - return parent::push($item, $key = null); - } - - /** - * Handler method called before the FieldSet is going to be manipulated. - */ - protected function onBeforeInsert($item) { - $this->flushFieldsCache(); - if($item->Name()) $this->rootFieldSet()->removeByName($item->Name(), true); - } - - - /** - * Set the Form instance for this FieldSet. - * - * @param Form $form The form to set this FieldSet to - */ - public function setForm($form) { - foreach($this as $field) $field->setForm($form); - } - - /** - * Load the given data into this form. - * - * @param data An map of data to load into the FieldSet - */ - public function setValues($data) { - foreach($this->dataFields() as $field) { - $fieldName = $field->Name(); - if(isset($data[$fieldName])) $field->setValue($data[$fieldName]); - } - } - - /** - * Return all fields - * in a form - including fields nested in {@link CompositeFields}. - * Useful when doing custom field layouts. - * - * @return FieldSet - */ - function HiddenFields() { - $hiddenFields = new HiddenFieldSet(); - $dataFields = $this->dataFields(); - - if($dataFields) foreach($dataFields as $field) { - if($field instanceof HiddenField) $hiddenFields->push($field); - } - - return $hiddenFields; - } - - /** - * Transform this FieldSet with a given tranform method, - * e.g. $this->transform(new ReadonlyTransformation()) - * - * @return FieldSet - */ - function transform($trans) { - $this->flushFieldsCache(); - $newFields = new FieldSet(); - foreach($this as $field) { - $newFields->push($field->transform($trans)); - } - return $newFields; - } - - /** - * Returns the root field set that this belongs to - */ - function rootFieldSet() { - if($this->containerField) return $this->containerField->rootFieldSet(); - else return $this; - } - - function setContainerField($field) { - $this->containerField = $field; - } - - /** - * Transforms this FieldSet instance to readonly. - * - * @return FieldSet - */ - function makeReadonly() { - return $this->transform(new ReadonlyTransformation()); - } - - /** - * Transform the named field into a readonly feld. - * - * @param string|FormField - */ - function makeFieldReadonly($field) { - $fieldName = ($field instanceof FormField) ? $field->Name() : $field; - $srcField = $this->dataFieldByName($fieldName); - $this->replaceField($fieldName, $srcField->performReadonlyTransformation()); - } - - /** - * Change the order of fields in this FieldSet by specifying an ordered list of field names. - * This works well in conjunction with SilverStripe's scaffolding functions: take the scaffold, and - * shuffle the fields around to the order that you want. - * - * Please note that any tabs or other dataless fields will be clobbered by this operation. - * - * @param array $fieldNames Field names can be given as an array, or just as a list of arguments. - */ - function changeFieldOrder($fieldNames) { - // Field names can be given as an array, or just as a list of arguments. - if(!is_array($fieldNames)) $fieldNames = func_get_args(); - - // Build a map of fields indexed by their name. This will make the 2nd step much easier. - $fieldMap = array(); - foreach($this->dataFields() as $field) $fieldMap[$field->Name()] = $field; - - // Iterate through the ordered list of names, building a new array to be put into $this->items. - // While we're doing this, empty out $fieldMap so that we can keep track of leftovers. - // Unrecognised field names are okay; just ignore them - $fields = array(); - foreach($fieldNames as $fieldName) { - if(isset($fieldMap[$fieldName])) { - $fields[] = $fieldMap[$fieldName]; - unset($fieldMap[$fieldName]); - } - } - - // Add the leftover fields to the end of the list. - $fields = $fields + array_values($fieldMap); - - // Update our internal $this->items parameter. - $this->items = $fields; - - $this->flushFieldsCache(); - } - - /** - * Find the numerical position of a field within - * the children collection. Doesn't work recursively. - * - * @param string|FormField - * @return Position in children collection (first position starts with 0). Returns FALSE if the field can't be found. - */ - function fieldPosition($field) { - if(is_object($field)) $field = $field->Name(); - - $i = 0; - foreach($this->dataFields() as $child) { - if($child->Name() == $field) return $i; - $i++; - } - - return false; - } - -} - -/** - * A fieldset designed to store a list of hidden fields. When inserted into a template, only the - * input tags will be included - * - * @package forms - * @subpackage fields-structural - */ -class HiddenFieldSet extends FieldSet { - function forTemplate() { - $output = ""; - foreach($this as $field) { - $output .= $field->Field(); - } - return $output; - } -} - -?> +} \ No newline at end of file From 1f6f7f0862e425dd15b82b97c6476d6db136e703 Mon Sep 17 00:00:00 2001 From: ajshort Date: Wed, 11 May 2011 17:51:54 +1000 Subject: [PATCH 64/64] API CHANGE: Deprecated CompositeField->FieldSet() in favour of CompositeField->FieldList(). MINOR: Replaced usage of FieldSet with FieldList. MINOR: Renamed FieldSetTest to FieldListTest. --- admin/code/GroupImportForm.php | 4 +- admin/code/LeftAndMain.php | 14 +- admin/code/MemberImportForm.php | 4 +- admin/code/MemberTableField.php | 4 +- admin/code/ModelAdmin.php | 18 +- admin/code/SecurityAdmin.php | 4 +- admin/tests/MemberTableFieldTest.php | 8 +- filesystem/File.php | 2 +- filesystem/Folder.php | 4 +- forms/ComplexTableField.php | 6 +- forms/CompositeField.php | 35 ++-- forms/ConfirmedPasswordField.php | 2 +- forms/FieldList.php | 2 +- forms/FileField.php | 4 +- forms/FileIFrameField.php | 8 +- forms/Form.php | 24 +-- forms/FormField.php | 2 +- forms/FormScaffolder.php | 2 +- forms/HtmlEditorField.php | 14 +- forms/OptionsetField.php | 2 +- forms/ScaffoldingComplexTableField.php | 2 +- forms/SelectionGroup.php | 2 +- forms/SimpleImageField.php | 4 +- forms/TableField.php | 10 +- model/DataExtension.php | 14 +- model/DataObject.php | 4 +- search/SearchContext.php | 6 +- security/ChangePasswordForm.php | 4 +- security/Group.php | 2 +- security/Member.php | 2 +- security/MemberLoginForm.php | 8 +- security/Security.php | 4 +- security/SecurityToken.php | 4 +- tests/control/RequestHandlingTest.php | 12 +- tests/forms/CheckboxSetFieldTest.php | 4 +- tests/forms/ComplexTableFieldTest.php | 8 +- tests/forms/DatetimeFieldTest.php | 4 +- .../{FieldSetTest.php => FieldListTest.php} | 198 +++++++++--------- tests/forms/FileFieldTest.php | 8 +- tests/forms/FormScaffolderTest.php | 12 +- tests/forms/FormTest.php | 32 +-- .../MemberDatetimeOptionsetFieldTest.php | 8 +- tests/forms/TableFieldTest.php | 14 +- tests/forms/TableListFieldTest.php | 36 ++-- tests/search/SearchContextTest.php | 2 +- tests/security/GroupTest.php | 4 +- tests/security/SecurityTokenTest.php | 4 +- 47 files changed, 291 insertions(+), 284 deletions(-) rename tests/forms/{FieldSetTest.php => FieldListTest.php} (78%) diff --git a/admin/code/GroupImportForm.php b/admin/code/GroupImportForm.php index 3cab0c7d3..01e33264b 100644 --- a/admin/code/GroupImportForm.php +++ b/admin/code/GroupImportForm.php @@ -35,7 +35,7 @@ class GroupImportForm extends Form { $importSpec = $importer->getImportSpec(); $helpHtml = sprintf($helpHtml, implode(', ', array_keys($importSpec['fields']))); - $fields = new FieldSet( + $fields = new FieldList( new LiteralField('Help', $helpHtml), $fileField = new FileField( 'CsvFile', @@ -48,7 +48,7 @@ class GroupImportForm extends Form { $fileField->getValidator()->setAllowedExtensions(array('csv')); } - if(!$actions) $actions = new FieldSet( + if(!$actions) $actions = new FieldList( new FormAction('doImport', _t('SecurityAdmin_MemberImportForm.BtnImport', 'Import')) ); diff --git a/admin/code/LeftAndMain.php b/admin/code/LeftAndMain.php index d07b0eb25..b18083b37 100644 --- a/admin/code/LeftAndMain.php +++ b/admin/code/LeftAndMain.php @@ -746,7 +746,7 @@ class LeftAndMain extends Controller { * Calls {@link SiteTree->getCMSFields()} * * @param Int $id - * @param FieldSet $fields + * @param FieldList $fields * @return Form */ public function getEditForm($id = null, $fields = null) { @@ -847,7 +847,7 @@ class LeftAndMain extends Controller { $form = new Form( $this, "EditForm", - new FieldSet( + new FieldList( // new HeaderField( // 'WelcomeHeader', // $this->getApplicationName() @@ -861,7 +861,7 @@ class LeftAndMain extends Controller { // ) // ) ), - new FieldSet() + new FieldList() ); $form->unsetValidator(); $form->addExtraClass('cms-edit-form'); @@ -881,10 +881,10 @@ class LeftAndMain extends Controller { $form = new Form( $this, 'AddForm', - new FieldSet( + new FieldList( new HiddenField('ParentID') ), - new FieldSet( + new FieldList( $addAction = new FormAction('doAdd', _t('AssetAdmin_left.ss.GO','Go')) ) ); @@ -949,7 +949,7 @@ class LeftAndMain extends Controller { $form = new Form( $this, 'BatchActionsForm', - new FieldSet( + new FieldList( new HiddenField('csvIDs'), new DropdownField( 'Action', @@ -957,7 +957,7 @@ class LeftAndMain extends Controller { $actionsMap ) ), - new FieldSet( + new FieldList( // TODO i18n new FormAction('submit', "Go") ) diff --git a/admin/code/MemberImportForm.php b/admin/code/MemberImportForm.php index af288852b..b84397df4 100644 --- a/admin/code/MemberImportForm.php +++ b/admin/code/MemberImportForm.php @@ -34,7 +34,7 @@ class MemberImportForm extends Form { $importSpec = $importer->getImportSpec(); $helpHtml = sprintf($helpHtml, implode(', ', array_keys($importSpec['fields']))); - $fields = new FieldSet( + $fields = new FieldList( new LiteralField('Help', $helpHtml), $fileField = new FileField( 'CsvFile', @@ -47,7 +47,7 @@ class MemberImportForm extends Form { $fileField->getValidator()->setAllowedExtensions(array('csv')); } - if(!$actions) $actions = new FieldSet( + if(!$actions) $actions = new FieldList( new FormAction('doImport', _t('SecurityAdmin_MemberImportForm.BtnImport', 'Import')) ); diff --git a/admin/code/MemberTableField.php b/admin/code/MemberTableField.php index 863b81999..f64fd6f26 100644 --- a/admin/code/MemberTableField.php +++ b/admin/code/MemberTableField.php @@ -228,7 +228,7 @@ class MemberTableField extends ComplexTableField { * Add existing member to group by name (with JS-autocompletion) */ function AddRecordForm() { - $fields = new FieldSet(); + $fields = new FieldList(); foreach($this->FieldList() as $fieldName => $fieldTitle) { // If we're adding the set password field, we want to hide the text from any peeping eyes if($fieldName == 'SetPassword') { @@ -240,7 +240,7 @@ class MemberTableField extends ComplexTableField { if($this->group) { $fields->push(new HiddenField('ctf[ID]', null, $this->group->ID)); } - $actions = new FieldSet( + $actions = new FieldList( new FormAction('addtogroup', _t('MemberTableField.ADD','Add')) ); diff --git a/admin/code/ModelAdmin.php b/admin/code/ModelAdmin.php index 2eab7a7e9..53db227a0 100755 --- a/admin/code/ModelAdmin.php +++ b/admin/code/ModelAdmin.php @@ -402,7 +402,7 @@ class ModelAdmin_CollectionController extends Controller { $form = new Form($this, "SearchForm", $fields, - new FieldSet( + new FieldList( new FormAction('search', _t('MemberTableField.SEARCH', 'Search')), $clearAction = new ResetFormAction('clearsearch', _t('ModelAdmin.CLEAR_SEARCH','Clear Search')) ), @@ -434,8 +434,8 @@ class ModelAdmin_CollectionController extends Controller { $buttonLabel = sprintf(_t('ModelAdmin.CREATEBUTTON', "Create '%s'", PR_MEDIUM, "Create a new instance from a model class"), singleton($modelName)->i18n_singular_name()); $form = new Form($this, "CreateForm", - new FieldSet(), - new FieldSet($createButton = new FormAction('add', $buttonLabel)), + new FieldList(), + new FieldList($createButton = new FormAction('add', $buttonLabel)), $validator = new RequiredFields() ); $createButton->addExtraClass('ss-ui-action-constructive'); @@ -467,7 +467,7 @@ class ModelAdmin_CollectionController extends Controller { if(!singleton($modelName)->canCreate(Member::currentUser())) return false; - $fields = new FieldSet( + $fields = new FieldList( new HiddenField('ClassName', _t('ModelAdmin.CLASSTYPE'), $modelName), new FileField('_CsvFile', false) ); @@ -493,7 +493,7 @@ class ModelAdmin_CollectionController extends Controller { $fields->push(new LiteralField("SpecFor{$modelName}", $specHTML)); $fields->push(new CheckboxField('EmptyBeforeImport', 'Clear Database before import', false)); - $actions = new FieldSet( + $actions = new FieldList( new FormAction('import', _t('ModelAdmin.IMPORT', 'Import from CSV')) ); @@ -766,11 +766,11 @@ class ModelAdmin_CollectionController extends Controller { $form = new Form( $this, 'ResultsForm', - new FieldSet( + new FieldList( new HeaderField('SearchResults', _t('ModelAdmin.SEARCHRESULTS','Search Results'), 2), $tf ), - new FieldSet() + new FieldList() ); // Include the search criteria on the results form URL, but not dodgy variables like those below @@ -839,7 +839,7 @@ class ModelAdmin_CollectionController extends Controller { if(!$validator) $validator = new RequiredFields(); $validator->setJavascriptValidationHandler('none'); - $actions = new FieldSet ( + $actions = new FieldList ( new FormAction("doCreate", _t('ModelAdmin.ADDBUTTON', "Add")) ); @@ -1030,7 +1030,7 @@ class ModelAdmin_RecordController extends Controller { */ public function ViewForm() { $fields = $this->currentRecord->getCMSFields(); - $form = new Form($this, "EditForm", $fields, new FieldSet()); + $form = new Form($this, "EditForm", $fields, new FieldList()); $form->loadDataFrom($this->currentRecord); $form->makeReadonly(); return $form; diff --git a/admin/code/SecurityAdmin.php b/admin/code/SecurityAdmin.php index 0e68649ab..f1ac5815a 100644 --- a/admin/code/SecurityAdmin.php +++ b/admin/code/SecurityAdmin.php @@ -110,7 +110,7 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider { // unset 'inlineadd' permission, we don't want inline addition $memberList->setPermissions(array('edit', 'delete', 'add')); - $fields = new FieldSet( + $fields = new FieldList( new TabSet( 'Root', new Tab('Members', singleton('Member')->i18n_plural_name(), @@ -155,7 +155,7 @@ class SecurityAdmin extends LeftAndMain implements PermissionProvider { $rolesTab->push($rolesCTF); } - $actions = new FieldSet( + $actions = new FieldList( new FormAction('addmember',_t('SecurityAdmin.ADDMEMBER','Add Member')) ); diff --git a/admin/tests/MemberTableFieldTest.php b/admin/tests/MemberTableFieldTest.php index e665dc1df..f61edd47c 100644 --- a/admin/tests/MemberTableFieldTest.php +++ b/admin/tests/MemberTableFieldTest.php @@ -116,8 +116,8 @@ class MemberTableFieldTest_Controller extends Controller implements TestOnly { return new Form( $this, 'FormNoGroup', - new FieldSet(new MemberTableField($this, "Members", $group1)), - new FieldSet(new FormAction('submit')) + new FieldList(new MemberTableField($this, "Members", $group1)), + new FieldList(new FormAction('submit')) ); } @@ -131,8 +131,8 @@ class MemberTableFieldTest_Controller extends Controller implements TestOnly { return new Form( $this, 'FormNoGroup', - new FieldSet(new MemberTableField($this, "Members")), - new FieldSet(new FormAction('submit')) + new FieldList(new MemberTableField($this, "Members")), + new FieldList(new FormAction('submit')) ); } diff --git a/filesystem/File.php b/filesystem/File.php index c07a267b3..c4e8311cd 100755 --- a/filesystem/File.php +++ b/filesystem/File.php @@ -779,7 +779,7 @@ class File extends DataObject { * @return FieldSet */ function uploadMetadataFields() { - $fields = new FieldSet(); + $fields = new FieldList(); $fields->push(new TextField('Title', $this->fieldLabel('Title'))); $this->extend('updateUploadMetadataFields', $fields); diff --git a/filesystem/Folder.php b/filesystem/Folder.php index 0ad64b3ea..86b00eae4 100755 --- a/filesystem/Folder.php +++ b/filesystem/Folder.php @@ -386,7 +386,7 @@ class Folder extends File { /** * Return the FieldSet used to edit this folder in the CMS. * You can modify this fieldset by subclassing folder, or by creating a {@link DataExtension} - * and implemeting updateCMSFields(FieldSet $fields) on that extension. + * and implemeting updateCMSFields(FieldList $fields) on that extension. */ function getCMSFields() { $fileList = new AssetTableField( @@ -407,7 +407,7 @@ class Folder extends File { $deleteButton = new HiddenField('deletemarked'); } - $fields = new FieldSet( + $fields = new FieldList( new HiddenField("Name"), new TabSet("Root", new Tab("Files", _t('Folder.FILESTAB', "Files"), diff --git a/forms/ComplexTableField.php b/forms/ComplexTableField.php index aba398a5e..062836486 100644 --- a/forms/ComplexTableField.php +++ b/forms/ComplexTableField.php @@ -189,7 +189,7 @@ class ComplexTableField extends TableListField { * @param string $name * @param string $sourceClass * @param array $fieldList - * @param FieldSet $detailFormFields + * @param FieldList $detailFormFields * @param string $sourceFilter * @param string $sourceSort * @param string $sourceJoin @@ -331,7 +331,7 @@ JS; * @return FieldSet */ function createFieldSet() { - $fieldset = new FieldSet(); + $fieldset = new FieldList(); foreach($this->fieldTypes as $key => $fieldType){ $fieldset->push(new $fieldType($key)); } @@ -813,7 +813,7 @@ class ComplexTableField_Popup extends Form { Requirements::clear(); Requirements::unblock_all(); - $actions = new FieldSet(); + $actions = new FieldList(); if(!$readonly) { $actions->push( $saveAction = new FormAction( diff --git a/forms/CompositeField.php b/forms/CompositeField.php index 6936a3efa..2e0e2c041 100755 --- a/forms/CompositeField.php +++ b/forms/CompositeField.php @@ -9,7 +9,7 @@ class CompositeField extends FormField { /** - * @var FieldSet + * @var FieldList */ protected $children; @@ -29,13 +29,13 @@ class CompositeField extends FormField { protected $columnCount = null; public function __construct($children = null) { - if($children instanceof FieldSet) { + if($children instanceof FieldList) { $this->children = $children; } elseif(is_array($children)) { - $this->children = new FieldSet($children); + $this->children = new FieldList($children); } else { $children = is_array(func_get_args()) ? func_get_args() : array(); - $this->children = new FieldSet($children); + $this->children = new FieldList($children); } $this->children->setContainerField($this); @@ -46,12 +46,19 @@ class CompositeField extends FormField { } /** - * Returns all the sub-fields, suitable for <% control FieldSet %> + * Returns all the sub-fields, suitable for <% control FieldList %> */ - public function FieldSet() { + public function FieldList() { return $this->children; } - + + /** + * @deprecated 3.0 Please use {@link FieldList()}. + */ + public function FieldSet() { + return $this->FieldList(); + } + public function setID($id) { $this->id = $id; } @@ -62,14 +69,14 @@ class CompositeField extends FormField { /** * Accessor method for $this->children - * @return FieldSet + * @return FieldList */ public function getChildren() { return $this->children; } /** - * @param FieldSet $children + * @param FieldList $children */ public function setChildren($children) { $this->children = $children; @@ -79,7 +86,7 @@ class CompositeField extends FormField { * Returns the fields nested inside another DIV */ function FieldHolder() { - $fs = $this->FieldSet(); + $fs = $this->FieldList(); $idAtt = isset($this->id) ? " id=\"{$this->id}\"" : ''; $className = ($this->columnCount) ? "field CompositeField {$this->extraClass()} multicolumn" : "field CompositeField {$this->extraClass()}"; $content = "
    \n"; @@ -102,7 +109,7 @@ class CompositeField extends FormField { * Returns the fields in the restricted field holder inside a DIV. */ function SmallFieldHolder() {//return $this->FieldHolder(); - $fs = $this->FieldSet(); + $fs = $this->FieldList(); $idAtt = isset($this->id) ? " id=\"{$this->id}\"" : ''; $className = ($this->columnCount) ? "field CompositeField {$this->extraClass()} multicolumn" : "field CompositeField {$this->extraClass()}"; $content = "
    "; @@ -168,7 +175,7 @@ class CompositeField extends FormField { } /** - * @uses FieldSet->insertBefore() + * @uses FieldList->insertBefore() */ public function insertBefore($field, $insertBefore) { $ret = $this->children->insertBefore($field, $insertBefore); @@ -209,7 +216,7 @@ class CompositeField extends FormField { * versions of all the children */ public function performReadonlyTransformation() { - $newChildren = new FieldSet(); + $newChildren = new FieldList(); $clone = clone $this; foreach($clone->getChildren() as $idx => $child) { if(is_object($child)) $child = $child->transform(new ReadonlyTransformation()); @@ -226,7 +233,7 @@ class CompositeField extends FormField { * versions of all the children */ public function performDisabledTransformation($trans) { - $newChildren = new FieldSet(); + $newChildren = new FieldList(); $clone = clone $this; if($clone->getChildren()) foreach($clone->getChildren() as $idx => $child) { if(is_object($child)) { diff --git a/forms/ConfirmedPasswordField.php b/forms/ConfirmedPasswordField.php index 0da16d93b..5d96c5ad9 100644 --- a/forms/ConfirmedPasswordField.php +++ b/forms/ConfirmedPasswordField.php @@ -69,7 +69,7 @@ class ConfirmedPasswordField extends FormField { */ function __construct($name, $title = null, $value = "", $form = null, $showOnClick = false, $titleConfirmField = null) { // naming with underscores to prevent values from actually being saved somewhere - $this->children = new FieldSet( + $this->children = new FieldList( new PasswordField( "{$name}[_Password]", (isset($title)) ? $title : _t('Member.PASSWORD', 'Password') diff --git a/forms/FieldList.php b/forms/FieldList.php index 4b6af6065..1fa85b4fa 100755 --- a/forms/FieldList.php +++ b/forms/FieldList.php @@ -441,7 +441,7 @@ class FieldList extends ArrayList { */ function transform($trans) { $this->flushFieldsCache(); - $newFields = new FieldSet(); + $newFields = new FieldList(); foreach($this as $field) { $newFields->push($field->transform($trans)); } diff --git a/forms/FileField.php b/forms/FileField.php index e1110fed5..bcae2dd7f 100755 --- a/forms/FileField.php +++ b/forms/FileField.php @@ -17,11 +17,11 @@ * class ExampleForm_Controller extends Page_Controller { * * public function Form() { - * $fields = new FieldSet( + * $fields = new FieldList( * new TextField('MyName'), * new FileField('MyFile') * ); - * $actions = new FieldSet( + * $actions = new FieldList( * new FormAction('doUpload', 'Upload file') * ); * $validator = new RequiredFields(array('MyName', 'MyFile')); diff --git a/forms/FileIFrameField.php b/forms/FileIFrameField.php index 76c775be8..29166c684 100755 --- a/forms/FileIFrameField.php +++ b/forms/FileIFrameField.php @@ -137,7 +137,7 @@ class FileIFrameField extends FileField { $fileSources["existing//$selectFile"] = new TreeDropdownField('ExistingFile', '', 'File'); - $fields = new FieldSet ( + $fields = new FieldList ( new HeaderField('EditFileHeader', $title), new SelectionGroup('FileSource', $fileSources) ); @@ -151,7 +151,7 @@ class FileIFrameField extends FileField { $this, 'EditFileForm', $fields, - new FieldSet( + new FieldList( new FormAction('save', $title) ) ); @@ -223,10 +223,10 @@ class FileIFrameField extends FileField { $form = new Form ( $this, 'DeleteFileForm', - new FieldSet ( + new FieldList ( new HiddenField('DeleteFile', null, false) ), - new FieldSet ( + new FieldList ( $deleteButton = new FormAction ( 'delete', sprintf(_t('FileIFrameField.DELETE', 'Delete %s'), $this->FileTypeName()) ) diff --git a/forms/Form.php b/forms/Form.php index 6394dd7bd..af2e9a6e1 100755 --- a/forms/Form.php +++ b/forms/Form.php @@ -142,15 +142,15 @@ class Form extends RequestHandler { * * @param Controller $controller The parent controller, necessary to create the appropriate form action tag. * @param String $name The method on the controller that will return this form object. - * @param FieldSet $fields All of the fields in the form - a {@link FieldSet} of {@link FormField} objects. - * @param FieldSet $actions All of the action buttons in the form - a {@link FieldSet} of {@link FormAction} objects + * @param FieldList $fields All of the fields in the form - a {@link FieldSet} of {@link FormField} objects. + * @param FieldList $actions All of the action buttons in the form - a {@link FieldSet} of {@link FormAction} objects * @param Validator $validator Override the default validator instance (Default: {@link RequiredFields}) */ - function __construct($controller, $name, FieldSet $fields, FieldSet $actions, $validator = null) { + function __construct($controller, $name, FieldList $fields, FieldList $actions, $validator = null) { parent::__construct(); - if(!$fields instanceof FieldSet) throw new InvalidArgumentException('$fields must be a valid FieldSet instance'); - if(!$actions instanceof FieldSet) throw new InvalidArgumentException('$fields must be a valid FieldSet instance'); + if(!$fields instanceof FieldList) throw new InvalidArgumentException('$fields must be a valid FieldList instance'); + if(!$actions instanceof FieldList) throw new InvalidArgumentException('$fields must be a valid FieldList instance'); if($validator && !$validator instanceof Validator) throw new InvalidArgumentException('$validator must be a Valdidator instance'); $fields->setForm($this); @@ -398,13 +398,13 @@ class Form extends RequestHandler { } function transform(FormTransformation $trans) { - $newFields = new FieldSet(); + $newFields = new FieldList(); foreach($this->fields as $field) { $newFields->push($field->transform($trans)); } $this->fields = $newFields; - $newActions = new FieldSet(); + $newActions = new FieldList(); foreach($this->actions as $action) { $newActions->push($action->transform($trans)); } @@ -445,7 +445,7 @@ class Form extends RequestHandler { * Convert this form to another format. */ function transformTo(FormTransformation $format) { - $newFields = new FieldSet(); + $newFields = new FieldList(); foreach($this->fields as $field) { $newFields->push($field->transformTo($format)); } @@ -463,7 +463,7 @@ class Form extends RequestHandler { * @return FieldSet */ public function getExtraFields() { - $extraFields = new FieldSet(); + $extraFields = new FieldList(); $token = $this->getSecurityToken(); $tokenField = $token->updateFieldSet($this->fields); @@ -507,7 +507,7 @@ class Form extends RequestHandler { /** * Setter for the form fields. * - * @param FieldSet $fields + * @param FieldList $fields */ function setFields($fields) { $this->fields = $fields; @@ -540,7 +540,7 @@ class Form extends RequestHandler { /** * Setter for the form actions. * - * @param FieldSet $actions + * @param FieldList $actions */ function setActions($actions) { $this->actions = $actions; @@ -550,7 +550,7 @@ class Form extends RequestHandler { * Unset all form actions */ function unsetAllActions(){ - $this->actions = new FieldSet(); + $this->actions = new FieldList(); } /** diff --git a/forms/FormField.php b/forms/FormField.php index afb3c7493..7ee6920c4 100755 --- a/forms/FormField.php +++ b/forms/FormField.php @@ -653,7 +653,7 @@ HTML; /** * Set the fieldset that contains this field. * - * @param FieldSet $containerFieldSet + * @param FieldList $containerFieldSet */ function setContainerFieldSet($containerFieldSet) { $this->containerFieldSet = $containerFieldSet; diff --git a/forms/FormScaffolder.php b/forms/FormScaffolder.php index 05bf8800e..31d26e1a0 100755 --- a/forms/FormScaffolder.php +++ b/forms/FormScaffolder.php @@ -65,7 +65,7 @@ class FormScaffolder extends Object { * @return FieldSet */ public function getFieldSet() { - $fields = new FieldSet(); + $fields = new FieldList(); // tabbed or untabbed if($this->tabbed) { diff --git a/forms/HtmlEditorField.php b/forms/HtmlEditorField.php index 3a7afb1c4..093d68b05 100755 --- a/forms/HtmlEditorField.php +++ b/forms/HtmlEditorField.php @@ -235,7 +235,7 @@ class HtmlEditorField_Toolbar extends RequestHandler { $form = new Form( $this->controller, "{$this->name}/LinkForm", - new FieldSet( + new FieldList( new LiteralField( 'Heading', sprintf('

    %s

    ', _t('HtmlEditorField.LINK', 'Link')) @@ -263,7 +263,7 @@ class HtmlEditorField_Toolbar extends RequestHandler { new HiddenField('Locale', null, $this->controller->Locale) ) ), - new FieldSet( + new FieldList( new FormAction('insert', _t('HtmlEditorField.BUTTONINSERTLINK', 'Insert link')), new FormAction('remove', _t('HtmlEditorField.BUTTONREMOVELINK', 'Remove link')) ) @@ -291,7 +291,7 @@ class HtmlEditorField_Toolbar extends RequestHandler { throw new Exception('ThumbnailStripField class required for HtmlEditorField->ImageForm()'); } - $fields = new FieldSet( + $fields = new FieldList( new LiteralField( 'Heading', sprintf('

    %s

    ', _t('HtmlEditorField.IMAGE', 'Image')) @@ -299,7 +299,7 @@ class HtmlEditorField_Toolbar extends RequestHandler { $contentComposite = new CompositeField( new TreeDropdownField('FolderID', _t('HtmlEditorField.FOLDER', 'Folder'), 'Folder'), - new CompositeField(new FieldSet( + new CompositeField(new FieldList( new LiteralField('ShowUpload', '

    '. _t('HtmlEditorField.SHOWUPLOADFORM', 'Upload File') .'

    '), new FileField("Files[0]" , _t('AssetAdmin.CHOOSEFILE','Choose file: ')), new LiteralField('Response', '
    '), @@ -327,7 +327,7 @@ class HtmlEditorField_Toolbar extends RequestHandler { ) ); - $actions = new FieldSet( + $actions = new FieldList( new FormAction('insertimage', _t('HtmlEditorField.BUTTONINSERTIMAGE', 'Insert image')) ); @@ -359,7 +359,7 @@ class HtmlEditorField_Toolbar extends RequestHandler { $form = new Form( $this->controller, "{$this->name}/FlashForm", - new FieldSet( + new FieldList( new LiteralField( 'Heading', sprintf('

    %s

    ', _t('HtmlEditorField.FLASH', 'Flash')) @@ -374,7 +374,7 @@ class HtmlEditorField_Toolbar extends RequestHandler { ) ) ), - new FieldSet( + new FieldList( new FormAction("insertflash", _t('HtmlEditorField.BUTTONINSERTFLASH', 'Insert Flash')) ) ); diff --git a/forms/OptionsetField.php b/forms/OptionsetField.php index 4b32ae151..fbf5c40e4 100755 --- a/forms/OptionsetField.php +++ b/forms/OptionsetField.php @@ -34,7 +34,7 @@ * $map = $myDoSet->toDropDownMap(); * * // Instantiate the OptionsetField - * $fieldset = new Fieldset( + * $fieldset = new FieldList( * new OptionsetField( * $name = "Foobar", * $title = "FooBar's optionset", diff --git a/forms/ScaffoldingComplexTableField.php b/forms/ScaffoldingComplexTableField.php index eaec132f9..24e20a231 100644 --- a/forms/ScaffoldingComplexTableField.php +++ b/forms/ScaffoldingComplexTableField.php @@ -16,7 +16,7 @@ class ScaffoldingComplexTableField_Popup extends ComplexTableField_Popup { Requirements::clear(); - $actions = new FieldSet(); + $actions = new FieldList(); if(!$readonly) { $actions->push( $saveAction = new FormAction("saveComplexTableField", "Save") diff --git a/forms/SelectionGroup.php b/forms/SelectionGroup.php index 5a8ca5af2..06bd0e8a9 100755 --- a/forms/SelectionGroup.php +++ b/forms/SelectionGroup.php @@ -42,7 +42,7 @@ class SelectionGroup extends CompositeField { $newChildren[$idx] = $child; } - $clone->setChildren(new FieldSet($newChildren)); + $clone->setChildren(new FieldList($newChildren)); $clone->setReadonly(true); return $clone; } diff --git a/forms/SimpleImageField.php b/forms/SimpleImageField.php index 7a9db216b..128aba0ae 100755 --- a/forms/SimpleImageField.php +++ b/forms/SimpleImageField.php @@ -32,12 +32,12 @@ * * * function Form() { - * return new Form($this, "Form", new FieldSet( + * return new Form($this, "Form", new FieldList( * new SimpleImageField ( * $name = "FileTypeID", * $title = "Upload your FileType" * ) - * ), new FieldSet( + * ), new FieldList( * * // List the action buttons here - doform executes the function 'doform' below * new FormAction("doform", "Submit") diff --git a/forms/TableField.php b/forms/TableField.php index 7c1e36fd6..12bca602f 100644 --- a/forms/TableField.php +++ b/forms/TableField.php @@ -217,7 +217,7 @@ class TableField extends TableListField { $this, null, $this->FieldSetForRow(), - new FieldSet() + new FieldList() ); $form->loadDataFrom($dataObj); @@ -281,7 +281,7 @@ class TableField extends TableListField { * @return FieldSet */ function FieldSetForRow() { - $fieldset = new FieldSet(); + $fieldset = new FieldList(); if($this->fieldTypes){ foreach($this->fieldTypes as $key => $fieldType) { if(isset($fieldType->class) && is_subclass_of($fieldType, 'FormField')) { @@ -360,7 +360,7 @@ class TableField extends TableListField { } } - $form = new Form($this, null, $fieldset, new FieldSet()); + $form = new Form($this, null, $fieldset, new FieldList()); foreach ($dataObjects as $objectid => $fieldValues) { // 'new' counts as an empty column, don't save it @@ -592,7 +592,7 @@ JS; class TableField_Item extends TableListField_Item { /** - * @var FieldSet $fields + * @var FieldList $fields */ protected $fields; @@ -719,7 +719,7 @@ class TableField_Item extends TableListField_Item { $i++; } } - return new FieldSet($this->fields); + return new FieldList($this->fields); } function Fields() { diff --git a/model/DataExtension.php b/model/DataExtension.php index e62ec8f16..15fcf4557 100755 --- a/model/DataExtension.php +++ b/model/DataExtension.php @@ -160,11 +160,11 @@ abstract class DataExtension extends Extension { * should just be used to add or modify tabs, or fields which * are specific to the CMS-context. * - * Caution: Use {@link FieldSet->addFieldToTab()} to add fields. + * Caution: Use {@link FieldList->addFieldToTab()} to add fields. * - * @param FieldSet $fields FieldSet with a contained TabSet + * @param FieldList $fields FieldSet with a contained TabSet */ - function updateCMSFields(FieldSet &$fields) { + function updateCMSFields(FieldList $fields) { } /** @@ -173,18 +173,18 @@ abstract class DataExtension extends Extension { * * Caution: Use {@link FieldSet->push()} to add fields. * - * @param FieldSet $fields FieldSet without TabSet nesting + * @param FieldList $fields FieldSet without TabSet nesting */ - function updateFrontEndFields(FieldSet &$fields) { + function updateFrontEndFields(FieldList $fields) { } /** * This is used to provide modifications to the form actions * used in the CMS. {@link DataObject->getCMSActions()}. * - * @param FieldSet $actions FieldSet + * @param FieldList $actions FieldSet */ - function updateCMSActions(FieldSet &$actions) { + function updateCMSActions(FieldList $actions) { } /** diff --git a/model/DataObject.php b/model/DataObject.php index e1b9aff55..644a19327 100644 --- a/model/DataObject.php +++ b/model/DataObject.php @@ -1782,7 +1782,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity ), (array)$_params ); - $fields = new FieldSet(); + $fields = new FieldList(); foreach($this->searchableFields() as $fieldName => $spec) { if($params['restrictFields'] && !in_array($fieldName, $params['restrictFields'])) continue; @@ -1899,7 +1899,7 @@ class DataObject extends ViewableData implements DataObjectInterface, i18nEntity * @return an Empty FieldSet(); need to be overload by solid subclass */ public function getCMSActions() { - $actions = new FieldSet(); + $actions = new FieldList(); $this->extend('updateCMSActions', $actions); return $actions; } diff --git a/search/SearchContext.php b/search/SearchContext.php index ca4be07c2..1a5406f1d 100644 --- a/search/SearchContext.php +++ b/search/SearchContext.php @@ -63,14 +63,14 @@ class SearchContext extends Object { * * @param string $modelClass The base {@link DataObject} class that search properties related to. * Also used to generate a set of result objects based on this class. - * @param FieldSet $fields Optional. FormFields mapping to {@link DataObject::$db} properties + * @param FieldList $fields Optional. FormFields mapping to {@link DataObject::$db} properties * which are to be searched. Derived from modelclass using * {@link DataObject::scaffoldSearchFields()} if left blank. * @param array $filters Optional. Derived from modelclass if left blank */ function __construct($modelClass, $fields = null, $filters = null) { $this->modelClass = $modelClass; - $this->fields = ($fields) ? $fields : new FieldSet(); + $this->fields = ($fields) ? $fields : new FieldList(); $this->filters = ($filters) ? $filters : array(); parent::__construct(); @@ -241,7 +241,7 @@ class SearchContext extends Object { /** * Apply a list of searchable fields to the current search context. * - * @param FieldSet $fields + * @param FieldList $fields */ public function setFields($fields) { $this->fields = $fields; diff --git a/security/ChangePasswordForm.php b/security/ChangePasswordForm.php index 50e9bfd7d..7e933b6d9 100755 --- a/security/ChangePasswordForm.php +++ b/security/ChangePasswordForm.php @@ -27,7 +27,7 @@ class ChangePasswordForm extends Form { } if(!$fields) { - $fields = new FieldSet(); + $fields = new FieldList(); // Security/changepassword?h=XXX redirects to Security/changepassword // without GET parameter to avoid potential HTTP referer leakage. @@ -40,7 +40,7 @@ class ChangePasswordForm extends Form { $fields->push(new PasswordField("NewPassword2", _t('Member.CONFIRMNEWPASSWORD', "Confirm New Password"))); } if(!$actions) { - $actions = new FieldSet( + $actions = new FieldList( new FormAction("doChangePassword", _t('Member.BUTTONCHANGEPASSWORD', "Change Password")) ); } diff --git a/security/Group.php b/security/Group.php index 2f5416406..ebc5581cb 100644 --- a/security/Group.php +++ b/security/Group.php @@ -62,7 +62,7 @@ class Group extends DataObject { public function getCMSFields() { Requirements::javascript(SAPPHIRE_DIR . '/javascript/PermissionCheckboxSetField.js'); - $fields = new FieldSet( + $fields = new FieldList( new TabSet("Root", new Tab('Members', _t('SecurityAdmin.MEMBERS', 'Members'), new TextField("Title", $this->fieldLabel('Title')), diff --git a/security/Member.php b/security/Member.php index d646a0fca..074c4adcc 100644 --- a/security/Member.php +++ b/security/Member.php @@ -1570,7 +1570,7 @@ class Member_ProfileForm extends Form { $fields = $member->getCMSFields(); $fields->push(new HiddenField('ID','ID',$member->ID)); - $actions = new FieldSet( + $actions = new FieldList( new FormAction('dosave',_t('CMSMain.SAVE', 'Save')) ); diff --git a/security/MemberLoginForm.php b/security/MemberLoginForm.php index d46a10dea..f88b88bc2 100644 --- a/security/MemberLoginForm.php +++ b/security/MemberLoginForm.php @@ -50,16 +50,16 @@ class MemberLoginForm extends LoginForm { } if($checkCurrentUser && Member::currentUser() && Member::logged_in_session_exists()) { - $fields = new FieldSet( + $fields = new FieldList( new HiddenField("AuthenticationMethod", null, $this->authenticator_class, $this) ); - $actions = new FieldSet( + $actions = new FieldList( new FormAction("logout", _t('Member.BUTTONLOGINOTHER', "Log in as someone else")) ); } else { if(!$fields) { $label=singleton('Member')->fieldLabel(Member::get_unique_identifier_field()); - $fields = new FieldSet( + $fields = new FieldList( new HiddenField("AuthenticationMethod", null, $this->authenticator_class, $this), //Regardless of what the unique identifer field is (usually 'Email'), it will be held in the 'Email' value, below: new TextField("Email", $label, Session::get('SessionForms.MemberLoginForm.Email'), null, $this), @@ -73,7 +73,7 @@ class MemberLoginForm extends LoginForm { } } if(!$actions) { - $actions = new FieldSet( + $actions = new FieldList( new FormAction('dologin', _t('Member.BUTTONLOGIN', "Log in")), new LiteralField( 'forgotPassword', diff --git a/security/Security.php b/security/Security.php index afc8662be..8505be676 100644 --- a/security/Security.php +++ b/security/Security.php @@ -470,10 +470,10 @@ class Security extends Controller { return Object::create('MemberLoginForm', $this, 'LostPasswordForm', - new FieldSet( + new FieldList( new EmailField('Email', _t('Member.EMAIL', 'Email')) ), - new FieldSet( + new FieldList( new FormAction( 'forgotPassword', _t('Security.BUTTONSEND', 'Send me the password reset link') diff --git a/security/SecurityToken.php b/security/SecurityToken.php index 964c47de4..c1a996ddb 100644 --- a/security/SecurityToken.php +++ b/security/SecurityToken.php @@ -164,7 +164,7 @@ class SecurityToken extends Object { * on the returned {@link HiddenField}, you'll need to take * care of this yourself. * - * @param FieldSet $fieldset + * @param FieldList $fieldset * @return HiddenField|false */ function updateFieldSet(&$fieldset) { @@ -234,7 +234,7 @@ class NullSecurityToken extends SecurityToken { } /** - * @param FieldSet $fieldset + * @param FieldList $fieldset * @return false */ function updateFieldSet(&$fieldset) { diff --git a/tests/control/RequestHandlingTest.php b/tests/control/RequestHandlingTest.php index 23e4f1ea9..9420d29f3 100755 --- a/tests/control/RequestHandlingTest.php +++ b/tests/control/RequestHandlingTest.php @@ -298,10 +298,10 @@ class RequestHandlingTest_Controller extends Controller implements TestOnly { } function TestForm() { - return new RequestHandlingTest_Form($this, "TestForm", new FieldSet( + return new RequestHandlingTest_Form($this, "TestForm", new FieldList( new RequestHandlingTest_FormField("MyField"), new RequestHandlingTest_SubclassedFormField("SubclassedField") - ), new FieldSet( + ), new FieldList( new FormAction("myAction") )); } @@ -350,10 +350,10 @@ class RequestHandlingTest_FormActionController extends Controller { return new Form( $this, "Form", - new FieldSet( + new FieldList( new TextField("MyField") ), - new FieldSet( + new FieldList( new FormAction("formaction"), new FormAction('formactionInAllowedActions') ) @@ -472,8 +472,8 @@ class RequestHandlingTest_ControllerFormWithAllowedActions extends Controller im return new RequestHandlingTest_FormWithAllowedActions( $this, 'Form', - new FieldSet(), - new FieldSet( + new FieldList(), + new FieldList( new FormAction('allowedformaction'), new FormAction('disallowedformaction') // disallowed through $allowed_actions in form ) diff --git a/tests/forms/CheckboxSetFieldTest.php b/tests/forms/CheckboxSetFieldTest.php index 11b80e459..994b0e04d 100644 --- a/tests/forms/CheckboxSetFieldTest.php +++ b/tests/forms/CheckboxSetFieldTest.php @@ -117,8 +117,8 @@ class CheckboxSetFieldTest extends SapphireTest { $form = new Form( new Controller(), 'Form', - new FieldSet($field), - new FieldSet() + new FieldList($field), + new FieldList() ); $form->loadDataFrom($articleWithTags); $this->assertEquals( diff --git a/tests/forms/ComplexTableFieldTest.php b/tests/forms/ComplexTableFieldTest.php index a4620cefd..68e20272c 100644 --- a/tests/forms/ComplexTableFieldTest.php +++ b/tests/forms/ComplexTableFieldTest.php @@ -108,11 +108,11 @@ class ComplexTableFieldTest_Controller extends Controller { $form = new Form( $this, 'ManyManyForm', - new FieldSet( + new FieldList( new HiddenField('ID', '', $team->ID), $playersField ), - new FieldSet( + new FieldList( new FormAction('doSubmit', 'Submit') ) ); @@ -137,11 +137,11 @@ class ComplexTableFieldTest_Controller extends Controller { $form = new Form( $this, 'HasManyForm', - new FieldSet( + new FieldList( new HiddenField('ID', '', $team->ID), $sponsorsField ), - new FieldSet( + new FieldList( new FormAction('doSubmit', 'Submit') ) ); diff --git a/tests/forms/DatetimeFieldTest.php b/tests/forms/DatetimeFieldTest.php index 6456c7921..7a922eca8 100644 --- a/tests/forms/DatetimeFieldTest.php +++ b/tests/forms/DatetimeFieldTest.php @@ -22,10 +22,10 @@ class DatetimeFieldTest extends SapphireTest { $form = new Form( new Controller(), 'Form', - new FieldSet( + new FieldList( $f = new DatetimeField('MyDatetime', null) ), - new FieldSet( + new FieldList( new FormAction('doSubmit') ) ); diff --git a/tests/forms/FieldSetTest.php b/tests/forms/FieldListTest.php similarity index 78% rename from tests/forms/FieldSetTest.php rename to tests/forms/FieldListTest.php index d1e0a4e21..7191a67c2 100644 --- a/tests/forms/FieldSetTest.php +++ b/tests/forms/FieldListTest.php @@ -1,34 +1,34 @@ setValues()}. Need to check + * @todo test for {@link FieldList->setValues()}. Need to check * that the values that were set are the correct ones given back. - * @todo test for {@link FieldSet->transform()} and {@link FieldSet->makeReadonly()}. - * Need to ensure that it correctly transforms the FieldSet object. - * @todo test for {@link FieldSet->HiddenFields()}. Need to check + * @todo test for {@link FieldList->transform()} and {@link FieldList->makeReadonly()}. + * Need to ensure that it correctly transforms the FieldList object. + * @todo test for {@link FieldList->HiddenFields()}. Need to check * the fields returned are the correct HiddenField objects for a - * given FieldSet instance. - * @todo test for {@link FieldSet->dataFields()}. - * @todo test for {@link FieldSet->findOrMakeTab()}. + * given FieldList instance. + * @todo test for {@link FieldList->dataFields()}. + * @todo test for {@link FieldList->findOrMakeTab()}. * @todo the same as above with insertBefore() and insertAfter() * */ -class FieldSetTest extends SapphireTest { +class FieldListTest extends SapphireTest { /** * Test adding a field to a tab in a set. */ function testAddFieldToTab() { - $fields = new FieldSet(); + $fields = new FieldList(); $tab = new Tab('Root'); $fields->push($tab); - /* We add field objects to the FieldSet, using two different methods */ + /* We add field objects to the FieldList, using two different methods */ $fields->addFieldToTab('Root', new TextField('Country')); $fields->addFieldsToTab('Root', array( new EmailField('Email'), @@ -53,7 +53,7 @@ class FieldSetTest extends SapphireTest { * Test removing a single field from a tab in a set. */ function testRemoveSingleFieldFromTab() { - $fields = new FieldSet(); + $fields = new FieldList(); $tab = new Tab('Root'); $fields->push($tab); @@ -71,7 +71,7 @@ class FieldSetTest extends SapphireTest { } function testRemoveTab() { - $fields = new FieldSet(new TabSet( + $fields = new FieldList(new TabSet( 'Root', $tab1 = new Tab('Tab1'), $tab2 = new Tab('Tab2'), @@ -85,12 +85,12 @@ class FieldSetTest extends SapphireTest { } function testHasTabSet() { - $untabbedFields = new FieldSet( + $untabbedFields = new FieldList( new TextField('Field1') ); $this->assertFalse($untabbedFields->hasTabSet()); - $tabbedFields = new FieldSet( + $tabbedFields = new FieldList( new TabSet('Root', new Tab('Tab1') ) @@ -102,7 +102,7 @@ class FieldSetTest extends SapphireTest { * Test removing an array of fields from a tab in a set. */ function testRemoveMultipleFieldsFromTab() { - $fields = new FieldSet(); + $fields = new FieldList(); $tab = new Tab('Root'); $fields->push($tab); @@ -131,9 +131,9 @@ class FieldSetTest extends SapphireTest { * Test removing a field from a set by it's name. */ function testRemoveFieldByName() { - $fields = new FieldSet(); + $fields = new FieldList(); - /* First of all, we add a field into our FieldSet object */ + /* First of all, we add a field into our FieldList object */ $fields->push(new TextField('Name', 'Your name')); /* We have 1 field in our set now */ @@ -150,7 +150,7 @@ class FieldSetTest extends SapphireTest { * Test replacing a field with another one. */ function testReplaceField() { - $fields = new FieldSet(); + $fields = new FieldList(); $tab = new Tab('Root'); $fields->push($tab); @@ -168,7 +168,7 @@ class FieldSetTest extends SapphireTest { } function testRenameField() { - $fields = new FieldSet(); + $fields = new FieldList(); $nameField = new TextField('Name', 'Before title'); $fields->push($nameField); @@ -186,8 +186,8 @@ class FieldSetTest extends SapphireTest { } function testReplaceAFieldInADifferentTab() { - /* A FieldSet gets created with a TabSet and some field objects */ - $fieldSet = new FieldSet( + /* A FieldList gets created with a TabSet and some field objects */ + $FieldList = new FieldList( new TabSet('Root', $main = new Tab('Main', new TextField('A'), new TextField('B') @@ -197,8 +197,8 @@ class FieldSetTest extends SapphireTest { )) ); - /* The field "A" gets added to the FieldSet we just created created */ - $fieldSet->addFieldToTab('Root.Other', $newA = new TextField('A', 'New Title')); + /* The field "A" gets added to the FieldList we just created created */ + $FieldList->addFieldToTab('Root.Other', $newA = new TextField('A', 'New Title')); /* The field named "A" has been removed from the Main tab to make way for our new field named "A" in Other tab. */ $this->assertEquals(1, $main->Fields()->Count()); @@ -209,7 +209,7 @@ class FieldSetTest extends SapphireTest { * Test finding a field that's inside a tabset, within another tab. */ function testNestedTabsFindingFieldByName() { - $fields = new FieldSet(); + $fields = new FieldList(); /* 2 tabs get created within a TabSet inside our set */ $tab = new TabSet('Root', @@ -241,7 +241,7 @@ class FieldSetTest extends SapphireTest { } function testTabTitles() { - $set = new FieldSet( + $set = new FieldList( $rootTabSet = new TabSet('Root', $tabSetWithoutTitle = new TabSet('TabSetWithoutTitle'), $tabSetWithTitle = new TabSet('TabSetWithTitle', 'My TabSet Title', @@ -281,10 +281,10 @@ class FieldSetTest extends SapphireTest { /** * Test pushing a field to a set. * - * This tests {@link FieldSet->push()}. + * This tests {@link FieldList->push()}. */ function testPushFieldToSet() { - $fields = new FieldSet(); + $fields = new FieldList(); /* A field named Country is added to the set */ $fields->push(new TextField('Country')); @@ -310,10 +310,10 @@ class FieldSetTest extends SapphireTest { /** * Test inserting a field before another in a set. * - * This tests {@link FieldSet->insertBefore()}. + * This tests {@link FieldList->insertBefore()}. */ function testInsertBeforeFieldToSet() { - $fields = new FieldSet(); + $fields = new FieldList(); /* 3 fields are added to the set */ $fields->push(new TextField('Country')); @@ -337,7 +337,7 @@ class FieldSetTest extends SapphireTest { } function testInsertBeforeMultipleFields() { - $fields = new FieldSet( + $fields = new FieldList( $root = new TabSet("Root", $main = new Tab("Main", $a = new TextField("A"), @@ -363,7 +363,7 @@ class FieldSetTest extends SapphireTest { * Test inserting a field after another in a set. */ function testInsertAfterFieldToSet() { - $fields = new FieldSet(); + $fields = new FieldList(); /* 3 fields are added to the set */ $fields->push(new TextField('Country')); @@ -379,16 +379,16 @@ class FieldSetTest extends SapphireTest { /* The field we just added actually exists in the set */ $this->assertNotNull($fields->dataFieldByName('Title')); - /* We now have 4 fields in the FieldSet */ + /* We now have 4 fields in the FieldList */ $this->assertEquals(4, $fields->Count()); /* The position of the Title field should be at number 2 */ $this->assertEquals('Title', $fields[1]->Name()); } - function testRootFieldSet() { - /* Given a nested set of FormField, CompositeField, and FieldSet objects */ - $fieldSet = new FieldSet( + function testrootFieldSet() { + /* Given a nested set of FormField, CompositeField, and FieldList objects */ + $FieldList = new FieldList( $root = new TabSet("Root", $main = new Tab("Main", $a = new TextField("A"), @@ -397,27 +397,27 @@ class FieldSetTest extends SapphireTest { ) ); - /* rootFieldSet() should always evaluate to the same object: the topmost fieldset */ - $this->assertSame($fieldSet, $fieldSet->rootFieldSet()); - $this->assertSame($fieldSet, $root->rootFieldSet()); - $this->assertSame($fieldSet, $main->rootFieldSet()); - $this->assertSame($fieldSet, $a->rootFieldSet()); - $this->assertSame($fieldSet, $b->rootFieldSet()); + /* rootFieldSet() should always evaluate to the same object: the topmost FieldList */ + $this->assertSame($FieldList, $FieldList->rootFieldSet()); + $this->assertSame($FieldList, $root->rootFieldSet()); + $this->assertSame($FieldList, $main->rootFieldSet()); + $this->assertSame($FieldList, $a->rootFieldSet()); + $this->assertSame($FieldList, $b->rootFieldSet()); /* If we push additional fields, they should also have the same rootFieldSet() */ $root->push($other = new Tab("Other")); $other->push($c = new TextField("C")); $root->push($third = new Tab("Third", $d = new TextField("D"))); - $this->assertSame($fieldSet, $other->rootFieldSet()); - $this->assertSame($fieldSet, $third->rootFieldSet()); - $this->assertSame($fieldSet, $c->rootFieldSet()); - $this->assertSame($fieldSet, $d->rootFieldSet()); + $this->assertSame($FieldList, $other->rootFieldSet()); + $this->assertSame($FieldList, $third->rootFieldSet()); + $this->assertSame($FieldList, $c->rootFieldSet()); + $this->assertSame($FieldList, $d->rootFieldSet()); } function testAddingDuplicateReplacesOldField() { - /* Given a nested set of FormField, CompositeField, and FieldSet objects */ - $fieldSet = new FieldSet( + /* Given a nested set of FormField, CompositeField, and FieldList objects */ + $FieldList = new FieldList( $root = new TabSet("Root", $main = new Tab("Main", $a = new TextField("A"), @@ -430,27 +430,27 @@ class FieldSetTest extends SapphireTest { $newA = new TextField("A", "New A"); $newB = new TextField("B", "New B"); - $fieldSet->addFieldToTab("Root.Main", $newA); - $fieldSet->addFieldToTab("Root.Other", $newB); + $FieldList->addFieldToTab("Root.Main", $newA); + $FieldList->addFieldToTab("Root.Other", $newB); - $this->assertSame($newA, $fieldSet->dataFieldByName("A")); - $this->assertSame($newB, $fieldSet->dataFieldByName("B")); + $this->assertSame($newA, $FieldList->dataFieldByName("A")); + $this->assertSame($newB, $FieldList->dataFieldByName("B")); $this->assertEquals(1, $main->Fields()->Count()); /* Pushing fields on the end of the field set should remove them from the tab */ $thirdA = new TextField("A", "Third A"); $thirdB = new TextField("B", "Third B"); - $fieldSet->push($thirdA); - $fieldSet->push($thirdB); + $FieldList->push($thirdA); + $FieldList->push($thirdB); - $this->assertSame($thirdA, $fieldSet->fieldByName("A")); - $this->assertSame($thirdB, $fieldSet->fieldByName("B")); + $this->assertSame($thirdA, $FieldList->fieldByName("A")); + $this->assertSame($thirdB, $FieldList->fieldByName("B")); $this->assertEquals(0, $main->Fields()->Count()); } function testAddingFieldToNonExistentTabCreatesThatTab() { - $fieldSet = new FieldSet( + $FieldList = new FieldList( $root = new TabSet("Root", $main = new Tab("Main", $a = new TextField("A") @@ -459,13 +459,13 @@ class FieldSetTest extends SapphireTest { ); /* Add a field to a non-existent tab, and it will be created */ - $fieldSet->addFieldToTab("Root.Other", $b = new TextField("B")); - $this->assertNotNull($fieldSet->fieldByName('Root')->fieldByName('Other')); - $this->assertSame($b, $fieldSet->fieldByName('Root')->fieldByName('Other')->Fields()->First()); + $FieldList->addFieldToTab("Root.Other", $b = new TextField("B")); + $this->assertNotNull($FieldList->fieldByName('Root')->fieldByName('Other')); + $this->assertSame($b, $FieldList->fieldByName('Root')->fieldByName('Other')->Fields()->First()); } function testAddingFieldToATabWithTheSameNameAsTheField() { - $fieldSet = new FieldSet( + $FieldList = new FieldList( $root = new TabSet("Root", $main = new Tab("Main", $a = new TextField("A") @@ -475,13 +475,13 @@ class FieldSetTest extends SapphireTest { /* If you have a tab with the same name as the field, then technically it's a duplicate. However, it's allowed because tab isn't a data field. Only duplicate data fields are problematic */ - $fieldSet->addFieldToTab("Root.MyName", $myName = new TextField("MyName")); - $this->assertNotNull($fieldSet->fieldByName('Root')->fieldByName('MyName')); - $this->assertSame($myName, $fieldSet->fieldByName('Root')->fieldByName('MyName')->Fields()->First()); + $FieldList->addFieldToTab("Root.MyName", $myName = new TextField("MyName")); + $this->assertNotNull($FieldList->fieldByName('Root')->fieldByName('MyName')); + $this->assertSame($myName, $FieldList->fieldByName('Root')->fieldByName('MyName')->Fields()->First()); } function testInsertBeforeWithNestedCompositeFields() { - $fieldSet = new FieldSet( + $FieldList = new FieldList( new TextField('A_pre'), new TextField('A'), new TextField('A_post'), @@ -497,34 +497,34 @@ class FieldSetTest extends SapphireTest { ) ); - $fieldSet->insertBefore( + $FieldList->insertBefore( $A_insertbefore = new TextField('A_insertbefore'), 'A' ); $this->assertSame( $A_insertbefore, - $fieldSet->dataFieldByName('A_insertbefore'), - 'Field on toplevel fieldset can be inserted' + $FieldList->dataFieldByName('A_insertbefore'), + 'Field on toplevel FieldList can be inserted' ); - $fieldSet->insertBefore( + $FieldList->insertBefore( $B_insertbefore = new TextField('B_insertbefore'), 'B' ); $this->assertSame( - $fieldSet->dataFieldByName('B_insertbefore'), + $FieldList->dataFieldByName('B_insertbefore'), $B_insertbefore, - 'Field on one nesting level fieldset can be inserted' + 'Field on one nesting level FieldList can be inserted' ); - $fieldSet->insertBefore( + $FieldList->insertBefore( $C_insertbefore = new TextField('C_insertbefore'), 'C' ); $this->assertSame( - $fieldSet->dataFieldByName('C_insertbefore'), + $FieldList->dataFieldByName('C_insertbefore'), $C_insertbefore, - 'Field on two nesting levels fieldset can be inserted' + 'Field on two nesting levels FieldList can be inserted' ); } @@ -532,7 +532,7 @@ class FieldSetTest extends SapphireTest { * @todo check actual placement of fields */ function testInsertBeforeWithNestedTabsets() { - $fieldSetA = new FieldSet( + $FieldListA = new FieldList( $tabSetA = new TabSet('TabSet_A', $tabA1 = new Tab('Tab_A1', new TextField('A_pre'), @@ -549,7 +549,7 @@ class FieldSetTest extends SapphireTest { 'A' ); $this->assertEquals( - $fieldSetA->dataFieldByName('A_insertbefore'), + $FieldListA->dataFieldByName('A_insertbefore'), $A_insertbefore, 'Field on toplevel tab can be inserted' ); @@ -559,7 +559,7 @@ class FieldSetTest extends SapphireTest { $this->assertEquals(2, $tabA1->fieldPosition('A')); $this->assertEquals(3, $tabA1->fieldPosition('A_post')); - $fieldSetB = new FieldSet( + $FieldListB = new FieldList( new TabSet('TabSet_A', $tabsetB = new TabSet('TabSet_B', $tabB1 = new Tab('Tab_B1', @@ -573,12 +573,12 @@ class FieldSetTest extends SapphireTest { ) ) ); - $fieldSetB->insertBefore( + $FieldListB->insertBefore( $B_insertbefore = new TextField('B_insertbefore'), 'B' ); $this->assertSame( - $fieldSetB->dataFieldByName('B_insertbefore'), + $FieldListB->dataFieldByName('B_insertbefore'), $B_insertbefore, 'Field on nested tab can be inserted' ); @@ -589,7 +589,7 @@ class FieldSetTest extends SapphireTest { } function testInsertAfterWithNestedCompositeFields() { - $fieldSet = new FieldSet( + $FieldList = new FieldList( new TextField('A_pre'), new TextField('A'), new TextField('A_post'), @@ -605,34 +605,34 @@ class FieldSetTest extends SapphireTest { ) ); - $fieldSet->insertAfter( + $FieldList->insertAfter( $A_insertafter = new TextField('A_insertafter'), 'A' ); $this->assertSame( $A_insertafter, - $fieldSet->dataFieldByName('A_insertafter'), - 'Field on toplevel fieldset can be inserted after' + $FieldList->dataFieldByName('A_insertafter'), + 'Field on toplevel FieldList can be inserted after' ); - $fieldSet->insertAfter( + $FieldList->insertAfter( $B_insertafter = new TextField('B_insertafter'), 'B' ); $this->assertSame( - $fieldSet->dataFieldByName('B_insertafter'), + $FieldList->dataFieldByName('B_insertafter'), $B_insertafter, - 'Field on one nesting level fieldset can be inserted after' + 'Field on one nesting level FieldList can be inserted after' ); - $fieldSet->insertAfter( + $FieldList->insertAfter( $C_insertafter = new TextField('C_insertafter'), 'C' ); $this->assertSame( - $fieldSet->dataFieldByName('C_insertafter'), + $FieldList->dataFieldByName('C_insertafter'), $C_insertafter, - 'Field on two nesting levels fieldset can be inserted after' + 'Field on two nesting levels FieldList can be inserted after' ); } @@ -640,7 +640,7 @@ class FieldSetTest extends SapphireTest { * @todo check actual placement of fields */ function testInsertAfterWithNestedTabsets() { - $fieldSetA = new FieldSet( + $FieldListA = new FieldList( $tabSetA = new TabSet('TabSet_A', $tabA1 = new Tab('Tab_A1', new TextField('A_pre'), @@ -657,7 +657,7 @@ class FieldSetTest extends SapphireTest { 'A' ); $this->assertEquals( - $fieldSetA->dataFieldByName('A_insertafter'), + $FieldListA->dataFieldByName('A_insertafter'), $A_insertafter, 'Field on toplevel tab can be inserted after' ); @@ -666,7 +666,7 @@ class FieldSetTest extends SapphireTest { $this->assertEquals(2, $tabA1->fieldPosition('A_insertafter')); $this->assertEquals(3, $tabA1->fieldPosition('A_post')); - $fieldSetB = new FieldSet( + $FieldListB = new FieldList( new TabSet('TabSet_A', $tabsetB = new TabSet('TabSet_B', $tabB1 = new Tab('Tab_B1', @@ -680,12 +680,12 @@ class FieldSetTest extends SapphireTest { ) ) ); - $fieldSetB->insertAfter( + $FieldListB->insertAfter( $B_insertafter = new TextField('B_insertafter'), 'B' ); $this->assertSame( - $fieldSetB->dataFieldByName('B_insertafter'), + $FieldListB->dataFieldByName('B_insertafter'), $B_insertafter, 'Field on nested tab can be inserted after' ); @@ -696,7 +696,7 @@ class FieldSetTest extends SapphireTest { } function testFieldPosition() { - $set = new FieldSet( + $set = new FieldList( new TextField('A'), new TextField('B'), new TextField('C') @@ -716,17 +716,17 @@ class FieldSetTest extends SapphireTest { } function testMakeFieldReadonly() { - $fieldSet = new FieldSet( + $FieldList = new FieldList( new TabSet('Root', new Tab('Main', new TextField('A'), new TextField('B') ) )); - $fieldSet->makeFieldReadonly('A'); + $FieldList->makeFieldReadonly('A'); $this->assertTrue( - $fieldSet->dataFieldByName('A')->isReadonly(), - 'Field nested inside a TabSet and FieldSet can be marked readonly by FieldSet->makeFieldReadonly()' + $FieldList->dataFieldByName('A')->isReadonly(), + 'Field nested inside a TabSet and FieldList can be marked readonly by FieldList->makeFieldReadonly()' ); } } diff --git a/tests/forms/FileFieldTest.php b/tests/forms/FileFieldTest.php index b9630739c..160a3ef10 100644 --- a/tests/forms/FileFieldTest.php +++ b/tests/forms/FileFieldTest.php @@ -12,10 +12,10 @@ class FileFieldTest extends FunctionalTest { $form = new Form( new Controller(), 'Form', - new FieldSet( + new FieldList( $fileField = new FileField('cv', 'Upload your CV') ), - new FieldSet() + new FieldList() ); $fileFieldValue = array( 'name' => 'aCV.txt', @@ -38,10 +38,10 @@ class FileFieldTest extends FunctionalTest { $form = new Form( new Controller(), 'Form', - new FieldSet( + new FieldList( $fileField = new FileField('cv', 'Upload your CV') ), - new FieldSet(), + new FieldList(), new RequiredFields('cv') ); // All fields are filled but for some reason an error occured when uploading the file => fails diff --git a/tests/forms/FormScaffolderTest.php b/tests/forms/FormScaffolderTest.php index 48a0e9a8a..3da9c0446 100644 --- a/tests/forms/FormScaffolderTest.php +++ b/tests/forms/FormScaffolderTest.php @@ -20,7 +20,7 @@ class FormScaffolderTest extends SapphireTest { function testGetCMSFieldsSingleton() { $fields = singleton('FormScaffolderTest_Article')->getCMSFields(); - $form = new Form(new Controller(), 'TestForm', $fields, new FieldSet()); + $form = new Form(new Controller(), 'TestForm', $fields, new FieldList()); $form->loadDataFrom(singleton('FormScaffolderTest_Article')); $this->assertTrue($fields->hasTabSet(), 'getCMSFields() produces a TabSet'); @@ -34,7 +34,7 @@ class FormScaffolderTest extends SapphireTest { $article1 = $this->objFromFixture('FormScaffolderTest_Article', 'article1'); $fields = $article1->getCMSFields(); - $form = new Form(new Controller(), 'TestForm', $fields, new FieldSet()); + $form = new Form(new Controller(), 'TestForm', $fields, new FieldList()); $form->loadDataFrom($article1); $this->assertNotNull($fields->dataFieldByName('AuthorID'), 'getCMSFields() includes has_one fields on instances'); @@ -45,7 +45,7 @@ class FormScaffolderTest extends SapphireTest { $article1 = $this->objFromFixture('FormScaffolderTest_Article', 'article1'); $fields = $article1->getCMSFields(); - $form = new Form(new Controller(), 'TestForm', $fields, new FieldSet()); + $form = new Form(new Controller(), 'TestForm', $fields, new FieldList()); $form->loadDataFrom($article1); $this->assertNotNull( @@ -60,7 +60,7 @@ class FormScaffolderTest extends SapphireTest { $fields = $article1->scaffoldFormFields(array( 'restrictFields' => array('Title') )); - $form = new Form(new Controller(), 'TestForm', $fields, new FieldSet()); + $form = new Form(new Controller(), 'TestForm', $fields, new FieldList()); $form->loadDataFrom($article1); $this->assertNotNull($fields->dataFieldByName('Title'), 'scaffoldCMSFields() includes explitly defined "restrictFields"'); @@ -73,7 +73,7 @@ class FormScaffolderTest extends SapphireTest { $fields = $article1->scaffoldFormFields(array( 'fieldClasses' => array('Title' => 'HtmlEditorField') )); - $form = new Form(new Controller(), 'TestForm', $fields, new FieldSet()); + $form = new Form(new Controller(), 'TestForm', $fields, new FieldList()); $form->loadDataFrom($article1); $this->assertNotNull( @@ -88,7 +88,7 @@ class FormScaffolderTest extends SapphireTest { function testGetFormFields() { $fields = singleton('FormScaffolderTest_Article')->getFrontEndFields(); - $form = new Form(new Controller(), 'TestForm', $fields, new FieldSet()); + $form = new Form(new Controller(), 'TestForm', $fields, new FieldList()); $form->loadDataFrom(singleton('FormScaffolderTest_Article')); $this->assertFalse($fields->hasTabSet(), 'getFrontEndFields() doesnt produce a TabSet by default'); diff --git a/tests/forms/FormTest.php b/tests/forms/FormTest.php index aaf522800..06463cc62 100755 --- a/tests/forms/FormTest.php +++ b/tests/forms/FormTest.php @@ -16,13 +16,13 @@ class FormTest extends FunctionalTest { $form = new Form( new Controller(), 'Form', - new FieldSet( + new FieldList( new TextField('key1'), new TextField('namespace[key2]'), new TextField('namespace[key3][key4]'), new TextField('othernamespace[key5][key6][key7]') ), - new FieldSet() + new FieldList() ); // url would be ?key1=val1&namespace[key2]=val2&namespace[key3][key4]=val4&othernamespace[key5][key6][key7]=val7 @@ -56,11 +56,11 @@ class FormTest extends FunctionalTest { $form = new Form( new Controller(), 'Form', - new FieldSet( + new FieldList( new TextField('key1'), new TextField('key2') ), - new FieldSet() + new FieldList() ); $form->loadDataFrom(array( 'key1' => 'save', @@ -81,14 +81,14 @@ class FormTest extends FunctionalTest { $form = new Form( new Controller(), 'Form', - new FieldSet( + new FieldList( new HeaderField('MyPlayerHeader','My Player'), new TextField('Name'), // appears in both Player and Team new TextareaField('Biography'), new DateField('Birthday'), new NumericField('BirthdayYear') // dynamic property ), - new FieldSet() + new FieldList() ); $captainWithDetails = $this->objFromFixture('FormTest_Player', 'captainWithDetails'); @@ -122,7 +122,7 @@ class FormTest extends FunctionalTest { $form = new Form( new Controller(), 'Form', - new FieldSet( + new FieldList( new HeaderField('MyPlayerHeader','My Player'), new TextField('Name'), // appears in both Player and Team new TextareaField('Biography'), @@ -131,7 +131,7 @@ class FormTest extends FunctionalTest { $unrelatedField = new TextField('UnrelatedFormField') //new CheckboxSetField('Teams') // relation editing ), - new FieldSet() + new FieldList() ); $unrelatedField->setValue("random value"); @@ -329,8 +329,8 @@ class FormTest extends FunctionalTest { return new Form( new Controller(), 'Form', - new FieldSet(new TextField('key1')), - new FieldSet() + new FieldList(new TextField('key1')), + new FieldList() ); } @@ -383,12 +383,12 @@ class FormTest_Controller extends Controller implements TestOnly { $form = new Form( $this, 'Form', - new FieldSet( + new FieldList( new EmailField('Email'), new TextField('SomeRequiredField'), new CheckboxSetField('Boxes', null, array('1'=>'one','2'=>'two')) ), - new FieldSet( + new FieldList( new FormAction('doSubmit') ), new RequiredFields( @@ -407,10 +407,10 @@ class FormTest_Controller extends Controller implements TestOnly { $form = new Form( $this, 'FormWithSecurityToken', - new FieldSet( + new FieldList( new EmailField('Email') ), - new FieldSet( + new FieldList( new FormAction('doSubmit') ) ); @@ -444,10 +444,10 @@ class FormTest_ControllerWithSecurityToken extends Controller implements TestOnl $form = new Form( $this, 'Form', - new FieldSet( + new FieldList( new EmailField('Email') ), - new FieldSet( + new FieldList( new FormAction('doSubmit') ) ); diff --git a/tests/forms/MemberDatetimeOptionsetFieldTest.php b/tests/forms/MemberDatetimeOptionsetFieldTest.php index 25f339b61..98616a165 100644 --- a/tests/forms/MemberDatetimeOptionsetFieldTest.php +++ b/tests/forms/MemberDatetimeOptionsetFieldTest.php @@ -45,7 +45,7 @@ class MemberDatetimeOptionsetFieldTest extends SapphireTest { function testDateFormatDefaultCheckedInFormField() { $field = $this->createDateFormatFieldForMember($this->objFromFixture('Member', 'noformatmember')); - $field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldSet(), new FieldSet())); // fake form + $field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldList(), new FieldList())); // fake form $parser = new CSSContentParser($field->Field()); $xmlArr = $parser->getBySelector('#Form_Form_DateFormat_MM_dd_yyyy'); $this->assertEquals('checked', (string) $xmlArr[0]['checked']); @@ -53,7 +53,7 @@ class MemberDatetimeOptionsetFieldTest extends SapphireTest { function testTimeFormatDefaultCheckedInFormField() { $field = $this->createTimeFormatFieldForMember($this->objFromFixture('Member', 'noformatmember')); - $field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldSet(), new FieldSet())); // fake form + $field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldList(), new FieldList())); // fake form $parser = new CSSContentParser($field->Field()); $xmlArr = $parser->getBySelector('#Form_Form_TimeFormat_hh_mm_a'); $this->assertEquals('checked', (string) $xmlArr[0]['checked']); @@ -63,7 +63,7 @@ class MemberDatetimeOptionsetFieldTest extends SapphireTest { $member = $this->objFromFixture('Member', 'noformatmember'); $member->setField('DateFormat', 'MM/dd/yyyy'); $field = $this->createDateFormatFieldForMember($member); - $field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldSet(), new FieldSet())); // fake form + $field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldList(), new FieldList())); // fake form $parser = new CSSContentParser($field->Field()); $xmlArr = $parser->getBySelector('#Form_Form_DateFormat_MM_dd_yyyy'); $this->assertEquals('checked', (string) $xmlArr[0]['checked']); @@ -73,7 +73,7 @@ class MemberDatetimeOptionsetFieldTest extends SapphireTest { $member = $this->objFromFixture('Member', 'noformatmember'); $member->setField('DateFormat', 'dd MM yy'); $field = $this->createDateFormatFieldForMember($member); - $field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldSet(), new FieldSet())); // fake form + $field->setForm(new Form(new MemberDatetimeOptionsetFieldTest_Controller(), 'Form', new FieldList(), new FieldList())); // fake form $parser = new CSSContentParser($field->Field()); $xmlInputArr = $parser->getBySelector('.valCustom input'); $xmlPreview = $parser->getBySelector('.preview'); diff --git a/tests/forms/TableFieldTest.php b/tests/forms/TableFieldTest.php index e18391f5d..dc68c44a6 100755 --- a/tests/forms/TableFieldTest.php +++ b/tests/forms/TableFieldTest.php @@ -28,8 +28,8 @@ class TableFieldTest extends SapphireTest { $form = new Form( new TableFieldTest_Controller(), "Form", - new FieldSet($tableField), - new FieldSet() + new FieldList($tableField), + new FieldList() ); // Test Insert @@ -106,8 +106,8 @@ class TableFieldTest extends SapphireTest { $form = new Form( new TableFieldTest_Controller(), "Form", - new FieldSet($tableField), - new FieldSet() + new FieldList($tableField), + new FieldList() ); $this->assertEquals(2, $tableField->sourceItems()->Count()); @@ -155,8 +155,8 @@ class TableFieldTest extends SapphireTest { $form = new Form( new TableFieldTest_Controller(), "Form", - new FieldSet($tableField), - new FieldSet() + new FieldList($tableField), + new FieldList() ); $this->assertContains($perm1->ID, $tableField->sourceItems()->column('ID')); @@ -185,7 +185,7 @@ class TableFieldTest extends SapphireTest { ); // Test with auto relation setting - $form = new Form(new TableFieldTest_Controller(), "Form", new FieldSet($tf), new FieldSet()); + $form = new Form(new TableFieldTest_Controller(), "Form", new FieldList($tf), new FieldList()); $form->loadDataFrom($o); $tf->setValue(array( diff --git a/tests/forms/TableListFieldTest.php b/tests/forms/TableListFieldTest.php index 8be886f49..dd3129634 100755 --- a/tests/forms/TableListFieldTest.php +++ b/tests/forms/TableListFieldTest.php @@ -17,9 +17,9 @@ class TableListFieldTest extends SapphireTest { "E" => "Col E", )); // A TableListField must be inside a form for its links to be generated - $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet( + $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList( $table - ), new FieldSet()); + ), new FieldList()); $result = $table->FieldHolder(); @@ -45,9 +45,9 @@ class TableListFieldTest extends SapphireTest { "E" => "Col E", )); // A TableListField must be inside a form for its links to be generated - $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet( + $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList( $table - ), new FieldSet()); + ), new FieldList()); $items = $table->sourceItems(); $this->assertNotNull($items); @@ -78,9 +78,9 @@ class TableListFieldTest extends SapphireTest { "E" => "Col E", )); // A TableListField must be inside a form for its links to be generated - $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet( + $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList( $table - ), new FieldSet()); + ), new FieldList()); $table->ShowPagination = true; $table->PageSize = 2; @@ -111,9 +111,9 @@ class TableListFieldTest extends SapphireTest { "E" => "Col E", )); // A TableListField must be inside a form for its links to be generated - $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet( + $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList( $table - ), new FieldSet()); + ), new FieldList()); $table->ShowPagination = true; $table->PageSize = 2; @@ -182,9 +182,9 @@ class TableListFieldTest extends SapphireTest { "B" => "Col B" )); - $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet( + $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList( $table - ), new FieldSet()); + ), new FieldList()); $csvResponse = $table->export(); @@ -219,7 +219,7 @@ class TableListFieldTest extends SapphireTest { function testLink() { // A TableListField must be inside a form for its links to be generated - $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet( + $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList( new TableListField("Tester", "TableListFieldTest_Obj", array( "A" => "Col A", "B" => "Col B", @@ -227,7 +227,7 @@ class TableListFieldTest extends SapphireTest { "D" => "Col D", "E" => "Col E", )) - ), new FieldSet()); + ), new FieldList()); $table = $form->dataFieldByName('Tester'); $this->assertEquals( @@ -252,9 +252,9 @@ class TableListFieldTest extends SapphireTest { "E" => "Col E", )); // A TableListField must be inside a form for its links to be generated - $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet( + $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList( $table - ), new FieldSet()); + ), new FieldList()); $table->ShowPagination = true; $table->PageSize = 2; @@ -307,7 +307,7 @@ class TableListFieldTest extends SapphireTest { $list = new ArrayList(array($one, $two, $three)); // A TableListField must be inside a form for its links to be generated - $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldSet( + $form = new Form(new TableListFieldTest_TestController(), "TestForm", new FieldList( new TableListField("Tester", $list, array( "A" => "Col A", "B" => "Col B", @@ -315,7 +315,7 @@ class TableListFieldTest extends SapphireTest { "D" => "Col D", "E" => "Col E", )) - ), new FieldSet()); + ), new FieldList()); $table = $form->dataFieldByName('Tester'); $rendered = $table->FieldHolder(); @@ -367,8 +367,8 @@ class TableListFieldTest_TestController extends Controller { $table->disableSorting(); // A TableListField must be inside a form for its links to be generated - return new Form($this, "TestForm", new FieldSet( + return new Form($this, "TestForm", new FieldList( $table - ), new FieldSet()); + ), new FieldList()); } } \ No newline at end of file diff --git a/tests/search/SearchContextTest.php b/tests/search/SearchContextTest.php index 720dc06c9..9a2917c9f 100644 --- a/tests/search/SearchContextTest.php +++ b/tests/search/SearchContextTest.php @@ -85,7 +85,7 @@ class SearchContextTest extends SapphireTest { $context = $company->getDefaultSearchContext(); $fields = $context->getFields(); $this->assertEquals( - new FieldSet( + new FieldList( new TextField("Name", 'Name'), new TextareaField("Industry", 'Industry'), new NumericField("AnnualProfit", 'The Almighty Annual Profit') diff --git a/tests/security/GroupTest.php b/tests/security/GroupTest.php index 37d1f395e..b386abdbe 100644 --- a/tests/security/GroupTest.php +++ b/tests/security/GroupTest.php @@ -129,7 +129,7 @@ class GroupTest_Member extends Member implements TestOnly { function getCMSFields() { $groups = DataObject::get('Group'); $groupsMap = ($groups) ? $groups->map() : false; - $fields = new FieldSet( + $fields = new FieldList( new HiddenField('ID', 'ID'), new CheckboxSetField( 'Groups', @@ -147,7 +147,7 @@ class GroupTest_MemberForm extends Form { function __construct($controller, $name) { $fields = singleton('GroupTest_Member')->getCMSFields(); - $actions = new FieldSet( + $actions = new FieldList( new FormAction('doSave','save') ); diff --git a/tests/security/SecurityTokenTest.php b/tests/security/SecurityTokenTest.php index f07567cbe..e1c111eff 100644 --- a/tests/security/SecurityTokenTest.php +++ b/tests/security/SecurityTokenTest.php @@ -106,7 +106,7 @@ class SecurityTokenTest extends SapphireTest { } function testUpdateFieldSet() { - $fs = new FieldSet(); + $fs = new FieldList(); $t = new SecurityToken(); $t->updateFieldSet($fs); $f = $fs->dataFieldByName($t->getName()); @@ -117,7 +117,7 @@ class SecurityTokenTest extends SapphireTest { } function testUpdateFieldSetDoesntAddTwice() { - $fs = new FieldSet(); + $fs = new FieldList(); $t = new SecurityToken(); $t->updateFieldSet($fs); // first $t->updateFieldSet($fs); // second