diff --git a/api/DataFormatter.php b/api/DataFormatter.php index 3295131fd..61dff928b 100644 --- a/api/DataFormatter.php +++ b/api/DataFormatter.php @@ -6,15 +6,40 @@ */ abstract class DataFormatter extends Object { + + /** + * Set priority from 0-100. + * If multiple formatters for the same extension exist, + * we select the one with highest priority. + * + * @var int + */ + public static $priority = 50; + + /** + * Follow relations for the {@link DataObject} instances + * ($has_one, $has_many, $many_many). + * Set to "0" to disable relation output. + * + * @todo Support more than one nesting level + * + * @var int + */ + public $relationDepth = 1; + /** * Get a DataFormatter object suitable for handling the given file extension */ static function for_extension($extension) { $classes = ClassInfo::subclassesFor("DataFormatter"); array_shift($classes); - + $sortedClasses = array(); foreach($classes as $class) { - $formatter = singleton($class); + $sortedClasses[$class] = singleton($class)->stat('priority'); + } + arsort($sortedClasses); + foreach($sortedClasses as $className => $priority) { + $formatter = singleton($className); if(in_array($extension, $formatter->supportedExtensions())) { return $formatter; } diff --git a/api/JSONDataFormatter.php b/api/JSONDataFormatter.php index ef501c4d0..cc3d91d4a 100644 --- a/api/JSONDataFormatter.php +++ b/api/JSONDataFormatter.php @@ -22,7 +22,7 @@ class JSONDataFormatter extends DataFormatter { $id = $obj->ID; $json = "{\n className : \"$className\",\n"; - $dbFields = array_merge($obj->databaseFields(), array('ID'=>'Int')); + $dbFields = array_merge($obj->inheritedDatabaseFields(), array('ID'=>'Int')); foreach($dbFields as $fieldName => $fieldType) { if(is_object($obj->$fieldName)) { $jsonParts[] = "$fieldName : " . $obj->$fieldName->toJSON(); @@ -31,36 +31,38 @@ class JSONDataFormatter extends DataFormatter { } } - foreach($obj->has_one() as $relName => $relClass) { - $fieldName = $relName . 'ID'; - if($obj->$fieldName) { - $href = Director::absoluteURL(self::$api_base . "$relClass/" . $obj->$fieldName); - } else { - $href = Director::absoluteURL(self::$api_base . "$className/$id/$relName"); + if($this->relationDepth > 0) { + foreach($obj->has_one() as $relName => $relClass) { + $fieldName = $relName . 'ID'; + if($obj->$fieldName) { + $href = Director::absoluteURL(self::$api_base . "$relClass/" . $obj->$fieldName); + } else { + $href = Director::absoluteURL(self::$api_base . "$className/$id/$relName"); + } + $jsonParts[] = "$relName : { className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }"; } - $jsonParts[] = "$relName : { className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }"; - } - - foreach($obj->has_many() as $relName => $relClass) { - $jsonInnerParts = array(); - $items = $obj->$relName(); - foreach($items as $item) { - //$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID"); - $href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID"); - $jsonInnerParts[] = "{ className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }"; + + foreach($obj->has_many() as $relName => $relClass) { + $jsonInnerParts = array(); + $items = $obj->$relName(); + foreach($items as $item) { + //$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID"); + $href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID"); + $jsonInnerParts[] = "{ className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }"; + } + $jsonParts[] = "$relName : [\n " . implode(",\n ", $jsonInnerParts) . " \n ]"; } - $jsonParts[] = "$relName : [\n " . implode(",\n ", $jsonInnerParts) . " \n ]"; - } - - foreach($obj->many_many() as $relName => $relClass) { - $jsonInnerParts = array(); - $items = $obj->$relName(); - foreach($items as $item) { - //$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID"); - $href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID"); - $jsonInnerParts[] = " { className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }"; + + foreach($obj->many_many() as $relName => $relClass) { + $jsonInnerParts = array(); + $items = $obj->$relName(); + foreach($items as $item) { + //$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID"); + $href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID"); + $jsonInnerParts[] = " { className : \"$relClass\", href : \"$href.json\", id : \"{$obj->$fieldName}\" }"; + } + $jsonParts[] = "$relName : [\n " . implode(",\n ", $jsonInnerParts) . "\n ]"; } - $jsonParts[] = "$relName : [\n " . implode(",\n ", $jsonInnerParts) . "\n ]"; } return "{\n " . implode(",\n ", $jsonParts) . "\n}"; } diff --git a/api/RestfulServer.php b/api/RestfulServer.php index 24296e995..27e555c15 100644 --- a/api/RestfulServer.php +++ b/api/RestfulServer.php @@ -122,8 +122,15 @@ class RestfulServer extends Controller { * @return String The serialized representation of the requested object(s) - usually XML or JSON. */ protected function getHandler($className, $id, $relation, $formatter) { - $limit = (int)$this->request->getVar('limit'); - + $sort = array( + 'sort' => $this->request->getVar('sort'), + 'dir' => $this->request->getVar('dir') + ); + $limit = array( + 'start' => $this->request->getVar('start'), + 'limit' => $this->request->getVar('limit') + ); + if($id) { $obj = DataObject::get_by_id($className, $id); if(!$obj) { @@ -135,12 +142,27 @@ class RestfulServer extends Controller { } if($relation) { - if($obj->hasMethod($relation)) $obj = $obj->$relation('', '', '', $limit); - else return $this->notFound(); + if($relationClass = $obj->many_many($relation)) { + $query = $obj->getManyManyComponentsQuery($relation); + } elseif($relationClass = $obj->has_many($relation)) { + $query = $obj->getComponentsQuery($relation); + } elseif($relationClass = $obj->has_one($relation)) { + $query = null; + } elseif($obj->hasMethod("{$relation}Query")) { + // @todo HACK Switch to ComponentSet->getQuery() once we implement it (and lazy loading) + $query = $obj->{"{$relation}Query"}(null, $sort, null, $limit); + $relationClass = $obj->{"{$relation}Class"}(); + } else { + return $this->notFound(); + } + + // get all results + $obj = $this->search($relationClass, $this->request->getVars(), $sort, $limit, $query); + if(!$obj) $obj = new DataObjectSet(); } } else { - $obj = DataObject::get($className, ""); + $obj = $this->search($className, $this->request->getVars(), $sort, $limit); // show empty serialized result when no records are present if(!$obj) $obj = new DataObjectSet(); if(!singleton($className)->stat('api_access')) { @@ -151,6 +173,25 @@ class RestfulServer extends Controller { if($obj instanceof DataObjectSet) return $formatter->convertDataObjectSet($obj); else return $formatter->convertDataObject($obj); } + + /** + * Uses the default {@link SearchContext} specified through + * {@link DataObject::getDefaultSearchContext()} to augument + * an existing query object (mostly a component query from {@link DataObject}) + * with search clauses. + * + * @todo Allow specifying of different searchcontext getters on model-by-model basis + * + * @param string $className + * @param array $params + * @return DataObjectSet + */ + protected function search($className, $params = null, $sort = null, $limit = null, $existingQuery = null) { + $searchContext = singleton($className)->getDefaultSearchContext(); + $query = $searchContext->getQuery($params, $sort, $limit, $existingQuery); + + return singleton($className)->buildDataObjectSet($query->execute()); + } /** * Handler for object delete @@ -195,7 +236,6 @@ class RestfulServer extends Controller { } - /** * Restful server handler for a DataObjectSet */ diff --git a/api/XMLDataFormatter.php b/api/XMLDataFormatter.php index 8e4f391cd..8b7aa000d 100644 --- a/api/XMLDataFormatter.php +++ b/api/XMLDataFormatter.php @@ -29,7 +29,7 @@ class XMLDataFormatter extends DataFormatter { $objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID"); $json = "<$className href=\"$objHref.xml\">\n"; - $dbFields = array_merge($obj->databaseFields(), array('ID'=>'Int')); + $dbFields = array_merge($obj->inheritedDatabaseFields(), array('ID'=>'Int')); foreach($dbFields as $fieldName => $fieldType) { if(is_object($obj->$fieldName)) { $json .= $obj->$fieldName->toXML(); @@ -38,36 +38,37 @@ class XMLDataFormatter extends DataFormatter { } } - - foreach($obj->has_one() as $relName => $relClass) { - $fieldName = $relName . 'ID'; - if($obj->$fieldName) { - $href = Director::absoluteURL(self::$api_base . "$relClass/" . $obj->$fieldName); - } else { - $href = Director::absoluteURL(self::$api_base . "$className/$id/$relName"); + if($this->relationDepth > 0) { + foreach($obj->has_one() as $relName => $relClass) { + $fieldName = $relName . 'ID'; + if($obj->$fieldName) { + $href = Director::absoluteURL(self::$api_base . "$relClass/" . $obj->$fieldName); + } else { + $href = Director::absoluteURL(self::$api_base . "$className/$id/$relName"); + } + $json .= "<$relName linktype=\"has_one\" href=\"$href.xml\" id=\"{$obj->$fieldName}\" />\n"; } - $json .= "<$relName linktype=\"has_one\" href=\"$href.xml\" id=\"{$obj->$fieldName}\" />\n"; - } - - foreach($obj->has_many() as $relName => $relClass) { - $json .= "<$relName linktype=\"has_many\" href=\"$objHref/$relName.xml\">\n"; - $items = $obj->$relName(); - foreach($items as $item) { - //$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID"); - $href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID"); - $json .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\" />\n"; + + foreach($obj->has_many() as $relName => $relClass) { + $json .= "<$relName linktype=\"has_many\" href=\"$objHref/$relName.xml\">\n"; + $items = $obj->$relName(); + foreach($items as $item) { + //$href = Director::absoluteURL(self::$api_base . "$className/$id/$relName/$item->ID"); + $href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID"); + $json .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\" />\n"; + } + $json .= "\n"; } - $json .= "\n"; - } - - foreach($obj->many_many() as $relName => $relClass) { - $json .= "<$relName linktype=\"many_many\" href=\"$objHref/$relName.xml\">\n"; - $items = $obj->$relName(); - foreach($items as $item) { - $href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID"); - $json .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\" />\n"; + + foreach($obj->many_many() as $relName => $relClass) { + $json .= "<$relName linktype=\"many_many\" href=\"$objHref/$relName.xml\">\n"; + $items = $obj->$relName(); + foreach($items as $item) { + $href = Director::absoluteURL(self::$api_base . "$relClass/$item->ID"); + $json .= "<$relClass href=\"$href.xml\" id=\"{$item->ID}\" />\n"; + } + $json .= "\n"; } - $json .= "\n"; } $json .= ""; diff --git a/core/model/DataObject.php b/core/model/DataObject.php index 25800acf1..c6a58e887 100644 --- a/core/model/DataObject.php +++ b/core/model/DataObject.php @@ -828,7 +828,7 @@ class DataObject extends ViewableData implements DataObjectInterface { user_error("DataObject::getComponent(): Unknown 1-to-1 component '$componentName' on class '$this->class'", E_USER_ERROR); } } - + /** * A cache used by component getting classes * @var array @@ -840,9 +840,9 @@ class DataObject extends ViewableData implements DataObjectInterface { * * @param string $componentName Name of the component * @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, the static field $default_sort on the component class will be used. + * @param string|array $sort A sort expression to be inserted into the ORDER BY clause. If omitted, the static field $default_sort on the component class 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. - * @param string $limit A limit expression to be inserted into the LIMIT clause + * @param string|array $limit A limit expression to be inserted into the LIMIT clause * * @return ComponentSet The components of the one-to-many relationship. */ @@ -861,14 +861,9 @@ class DataObject extends ViewableData implements DataObjectInterface { $joinField = $this->getComponentJoinField($componentName); if($this->isInDB()) { //Check to see whether we should query the db - $componentObj = singleton($componentClass); - $id = $this->getField("ID"); - - // get filter - $combinedFilter = "$joinField = '$id'"; - if($filter) $combinedFilter .= " AND {$filter}"; - - $result = $componentObj->instance_get($combinedFilter, $sort, $join, $limit, "ComponentSet"); + $query = $this->getComponentsQuery($componentName, $filter, $sort, $join, $limit); + $result = $this->buildDataObjectSet($query->execute(), 'ComponentSet', $query, $componentClass); + if($result) $result->parseQueryLimit($query); } if(!$result) { @@ -882,6 +877,37 @@ class DataObject extends ViewableData implements DataObjectInterface { return $result; } + + /** + * 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 + * @param string $join + * @param string|array $limit + * @return SQLQuery + */ + public function getComponentsQuery($componentName, $filter = "", $sort = "", $join = "", $limit = "") { + if(!$componentClass = $this->has_many($componentName)) { + user_error("DataObject::getComponentsQuery(): Unknown 1-to-many component '$componentName' on class '$this->class'", E_USER_ERROR); + } + + $joinField = $this->getComponentJoinField($componentName); + + $id = $this->getField("ID"); + + // get filter + $combinedFilter = "$joinField = '$id'"; + if($filter) $combinedFilter .= " AND {$filter}"; + + return singleton($componentClass)->extendedSQL($combinedFilter, $sort, $limit, $join); + } /** * Tries to find the db-key for storing a relation (defaults to "ParentID" if no relation is found). @@ -893,7 +919,7 @@ class DataObject extends ViewableData implements DataObjectInterface { */ public function getComponentJoinField($componentName) { if(!$componentClass = $this->has_many($componentName)) { - user_error("DataObject::getComsponents(): Unknown 1-to-many component '$componentName' on class '$this->class'", E_USER_ERROR); + user_error("DataObject::getComponents(): Unknown 1-to-many component '$componentName' on class '$this->class'", E_USER_ERROR); } $componentObj = singleton($componentClass); @@ -947,24 +973,14 @@ class DataObject extends ViewableData implements DataObjectInterface { 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) { - $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 - ); - array_unshift($query->select, "`$table`.*"); - - if($filter) $query->where[] = $filter; - if($join) $query->from[] = $join; + $query = $this->getManyManyComponentsQuery($componentName, $filter, $sort, $join, $limit); $records = $query->execute(); $result = $this->buildDataObjectSet($records, "ComponentSet", $query, $componentBaseClass); @@ -986,6 +1002,43 @@ class DataObject extends ViewableData implements DataObjectInterface { 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 + ); + array_unshift($query->select, "`$table`.*"); + + if($filter) $query->where[] = $filter; + if($join) $query->from[] = $join; + + return $query; + } /** * Creates an empty component for the given one-one or one-many relationship @@ -1004,6 +1057,61 @@ class DataObject extends ViewableData implements DataObjectInterface { } } + /** + * Add the scaffold-generated relation fields to the given field set + */ + protected function addScaffoldRelationFields($fieldSet) { + if ($this->has_many() || $this->_many_many()) { + $oldFields = $fieldSet; + $fieldSet = new FieldSet( + new TabSet("Root", new Tab("Main")) + ); + foreach($oldFields as $field) { + $fieldSet->addFieldToTab("Root.Main", $field); + } + } + if($this->has_many()) { + // Add each relation as a separate tab + foreach($this->has_many() as $relationship => $component) { + $relationshipFields = singleton($component)->summary_fields(); + $foreignKey = $this->getComponentJoinField($relationship); + $fieldSet->addFieldToTab("Root.$relationship", new ComplexTableField($this, $relationship, $component, $relationshipFields, "getCMSFields", "$foreignKey = $this->ID")); + } + } + if ($this->many_many()) { + foreach($this->many_many() as $relationship => $component) { + $relationshipFields = singleton($component)->summary_fields(); + $filterJoin = $this->getManyManyJoin($relationship, $component); + $tableField = new ComplexTableField($this, $relationship, $component, $relationshipFields, "getCMSFields", '', '', $filterJoin); + $tableField->popupClass = "ScaffoldingComplexTableField_Popup"; + $fieldSet->addFieldToTab("Root.$relationship", $tableField); + } + } + return $fieldSet; + } + + /** + * 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); + + if($baseTable == $parentClass) { + return "LEFT JOIN `$table` ON (`$parentField` = `$parentClass`.`ID` AND `$componentField` = '{$this->ID}')"; + } else { + return "LEFT JOIN `$table` ON (`$componentField` = `$componentClass`.`ID` AND `$parentField` = '{$this->ID}')"; + } + } + /** * Return the class of a one-to-one component. If $component is null, return all of the one-to-one components and their classes. * @@ -1225,29 +1333,6 @@ class DataObject extends ViewableData implements DataObjectInterface { return $fields; } - /** - * Add the scaffold-generated relation fields to the given field set - */ - protected function addScaffoldRelationFields($fieldSet) { - - if($this->has_many()) { - // Refactor the fields that we have been given into a tab, "Main", in a tabset - $oldFields = $fieldSet; - $fieldSet = new FieldSet( - new TabSet("Root", new Tab("Main")) - ); - foreach($oldFields as $field) $fieldSet->addFieldToTab("Root.Main", $field); - - // Add each relation as a separate tab - foreach($this->has_many() as $relationship => $component) { - $relationshipFields = singleton($component)->summary_fields(); - $foreignKey = $this->getComponentJoinField($relationship); - $fieldSet->addFieldToTab("Root.$relationship", new ComplexTableField($this, $relationship, $component, $relationshipFields, "getCMSFields", "$foreignKey = $this->ID")); - } - } - return $fieldSet; - } - /** * Centerpiece of every data administration interface in Silverstripe, * which returns a {@link FieldSet} suitable for a {@link Form} object. @@ -1643,8 +1728,8 @@ class DataObject extends ViewableData implements DataObjectInterface { * Build a {@link SQLQuery} object to perform the given query. * * @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 $limit A limit expression to be inserted into the LIMIT 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. @@ -1666,43 +1751,13 @@ class DataObject extends ViewableData implements DataObjectInterface { $baseClass = array_shift($tableClasses); $select = array("`$baseClass`.*"); - // If sort contains a function call, let's move the sort clause into a separate selected field. - // Some versions of MySQL choke if you have a group function referenced directly in the ORDER BY - if($sort && strpos($sort,'(') !== false) { - // Sort can be "Col1 DESC|ASC, Col2 DESC|ASC", we need to handle that - $sortParts = explode(",", $sort); - - // If you have select if(X,A,B),C then the array will return 'if(X','A','B)','C'. - // Turn this into 'if(X,A,B)','C' by counting brackets - while(list($i,$sortPart) = each($sortParts)) { - while(substr_count($sortPart,'(') > substr_count($sortPart,')')) { - list($i,$nextSortPart) = each($sortParts); - if($i === null) break; - $sortPart .= ',' . $nextSortPart; - } - $lumpedSortParts[] = $sortPart; - } - - foreach($lumpedSortParts as $i => $sortPart) { - $sortPart = trim($sortPart); - if(substr(strtolower($sortPart),-5) == ' desc') { - $select[] = substr($sortPart,0,-5) . " AS _SortColumn{$i}"; - $newSorts[] = "_SortColumn{$i} DESC"; - } else if(substr(strtolower($sortPart),-4) == ' asc') { - $select[] = substr($sortPart,0,-4) . " AS _SortColumn{$i}"; - $newSorts[] = "_SortColumn{$i} ASC"; - } else { - $select[] = "$sortPart AS _SortColumn{$i}"; - $newSorts[] = "_SortColumn{$i} ASC"; - } - } - - $sort = implode(", ", $newSorts); - } - // Build our intial query - $query = new SQLQuery($select, "`$baseClass`", $filter, $sort); - + $query = new SQLQuery($select); + $query->from("`$baseClass`"); + $query->where($filter); + $query->orderby($sort); + $query->limit($limit); + // Add SQL for multi-value fields on the base table $databaseFields = $this->databaseFields(); if($databaseFields) foreach($databaseFields as $k => $v) { @@ -1712,7 +1767,6 @@ class DataObject extends ViewableData implements DataObjectInterface { } } } - // Join all the tables if($tableClasses) { foreach($tableClasses as $tableClass) { @@ -1752,10 +1806,6 @@ class DataObject extends ViewableData implements DataObjectInterface { $query->where[] = "`$baseClass`.ClassName IN ('" . implode("','", $classNames) . "')"; } - if($limit) { - $query->limit = $limit; - } - if($having) { $query->having[] = $having; } @@ -1772,8 +1822,8 @@ class DataObject extends ViewableData implements DataObjectInterface { * Like {@link buildSQL}, but applies the extension modifications. * * @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 $limit A limit expression to be inserted into the LIMIT 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. * @@ -1808,9 +1858,9 @@ class DataObject extends ViewableData implements DataObjectInterface { * * @param string $callerClass The class of objects to be returned * @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|array $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. - * @param string $limit A limit expression to be inserted into the LIMIT clause. + * @param string|array $limit A limit expression to be inserted into the LIMIT clause. * @param string $containerClass The container class to return the results in. * * @return mixed The objects matching the filter, in the class specified by $containerClass @@ -2153,7 +2203,7 @@ class DataObject extends ViewableData implements DataObjectInterface { $fields = array(); $currentObj = $this; while(get_class($currentObj) != 'DataObject') { - $fields = array_merge($fields, $currentObj->customDatabaseFields()); + $fields = array_merge($fields, (array)$currentObj->customDatabaseFields()); $currentObj = singleton($currentObj->parentClass()); } return $fields; diff --git a/core/model/SQLQuery.php b/core/model/SQLQuery.php index 53f891685..d611894b1 100755 --- a/core/model/SQLQuery.php +++ b/core/model/SQLQuery.php @@ -15,19 +15,19 @@ class SQLQuery extends Object { * An array of fields to select. * @var array */ - public $select; + public $select = array(); /** * An array of join clauses. The first one is just the table name. * @var array */ - public $from; + public $from = array(); /** * An array of filters. * @var array */ - public $where; + public $where = array(); /** * An ORDER BY clause. @@ -39,13 +39,13 @@ class SQLQuery extends Object { * An array of fields to group by. * @var array */ - public $groupby; + public $groupby = array(); /** * An array of having clauses. * @var array */ - public $having; + public $having = array(); /** * A limit clause. @@ -57,13 +57,13 @@ class SQLQuery extends Object { * If this is true DISTINCT will be added to the SQL. * @var boolean */ - public $distinct; + public $distinct = false; /** * If this is true, this statement will delete rather than select. * @var boolean */ - public $delete; + public $delete = false; /** * The logical connective used to join WHERE clauses. Defaults to AND. @@ -83,13 +83,14 @@ class SQLQuery extends Object { * @param string $limit A LIMIT clause. */ function __construct($select = "*", $from = array(), $where = "", $orderby = "", $groupby = "", $having = "", $limit = "") { - if($select) $this->select = is_array($select) ? $select : array($select); - if($from) $this->from = is_array($from) ? $from : array(str_replace('`','',$from) => $from); - if($where) $this->where = is_array($where) ? $where : array($where); - $this->orderby = $orderby; - if($groupby) $this->groupby = is_array($groupby) ? $groupby : array($groupby); - if($having) $this->having = is_array($having) ? $having : array($having); - $this->limit = $limit; + $this->select($select); + // @todo + $this->from = is_array($from) ? $from : array(str_replace('`','',$from) => $from); + $this->where($where); + $this->orderby($orderby); + $this->groupby($groupby); + $this->having($having); + $this->limit($limit); parent::__construct(); } @@ -114,6 +115,7 @@ class SQLQuery extends Object { } else { $this->select = is_array($fields) ? $fields : array($fields); } + return $this; } @@ -125,18 +127,147 @@ class SQLQuery extends Object { * * * @param string $table - * @return SQLQuery + * @return SQLQuery This instance */ public function from($table) { $this->from[] = $table; + return $this; } /** - * Add a LEFT JOIN criteria to the FROM clause. + * Add a LEFT JOIN criteria to the FROM clause. + * + * @return SQLQuery This instance */ public function leftJoin($table, $onPredicate) { $this->from[] = "LEFT JOIN $table ON $onPredicate"; + + return $this; + } + + /** + * Pass LIMIT clause either as SQL snippet or in array format. + * + * @param string|array $limit + * @return SQLQuery This instance + */ + public function limit($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(isset($limit['start']) && is_numeric($limit['start']) && isset($limit['limit']) && is_numeric($limit['limit'])) { + // @todo MySQL specific LIMIT syntax + $combinedLimit = (int)$limit['start'] . ',' . (int)$limit['limit']; + } elseif(isset($limit['limit']) && is_numeric($limit['limit'])) { + $combinedLimit = (int)$limit['limit']; + } else { + $combinedLimit = false; + } + } else { + $combinedLimit = $limit; + } + + if(!empty($combinedLimit)) $this->limit = $combinedLimit; + + return $this; + } + + /** + * Pass ORDER BY clause either as SQL snippet or in array format. + * + * @todo Implement passing of multiple orderby pairs in nested array syntax, + * e.g. array(array('sort'=>'A','dir'=>'asc'),array('sort'=>'B')) + * + * @param string|array $orderby + * @return SQLQuery This instance + */ + public function orderby($orderby) { + // if passed as an array, assume two array values with column and direction (asc|desc) + if(is_array($orderby)) { + if(!array_key_exists('sort', $orderby)) user_error('SQLQuery::orderby(): Wrong format for $orderby array', E_USER_ERROR); + + if(isset($orderby['sort']) && !empty($orderby['sort']) && isset($orderby['dir']) && !empty($orderby['dir'])) { + $combinedOrderby = "`" . Convert::raw2sql($orderby['sort']) . "` " . Convert::raw2sql(strtoupper($orderby['dir'])); + } elseif(isset($orderby['sort']) && !empty($orderby['sort'])) { + $combinedOrderby = "`" . Convert::raw2sql($orderby['sort']) . "`"; + } else { + $combinedOrderby = false; + } + } else { + $combinedOrderby = $orderby; + } + + // If sort contains a function call, let's move the sort clause into a separate selected field. + // Some versions of MySQL choke if you have a group function referenced directly in the ORDER BY + if($combinedOrderby && strpos($combinedOrderby,'(') !== false) { + // Sort can be "Col1 DESC|ASC, Col2 DESC|ASC", we need to handle that + $sortParts = explode(",", $combinedOrderby); + + // If you have select if(X,A,B),C then the array will return 'if(X','A','B)','C'. + // Turn this into 'if(X,A,B)','C' by counting brackets + while(list($i,$sortPart) = each($sortParts)) { + while(substr_count($sortPart,'(') > substr_count($sortPart,')')) { + list($i,$nextSortPart) = each($sortParts); + if($i === null) break; + $sortPart .= ',' . $nextSortPart; + } + $lumpedSortParts[] = $sortPart; + } + + foreach($lumpedSortParts as $i => $sortPart) { + $sortPart = trim($sortPart); + if(substr(strtolower($sortPart),-5) == ' desc') { + $select[] = substr($sortPart,0,-5) . " AS _SortColumn{$i}"; + $newSorts[] = "_SortColumn{$i} DESC"; + } else if(substr(strtolower($sortPart),-4) == ' asc') { + $select[] = substr($sortPart,0,-4) . " AS _SortColumn{$i}"; + $newSorts[] = "_SortColumn{$i} ASC"; + } else { + $select[] = "$sortPart AS _SortColumn{$i}"; + $newSorts[] = "_SortColumn{$i} ASC"; + } + } + + $combinedOrderby = implode(", ", $newSorts); + } + + if(!empty($combinedOrderby)) $this->orderby = $combinedOrderby; + + return $this; + } + + /** + * Add a GROUP BY clause. + * + * @param string|array $groupby + * @return SQLQuery + */ + public function groupby($groupby) { + if(is_array($groupby)) { + $this->groupby = array_merge($this->groupby, $groupby); + } elseif(!empty($groupby)) { + $this->groupby[] = $groupby; + } + + return $this; + } + + /** + * Add a HAVING clause. + * + * @param string|array $having + * @return SQLQuery + */ + public function having($having) { + if(is_array($having)) { + $this->having = array_merge($this->having, $having); + } elseif(!empty($having)) { + $this->having[] = $having; + } + + return $this; } /** @@ -166,7 +297,13 @@ class SQLQuery extends Object { } else { $filter = $args[0]; } - $this->where[] = $filter; + + if(is_array($filter)) { + $this->where = array_merge($this->where,$filter); + } elseif(!empty($filter)) { + $this->where[] = $filter; + } + return $this; } @@ -246,7 +383,7 @@ class SQLQuery extends Object { $text = "SELECT $distinct" . implode(", ", $this->select); } $text .= " FROM " . implode(" ", $this->from); - + if($this->where) $text .= " WHERE (" . $this->getFilter(). ")"; if($this->groupby) $text .= " GROUP BY " . implode(", ", $this->groupby); if($this->having) $text .= " HAVING ( " . implode(" ) AND ( ", $this->having) . " )"; diff --git a/forms/ComplexTableField.php b/forms/ComplexTableField.php index 6f99c73af..310fb6610 100755 --- a/forms/ComplexTableField.php +++ b/forms/ComplexTableField.php @@ -888,4 +888,70 @@ class ComplexTableField_Popup extends Form { } } +/** + * Used by ModelAdmin scaffolding, to manage many-many relationships. + */ +class ScaffoldingComplexTableField_Popup extends Form { + protected $sourceClass; + protected $dataObject; + + function __construct($controller, $name, $fields, $validator, $readonly, $dataObject) { + $this->dataObject = $dataObject; + + /** + * WARNING: DO NOT CHANGE THE ORDER OF THESE JS FILES + * Some have special requirements. + */ + //Requirements::css('cms/css/layout.css'); + Requirements::css('jsparty/tabstrip/tabstrip.css'); + Requirements::css('sapphire/css/Form.css'); + Requirements::css('sapphire/css/ComplexTableField_popup.css'); + Requirements::css('cms/css/typography.css'); + Requirements::css('cms/css/cms_right.css'); + Requirements::css('jsparty/jquery/plugins/autocomplete/jquery.ui.autocomplete.css'); + Requirements::javascript("jsparty/prototype.js"); + Requirements::javascript("jsparty/behaviour.js"); + Requirements::javascript("jsparty/prototype_improvements.js"); + Requirements::javascript("jsparty/loader.js"); + Requirements::javascript("jsparty/tabstrip/tabstrip.js"); + Requirements::javascript("jsparty/scriptaculous/scriptaculous.js"); + Requirements::javascript("jsparty/scriptaculous/controls.js"); + Requirements::javascript("jsparty/layout_helpers.js"); + Requirements::javascript("cms/javascript/LeftAndMain.js"); + Requirements::javascript("cms/javascript/LeftAndMain_right.js"); + Requirements::javascript("sapphire/javascript/TableField.js"); + Requirements::javascript("sapphire/javascript/ComplexTableField.js"); + Requirements::javascript("sapphire/javascript/ComplexTableField_popup.js"); + // jQuery requirements (how many of these are actually needed?) + Requirements::javascript('jsparty/jquery/jquery.js'); + Requirements::javascript('jsparty/jquery/plugins/livequery/jquery.livequery.js'); + Requirements::javascript('jsparty/jquery/ui/ui.core.js'); + Requirements::javascript('jsparty/jquery/ui/ui.tabs.js'); + Requirements::javascript('jsparty/jquery/plugins/form/jquery.form.js'); + Requirements::javascript('jsparty/jquery/plugins/dimensions/jquery.dimensions.js'); + Requirements::javascript('jsparty/jquery/plugins/autocomplete/jquery.ui.autocomplete.js'); + Requirements::javascript('sapphire/javascript/ScaffoldComplexTableField.js'); + Requirements::javascript('cms/javascript/ModelAdmin.js'); + + if($this->dataObject->hasMethod('getRequirementsForPopup')) { + $this->dataObject->getRequirementsForPopup(); + } + + $actions = new FieldSet(); + if(!$readonly) { + $actions->push( + $saveAction = new FormAction("saveComplexTableField", "Save") + ); + $saveAction->addExtraClass('save'); + } + + parent::__construct($controller, $name, $fields, $actions, $validator); + } + + function FieldHolder() { + return $this->renderWith('ComplexTableField_Form'); + } + +} + ?> diff --git a/forms/FieldSet.php b/forms/FieldSet.php index d5ce93e84..f5069060e 100755 --- a/forms/FieldSet.php +++ b/forms/FieldSet.php @@ -139,7 +139,7 @@ class FieldSet extends DataObjectSet { // Create any missing tabs if(!$currentPointer) { if(is_a($parentPointer,'TabSet')) { - $currentPointer = new Tab($tabName); + $currentPointer = new Tab($part); $parentPointer->push($currentPointer); } else { user_error("FieldSet::addFieldToTab() Tried to add a tab to a " . $parentPointer->class . " object - '$part' didn't exist.", E_USER_ERROR); diff --git a/forms/Tab.php b/forms/Tab.php index deeabc615..caddd2f6b 100755 --- a/forms/Tab.php +++ b/forms/Tab.php @@ -7,10 +7,13 @@ class Tab extends CompositeField { protected $tabSet; - public function __construct($title) { + public function __construct($name) { $args = func_get_args(); - $this->title = array_shift($args); - $this->id = ereg_replace('[^0-9A-Za-z]+', '', $this->title); + $name = array_shift($args); + + $this->id = preg_replace('/[^0-9A-Za-z]+/', '', $name); + $this->title = preg_replace('/([a-z0-9])([A-Z])/', '\\1 \\2', $name); + $this->name = $name; parent::__construct($args); } diff --git a/javascript/ScaffoldComplexTableField.js b/javascript/ScaffoldComplexTableField.js new file mode 100644 index 000000000..c4b3594a1 --- /dev/null +++ b/javascript/ScaffoldComplexTableField.js @@ -0,0 +1,5 @@ +window.onload = function() { + + jQuery("fieldset input:first").attr('autocomplete', 'off').autocomplete({list: ["mark rickerby", "maxwell sparks"]}); + +}; \ No newline at end of file diff --git a/search/SearchContext.php b/search/SearchContext.php index 34120746b..8e3e22b9c 100644 --- a/search/SearchContext.php +++ b/search/SearchContext.php @@ -77,19 +77,20 @@ class SearchContext extends Object { } /** + * @refactor move to SQLQuery * @todo fix hack */ protected function applyBaseTableFields() { $classes = ClassInfo::dataClassesFor($this->modelClass); - //Debug::dump($classes); - //die(); - $fields = array($classes[0].'.*', $this->modelClass.'.*'); + $fields = array($this->modelClass.'.*'); + if($this->modelClass != $classes[0]) $fields[] = $classes[0].'.*'; //$fields = array_keys($model->db()); $fields[] = $classes[0].'.ClassName AS RecordClassName'; return $fields; } /** + * @refactor move to SQLQuery * @todo fix hack */ protected function applyBaseTable() { @@ -100,6 +101,7 @@ class SearchContext extends Object { /** * @todo only works for one level deep of inheritance * @todo fix hack + * @deprecated - remove me! */ protected function applyBaseTableJoin($query) { $classes = ClassInfo::dataClassesFor($this->modelClass); @@ -111,28 +113,30 @@ class SearchContext extends Object { * list of query parameters. * * @param array $searchParams + * @param string|array $sort Database column to sort on. Falls back to {@link DataObject::$default_sort} if not provided. + * @param string|array $limit + * @param SQLQuery $existingQuery * @return SQLQuery */ - public function getQuery($searchParams, $start = false, $limit = false) { + public function getQuery($searchParams, $sort = false, $limit = false, $existingQuery = null) { $model = singleton($this->modelClass); $fields = $this->applyBaseTableFields($model); - $query = new SQLQuery($fields); - - $baseTable = $this->applyBaseTable(); - $query->from($baseTable); - - if($limit) $query->limit = (!empty($start)) ? "{$start},{$limit}" : $limit; - - // SRM: This stuff is copied from DataObject, - if($this->modelClass != $baseTable) { - $classNames = ClassInfo::subclassesFor($this->modelClass); - $query->where[] = "`$baseTable`.ClassName IN ('" . implode("','", $classNames) . "')"; + if($existingQuery) { + $query = $existingQuery; + $query->select = array_merge($query->select,$fields); + } else { + $query = $model->buildSQL(); + $query->select($fields); } - - $this->applyBaseTableJoin($query); + $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); + foreach($searchParams as $key => $value) { if ($value != '0') { @@ -145,6 +149,7 @@ class SearchContext extends Object { } } } + return $query; } @@ -154,14 +159,16 @@ class SearchContext extends Object { * @todo rearrange start and limit params to reflect DataObject * * @param array $searchParams - * @param int $start - * @param int $limit + * @param string|array $sort + * @param string|array $limit * @return DataObjectSet */ - public function getResults($searchParams, $start = false, $limit = false) { + public function getResults($searchParams, $sort = false, $limit = false) { $searchParams = array_filter($searchParams, array($this,'clearEmptySearchFields')); - $query = $this->getQuery($searchParams, $start, $limit); - + $query = $this->getQuery($searchParams, $sort, $limit); + + //Debug::dump($query->sql()); + // use if a raw SQL query is needed $results = new DataObjectSet(); foreach($query->execute() as $row) { diff --git a/tests/DataObjectDecoratorTest.php b/tests/DataObjectDecoratorTest.php index 128ae92e3..0ff1440fb 100644 --- a/tests/DataObjectDecoratorTest.php +++ b/tests/DataObjectDecoratorTest.php @@ -6,14 +6,25 @@ class DataObjectDecoratorTest extends SapphireTest { function testOneToManyAssociationWithDecorator() { $contact = new DataObjectDecoratorTest_Member(); $contact->Website = "http://www.example.com"; + $object = new DataObjectDecoratorTest_RelatedObject(); $object->FieldOne = "Lorem ipsum dolor"; $object->FieldTwo = "Random notes"; + + /* The following code doesn't currently work: $contact->RelatedObjects()->add($object); $contact->write(); + */ + + /* Instead we have to do the following */ + $contact->write(); + $object->ContactID = $contact->ID; + $object->write(); + unset($contact); $contact = DataObject::get_one("DataObjectDecoratorTest_Member", "Website='http://www.example.com'"); + $this->assertType('DataObjectDecoratorTest_RelatedObject', $contact->RelatedObjects()->First()); $this->assertEquals("Lorem ipsum dolor", $contact->RelatedObjects()->First()->FieldOne); $this->assertEquals("Random notes", $contact->RelatedObjects()->First()->FieldTwo); @@ -50,11 +61,11 @@ class DataObjectDecoratorTest_RelatedObject extends DataObject implements TestOn static $db = array( "FieldOne" => "Text", - "FieldOne" => "Text" + "FieldTwo" => "Text" ); static $has_one = array( - "Contact" => "Member" + "Contact" => "DataObjectDecoratorTest_Member" ); } diff --git a/tests/SQLQueryTest.php b/tests/SQLQueryTest.php index 098b714b8..76407b4ea 100644 --- a/tests/SQLQueryTest.php +++ b/tests/SQLQueryTest.php @@ -55,18 +55,70 @@ class SQLQueryTest extends SapphireTest { function testSelectWithPredicateFilters() { $query = new SQLQuery(); - $query->select(array("Name"))->from("MyTable"); + $query->select(array("Name"))->from("SQLQueryTest_DO"); + $match = new ExactMatchFilter("Name", "Value"); + $match->setModel('SQLQueryTest_DO'); $match->apply($query); + $match = new PartialMatchFilter("Meta", "Value"); + $match->setModel('SQLQueryTest_DO'); $match->apply($query); - $this->assertEquals("SELECT Name FROM MyTable WHERE (Name = 'Value') AND (Meta LIKE '%Value%')", $query->sql()); + + $this->assertEquals("SELECT Name FROM SQLQueryTest_DO WHERE (SQLQueryTest_DO.Name = 'Value') AND (SQLQueryTest_DO.Meta LIKE '%Value%')", $query->sql()); } function testSelectWithLimitClause() { - // not implemented + // numeric limit + $query = new SQLQuery(); + $query->from[] = "MyTable"; + $query->limit("99"); + $this->assertEquals("SELECT * FROM MyTable LIMIT 99", $query->sql()); + + // array limit + $query = new SQLQuery(); + $query->from[] = "MyTable"; + $query->limit(array('limit'=>99)); + $this->assertEquals("SELECT * FROM MyTable LIMIT 99", $query->sql()); + + // array limit with start (MySQL specific) + $query = new SQLQuery(); + $query->from[] = "MyTable"; + $query->limit(array('limit'=>99, 'start'=>97)); + $this->assertEquals("SELECT * FROM MyTable LIMIT 97,99", $query->sql()); } + function testSelectWithOrderbyClause() { + // numeric limit + $query = new SQLQuery(); + $query->from[] = "MyTable"; + $query->orderby('MyName ASC'); + // can't escape as we don't know if ASC or DESC is appended + $this->assertEquals("SELECT * FROM MyTable ORDER BY MyName ASC", $query->sql()); + + // array limit + $query = new SQLQuery(); + $query->from[] = "MyTable"; + $query->orderby(array('sort'=>'MyName')); + $this->assertEquals("SELECT * FROM MyTable ORDER BY `MyName`", $query->sql()); + + // array limit with start (MySQL specific) + $query = new SQLQuery(); + $query->from[] = "MyTable"; + $query->orderby(array('sort'=>'MyName','dir'=>'desc')); + $this->assertEquals("SELECT * FROM MyTable ORDER BY `MyName` DESC", $query->sql()); + } + + function testSelectWithComplexOrderbyClause() { + // @todo Test "ORDER BY RANDOM() ASC,MyName DESC" etc. + } +} + +class SQLQueryTest_DO extends DataObject implements TestOnly { + static $db = array( + "Name" => "Varchar", + "Meta" => "Varchar", + ); } ?> \ No newline at end of file diff --git a/tests/SearchContextTest.php b/tests/SearchContextTest.php index 6ad595316..4eb47e479 100644 --- a/tests/SearchContextTest.php +++ b/tests/SearchContextTest.php @@ -30,15 +30,15 @@ class SearchContextTest extends SapphireTest { $this->assertContains('Industry', $company->summary_fields()); } - function testExactMatchUsedByDefaultWhenNotExplicitlySet() { + function testPartialMatchUsedByDefaultWhenNotExplicitlySet() { $person = singleton('SearchContextTest_Person'); $context = $person->getDefaultSearchContext(); $this->assertEquals( array( - "Name" => new ExactMatchFilter("Name"), - "HairColor" => new ExactMatchFilter("HairColor"), - "EyeColor" => new ExactMatchFilter("EyeColor") + "Name" => new PartialMatchFilter("Name"), + "HairColor" => new PartialMatchFilter("HairColor"), + "EyeColor" => new PartialMatchFilter("EyeColor") ), $context->getFilters() ); @@ -50,7 +50,7 @@ class SearchContextTest extends SapphireTest { $this->assertEquals( array( - "Title" => new ExactMatchFilter("Title") + "Title" => new PartialMatchFilter("Title") ), $context->getFilters() ); @@ -63,7 +63,7 @@ class SearchContextTest extends SapphireTest { $this->assertEquals( array( "Name" => new PartialMatchFilter("Name"), - "Industry" => new ExactMatchFilter("Industry"), + "Industry" => new PartialMatchFilter("Industry"), "AnnualProfit" => new PartialMatchFilter("AnnualProfit") ), $context->getFilters() @@ -80,7 +80,7 @@ class SearchContextTest extends SapphireTest { $this->assertEquals(1, $results->Count()); - Debug::dump(DB::query("select * from SearchContextTest_Deadline")->next()); + //Debug::dump(DB::query("select * from SearchContextTest_Deadline")->next()); $project = $results->First(); @@ -88,7 +88,7 @@ class SearchContextTest extends SapphireTest { $this->assertEquals("Blog Website", $project->Name); $this->assertEquals(2, $project->Actions()->Count()); $this->assertEquals("Get RSS feeds working", $project->Actions()->First()->Description); - Debug::dump($project->Deadline()->CompletionDate); + //Debug::dump($project->Deadline()->CompletionDate); //$this->assertEquals() }