(merged from branches/roa. use "svn log -c <changeset> -g <module-svn-path>" for detailed commit message)

git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@60209 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2008-08-09 04:38:44 +00:00
parent a599df309c
commit 016cff2093
32 changed files with 711 additions and 405 deletions

View File

@ -32,10 +32,7 @@ Director::addRules(10, array(
)); ));
Director::addRules(1, array( Director::addRules(1, array(
'$URLSegment/$Action/$ID/$OtherID' => array( '$URLSegment//$Action/$ID/$OtherID' => 'ModelAsController',
'_PopTokeniser' => 1,
'Controller' => 'ModelAsController',
),
)); ));
/** /**

42
api/DataFormatter.php Normal file
View File

@ -0,0 +1,42 @@
<?php
/**
* A DataFormatter object handles transformation of data from Sapphire model objects to a particular output format, and vice versa.
* This is most commonly used in developing RESTful APIs.
*/
abstract class DataFormatter extends Object {
/**
* Get a DataFormatter object suitable for handling the given file extension
*/
static function for_extension($extension) {
$classes = ClassInfo::subclassesFor("DataFormatter");
array_shift($classes);
foreach($classes as $class) {
$formatter = singleton($class);
if(in_array($extension, $formatter->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);
}

81
api/JSONDataFormatter.php Normal file
View File

@ -0,0 +1,81 @@
<?php
class JSONDataFormatter extends DataFormatter {
/**
* @todo pass this from the API to the data formatter somehow
*/
static $api_base = "api/v1/";
public function supportedExtensions() {
return array('json', 'js');
}
/**
* Generate an XML representation of the given {@link DataObject}.
*
* @param DataObject $obj
* @param $includeHeader Include <?xml ...?> 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]";
}
}

View File

@ -71,16 +71,19 @@ class RestfulServer extends Controller {
'html' => 'text/html', 'html' => 'text/html',
); );
$contentType = isset($contentMap[$extension]) ? $contentMap[$extension] : 'text/xml'; $contentType = isset($contentMap[$extension]) ? $contentMap[$extension] : 'text/xml';
if(!$extension) $extension = "xml";
$formatter = DataFormatter::for_extension($extension); //$this->dataFormatterFromMime($contentType);
switch($requestMethod) { switch($requestMethod) {
case 'GET': case 'GET':
return $this->getHandler($className, $id, $relation, $contentType); return $this->getHandler($className, $id, $relation, $formatter);
case 'PUT': case 'PUT':
return $this->putHandler($className, $id, $relation, $contentType); return $this->putHandler($className, $id, $relation, $formatter);
case 'DELETE': case 'DELETE':
return $this->deleteHandler($className, $id, $relation, $contentType); return $this->deleteHandler($className, $id, $relation, $formatter);
case 'POST': case 'POST':
} }
@ -118,7 +121,7 @@ class RestfulServer extends Controller {
* @param String $contentType * @param String $contentType
* @return String The serialized representation of the requested object(s) - usually XML or JSON. * @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) { if($id) {
$obj = DataObject::get_by_id($className, $id); $obj = DataObject::get_by_id($className, $id);
if(!$obj) { if(!$obj) {
@ -142,176 +145,10 @@ class RestfulServer extends Controller {
return $this->permissionFailure(); 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": if($obj instanceof DataObjectSet) return $formatter->convertDataObjectSet($obj);
//$this->getResponse()->addHeader("Content-type", "text/json"); else return $formatter->convertDataObject($obj);
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);
}
} }
/**
* Generate an XML representation of the given {@link DataObject}.
*
* @param DataObject $obj
* @param $includeHeader Include <?xml ...?> 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 .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\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) . "</$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 .= "</$relName>\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 .= "</$relName>\n";
}
$json .= "</$className>";
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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<$className>\n";
foreach($set as $item) {
if($item->canView()) $xml .= $this->dataObjectAsXML($item, false);
}
$xml .= "</$className>";
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 * Handler for object delete
@ -324,7 +161,6 @@ class RestfulServer extends Controller {
} else { } else {
return $this->permissionFailure(); return $this->permissionFailure();
} }
} }
} }

96
api/XMLDataFormatter.php Normal file
View File

@ -0,0 +1,96 @@
<?php
class XMLDataFormatter extends DataFormatter {
/**
* @todo pass this from the API to the data formatter somehow
*/
static $api_base = "api/v1/";
public function supportedExtensions() {
return array('xml');
}
/**
* Generate an XML representation of the given {@link DataObject}.
*
* @param DataObject $obj
* @param $includeHeader Include <?xml ...?> header (Default: true)
* @return String XML
*/
public function convertDataObject(DataObjectInterface $obj) {
Controller::curr()->getResponse()->addHeader("Content-type", "text/xml");
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\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) . "</$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 .= "</$relName>\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 .= "</$relName>\n";
}
$json .= "</$className>";
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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<$className>\n";
foreach($set as $item) {
if($item->canView()) $xml .= $this->convertDataObjectWithoutHeader($item);
}
$xml .= "</$className>";
return $xml;
}
}

View File

@ -1,6 +1,10 @@
#!/usr/bin/php5 #!/usr/bin/php5
<?php <?php
if(isset($_SERVER['HTTP_HOST'])) {
echo "cli-script.php can't be run from a web request, you have to run it on the command-line.";
die();
}
/** /**
* File similar to main.php designed for command-line scripts * File similar to main.php designed for command-line scripts
@ -98,7 +102,7 @@ require_once(MANIFEST_FILE);
if(isset($_GET['debugmanifest'])) Debug::show(file_get_contents(MANIFEST_FILE)); if(isset($_GET['debugmanifest'])) Debug::show(file_get_contents(MANIFEST_FILE));
if(!isset(Director::$environment_type)) Director::set_environment_type($envType); if(!isset(Director::$environment_type) && $envType) Director::set_environment_type($envType);
// Load error handlers // Load error handlers
Debug::loadErrorHandlers(); Debug::loadErrorHandlers();

View File

@ -62,6 +62,13 @@ class ArrayData extends ViewableData {
return $arr; return $arr;
} }
/**
* This is pretty crude, but it helps diagnose error situations
*/
function forTemplate() {
return var_export($this->array, true);
}
} }
?> ?>

View File

@ -109,6 +109,7 @@ class SSViewer extends Object {
*/ */
public function dontRewriteHashlinks() { public function dontRewriteHashlinks() {
$this->rewriteHashlinks = false; $this->rewriteHashlinks = false;
self::$options['rewriteHashlinks'] = false;
return $this; return $this;
} }

View File

@ -577,7 +577,8 @@ class Director {
*/ */
static function set_environment_type($et) { static function set_environment_type($et) {
if($et != 'dev' && $et != 'test' && $et != 'live') { 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 { } else {
self::$environment_type = $et; self::$environment_type = $et;
} }

View File

@ -1206,7 +1206,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
$model = singleton($component); $model = singleton($component);
$records = DataObject::get($component); $records = DataObject::get($component);
$collect = ($model->hasMethod('customSelectOption')) ? 'customSelectOption' : current($model->summary_fields()); $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)); $fields->push(new DropdownField($relationship.'ID', $relationship, $options));
} }
return $fields; return $fields;
@ -1216,9 +1216,21 @@ class DataObject extends ViewableData implements DataObjectInterface {
* Add the scaffold-generated relation fields to the given field set * Add the scaffold-generated relation fields to the given field set
*/ */
protected function addScaffoldRelationFields($fieldSet) { protected function addScaffoldRelationFields($fieldSet) {
foreach($this->has_many() as $relationship => $component) {
$relationshipFields = array_keys($this->searchable_fields()); if($this->has_many()) {
$fieldSet->push(new ComplexTableField($this, $relationship, $component, $relationshipFields)); // 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; return $fieldSet;
} }
@ -1249,7 +1261,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
$fields = $this->scaffoldFormFields(); $fields = $this->scaffoldFormFields();
// If we don't have an ID, then relation fields don't work // If we don't have an ID, then relation fields don't work
if($this->ID) { if($this->ID) {
$this->addScaffoldRelationFields($fields); $fields = $this->addScaffoldRelationFields($fields);
} }
return $fields; return $fields;
} }
@ -2085,7 +2097,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
public function searchable_fields() { public function searchable_fields() {
$fields = $this->stat('searchable_fields'); $fields = $this->stat('searchable_fields');
if (!$fields) { if (!$fields) {
$fields = array_fill_keys($this->summary_fields(), 'TextField'); $fields = array_fill_keys(array_keys($this->summary_fields()), 'TextField');
} }
return $fields; return $fields;
} }
@ -2101,11 +2113,19 @@ class DataObject extends ViewableData implements DataObjectInterface {
$fields = $this->stat('summary_fields'); $fields = $this->stat('summary_fields');
if (!$fields) { if (!$fields) {
$fields = array(); $fields = array();
if ($this->hasField('Name')) $fields[] = 'Name'; if ($this->hasField('Name')) $fields['Name'] = 'Name';
if ($this->hasField('Title')) $fields[] = 'Title'; if ($this->hasField('Title')) $fields['Title'] = 'Title';
if ($this->hasField('Description')) $fields[] = 'Description'; if ($this->hasField('Description')) $fields['Description'] = 'Description';
if ($this->hasField('Firstname')) $fields[] = 'Firstname'; 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; return $fields;
} }
@ -2128,7 +2148,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
} else { } else {
if (is_array($type)) { if (is_array($type)) {
$filter = current($type); $filter = current($type);
$filters[$name] = new $filter(); $filters[$name] = new $filter($name);
} else { } else {
if (is_subclass_of($type, 'SearchFilter')) { if (is_subclass_of($type, 'SearchFilter')) {
$filters[$name] = new $type($name); $filters[$name] = new $type($name);

View File

@ -75,7 +75,7 @@ class DatabaseAdmin extends Controller {
* Updates the database schema, creating tables & fields as necessary. * Updates the database schema, creating tables & fields as necessary.
*/ */
function build() { 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, Security::permissionFailure($this,
"This page is secured and you need administrator rights to access it. " . "This page is secured and you need administrator rights to access it. " .
"Enter your credentials below and we will send you right along."); "Enter your credentials below and we will send you right along.");

View File

@ -68,6 +68,7 @@ class SQLQuery extends Object {
/** /**
* Construct a new SQLQuery. * Construct a new SQLQuery.
*
* @param array $select An array of fields to select. * @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 $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. * @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 array $having An array of having clauses.
* @param string $limit A LIMIT clause. * @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($select) $this->select = is_array($select) ? $select : array($select);
if($from) $this->from = is_array($from) ? $from : array(str_replace('`','',$from) => $from); if($from) $this->from = is_array($from) ? $from : array(str_replace('`','',$from) => $from);
if($where) $this->where = is_array($where) ? $where : array($where); if($where) $this->where = is_array($where) ? $where : array($where);
@ -87,7 +88,76 @@ class SQLQuery extends Object {
parent::__construct(); parent::__construct();
} }
/**
* Specify the list of columns to be selected by the query.
*
* <code>
* // 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");
* </code>
*
* @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.
*
* <code>
* $query->from("MyTable"); // SELECT * FROM MyTable
* </code>
*
* @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:
*
* <code>
* // 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");
* </code>
*
*/
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. * Use the disjunctive operator 'OR' to join filter expressions in the WHERE clause.
*/ */
@ -147,19 +217,21 @@ class SQLQuery extends Object {
* @return string * @return string
*/ */
function getFilter() { function getFilter() {
return implode(") {$this->connective} (" , $this->where); return ($this->where) ? implode(") {$this->connective} (" , $this->where) : '';
} }
/** /**
* Generate the SQL statement for this query. * Generate the SQL statement for this query.
*
* @return string * @return string
*/ */
function sql() { function sql() {
if (!$this->from) return '';
$distinct = $this->distinct ? "DISTINCT " : ""; $distinct = $this->distinct ? "DISTINCT " : "";
if($this->select) { if($this->delete) {
$text = "DELETE ";
} else if($this->select) {
$text = "SELECT $distinct" . implode(", ", $this->select); $text = "SELECT $distinct" . implode(", ", $this->select);
} else {
if($this->delete) $text = "DELETE ";
} }
$text .= " FROM " . implode(" ", $this->from); $text .= " FROM " . implode(" ", $this->from);
@ -172,6 +244,15 @@ class SQLQuery extends Object {
return $text; return $text;
} }
/**
* Return the generated SQL string for this query
*
* @return string
*/
function __toString() {
return $this->sql();
}
/** /**
* Execute this query. * Execute this query.
* @return Query * @return Query

View File

@ -53,6 +53,9 @@ class ComplexTableField extends TableListField {
*/ */
protected $permissions = array( protected $permissions = array(
"add", "add",
"edit",
"show",
"delete",
//"export", //"export",
); );
@ -213,7 +216,6 @@ JS;
} }
$this->sourceItems = DataObject::get($this->sourceClass, $this->sourceFilter, $sort, $this->sourceJoin, $limitClause); $this->sourceItems = DataObject::get($this->sourceClass, $this->sourceFilter, $sort, $this->sourceJoin, $limitClause);
$this->unpagedSourceItems = DataObject::get($this->sourceClass, $this->sourceFilter, $sort, $this->sourceJoin); $this->unpagedSourceItems = DataObject::get($this->sourceClass, $this->sourceFilter, $sort, $this->sourceJoin);
$this->totalCount = ($this->unpagedSourceItems) ? $this->unpagedSourceItems->TotalItems() : null; $this->totalCount = ($this->unpagedSourceItems) ? $this->unpagedSourceItems->TotalItems() : null;
@ -436,8 +438,11 @@ JS;
// add relational fields // add relational fields
$detailFields->push(new HiddenField("ctf[parentClass]"," ",$this->getParentClass())); $detailFields->push(new HiddenField("ctf[parentClass]"," ",$this->getParentClass()));
if( $this->relationAutoSetting ) if( $this->relationAutoSetting ) {
$detailFields->push(new HiddenField("$parentIdName"," ",$childData->ID)); // 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}). * @see {Form::ReferencedField}).
*/ */
function saveComplexTableField($params) { function saveComplexTableField($data, $form, $params) {
$className = $this->sourceClass(); $className = $this->sourceClass();
$childData = new $className(); $childData = new $className();
$form->saveInto($childData);
$this->saveInto($childData);
$childData->write(); $childData->write();
// if ajax-call in an iframe, update window // if ajax-call in an iframe, update window
if(Director::is_ajax()) { if(Director::is_ajax()) {
// Newly saved objects need their ID reflected in the reloaded form to avoid double saving // Newly saved objects need their ID reflected in the reloaded form to avoid double saving
$form = $this->controller->DetailForm(); $childRequestHandler = new ComplexTableField_ItemRequest($this, $childData->ID);
//$form->loadDataFrom($this->dataObject); $form = $childRequestHandler->DetailForm();
FormResponse::update_dom_id($form->FormName(), $form->formHtmlContent(), true, 'update'); FormResponse::update_dom_id($form->FormName(), $form->formHtmlContent(), true, 'update');
return FormResponse::respond(); return FormResponse::respond();
} else { } else {
@ -542,18 +546,20 @@ class ComplexTableField_ItemRequest extends RequestHandlingData {
} }
$this->methodName = "show"; $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); 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()} * Just a hook, processed in {DetailForm()}
@ -566,25 +572,23 @@ class ComplexTableField_ItemRequest extends RequestHandlingData {
} }
$this->methodName = "edit"; $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); 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 * Return the data object being manipulated
*/ */
function obj() { function dataObj() {
// used to discover fields if requested and for population of field // used to discover fields if requested and for population of field
if(is_numeric($this->itemID)) { if(is_numeric($this->itemID)) {
// we have to use the basedataclass, otherwise we might exclude other subclasses // we have to use the basedataclass, otherwise we might exclude other subclasses
@ -605,7 +609,7 @@ class ComplexTableField_ItemRequest extends RequestHandlingData {
* @param int $childID * @param int $childID
*/ */
function DetailForm($childID = null) { function DetailForm($childID = null) {
$childData = $this->obj(); $childData = $this->dataObj();
$fields = $this->ctf->getFieldsFor($childData); $fields = $this->ctf->getFieldsFor($childData);
$validator = $this->ctf->getValidatorFor($childData); $validator = $this->ctf->getValidatorFor($childData);
@ -631,13 +635,13 @@ class ComplexTableField_ItemRequest extends RequestHandlingData {
* @see {Form::ReferencedField}). * @see {Form::ReferencedField}).
*/ */
function saveComplexTableField($data, $form, $request) { function saveComplexTableField($data, $form, $request) {
$form->saveInto($this->obj()); $form->saveInto($this->dataObj());
$this->obj()->write(); $this->dataObj()->write();
// if ajax-call in an iframe, update window // if ajax-call in an iframe, update window
if(Director::is_ajax()) { if(Director::is_ajax()) {
// Newly saved objects need their ID reflected in the reloaded form to avoid double saving // 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); //$form->loadDataFrom($this->dataObject);
FormResponse::update_dom_id($form->FormName(), $form->formHtmlContent(), true, 'update'); FormResponse::update_dom_id($form->FormName(), $form->formHtmlContent(), true, 'update');
return FormResponse::respond(); 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('&amp;','&',HTTP::setGetVar('ctf[ID]',$this->sourceID(),$link));
}
return $link;
}
function PopupCurrentItem() { function PopupCurrentItem() {
return $_REQUEST['ctf']['start']+1; return $_REQUEST['ctf']['start']+1;
} }
function PopupFirstLink() { 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; return null;
} }
$item = $this->unpagedSourceItems->First(); $item = $this->unpagedSourceItems->First();
$start = 0; $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() { 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; return null;
} }
$item = $this->unpagedSourceItems->Last(); $item = $this->unpagedSourceItems->Last();
$start = $this->totalCount - 1; $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() { 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; return null;
} }
$item = $this->unpagedSourceItems->getIterator()->getOffset($_REQUEST['ctf']['start'] + 1); $item = $this->unpagedSourceItems->getIterator()->getOffset($_REQUEST['ctf']['start'] + 1);
$start = $_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() { 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; return null;
} }
$item = $this->unpagedSourceItems->getIterator()->getOffset($_REQUEST['ctf']['start'] - 1); $item = $this->unpagedSourceItems->getIterator()->getOffset($_REQUEST['ctf']['start'] - 1);
$start = $_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++) { for($i = $offset;$i <= $offset + $this->pageSize && $i <= $this->totalCount;$i++) {
$start = $i - 1; $start = $i - 1;
$item = $this->unpagedSourceItems->getIterator()->getOffset($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['number'] = $i;
$links['active'] = $i == $currentItem ? false : true; $links['active'] = $i == $currentItem ? false : true;
$result->push(new ArrayData($links)); $result->push(new ArrayData($links));
@ -730,6 +728,9 @@ class ComplexTableField_ItemRequest extends RequestHandlingData {
return $result; return $result;
} }
function ShowPagination() {
return false;
}
/** /**
@ -886,10 +887,6 @@ class ComplexTableField_Popup extends Form {
function FieldHolder() { function FieldHolder() {
return $this->renderWith('ComplexTableField_Form'); return $this->renderWith('ComplexTableField_Form');
} }
function ShowPagination() {
return $this->controller->ShowPagination();
}
} }
?> ?>

View File

@ -32,6 +32,7 @@ class FieldSet extends DataObjectSet {
if($field->hasData()) { if($field->hasData()) {
$name = $field->Name(); $name = $field->Name();
if(isset($list[$name])) { if(isset($list[$name])) {
$errSuffix = "";
if($this->form) $errSuffix = " in your '{$this->form->class}' form called '" . $this->form->Name() . "'"; if($this->form) $errSuffix = " in your '{$this->form->class}' form called '" . $this->form->Name() . "'";
else $errSuffix = ''; else $errSuffix = '';
user_error("collateDataFields() I noticed that a field called '$name' appears twice$errSuffix.", E_USER_ERROR); user_error("collateDataFields() I noticed that a field called '$name' appears twice$errSuffix.", E_USER_ERROR);

View File

@ -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 <form> tag itself. * Returns an HTML rendition of this form, without the <form> tag itself.
* Attaches 3 extra hidden files, _form_action, _form_name, _form_method, and _form_enctype. These are * Attaches 3 extra hidden files, _form_action, _form_name, _form_method, and _form_enctype. These are

View File

@ -222,7 +222,7 @@ JS
// sorting links (only if we have a form to refresh with) // sorting links (only if we have a form to refresh with)
if($this->form) { if($this->form) {
$sortLink = $this->BaseLink(); $sortLink = $this->Link();
$sortLink = HTTP::setGetVar("ctf[{$this->Name()}][sort]", $fieldName, $sortLink); $sortLink = HTTP::setGetVar("ctf[{$this->Name()}][sort]", $fieldName, $sortLink);
if(isset($_REQUEST['ctf'][$this->Name()]['dir'])) { if(isset($_REQUEST['ctf'][$this->Name()]['dir'])) {
$XML_sort = (isset($_REQUEST['ctf'][$this->Name()]['dir'])) ? Convert::raw2xml($_REQUEST['ctf'][$this->Name()]['dir']) : null; $XML_sort = (isset($_REQUEST['ctf'][$this->Name()]['dir'])) ? Convert::raw2xml($_REQUEST['ctf'][$this->Name()]['dir']) : null;
@ -644,7 +644,7 @@ JS
return null; 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() { function PrevLink() {
@ -656,7 +656,7 @@ JS
$start = ($_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize < 0) ? 0 : $_REQUEST['ctf'][$this->Name()]['start'] - $this->pageSize; $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() { function NextLink() {
@ -665,7 +665,7 @@ JS
if($currentStart >= $start-1) { if($currentStart >= $start-1) {
return null; 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() { function LastLink() {
@ -678,7 +678,7 @@ JS
return null; 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() { 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. * We need to instanciate this button manually as a normal button has no means of adding inline onclick-behaviour.
*/ */
function ExportLink() { function ExportLink() {
return Director::absoluteURL($this->FormAction()) . "&action_callfieldmethod&fieldName={$this->Name()}&methodName=export"; return Controller::join_links($this->Link(), 'export');
} }
function printall() { function printall() {
@ -832,7 +832,7 @@ JS
} }
function PrintLink() { 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'])) { if(isset($_REQUEST['ctf'][$this->Name()]['sort'])) {
$link = HTTP::setGetVar("ctf[{$this->Name()}][sort]",Convert::raw2xml($_REQUEST['ctf'][$this->Name()]['sort']), $link); $link = HTTP::setGetVar("ctf[{$this->Name()}][sort]",Convert::raw2xml($_REQUEST['ctf'][$this->Name()]['sort']), $link);
} }
@ -913,50 +913,12 @@ JS
function setTemplate($template) { function setTemplate($template) {
$this->template = $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 * @return Int
*/ */
@ -1069,11 +1031,15 @@ class TableListField_Item extends ViewableData {
} }
function BaseLink() { 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() { function DeleteLink() {
return $this->BaseLink() . "&methodName=delete"; return Controller::join_links($this->Link(), "delete");
} }
function MarkingCheckbox() { function MarkingCheckbox() {

View File

@ -92,7 +92,7 @@ ComplexTableField.prototype = {
var table = Event.findElement(e,"table"); var table = Event.findElement(e,"table");
if(Event.element(e).nodeName == "IMG") { if(Event.element(e).nodeName == "IMG") {
link = Event.findElement(e,"a"); link = Event.findElement(e,"a");
popupLink = link.href+"&ajax=1"; popupLink = link.href+"?ajax=1";
} else { } else {
el = Event.findElement(e,"tr"); el = Event.findElement(e,"tr");
var link = $$("a",el)[0]; var link = $$("a",el)[0];
@ -112,16 +112,13 @@ ComplexTableField.prototype = {
GB_OpenerObj = this; GB_OpenerObj = this;
// use same url to refresh the table after saving the popup, but use a generic rendering method // use same url to refresh the table after saving the popup, but use a generic rendering method
GB_RefreshLink = popupLink; GB_RefreshLink = this.getAttribute('href');
GB_RefreshLink = GB_RefreshLink.replace(/(methodName=)[^&]*/,"$1ajax_refresh");
// dont include pagination index
GB_RefreshLink = GB_RefreshLink.replace(/ctf\[start\][^&]*/,"");
GB_RefreshLink += '&forcehtml=1';
if(this.GB_Caption) { if(this.GB_Caption) {
var title = this.GB_Caption; var title = this.GB_Caption;
} else { } 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() : ""; var title = (type && type[1]) ? type[1].ucfirst() : "";
} }

View File

@ -4,11 +4,12 @@ ComplexTableFieldPopupForm.prototype = {
errorMessage: "Error talking to server", errorMessage: "Error talking to server",
initialize: function() { initialize: function() {
Behaviour.register({ var rules = {};
"form#ComplexTableField_Popup_DetailForm .Actions input.action": { rules["#" + this.id + " .Actions input.action"] = {
onclick: this.submitForm.bind(this) 'onclick' : this.submitForm.bind(this)
} };
});
Behaviour.register(rules);
}, },
loadNewPage : function(content) { loadNewPage : function(content) {
@ -33,7 +34,6 @@ ComplexTableFieldPopupForm.prototype = {
submitButton.disabled = true; submitButton.disabled = true;
Element.addClassName(submitButton,'loading'); Element.addClassName(submitButton,'loading');
} }
new parent.parent.Ajax.Request( new parent.parent.Ajax.Request(
theForm.getAttribute("action"), theForm.getAttribute("action"),
{ {
@ -58,7 +58,6 @@ ComplexTableFieldPopupForm.prototype = {
// don't update when validation is present and failed // don't update when validation is present and failed
if(!this.validate || (this.validate && !hasHadFormError())) { if(!this.validate || (this.validate && !hasHadFormError())) {
alert("GB:" + parent.parent.GB_RefreshLink);
new parent.parent.Ajax.Request( new parent.parent.Ajax.Request(
parent.parent.GB_RefreshLink, parent.parent.GB_RefreshLink,
{ {
@ -113,8 +112,9 @@ ComplexTableFieldPopupForm.prototype = {
} }
// causes IE6 to go nuts // causes IE6 to go nuts
//this.GB_hide(); this.GB_hide();
} }
} }
ComplexTableFieldPopupForm.applyTo('form#ComplexTableField_Popup_DetailForm'); ComplexTableFieldPopupForm.applyTo('#ComplexTableField_Popup_DetailForm');
ComplexTableFieldPopupForm.applyTo('#ComplexTableField_Popup_AddForm');

40
sake Executable file
View File

@ -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"

View File

@ -91,7 +91,8 @@ class SearchContext extends Object {
foreach($searchParams as $key => $value) { foreach($searchParams as $key => $value) {
$filter = $this->getFilter($key); $filter = $this->getFilter($key);
if ($filter) { if ($filter) {
$query->where[] = $filter->apply($value); $filter->setValue($value);
$filter->apply($query);
} }
} }
return $query; return $query;
@ -108,6 +109,7 @@ class SearchContext extends Object {
* @return DataObjectSet * @return DataObjectSet
*/ */
public function getResults($searchParams, $start = false, $limit = false) { public function getResults($searchParams, $start = false, $limit = false) {
$searchParams = array_filter($searchParams, array($this,'clearEmptySearchFields'));
$query = $this->getQuery($searchParams, $start, $limit); $query = $this->getQuery($searchParams, $start, $limit);
// //
// use if a raw SQL query is needed // use if a raw SQL query is needed
@ -121,6 +123,17 @@ class SearchContext extends Object {
return DataObject::get($this->modelClass, $query->getFilter(), "", "", $limit); 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 documentation
* @todo implementation * @todo implementation
@ -138,8 +151,15 @@ class SearchContext extends Object {
} }
} }
$query->where = $conditions; $query->where = $conditions;
return $query;
} }
/**
* Accessor for the filter attached to a named field.
*
* @param string $name
* @return SearchFilter
*/
public function getFilter($name) { public function getFilter($name) {
if (isset($this->filters[$name])) { if (isset($this->filters[$name])) {
return $this->filters[$name]; return $this->filters[$name];
@ -148,29 +168,42 @@ class SearchContext extends Object {
} }
} }
public function getFields() { /**
return $this->fields; * Get the map of filters in the current search context.
} *
* @return array
public function setFields($fields) { */
$this->fields = $fields;
}
public function getFilters() { public function getFilters() {
return $this->filters; return $this->filters;
} }
public function setFilters($filters) { public function setFilters($filters) {
$this->filters = $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 * Placeholder, until I figure out the rest of the SQLQuery stuff
* and link the $searchable_fields array to the SearchContext * and link the $searchable_fields array to the SearchContext
*
* @deprecated in favor of getResults
*/ */
public function getResultSet($fields) { public function getResultSet($fields) {
$filter = ""; $filter = "";

View File

@ -15,8 +15,8 @@ class ExactMatchFilter extends SearchFilter {
* *
* @return unknown * @return unknown
*/ */
public function apply($value) { public function apply(SQLQuery $query) {
return "{$this->name}='$value'"; return $query->where("{$this->name} = '{$this->value}'");
} }
} }

View File

@ -1,15 +1,30 @@
<?php <?php
/** /**
* Matches FULLTEXT indices. * Filters by full-text matching on the given field.
*
* Full-text indexes are only available with MyISAM tables. The following column types are
* supported:
* - Char
* - Varchar
* - Text
*
* To enable full-text matching on fields, you also need to add an index to the
* database table, using the {$indexes} hash in your DataObject subclass:
*
* <code>
* static $indexes = array(
* 'SearchFields' => 'fulltext(Name, Title, Description)'
* );
* </code>
* *
* @package sapphire * @package sapphire
* @subpackage search * @subpackage search
*/ */
class FulltextFilter extends SearchFilter { class FulltextFilter extends SearchFilter {
public function apply($value) { public function apply(SQLQuery $query) {
return ""; return "";
} }
} }
?> ?>

View File

@ -12,8 +12,8 @@
*/ */
class NegationFilter extends SearchFilter { class NegationFilter extends SearchFilter {
public function apply($value) { public function apply(SQLQuery $query) {
return "{$this->name} != '$value'"; return $query->where("{$this->name} != '{$this->value}'");
} }
} }

View File

@ -7,8 +7,8 @@
*/ */
class PartialMatchFilter extends SearchFilter { class PartialMatchFilter extends SearchFilter {
public function apply($value) { public function apply(SQLQuery $query) {
return "{$this->name} LIKE '%$value%'"; return $query->where("{$this->name} LIKE '%{$this->value}%'");
} }
} }

View File

@ -8,12 +8,23 @@
abstract class SearchFilter extends Object { abstract class SearchFilter extends Object {
protected $name; protected $name;
protected $value;
function __construct($name) { function __construct($name, $value = false) {
$this->name = $name; $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);
} }
?> ?>

View File

@ -0,0 +1,16 @@
<?php
/**
* Uses a substring match against content in column rows.
*
* @package sapphire
* @subpackage search
*/
class SubstringFilter extends SearchFilter {
public function apply(SQLQuery $query) {
return $query->where("LOCATE({$this->name}, $value)");
}
}
?>

View File

@ -1,16 +0,0 @@
<?php
/**
* Uses a substring match against content in column rows.
*
* @package sapphire
* @subpackage search
*/
class SubstringMatchFilter extends SearchFilter {
public function apply($value) {
return "";
}
}
?>

View File

@ -24,7 +24,7 @@ class BasicAuth extends Object {
*/ */
static function requireLogin($realm, $permissionCode) { static function requireLogin($realm, $permissionCode) {
if(self::$disabled) return true; 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'])) { if(isset($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {

View File

@ -1,4 +1,4 @@
<div id="$id" class="$CSSClasses field"> <div id="$id" class="$CSSClasses field" href="$Link">
<div class="middleColumn"> <div class="middleColumn">
<% include TableListField_PageControls %> <% include TableListField_PageControls %>
<table class="data"> <table class="data">

View File

@ -7,36 +7,34 @@
<div class="right $PopupClasses"> <div class="right $PopupClasses">
$DetailForm $DetailForm
</div> </div>
<% if IsAddMode %>
<% else %> <% if ShowPagination %>
<% if ShowPagination %> <table id="ComplexTableField_Pagination">
<table id="ComplexTableField_Pagination"> <tr>
<tr> <% if Paginator.PrevLink %>
<% if PopupPrevLink %> <td id="ComplexTableField_Pagination_Previous">
<td id="ComplexTableField_Pagination_Previous"> <a href="$Paginator.PrevLink"><img src="cms/images/pagination/record-prev.png" /><% _t('PREVIOUS', 'Previous') %></a>
<a href="$PopupPrevLink"><img src="cms/images/pagination/record-prev.png" /><% _t('PREVIOUS', 'Previous') %></a> </td>
</td> <% end_if %>
<% end_if %> <% if xdsfdsf %>
<% if TotalCount == 1 %> <% else %>
<% else %> <td>
<td> <% control Paginator.Pages %>
<% control Pagination %> <% if active %>
<% if active %> <a href="$link">$number</a>
<a href="$link">$number</a> <% else %>
<% else %> <span>$number</span>
<span>$number</span> <% end_if %>
<% end_if %> <% end_control %>
<% end_control %> </td>
</td> <% end_if %>
<% end_if %> <% if Paginator.NextLink %>
<% if PopupNextLink %> <td id="ComplexTableField_Pagination_Next">
<td id="ComplexTableField_Pagination_Next"> <a href="$Paginator.NextLink"><% _t('NEXT', 'Next') %><img src="cms/images/pagination/record-next.png" /></a>
<a href="$PopupNextLink"><% _t('NEXT', 'Next') %><img src="cms/images/pagination/record-next.png" /></a> </td>
</td> <% end_if %>
<% end_if %> </tr>
</tr> </table>
</table>
<% end_if %>
<% end_if %> <% end_if %>
</body> </body>
</html> </html>

72
tests/SQLQueryTest.php Normal file
View File

@ -0,0 +1,72 @@
<?php
class SQLQueryTest extends SapphireTest {
static $fixture_file = null;
function testEmptyQueryReturnsNothing() {
$query = new SQLQuery();
$this->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
}
}
?>

View File

@ -7,15 +7,14 @@ class SearchContextTest extends SapphireTest {
$person = singleton('SearchContextTest_Person'); $person = singleton('SearchContextTest_Person');
$context = $person->getDefaultSearchContext(); $context = $person->getDefaultSearchContext();
$results = $context->getResultSet(array('Name'=>'')); $results = $context->getResults(array('Name'=>''));
$this->assertEquals(5, $results->Count()); $this->assertEquals(5, $results->Count());
$results = $context->getResultSet(array('EyeColor'=>'green')); $results = $context->getResults(array('EyeColor'=>'green'));
$this->assertEquals(2, $results->Count()); $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()); $this->assertEquals(1, $results->Count());
} }
function testSummaryIncludesDefaultFieldsIfNotDefined() { function testSummaryIncludesDefaultFieldsIfNotDefined() {
@ -58,26 +57,26 @@ class SearchContextTest extends SapphireTest {
} }
function testUserDefinedFiltersAppearInSearchContext() { function testUserDefinedFiltersAppearInSearchContext() {
//$company = singleton('SearchContextTest_Company'); $company = singleton('SearchContextTest_Company');
//$context = $company->getDefaultSearchContext(); $context = $company->getDefaultSearchContext();
/*$this->assertEquals( $this->assertEquals(
array( array(
"Name" => new PartialMatchFilter("Name"), "Name" => new PartialMatchFilter("Name"),
"Industry" => new ExactMatchFilter("Industry"), "Industry" => new ExactMatchFilter("Industry"),
"AnnualProfit" => new PartialMatchFilter("AnnualProfit") "AnnualProfit" => new PartialMatchFilter("AnnualProfit")
), ),
$context->getFilters() $context->getFilters()
);*/ );
} }
function testRelationshipObjectsLinkedInSearch() { function testRelationshipObjectsLinkedInSearch() {
//$project = singleton('SearchContextTest_Project'); $project = singleton('SearchContextTest_Project');
//$context = $project->getDefaultSearchContext(); $context = $project->getDefaultSearchContext();
//$query = array("Name"=>"Blog Website"); $query = array("Name"=>"Blog Website");
//$results = $context->getQuery($query); $results = $context->getQuery($query);
} }
function testCanGenerateQueryUsingAllFilterTypes() { function testCanGenerateQueryUsingAllFilterTypes() {
@ -194,13 +193,15 @@ class SearchContextTest_AllFilterTypes extends DataObject implements TestOnly {
"ExactMatch" => "Text", "ExactMatch" => "Text",
"PartialMatch" => "Text", "PartialMatch" => "Text",
"Negation" => "Text", "Negation" => "Text",
"HiddenValue" => "Text" "SubstringMatch" => "Text",
"HiddenValue" => "Text",
); );
static $searchable_fields = array( static $searchable_fields = array(
"ExactMatch" => "ExactMatchFilter", "ExactMatch" => "ExactMatchFilter",
"PartialMatch" => "PartialMatchFilter", "PartialMatch" => "PartialMatchFilter",
"Negation" => "NegationFilter" "Negation" => "NegationFilter",
"SubstringMatch" => "SubstringFilter"
); );
} }