From 016cff2093701e1a824af7412eaed54207512d49 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Sat, 9 Aug 2008 04:38:44 +0000 Subject: [PATCH] (merged from branches/roa. use "svn log -c -g " for detailed commit message) git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@60209 467b73ca-7a2a-4603-9d3b-597d59a354a9 --- _config.php | 5 +- api/DataFormatter.php | 42 ++++++ api/JSONDataFormatter.php | 81 +++++++++++ api/RestfulServer.php | 184 ++---------------------- api/XMLDataFormatter.php | 96 +++++++++++++ cli-script.php | 6 +- core/ArrayData.php | 7 + core/SSViewer.php | 1 + core/control/Director.php | 3 +- core/model/DataObject.php | 42 ++++-- core/model/DatabaseAdmin.php | 2 +- core/model/SQLQuery.php | 93 +++++++++++- forms/ComplexTableField.php | 107 +++++++------- forms/FieldSet.php | 1 + forms/Form.php | 9 ++ forms/TableListField.php | 70 +++------ javascript/ComplexTableField.js | 11 +- javascript/ComplexTableField_popup.js | 18 +-- sake | 40 ++++++ search/SearchContext.php | 55 +++++-- search/filters/ExactMatchFilter.php | 4 +- search/filters/FulltextFilter.php | 21 ++- search/filters/NegationFilter.php | 4 +- search/filters/PartialMatchFilter.php | 4 +- search/filters/SearchFilter.php | 15 +- search/filters/SubstringFilter.php | 16 +++ search/filters/SubstringMatchFilter.php | 16 --- security/BasicAuth.php | 2 +- templates/ComplexTableField.ss | 2 +- templates/ComplexTableField_popup.ss | 58 ++++---- tests/SQLQueryTest.php | 72 ++++++++++ tests/SearchContextTest.php | 29 ++-- 32 files changed, 711 insertions(+), 405 deletions(-) create mode 100644 api/DataFormatter.php create mode 100644 api/JSONDataFormatter.php create mode 100644 api/XMLDataFormatter.php create mode 100755 sake create mode 100644 search/filters/SubstringFilter.php delete mode 100644 search/filters/SubstringMatchFilter.php create mode 100644 tests/SQLQueryTest.php diff --git a/_config.php b/_config.php index 61a5a9de4..931351e24 100644 --- a/_config.php +++ b/_config.php @@ -32,10 +32,7 @@ Director::addRules(10, array( )); Director::addRules(1, array( - '$URLSegment/$Action/$ID/$OtherID' => array( - '_PopTokeniser' => 1, - 'Controller' => 'ModelAsController', - ), + '$URLSegment//$Action/$ID/$OtherID' => 'ModelAsController', )); /** diff --git a/api/DataFormatter.php b/api/DataFormatter.php new file mode 100644 index 000000000..3295131fd --- /dev/null +++ b/api/DataFormatter.php @@ -0,0 +1,42 @@ +supportedExtensions())) { + return $formatter; + } + } + } + + /** + * Return an array of the extensions that this data formatter supports + */ + abstract function supportedExtensions(); + + + /** + * Convert a single data object to this format. Return a string. + * @todo Add parameters for things like selecting output columns + */ + abstract function convertDataObject(DataObjectInterface $do); + + /** + * Convert a data object set to this format. Return a string. + * @todo Add parameters for things like selecting output columns + */ + abstract function convertDataObjectSet(DataObjectSet $set); + +} \ No newline at end of file diff --git a/api/JSONDataFormatter.php b/api/JSONDataFormatter.php new file mode 100644 index 000000000..ef501c4d0 --- /dev/null +++ b/api/JSONDataFormatter.php @@ -0,0 +1,81 @@ + header (Default: true) + * @return String XML + */ + public function convertDataObject(DataObjectInterface $obj) { + $className = $obj->class; + $id = $obj->ID; + + $json = "{\n className : \"$className\",\n"; + $dbFields = array_merge($obj->databaseFields(), array('ID'=>'Int')); + foreach($dbFields as $fieldName => $fieldType) { + if(is_object($obj->$fieldName)) { + $jsonParts[] = "$fieldName : " . $obj->$fieldName->toJSON(); + } else { + $jsonParts[] = "$fieldName : \"" . Convert::raw2js($obj->$fieldName) . "\""; + } + } + + 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}\" }"; + } + + 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 ]"; + } + + 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 ]"; + } + + return "{\n " . implode(",\n ", $jsonParts) . "\n}"; } + + /** + * Generate an XML representation of the given {@link DataObjectSet}. + * + * @param DataObjectSet $set + * @return String XML + */ + public function convertDataObjectSet(DataObjectSet $set) { + $jsonParts = array(); + foreach($set as $item) { + if($item->canView()) $jsonParts[] = $this->convertDataObject($item); + } + return "[\n" . implode(",\n", $jsonParts) . "\n]"; + } +} \ No newline at end of file diff --git a/api/RestfulServer.php b/api/RestfulServer.php index c6c314cd9..602235088 100644 --- a/api/RestfulServer.php +++ b/api/RestfulServer.php @@ -71,16 +71,19 @@ class RestfulServer extends Controller { 'html' => 'text/html', ); $contentType = isset($contentMap[$extension]) ? $contentMap[$extension] : 'text/xml'; - + + if(!$extension) $extension = "xml"; + $formatter = DataFormatter::for_extension($extension); //$this->dataFormatterFromMime($contentType); + switch($requestMethod) { case 'GET': - return $this->getHandler($className, $id, $relation, $contentType); + return $this->getHandler($className, $id, $relation, $formatter); case 'PUT': - return $this->putHandler($className, $id, $relation, $contentType); + return $this->putHandler($className, $id, $relation, $formatter); case 'DELETE': - return $this->deleteHandler($className, $id, $relation, $contentType); + return $this->deleteHandler($className, $id, $relation, $formatter); case 'POST': } @@ -118,7 +121,7 @@ class RestfulServer extends Controller { * @param String $contentType * @return String The serialized representation of the requested object(s) - usually XML or JSON. */ - protected function getHandler($className, $id, $relation, $contentType) { + protected function getHandler($className, $id, $relation, $formatter) { if($id) { $obj = DataObject::get_by_id($className, $id); if(!$obj) { @@ -142,176 +145,10 @@ class RestfulServer extends Controller { return $this->permissionFailure(); } } - - // TO DO - inspect that Accept header as well. $_GET['accept'] can still be checked, as it's handy for debugging - switch($contentType) { - case "text/xml": - $this->getResponse()->addHeader("Content-type", "text/xml"); - if($obj instanceof DataObjectSet) return $this->dataObjectSetAsXML($obj); - else return $this->dataObjectAsXML($obj); - case "text/json": - //$this->getResponse()->addHeader("Content-type", "text/json"); - if($obj instanceof DataObjectSet) return $this->dataObjectSetAsJSON($obj); - else return $this->dataObjectAsJSON($obj); - - case "text/html": - case "application/xhtml+xml": - if($obj instanceof DataObjectSet) return $this->dataObjectSetAsXHTML($obj); - else return $this->dataObjectAsXHTML($obj); - } + if($obj instanceof DataObjectSet) return $formatter->convertDataObjectSet($obj); + else return $formatter->convertDataObject($obj); } - - /** - * Generate an XML representation of the given {@link DataObject}. - * - * @param DataObject $obj - * @param $includeHeader Include header (Default: true) - * @return String XML - */ - public function dataObjectAsXML(DataObject $obj, $includeHeader = true) { - $className = $obj->class; - $id = $obj->ID; - $objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID"); - - $json = ""; - if($includeHeader) $json .= "\n"; - $json .= "<$className href=\"$objHref.xml\">\n"; - $dbFields = array_merge($obj->databaseFields(), array('ID'=>'Int')); - foreach($dbFields as $fieldName => $fieldType) { - if(is_object($obj->$fieldName)) { - $json .= $obj->$fieldName->toXML(); - } else { - $json .= "<$fieldName>" . Convert::raw2xml($obj->$fieldName) . "\n"; - } - } - - - 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"; - } - - 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"; - } - - 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 .= ""; - - return $json; - } - - /** - * Generate an XML representation of the given {@link DataObjectSet}. - * - * @param DataObjectSet $set - * @return String XML - */ - public function dataObjectSetAsXML(DataObjectSet $set) { - $className = $set->class; - - $xml = "\n<$className>\n"; - foreach($set as $item) { - if($item->canView()) $xml .= $this->dataObjectAsXML($item, false); - } - $xml .= ""; - - return $xml; - } - - /** - * Generate an JSON representation of the given {@link DataObject}. - * - * @see http://json.org - * - * @param DataObject $obj - * @return String JSON - */ - public function dataObjectAsJSON(DataObject $obj) { - $className = $obj->class; - $id = $obj->ID; - - $json = "{\n className : \"$className\",\n"; - $dbFields = array_merge($obj->databaseFields(), array('ID'=>'Int')); - foreach($dbFields as $fieldName => $fieldType) { - if(is_object($obj->$fieldName)) { - $jsonParts[] = "$fieldName : " . $obj->$fieldName->toJSON(); - } else { - $jsonParts[] = "$fieldName : \"" . Convert::raw2js($obj->$fieldName) . "\""; - } - } - - 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}\" }"; - } - - 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 ]"; - } - - 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 ]"; - } - - return "{\n " . implode(",\n ", $jsonParts) . "\n}"; - } - - /** - * Generate an JSON representation of the given {@link DataObjectSet}. - * - * @param DataObjectSet $set - * @return String JSON - */ - public function dataObjectSetAsJSON(DataObjectSet $set) { - $jsonParts = array(); - foreach($set as $item) { - if($item->canView()) $jsonParts[] = $this->dataObjectAsJSON($item); - } - return "[\n" . implode(",\n", $jsonParts) . "\n]"; - } - /** * Handler for object delete @@ -324,7 +161,6 @@ class RestfulServer extends Controller { } else { return $this->permissionFailure(); } - } } diff --git a/api/XMLDataFormatter.php b/api/XMLDataFormatter.php new file mode 100644 index 000000000..8e4f391cd --- /dev/null +++ b/api/XMLDataFormatter.php @@ -0,0 +1,96 @@ + header (Default: true) + * @return String XML + */ + public function convertDataObject(DataObjectInterface $obj) { + Controller::curr()->getResponse()->addHeader("Content-type", "text/xml"); + return "\n" . $this->convertDataObjectWithoutHeader($obj); + } + + + public function convertDataObjectWithoutHeader(DataObject $obj) { + $className = $obj->class; + $id = $obj->ID; + $objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID"); + + $json = "<$className href=\"$objHref.xml\">\n"; + $dbFields = array_merge($obj->databaseFields(), array('ID'=>'Int')); + foreach($dbFields as $fieldName => $fieldType) { + if(is_object($obj->$fieldName)) { + $json .= $obj->$fieldName->toXML(); + } else { + $json .= "<$fieldName>" . Convert::raw2xml($obj->$fieldName) . "\n"; + } + } + + + 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"; + } + + 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"; + } + + 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 .= ""; + + return $json; + } + + /** + * Generate an XML representation of the given {@link DataObjectSet}. + * + * @param DataObjectSet $set + * @return String XML + */ + public function convertDataObjectSet(DataObjectSet $set) { + Controller::curr()->getResponse()->addHeader("Content-type", "text/xml"); + $className = $set->class; + + $xml = "\n<$className>\n"; + foreach($set as $item) { + if($item->canView()) $xml .= $this->convertDataObjectWithoutHeader($item); + } + $xml .= ""; + + return $xml; + } +} \ No newline at end of file diff --git a/cli-script.php b/cli-script.php index 75daafcbf..0e4dffa1a 100755 --- a/cli-script.php +++ b/cli-script.php @@ -1,6 +1,10 @@ #!/usr/bin/php5 array, true); + } + } ?> \ No newline at end of file diff --git a/core/SSViewer.php b/core/SSViewer.php index be5eda3fb..24fd40c87 100644 --- a/core/SSViewer.php +++ b/core/SSViewer.php @@ -109,6 +109,7 @@ class SSViewer extends Object { */ public function dontRewriteHashlinks() { $this->rewriteHashlinks = false; + self::$options['rewriteHashlinks'] = false; return $this; } diff --git a/core/control/Director.php b/core/control/Director.php index 578273e3c..9689a64d3 100644 --- a/core/control/Director.php +++ b/core/control/Director.php @@ -577,7 +577,8 @@ class Director { */ static function set_environment_type($et) { if($et != 'dev' && $et != 'test' && $et != 'live') { - user_error("Director::set_environment_type passed '$et'. It should be passed dev, test, or live"); + Debug::backtrace(); + user_error("Director::set_environment_type passed '$et'. It should be passed dev, test, or live", E_USER_WARNING); } else { self::$environment_type = $et; } diff --git a/core/model/DataObject.php b/core/model/DataObject.php index e09fa3e05..a58c8bf0d 100644 --- a/core/model/DataObject.php +++ b/core/model/DataObject.php @@ -1206,7 +1206,7 @@ class DataObject extends ViewableData implements DataObjectInterface { $model = singleton($component); $records = DataObject::get($component); $collect = ($model->hasMethod('customSelectOption')) ? 'customSelectOption' : current($model->summary_fields()); - $options = $records->filter_map('ID', $collect); + $options = $records ? $records->filter_map('ID', $collect) : array(); $fields->push(new DropdownField($relationship.'ID', $relationship, $options)); } return $fields; @@ -1216,9 +1216,21 @@ class DataObject extends ViewableData implements DataObjectInterface { * Add the scaffold-generated relation fields to the given field set */ protected function addScaffoldRelationFields($fieldSet) { - foreach($this->has_many() as $relationship => $component) { - $relationshipFields = array_keys($this->searchable_fields()); - $fieldSet->push(new ComplexTableField($this, $relationship, $component, $relationshipFields)); + + 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; } @@ -1249,7 +1261,7 @@ class DataObject extends ViewableData implements DataObjectInterface { $fields = $this->scaffoldFormFields(); // If we don't have an ID, then relation fields don't work if($this->ID) { - $this->addScaffoldRelationFields($fields); + $fields = $this->addScaffoldRelationFields($fields); } return $fields; } @@ -2085,7 +2097,7 @@ class DataObject extends ViewableData implements DataObjectInterface { public function searchable_fields() { $fields = $this->stat('searchable_fields'); if (!$fields) { - $fields = array_fill_keys($this->summary_fields(), 'TextField'); + $fields = array_fill_keys(array_keys($this->summary_fields()), 'TextField'); } return $fields; } @@ -2101,11 +2113,19 @@ class DataObject extends ViewableData implements DataObjectInterface { $fields = $this->stat('summary_fields'); if (!$fields) { $fields = array(); - if ($this->hasField('Name')) $fields[] = 'Name'; - if ($this->hasField('Title')) $fields[] = 'Title'; - if ($this->hasField('Description')) $fields[] = 'Description'; - if ($this->hasField('Firstname')) $fields[] = 'Firstname'; + if ($this->hasField('Name')) $fields['Name'] = 'Name'; + if ($this->hasField('Title')) $fields['Title'] = 'Title'; + if ($this->hasField('Description')) $fields['Description'] = 'Description'; + if ($this->hasField('Firstname')) $fields['Firstname'] = 'Firstname'; } + + // Final fail-over, just list all the fields :-S + if(!$fields) { + foreach(array_keys($this->db()) as $field) { + $fields[$field] = $field; + } + } + return $fields; } @@ -2128,7 +2148,7 @@ class DataObject extends ViewableData implements DataObjectInterface { } else { if (is_array($type)) { $filter = current($type); - $filters[$name] = new $filter(); + $filters[$name] = new $filter($name); } else { if (is_subclass_of($type, 'SearchFilter')) { $filters[$name] = new $type($name); diff --git a/core/model/DatabaseAdmin.php b/core/model/DatabaseAdmin.php index 63e1a5751..cd964ad71 100644 --- a/core/model/DatabaseAdmin.php +++ b/core/model/DatabaseAdmin.php @@ -75,7 +75,7 @@ class DatabaseAdmin extends Controller { * Updates the database schema, creating tables & fields as necessary. */ function build() { - if(Director::isLive() && Security::database_is_ready() && (!Member::currentUser() || !Member::currentUser()->isAdmin())) { + if(Director::isLive() && Security::database_is_ready() && !Director::is_cli() && (!Member::currentUser() || !Member::currentUser()->isAdmin())) { Security::permissionFailure($this, "This page is secured and you need administrator rights to access it. " . "Enter your credentials below and we will send you right along."); diff --git a/core/model/SQLQuery.php b/core/model/SQLQuery.php index 21053e44e..374a48db2 100755 --- a/core/model/SQLQuery.php +++ b/core/model/SQLQuery.php @@ -68,6 +68,7 @@ class SQLQuery extends Object { /** * Construct a new SQLQuery. + * * @param array $select An array of fields to select. * @param array $from An array of join clauses. The first one should be just the table name. * @param array $where An array of filters, to be inserted into the WHERE clause. @@ -76,7 +77,7 @@ class SQLQuery extends Object { * @param array $having An array of having clauses. * @param string $limit A LIMIT clause. */ - function __construct($select = array(), $from = array(), $where = "", $orderby = "", $groupby = "", $having = "", $limit = "") { + 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); @@ -87,7 +88,76 @@ class SQLQuery extends Object { parent::__construct(); } - + + /** + * Specify the list of columns to be selected by the query. + * + * + * // pass fields to select as single parameter array + * $query->select(array("Col1","Col2"))->from("MyTable"); + * + * // pass fields to select as multiple parameters + * $query->select("Col1", "Col2")->from("MyTable"); + * + * + * @param mixed $fields + * @return SQLQuery + */ + public function select($fields) { + if (func_num_args() > 1) { + $this->select = func_get_args(); + } else { + $this->select = is_array($fields) ? $fields : array($fields); + } + return $this; + } + + /** + * Specify the target table to select from. + * + * + * $query->from("MyTable"); // SELECT * FROM MyTable + * + * + * @param string $table + * @return SQLQuery + */ + public function from($table) { + $this->from[] = $table; + return $this; + } + + /** + * Apply a predicate filter to the where clause. + * + * Accepts a variable length of arguments, which represent + * different ways of formatting a predicate in a where clause: + * + * + * // the entire predicate as a single string + * $query->where("Column = 'Value'"); + * + * // an exact match predicate with a key value pair + * $query->where("Column", "Value"); + * + * // a predicate with user defined operator + * $query->where("Column", "!=", "Value"); + * + * + */ + public function where() { + $args = func_get_args(); + if (func_num_args() == 3) { + $filter = "{$args[0]} {$args[1]} '{$args[2]}'"; + } elseif (func_num_args() == 2) { + $filter = "{$args[0]} = '{$args[1]}'"; + } else { + $filter = $args[0]; + } + $this->where[] = $filter; + return $this; + } + /** * Use the disjunctive operator 'OR' to join filter expressions in the WHERE clause. */ @@ -147,19 +217,21 @@ class SQLQuery extends Object { * @return string */ function getFilter() { - return implode(") {$this->connective} (" , $this->where); + return ($this->where) ? implode(") {$this->connective} (" , $this->where) : ''; } /** * Generate the SQL statement for this query. + * * @return string */ function sql() { + if (!$this->from) return ''; $distinct = $this->distinct ? "DISTINCT " : ""; - if($this->select) { + if($this->delete) { + $text = "DELETE "; + } else if($this->select) { $text = "SELECT $distinct" . implode(", ", $this->select); - } else { - if($this->delete) $text = "DELETE "; } $text .= " FROM " . implode(" ", $this->from); @@ -172,6 +244,15 @@ class SQLQuery extends Object { return $text; } + /** + * Return the generated SQL string for this query + * + * @return string + */ + function __toString() { + return $this->sql(); + } + /** * Execute this query. * @return Query diff --git a/forms/ComplexTableField.php b/forms/ComplexTableField.php index d80b7171f..5b931caf8 100755 --- a/forms/ComplexTableField.php +++ b/forms/ComplexTableField.php @@ -53,6 +53,9 @@ class ComplexTableField extends TableListField { */ protected $permissions = array( "add", + "edit", + "show", + "delete", //"export", ); @@ -213,7 +216,6 @@ JS; } $this->sourceItems = DataObject::get($this->sourceClass, $this->sourceFilter, $sort, $this->sourceJoin, $limitClause); - $this->unpagedSourceItems = DataObject::get($this->sourceClass, $this->sourceFilter, $sort, $this->sourceJoin); $this->totalCount = ($this->unpagedSourceItems) ? $this->unpagedSourceItems->TotalItems() : null; @@ -436,8 +438,11 @@ JS; // add relational fields $detailFields->push(new HiddenField("ctf[parentClass]"," ",$this->getParentClass())); - if( $this->relationAutoSetting ) - $detailFields->push(new HiddenField("$parentIdName"," ",$childData->ID)); + 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())); + } } } @@ -485,18 +490,17 @@ JS; * * @see {Form::ReferencedField}). */ - function saveComplexTableField($params) { + function saveComplexTableField($data, $form, $params) { $className = $this->sourceClass(); $childData = new $className(); - - $this->saveInto($childData); + $form->saveInto($childData); $childData->write(); // if ajax-call in an iframe, update window if(Director::is_ajax()) { // Newly saved objects need their ID reflected in the reloaded form to avoid double saving - $form = $this->controller->DetailForm(); - //$form->loadDataFrom($this->dataObject); + $childRequestHandler = new ComplexTableField_ItemRequest($this, $childData->ID); + $form = $childRequestHandler->DetailForm(); FormResponse::update_dom_id($form->FormName(), $form->formHtmlContent(), true, 'update'); return FormResponse::respond(); } else { @@ -542,18 +546,20 @@ class ComplexTableField_ItemRequest extends RequestHandlingData { } $this->methodName = "show"; - /* - $this->sourceItems = $this->ctg->sourceItems(); - - $this->pageSize = 1; - - if(isset($_REQUEST['ctf'][$this->Name()]['start']) && is_numeric($_REQUEST['ctf'][$this->Name()]['start'])) { - $this->unpagedSourceItems->setPageLimits($_REQUEST['ctf'][$this->Name()]['start'], $this->pageSize, $this->totalCount); - } - */ - echo $this->renderWith($this->ctf->templatePopup); } + + /** + * Returns a 1-element data object set that can be used for pagination. + */ + /* this doesn't actually work :-( + function Paginator() { + $paginatingSet = new DataObjectSet(array($this->dataObj())); + $start = isset($_REQUEST['ctf']['start']) ? $_REQUEST['ctf']['start'] : 0; + $paginatingSet->setPageLimits($start, 1, $this->ctf->TotalCount()); + return $paginatingSet; + } + */ /** * Just a hook, processed in {DetailForm()} @@ -566,25 +572,23 @@ class ComplexTableField_ItemRequest extends RequestHandlingData { } $this->methodName = "edit"; - /* - $this->sourceItems = $this->sourceItems(); - - $this->pageSize = 1; - - if(is_numeric($_REQUEST['ctf']['start'])) { - $this->unpagedSourceItems->setPageLimits($_REQUEST['ctf']['start'], $this->pageSize, $this->totalCount); - } - */ - echo $this->renderWith($this->ctf->templatePopup); } + function delete() { + if($this->ctf->Can('delete') !== true) { + return false; + } + + $this->dataObj()->delete(); + } + /////////////////////////////////////////////////////////////////////////////////////////////////// /** * Return the data object being manipulated */ - function obj() { + function dataObj() { // 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 @@ -605,7 +609,7 @@ class ComplexTableField_ItemRequest extends RequestHandlingData { * @param int $childID */ function DetailForm($childID = null) { - $childData = $this->obj(); + $childData = $this->dataObj(); $fields = $this->ctf->getFieldsFor($childData); $validator = $this->ctf->getValidatorFor($childData); @@ -631,13 +635,13 @@ class ComplexTableField_ItemRequest extends RequestHandlingData { * @see {Form::ReferencedField}). */ function saveComplexTableField($data, $form, $request) { - $form->saveInto($this->obj()); - $this->obj()->write(); + $form->saveInto($this->dataObj()); + $this->dataObj()->write(); // if ajax-call in an iframe, update window if(Director::is_ajax()) { // Newly saved objects need their ID reflected in the reloaded form to avoid double saving - $form = $this->controller->DetailForm(); + $form = $this->DetailForm(); //$form->loadDataFrom($this->dataObject); FormResponse::update_dom_id($form->FormName(), $form->formHtmlContent(), true, 'update'); return FormResponse::respond(); @@ -647,58 +651,52 @@ class ComplexTableField_ItemRequest extends RequestHandlingData { } } - function PopupBaseLink() { - $link = $this->FormAction() . "&action_callfieldmethod&fieldName={$this->Name()}"; - if(!strpos($link,'ctf[ID]')) { - $link = str_replace('&','&',HTTP::setGetVar('ctf[ID]',$this->sourceID(),$link)); - } - return $link; - } - function PopupCurrentItem() { return $_REQUEST['ctf']['start']+1; } - + function PopupFirstLink() { - if(!is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == 0) { + $this->ctf->LinkToItem(); + + if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == 0) { return null; } $item = $this->unpagedSourceItems->First(); $start = 0; - return Convert::raw2att($this->PopupBaseLink() . "&methodName={$_REQUEST['methodName']}&ctf[childID]={$item->ID}&ctf[start]={$start}"); + return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}"); } function PopupLastLink() { - if(!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; } $item = $this->unpagedSourceItems->Last(); $start = $this->totalCount - 1; - return Convert::raw2att($this->PopupBaseLink() . "&methodName={$_REQUEST['methodName']}&ctf[childID]={$item->ID}&ctf[start]={$start}"); + return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}"); } function PopupNextLink() { - if(!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; } $item = $this->unpagedSourceItems->getIterator()->getOffset($_REQUEST['ctf']['start'] + 1); $start = $_REQUEST['ctf']['start'] + 1; - return Convert::raw2att($this->PopupBaseLink() . "&methodName={$_REQUEST['methodName']}&ctf[childID]={$item->ID}&ctf[start]={$start}"); + return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}"); } function PopupPrevLink() { - if(!is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == 0) { + if(!isset($_REQUEST['ctf']['start']) || !is_numeric($_REQUEST['ctf']['start']) || $_REQUEST['ctf']['start'] == 0) { return null; } $item = $this->unpagedSourceItems->getIterator()->getOffset($_REQUEST['ctf']['start'] - 1); $start = $_REQUEST['ctf']['start'] - 1; - return Convert::raw2att($this->PopupBaseLink() . "&methodName={$_REQUEST['methodName']}&ctf[childID]={$item->ID}&ctf[start]={$start}"); + return Controller::join_links($this->Link(), "$this->methodName?ctf[start]={$start}"); } /** @@ -722,7 +720,7 @@ class ComplexTableField_ItemRequest extends RequestHandlingData { for($i = $offset;$i <= $offset + $this->pageSize && $i <= $this->totalCount;$i++) { $start = $i - 1; $item = $this->unpagedSourceItems->getIterator()->getOffset($i-1); - $links['link'] = Convert::raw2att($this->PopupBaseLink() . "&methodName={$_REQUEST['methodName']}&ctf[childID]={$item->ID}&ctf[start]={$start}"); + $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)); @@ -730,6 +728,9 @@ class ComplexTableField_ItemRequest extends RequestHandlingData { return $result; } + function ShowPagination() { + return false; + } /** @@ -886,10 +887,6 @@ class ComplexTableField_Popup extends Form { function FieldHolder() { return $this->renderWith('ComplexTableField_Form'); } - - function ShowPagination() { - return $this->controller->ShowPagination(); - } } ?> diff --git a/forms/FieldSet.php b/forms/FieldSet.php index e39b73552..d5ce93e84 100755 --- a/forms/FieldSet.php +++ b/forms/FieldSet.php @@ -32,6 +32,7 @@ class FieldSet extends DataObjectSet { if($field->hasData()) { $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); diff --git a/forms/Form.php b/forms/Form.php index 6e907514f..60b9c44ce 100644 --- a/forms/Form.php +++ b/forms/Form.php @@ -813,6 +813,15 @@ class Form extends RequestHandlingData { )); } + /** + * Return a rendered version of this form, suitable for ajax post-back. + * It triggers slightly different behaviour, such as disabling the rewriting of # links + */ + function forAjaxTemplate() { + $view = new SSViewer("Form"); + return $view->dontRewriteHashlinks()->process($this); + } + /** * Returns an HTML rendition of this form, without the
tag itself. * Attaches 3 extra hidden files, _form_action, _form_name, _form_method, and _form_enctype. These are diff --git a/forms/TableListField.php b/forms/TableListField.php index 34f2e877a..282803e71 100755 --- a/forms/TableListField.php +++ b/forms/TableListField.php @@ -222,7 +222,7 @@ JS // sorting links (only if we have a form to refresh with) if($this->form) { - $sortLink = $this->BaseLink(); + $sortLink = $this->Link(); $sortLink = HTTP::setGetVar("ctf[{$this->Name()}][sort]", $fieldName, $sortLink); if(isset($_REQUEST['ctf'][$this->Name()]['dir'])) { $XML_sort = (isset($_REQUEST['ctf'][$this->Name()]['dir'])) ? Convert::raw2xml($_REQUEST['ctf'][$this->Name()]['dir']) : null; @@ -644,7 +644,7 @@ JS return null; } - return $this->BaseLink() . "&ctf[{$this->Name()}][start]={$start}{$this->filterString()}"; + return $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]={$start}{$this->filterString()}"; } function PrevLink() { @@ -656,7 +656,7 @@ JS $start = ($_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize < 0) ? 0 : $_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize; - return $this->BaseLink() . "&ctf[{$this->Name()}][start]=$start{$this->filterString()}"; + return $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]=$start{$this->filterString()}"; } function NextLink() { @@ -665,7 +665,7 @@ JS if($currentStart >= $start-1) { return null; } - return $this->BaseLink() . "&ctf[{$this->Name()}][start]={$start}{$this->filterString()}"; + return $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]={$start}{$this->filterString()}"; } function LastLink() { @@ -678,7 +678,7 @@ JS return null; } - return $this->BaseLink() . "&ctf[{$this->Name()}][start]=$start{$this->filterString()}"; + return $this->Link() . "/ajax_refresh?ctf[{$this->Name()}][start]=$start{$this->filterString()}"; } function FirstItem() { @@ -815,7 +815,7 @@ JS * We need to instanciate this button manually as a normal button has no means of adding inline onclick-behaviour. */ function ExportLink() { - return Director::absoluteURL($this->FormAction()) . "&action_callfieldmethod&fieldName={$this->Name()}&methodName=export"; + return Controller::join_links($this->Link(), 'export'); } function printall() { @@ -832,7 +832,7 @@ JS } function PrintLink() { - $link = Director::absoluteURL($this->FormAction()) . "&action_callfieldmethod&fieldName={$this->Name()}&methodName=printall"; + $link = Controller::join_links($this->Link(), 'printall'); if(isset($_REQUEST['ctf'][$this->Name()]['sort'])) { $link = HTTP::setGetVar("ctf[{$this->Name()}][sort]",Convert::raw2xml($_REQUEST['ctf'][$this->Name()]['sort']), $link); } @@ -913,50 +913,12 @@ JS function setTemplate($template) { $this->template = $template; } - - function BaseLink() { - return $this->FormAction() . "&action_callfieldmethod&fieldName={$this->Name()}&ctf[ID]={$this->sourceID()}&methodName=ajax_refresh&SecurityID=" . Session::get('SecurityID'); - } - - /** - * Returns the action of the surrounding form - needed to maintain context on subsequent calls. - * It is only needed to embed this field into a form if you want to use more than "display-functionality". - * We try to mirror the existing GET-properties to achieve the same application-state. - * - * @return String - */ - function FormAction() { - $params = $_GET; - - // we don't want this to be overriding our new actions - unset($params['executeForm']); - unset($params['fieldName']); - unset($params['url']); - unset($params['methodName']); - unset($params['forcehtml']); - // TODO Refactor - unset($params['ctf']); - $params['ctf'][$this->Name()]['search'] = (isset($_REQUEST['ctf'][$this->Name()]['search'])) ? $_REQUEST['ctf'][$this->Name()]['search'] : null; - $params['SecurityID'] = Session::get('SecurityID'); - - // unset all actions (otherwise they override action_callfieldmethod) - foreach($params as $paramKey => $paramVal) { - if(strpos($paramKey, 'action_') === 0) { - unset($params[$paramKey]); - } - } - - - try { - $link = $this->form->FormAction(); - $link .= (!strpos($link,'?')) ? '?' : '&'; - $link .= urldecode (http_build_query($params)); - } catch(Exception $e) { - user_error('Please embed this field into a form if you want to use actions such as "add", "edit" or "delete"', E_USER_ERROR); - } - return $link; - } + function BaseLink() { + user_error("TableListField::BaseLink() deprecated, use Link() instead", E_USER_NOTICE); + return $this->Link(); + } + /** * @return Int */ @@ -1069,11 +1031,15 @@ class TableListField_Item extends ViewableData { } function BaseLink() { - return $this->parent->FormAction() . "&action_callfieldmethod&fieldName={$this->parent->Name()}&ctf[childID]={$this->item->ID}"; + user_error("TableListField_Item::BaseLink() deprecated, use Link() instead", E_USER_NOTICE); + return $this->Link() . '/ajax_refresh'; + } + function Link() { + return Controller::join_links($this->parent->Link() . "item/" . $this->item->ID); } function DeleteLink() { - return $this->BaseLink() . "&methodName=delete"; + return Controller::join_links($this->Link(), "delete"); } function MarkingCheckbox() { diff --git a/javascript/ComplexTableField.js b/javascript/ComplexTableField.js index f320bf146..fca380457 100755 --- a/javascript/ComplexTableField.js +++ b/javascript/ComplexTableField.js @@ -92,7 +92,7 @@ ComplexTableField.prototype = { var table = Event.findElement(e,"table"); if(Event.element(e).nodeName == "IMG") { link = Event.findElement(e,"a"); - popupLink = link.href+"&ajax=1"; + popupLink = link.href+"?ajax=1"; } else { el = Event.findElement(e,"tr"); var link = $$("a",el)[0]; @@ -112,16 +112,13 @@ ComplexTableField.prototype = { GB_OpenerObj = this; // use same url to refresh the table after saving the popup, but use a generic rendering method - GB_RefreshLink = popupLink; - GB_RefreshLink = GB_RefreshLink.replace(/(methodName=)[^&]*/,"$1ajax_refresh"); - // dont include pagination index - GB_RefreshLink = GB_RefreshLink.replace(/ctf\[start\][^&]*/,""); - GB_RefreshLink += '&forcehtml=1'; + GB_RefreshLink = this.getAttribute('href'); if(this.GB_Caption) { var title = this.GB_Caption; } else { - type = popupLink.match(/methodName=([^&]*)/); + // Getting the title from the URL is pretty ugly, but it works for now + type = popupLink.match(/[0-9]+\/([^\/?&]*)([?&]|$)/); var title = (type && type[1]) ? type[1].ucfirst() : ""; } diff --git a/javascript/ComplexTableField_popup.js b/javascript/ComplexTableField_popup.js index 20633e871..22749fdab 100755 --- a/javascript/ComplexTableField_popup.js +++ b/javascript/ComplexTableField_popup.js @@ -4,11 +4,12 @@ ComplexTableFieldPopupForm.prototype = { errorMessage: "Error talking to server", initialize: function() { - Behaviour.register({ - "form#ComplexTableField_Popup_DetailForm .Actions input.action": { - onclick: this.submitForm.bind(this) - } - }); + var rules = {}; + rules["#" + this.id + " .Actions input.action"] = { + 'onclick' : this.submitForm.bind(this) + }; + + Behaviour.register(rules); }, loadNewPage : function(content) { @@ -33,7 +34,6 @@ ComplexTableFieldPopupForm.prototype = { submitButton.disabled = true; Element.addClassName(submitButton,'loading'); } - new parent.parent.Ajax.Request( theForm.getAttribute("action"), { @@ -58,7 +58,6 @@ ComplexTableFieldPopupForm.prototype = { // don't update when validation is present and failed if(!this.validate || (this.validate && !hasHadFormError())) { - alert("GB:" + parent.parent.GB_RefreshLink); new parent.parent.Ajax.Request( parent.parent.GB_RefreshLink, { @@ -113,8 +112,9 @@ ComplexTableFieldPopupForm.prototype = { } // causes IE6 to go nuts - //this.GB_hide(); + this.GB_hide(); } } -ComplexTableFieldPopupForm.applyTo('form#ComplexTableField_Popup_DetailForm'); \ No newline at end of file +ComplexTableFieldPopupForm.applyTo('#ComplexTableField_Popup_DetailForm'); +ComplexTableFieldPopupForm.applyTo('#ComplexTableField_Popup_AddForm'); \ No newline at end of file diff --git a/sake b/sake new file mode 100755 index 000000000..5196b10ef --- /dev/null +++ b/sake @@ -0,0 +1,40 @@ +# Check for an argument +if [ $1 = "" ]; then + echo "Sapphire Sake + +Usage: $0 (command-url) (params) +Executes a Sapphire command" + exit 1 +fi + +# Special case for "sake installsake" +if [ "$1" = "installsake" ]; then + echo "Installing sake to /usr/bin..." + cp $0 /usr/bin + exit 0 +fi + +# Find the PHP binary +for candidatephp in php5 php; do + if [ -f `which $candidatephp` ]; then + php=`which $candidatephp` + break + fi +done + +if [ "$php" = "" ]; then + echo "Can't find any php binary" + exit 2 +fi + +if [ -d ./sapphire ]; then + $php ./sapphire/cli-script.php $1 $2 + exit 0 +fi + +if [ -f ./cli-script.php ]; then + $php ./cli-script.php $1 $2 + exit 0 +fi + +echo "Can't find ./sapphire/cli-script.php or ./cli-script.php" \ No newline at end of file diff --git a/search/SearchContext.php b/search/SearchContext.php index 83aaf097d..3aed17924 100644 --- a/search/SearchContext.php +++ b/search/SearchContext.php @@ -91,7 +91,8 @@ class SearchContext extends Object { foreach($searchParams as $key => $value) { $filter = $this->getFilter($key); if ($filter) { - $query->where[] = $filter->apply($value); + $filter->setValue($value); + $filter->apply($query); } } return $query; @@ -108,6 +109,7 @@ class SearchContext extends Object { * @return DataObjectSet */ public function getResults($searchParams, $start = false, $limit = false) { + $searchParams = array_filter($searchParams, array($this,'clearEmptySearchFields')); $query = $this->getQuery($searchParams, $start, $limit); // // use if a raw SQL query is needed @@ -121,6 +123,17 @@ class SearchContext extends Object { return DataObject::get($this->modelClass, $query->getFilter(), "", "", $limit); } + /** + * Callback map function to filter fields with empty values from + * being included in the search expression. + * + * @param unknown_type $value + * @return boolean + */ + function clearEmptySearchFields($value) { + return ($value != ''); + } + /** * @todo documentation * @todo implementation @@ -138,8 +151,15 @@ class SearchContext extends Object { } } $query->where = $conditions; + return $query; } + /** + * Accessor for the filter attached to a named field. + * + * @param string $name + * @return SearchFilter + */ public function getFilter($name) { if (isset($this->filters[$name])) { return $this->filters[$name]; @@ -148,29 +168,42 @@ class SearchContext extends Object { } } - public function getFields() { - return $this->fields; - } - - public function setFields($fields) { - $this->fields = $fields; - } - + /** + * Get the map of filters in the current search context. + * + * @return array + */ public function getFilters() { return $this->filters; } public function setFilters($filters) { $this->filters = $filters; + } + + /** + * Get the list of searchable fields in the current search context. + * + * @return array + */ + public function getFields() { + return $this->fields; } - function clearEmptySearchFields($value) { - return ($value != ''); + /** + * Apply a list of searchable fields to the current search context. + * + * @param array $fields + */ + public function setFields($fields) { + $this->fields = $fields; } /** * Placeholder, until I figure out the rest of the SQLQuery stuff * and link the $searchable_fields array to the SearchContext + * + * @deprecated in favor of getResults */ public function getResultSet($fields) { $filter = ""; diff --git a/search/filters/ExactMatchFilter.php b/search/filters/ExactMatchFilter.php index 3ce3255b9..e02b12a2b 100644 --- a/search/filters/ExactMatchFilter.php +++ b/search/filters/ExactMatchFilter.php @@ -15,8 +15,8 @@ class ExactMatchFilter extends SearchFilter { * * @return unknown */ - public function apply($value) { - return "{$this->name}='$value'"; + public function apply(SQLQuery $query) { + return $query->where("{$this->name} = '{$this->value}'"); } } diff --git a/search/filters/FulltextFilter.php b/search/filters/FulltextFilter.php index 373a60755..dd0456c57 100644 --- a/search/filters/FulltextFilter.php +++ b/search/filters/FulltextFilter.php @@ -1,15 +1,30 @@ + * static $indexes = array( + * 'SearchFields' => 'fulltext(Name, Title, Description)' + * ); + * * * @package sapphire * @subpackage search */ class FulltextFilter extends SearchFilter { - public function apply($value) { + public function apply(SQLQuery $query) { return ""; } - + } ?> \ No newline at end of file diff --git a/search/filters/NegationFilter.php b/search/filters/NegationFilter.php index 915089971..21b692128 100644 --- a/search/filters/NegationFilter.php +++ b/search/filters/NegationFilter.php @@ -12,8 +12,8 @@ */ class NegationFilter extends SearchFilter { - public function apply($value) { - return "{$this->name} != '$value'"; + public function apply(SQLQuery $query) { + return $query->where("{$this->name} != '{$this->value}'"); } } diff --git a/search/filters/PartialMatchFilter.php b/search/filters/PartialMatchFilter.php index 557d0f3af..b87e145c6 100644 --- a/search/filters/PartialMatchFilter.php +++ b/search/filters/PartialMatchFilter.php @@ -7,8 +7,8 @@ */ class PartialMatchFilter extends SearchFilter { - public function apply($value) { - return "{$this->name} LIKE '%$value%'"; + public function apply(SQLQuery $query) { + return $query->where("{$this->name} LIKE '%{$this->value}%'"); } } diff --git a/search/filters/SearchFilter.php b/search/filters/SearchFilter.php index 9937f05c4..5c377a046 100644 --- a/search/filters/SearchFilter.php +++ b/search/filters/SearchFilter.php @@ -8,12 +8,23 @@ abstract class SearchFilter extends Object { protected $name; + protected $value; - function __construct($name) { + function __construct($name, $value = false) { $this->name = $name; + $this->value = $value; } - abstract public function apply($value); + public function setValue($value) { + $this->value = $value; + } + + /** + * Apply filter criteria to a SQL query. + * + * @param SQLQuery $query + */ + abstract public function apply(SQLQuery $query); } ?> \ No newline at end of file diff --git a/search/filters/SubstringFilter.php b/search/filters/SubstringFilter.php new file mode 100644 index 000000000..0d85e441a --- /dev/null +++ b/search/filters/SubstringFilter.php @@ -0,0 +1,16 @@ +where("LOCATE({$this->name}, $value)"); + } + +} + +?> \ No newline at end of file diff --git a/search/filters/SubstringMatchFilter.php b/search/filters/SubstringMatchFilter.php deleted file mode 100644 index 4314160d1..000000000 --- a/search/filters/SubstringMatchFilter.php +++ /dev/null @@ -1,16 +0,0 @@ - \ No newline at end of file diff --git a/security/BasicAuth.php b/security/BasicAuth.php index 95859905f..2579d179c 100755 --- a/security/BasicAuth.php +++ b/security/BasicAuth.php @@ -24,7 +24,7 @@ class BasicAuth extends Object { */ static function requireLogin($realm, $permissionCode) { if(self::$disabled) return true; - if(!Security::database_is_ready()) return true; + if(!Security::database_is_ready() || Director::is_cli()) return true; if(isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) { diff --git a/templates/ComplexTableField.ss b/templates/ComplexTableField.ss index 819607643..27e0e0ef4 100755 --- a/templates/ComplexTableField.ss +++ b/templates/ComplexTableField.ss @@ -1,4 +1,4 @@ -
+
<% include TableListField_PageControls %> diff --git a/templates/ComplexTableField_popup.ss b/templates/ComplexTableField_popup.ss index d672cf82a..4f2a6bb5e 100755 --- a/templates/ComplexTableField_popup.ss +++ b/templates/ComplexTableField_popup.ss @@ -7,36 +7,34 @@
$DetailForm
- <% if IsAddMode %> - <% else %> - <% if ShowPagination %> -
- - <% if PopupPrevLink %> - - <% end_if %> - <% if TotalCount == 1 %> - <% else %> - - <% end_if %> - <% if PopupNextLink %> - - <% end_if %> - -
- <% _t('PREVIOUS', 'Previous') %> - - <% control Pagination %> - <% if active %> - $number - <% else %> - $number - <% end_if %> - <% end_control %> - - <% _t('NEXT', 'Next') %> -
- <% end_if %> + + <% if ShowPagination %> + + + <% if Paginator.PrevLink %> + + <% end_if %> + <% if xdsfdsf %> + <% else %> + + <% end_if %> + <% if Paginator.NextLink %> + + <% end_if %> + +
+ <% _t('PREVIOUS', 'Previous') %> + + <% control Paginator.Pages %> + <% if active %> + $number + <% else %> + $number + <% end_if %> + <% end_control %> + + <% _t('NEXT', 'Next') %> +
<% end_if %> diff --git a/tests/SQLQueryTest.php b/tests/SQLQueryTest.php new file mode 100644 index 000000000..5d78d534a --- /dev/null +++ b/tests/SQLQueryTest.php @@ -0,0 +1,72 @@ +assertEquals('', $query->sql()); + } + + function testSelectFromBasicTable() { + $query = new SQLQuery(); + $query->from[] = "MyTable"; + $this->assertEquals("SELECT * FROM MyTable", $query->sql()); + $query->from[] = "MyJoin"; + $this->assertEquals("SELECT * FROM MyTable, MyJoin", $query->sql()); + } + + function testSelectFromUserSpecifiedFields() { + $query = new SQLQuery(); + $query->select = array("Name", "Title", "Description"); + $query->from[] = "MyTable"; + $this->assertEquals("SELECT Name, Title, Description FROM MyTable", $query->sql()); + } + + function testSelectWithWhereClauseFilter() { + $query = new SQLQuery(); + $query->select = array("Name","Meta"); + $query->from[] = "MyTable"; + $query->where[] = "Name = 'Name'"; + $query->where[] = "Meta = 'Test'"; + $this->assertEquals("SELECT Name, Meta FROM MyTable WHERE (Name = 'Name') AND (Meta = 'Test')", $query->sql()); + } + + function testSelectWithConstructorParameters() { + $query = new SQLQuery(array("Foo", "Bar"), "FooBarTable"); + $this->assertEquals("SELECT Foo, Bar FROM FooBarTable", $query->sql()); + $query = new SQLQuery(array("Foo", "Bar"), "FooBarTable", array("Foo = 'Boo'")); + $this->assertEquals("SELECT Foo, Bar FROM FooBarTable WHERE (Foo = 'Boo')", $query->sql()); + } + + function testSelectWithChainedMethods() { + $query = new SQLQuery(); + $query->select("Name","Meta")->from("MyTable")->where("Name", "Name")->where("Meta", "Test"); + $this->assertEquals("SELECT Name, Meta FROM MyTable WHERE (Name = 'Name') AND (Meta = 'Test')", $query->sql()); + } + + function testSelectWithChainedFilterParameters() { + $query = new SQLQuery(); + $query->select(array("Name","Meta"))->from("MyTable"); + $query->where("Name = 'Name'")->where("Meta","Test")->where("Beta", "!=", "Gamma"); + $this->assertEquals("SELECT Name, Meta FROM MyTable WHERE (Name = 'Name') AND (Meta = 'Test') AND (Beta != 'Gamma')", $query->sql()); + } + + function testSelectWithPredicateFilters() { + $query = new SQLQuery(); + $query->select(array("Name"))->from("MyTable"); + $match = new ExactMatchFilter("Name", "Value"); + $match->apply($query); + $match = new PartialMatchFilter("Meta", "Value"); + $match->apply($query); + $this->assertEquals("SELECT Name FROM MyTable WHERE (Name = 'Value') AND (Meta LIKE '%Value%')", $query->sql()); + } + + function testSelectWithLimitClause() { + // not implemented + } + +} + +?> \ No newline at end of file diff --git a/tests/SearchContextTest.php b/tests/SearchContextTest.php index e6b0e3fd5..bf065cb33 100644 --- a/tests/SearchContextTest.php +++ b/tests/SearchContextTest.php @@ -7,15 +7,14 @@ class SearchContextTest extends SapphireTest { $person = singleton('SearchContextTest_Person'); $context = $person->getDefaultSearchContext(); - $results = $context->getResultSet(array('Name'=>'')); + $results = $context->getResults(array('Name'=>'')); $this->assertEquals(5, $results->Count()); - $results = $context->getResultSet(array('EyeColor'=>'green')); + $results = $context->getResults(array('EyeColor'=>'green')); $this->assertEquals(2, $results->Count()); - $results = $context->getResultSet(array('EyeColor'=>'green', 'HairColor'=>'black')); + $results = $context->getResults(array('EyeColor'=>'green', 'HairColor'=>'black')); $this->assertEquals(1, $results->Count()); - } function testSummaryIncludesDefaultFieldsIfNotDefined() { @@ -58,26 +57,26 @@ class SearchContextTest extends SapphireTest { } function testUserDefinedFiltersAppearInSearchContext() { - //$company = singleton('SearchContextTest_Company'); - //$context = $company->getDefaultSearchContext(); + $company = singleton('SearchContextTest_Company'); + $context = $company->getDefaultSearchContext(); - /*$this->assertEquals( + $this->assertEquals( array( "Name" => new PartialMatchFilter("Name"), "Industry" => new ExactMatchFilter("Industry"), "AnnualProfit" => new PartialMatchFilter("AnnualProfit") ), $context->getFilters() - );*/ + ); } function testRelationshipObjectsLinkedInSearch() { - //$project = singleton('SearchContextTest_Project'); - //$context = $project->getDefaultSearchContext(); + $project = singleton('SearchContextTest_Project'); + $context = $project->getDefaultSearchContext(); - //$query = array("Name"=>"Blog Website"); + $query = array("Name"=>"Blog Website"); - //$results = $context->getQuery($query); + $results = $context->getQuery($query); } function testCanGenerateQueryUsingAllFilterTypes() { @@ -194,13 +193,15 @@ class SearchContextTest_AllFilterTypes extends DataObject implements TestOnly { "ExactMatch" => "Text", "PartialMatch" => "Text", "Negation" => "Text", - "HiddenValue" => "Text" + "SubstringMatch" => "Text", + "HiddenValue" => "Text", ); static $searchable_fields = array( "ExactMatch" => "ExactMatchFilter", "PartialMatch" => "PartialMatchFilter", - "Negation" => "NegationFilter" + "Negation" => "NegationFilter", + "SubstringMatch" => "SubstringFilter" ); }