(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@60265 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
Ingo Schommer 2008-08-10 23:29:30 +00:00
parent 95d4634e2e
commit 8d0166e298
21 changed files with 401 additions and 202 deletions

View File

@ -44,6 +44,15 @@ abstract class DataFormatter extends Object {
*/ */
protected $customAddFields = null; protected $customAddFields = null;
/**
* Fields which should be expicitly excluded from the export.
* Comes in handy for field-level permissions.
* Will overrule both {@link $customAddFields} and {@link $customFields}
*
* @var array
*/
protected $removeFields = null;
/** /**
* Specifies the mimetype in which all strings * Specifies the mimetype in which all strings
* returned from the convert*() methods should be used, * returned from the convert*() methods should be used,
@ -155,6 +164,20 @@ abstract class DataFormatter extends Object {
return $this->customAddFields; return $this->customAddFields;
} }
/**
* @param array $fields
*/
public function setRemoveFields($fields) {
$this->removeFields = $fields;
}
/**
* @return array
*/
public function getRemoveFields() {
return $this->removeFields;
}
public function getOutputContentType() { public function getOutputContentType() {
return $this->outputContentType; return $this->outputContentType;
} }
@ -193,6 +216,11 @@ abstract class DataFormatter extends Object {
// add default required fields // add default required fields
$dbFields = array_merge($dbFields, array('ID'=>'Int')); $dbFields = array_merge($dbFields, array('ID'=>'Int'));
// @todo Requires PHP 5.1+
if($this->removeFields) {
$dbFields = array_diff_key($dbFields, array_combine($this->removeFields,$this->removeFields));
}
return $dbFields; return $dbFields;
} }

View File

@ -38,6 +38,12 @@
* @todo Make SearchContext specification customizeable for each class * @todo Make SearchContext specification customizeable for each class
* @todo Allow for range-searches (e.g. on Created column) * @todo Allow for range-searches (e.g. on Created column)
* @todo Allow other authentication methods (currently only HTTP BasicAuth) * @todo Allow other authentication methods (currently only HTTP BasicAuth)
* @todo Filter relation listings by $api_access and canView() permissions
* @todo Exclude relations when "fields" are specified through URL (they should be explicitly requested in this case)
* @todo Custom filters per DataObject subclass, e.g. to disallow showing unpublished pages in SiteTree/Versioned/Hierarchy
* @todo URL parameter namespacing for search-fields, limit, fields, add_fields (might all be valid dataobject properties)
* e.g. you wouldn't be able to search for a "limit" property on your subclass as its overlayed with the search logic
* @todo i18n integration (e.g. Page/1.xml?lang=de_DE)
*/ */
class RestfulServer extends Controller { class RestfulServer extends Controller {
static $url_handlers = array( static $url_handlers = array(

View File

@ -1299,7 +1299,11 @@ class DataObject extends ViewableData implements DataObjectInterface {
* @return SearchContext * @return SearchContext
*/ */
public function getDefaultSearchContext() { public function getDefaultSearchContext() {
return new SearchContext($this->class, new FieldSet($this->searchable_fields()), $this->defaultSearchFilters()); return new SearchContext(
$this->class,
$this->scaffoldSearchFields(),
$this->defaultSearchFilters()
);
} }
/** /**
@ -1315,31 +1319,35 @@ class DataObject extends ViewableData implements DataObjectInterface {
*/ */
public function scaffoldSearchFields() { public function scaffoldSearchFields() {
$fields = new FieldSet(); $fields = new FieldSet();
foreach($this->searchable_fields() as $fieldName => $fieldType) { foreach($this->searchableFields() as $fieldName => $spec) {
// If we explicitly set a field, then construct that
if(isset($spec['field'])) {
$fieldClass = $spec['field'];
$field = new $fieldClass($fieldName);
// Otherwise, use the database field's scaffolder
} else {
$field = $this->relObject($fieldName)->scaffoldSearchField(); $field = $this->relObject($fieldName)->scaffoldSearchField();
}
if (strstr($fieldName, '.')) { if (strstr($fieldName, '.')) {
$field->setName(str_replace('.', '__', $fieldName)); $field->setName(str_replace('.', '__', $fieldName));
} }
$field->setTitle($this->searchable_fields_labels($fieldName)); $field->setTitle($spec['title']);
$fields->push($field); $fields->push($field);
} }
return $fields; return $fields;
} }
/**
* CamelCase to sentence case label transform
*
* @todo move into utility class (String::toLabel)
*/
function toLabel($string) {
return preg_replace("/([a-z]+)([A-Z])/","$1 $2", $string);
}
/** /**
* Scaffold a simple edit form for all properties on this dataobject, * Scaffold a simple edit form for all properties on this dataobject,
* based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()} * based on default {@link FormField} mapping in {@link DBField::scaffoldFormField()}.
* Field labels/titles will be auto generated from {@link DataObject::fieldLabels()}.
* *
* @uses {@link DBField::scaffoldFormField()} * @uses {@link DBField::scaffoldFormField()}
* @uses {@link DataObject::fieldLabels()}
* @param array $fieldClasses Optional mapping of fieldnames to subclasses of {@link DBField} * @param array $fieldClasses Optional mapping of fieldnames to subclasses of {@link DBField}
* @return FieldSet * @return FieldSet
*/ */
@ -1356,6 +1364,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
} else { } else {
$fieldObject = $this->dbObject($fieldName)->scaffoldFormField(); $fieldObject = $this->dbObject($fieldName)->scaffoldFormField();
} }
$fieldObject->setTitle($this->fieldLabels($fieldName));
$fields->push($fieldObject); $fields->push($fieldObject);
} }
foreach($this->has_one() as $relationship => $component) { foreach($this->has_one() as $relationship => $component) {
@ -1769,7 +1778,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
$object = $component->dbObject($fieldName); $object = $component->dbObject($fieldName);
if (!($object instanceof DBField)) { if (!($object instanceof DBField)) {
user_error("Unable to traverse to related object field [$fieldPath]", E_USER_ERROR); user_error("Unable to traverse to related object field [$fieldPath] on [$this->class]", E_USER_ERROR);
} }
return $object; return $object;
} }
@ -2291,26 +2300,46 @@ class DataObject extends ViewableData implements DataObjectInterface {
* *
* @return array * @return array
*/ */
public function searchable_fields() { public function searchableFields() {
// can have mixed format, need to make consistent in most verbose form
$fields = $this->stat('searchable_fields'); $fields = $this->stat('searchable_fields');
$labels = $this->fieldLabels();
// if fields were passed in numeric array, // fallback to summary fields
// convert to an associative array if(!$fields) $fields = array_keys($this->summaryFields());
if($fields && array_key_exists(0, $fields)) {
$fields = array_fill_keys(array_values($fields), 'TextField');
}
if (!$fields) {
$fields = array_fill_keys(array_keys($this->summaryFields()), 'TextField');
} else {
// rewrite array, if it is using shorthand syntax // rewrite array, if it is using shorthand syntax
$rewrite = array(); $rewrite = array();
foreach($fields as $name => $type) { foreach($fields as $name => $specOrName) {
if (is_int($name)) $rewrite[$type] = 'TextField'; $identifer = (is_int($name)) ? $specOrName : $name;
else $rewrite[$name] = $type; if(is_int($name)) {
// Format: array('MyFieldName')
$rewrite[$identifer] = array();
} elseif(is_array($specOrName)) {
// Format: array('MyFieldName' => array(
// 'filter => 'ExactMatchFilter',
// 'field' => 'NumericField', // optional
// 'title' => 'My Title', // optiona.
// )
$rewrite[$identifer] = array_merge(
array('filter' => $this->relObject($identifer)->stat('default_search_filter_class')),
(array)$specOrName
);
} else {
// Format: array('MyFieldName' => 'ExactMatchFilter')
$rewrite[$identifer] = array(
'filter' => $specOrName,
);
}
if(!isset($rewrite[$identifer]['title'])) {
$rewrite[$identifer]['title'] = (isset($labels[$identifer])) ? $labels[$identifer] : FormField::name_to_label($identifer);
}
if(!isset($rewrite[$identifer]['filter'])) {
$rewrite[$identifer]['filter'] = 'PartialMatchFilter';
}
} }
$fields = $rewrite; $fields = $rewrite;
}
return $fields; return $fields;
} }
@ -2329,28 +2358,20 @@ class DataObject extends ViewableData implements DataObjectInterface {
* Generates labels based on name of the field itself, if no static property * Generates labels based on name of the field itself, if no static property
* {@link self::searchable_fields_labels} exists. * {@link self::searchable_fields_labels} exists.
* *
* @todo fix bad code
*
* @param $fieldName name of the field to retrieve * @param $fieldName name of the field to retrieve
* @return array of all element labels if no argument given * @return array of all element labels if no argument given
* @return string of label if field * @return string of label if field
*/ */
public function searchable_fields_labels($fieldName=false) { public function fieldLabels($fieldName = false) {
$custom_labels = $this->stat('searchable_fields_labels'); $customLabels = $this->stat('field_labels');
$autoLabels = array();
$fields = array_keys($this->searchable_fields()); foreach($this->databaseFields() as $name => $type) {
$labels = array_combine($fields, $fields); $autoLabels[$name] = FormField::name_to_label($name);
if(is_array($custom_labels)) $labels = array_merge($labels, $custom_labels);
if ($fieldName) {
if(array_key_exists($fieldName, $labels)) {
return $labels[$fieldName];
} elseif (strstr($fieldName, '.')) {
$parts = explode('.', $fieldName);
$label = $parts[count($parts)-2] . ' ' . $parts[count($parts)-1];
return $this->toLabel($label);
} else {
return $this->toLabel($fieldName);
} }
$labels = array_merge((array)$autoLabels, (array)$customLabels);
if($fieldName) {
return (isset($labels[$fieldName])) ? $labels[$fieldName] : FormField::name_to_label($fieldName);
} else { } else {
return $labels; return $labels;
} }
@ -2404,21 +2425,9 @@ class DataObject extends ViewableData implements DataObjectInterface {
*/ */
public function defaultSearchFilters() { public function defaultSearchFilters() {
$filters = array(); $filters = array();
foreach($this->searchable_fields() as $name => $type) { foreach($this->searchableFields() as $name => $spec) {
if (is_int($name)) { $filterClass = $spec['filter'];
$filters[$type] = $this->relObject($type)->defaultSearchFilter(); $filters[$name] = new $filterClass($name);
} else {
if(is_array($type)) {
$filter = current($type);
$filters[$name] = new $filter($name);
} else {
if(is_subclass_of($type, 'SearchFilter')) {
$filters[$name] = new $type($name);
} else {
$filters[$name] = $this->relObject($name)->defaultSearchFilter($name);
}
}
}
} }
return $filters; return $filters;
} }
@ -2574,18 +2583,6 @@ class DataObject extends ViewableData implements DataObjectInterface {
* Default list of fields that can be scaffolded by the ModelAdmin * Default list of fields that can be scaffolded by the ModelAdmin
* search interface. * search interface.
* *
* Defining a basic set of searchable fields:
* <code>
* static $searchable_fields = array("Name", "Email");
* </code>
*
* Overriding the default form fields, with a custom defined field:
* <code>
* static $searchable_fields = array(
* "Name" => "TextField"
* );
* </code>
*
* Overriding the default filter, with a custom defined filter: * Overriding the default filter, with a custom defined filter:
* <code> * <code>
* static $searchable_fields = array( * static $searchable_fields = array(
@ -2593,10 +2590,25 @@ class DataObject extends ViewableData implements DataObjectInterface {
* ); * );
* </code> * </code>
* *
* Overriding the default form field and filter: * Overriding the default form fields, with a custom defined field.
* The 'filter' parameter will be generated from {@link DBField::$default_search_filter_class}.
* The 'title' parameter will be generated from {@link DataObject->fieldLabels()}.
* <code> * <code>
* static $searchable_fields = array( * static $searchable_fields = array(
* "Name" => array("TextField" => "PartialMatchFilter") * "Name" => array(
* "field" => "TextField"
* )
* );
* </code>
*
* Overriding the default form field, filter and title:
* <code>
* static $searchable_fields = array(
* "Organisation.ZipCode" => array(
* "field" => "TextField",
* "filter" => "PartialMatchFilter",
* "title" => 'Organisation ZIP'
* )
* ); * );
* </code> * </code>
*/ */
@ -2606,7 +2618,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
* User defined labels for searchable_fields, used to override * User defined labels for searchable_fields, used to override
* default display in the search form. * default display in the search form.
*/ */
public static $searchable_fields_labels = null; public static $field_labels = null;
/** /**
* Provides a default list of fields to be used by a 'summary' * Provides a default list of fields to be used by a 'summary'

View File

@ -141,7 +141,7 @@ class SQLQuery extends Object {
* @return SQLQuery This instance * @return SQLQuery This instance
*/ */
public function leftJoin($table, $onPredicate) { public function leftJoin($table, $onPredicate) {
$this->from[] = "LEFT JOIN $table ON $onPredicate"; $this->from[$table] = "LEFT JOIN $table ON $onPredicate";
return $this; return $this;
} }
@ -151,10 +151,17 @@ class SQLQuery extends Object {
* @return SQLQuery This instance * @return SQLQuery This instance
*/ */
public function innerJoin($table, $onPredicate) { public function innerJoin($table, $onPredicate) {
$this->from[] = "INNER JOIN $table ON $onPredicate"; $this->from[$table] = "INNER JOIN $table ON $onPredicate";
return $this; return $this;
} }
/**
* Returns true if we are already joining to the given table alias
*/
public function isJoinedTo($tableAlias) {
return isset($this->from[$tableAlias]);
}
/** /**
* Pass LIMIT clause either as SQL snippet or in array format. * Pass LIMIT clause either as SQL snippet or in array format.
* *

View File

@ -80,6 +80,10 @@ class YamlFixture extends Object {
protected $fixtureDictionary; protected $fixtureDictionary;
function __construct($fixtureFile) { function __construct($fixtureFile) {
if(!file_exists(Director::baseFolder().'/'. $fixtureFile)) {
user_error('YamlFixture::__construct(): Fixture path "' . $fixtureFile . '" not found', E_USER_ERROR);
}
$this->fixtureFile = $fixtureFile; $this->fixtureFile = $fixtureFile;
} }
@ -118,9 +122,7 @@ class YamlFixture extends Object {
function saveIntoDatabase() { function saveIntoDatabase() {
$parser = new Spyc(); $parser = new Spyc();
$fixtureContent = $parser->load(Director::baseFolder().'/'.$this->fixtureFile); $fixtureContent = $parser->load(Director::baseFolder().'/'.$this->fixtureFile);
$this->fixtureDictionary = array(); $this->fixtureDictionary = array();
foreach($fixtureContent as $dataClass => $items) { foreach($fixtureContent as $dataClass => $items) {
foreach($items as $identifier => $fields) { foreach($items as $identifier => $fields) {
$obj = new $dataClass(); $obj = new $dataClass();

View File

@ -14,6 +14,8 @@ class PrimaryKey extends Int {
*/ */
protected $object; protected $object;
protected static $default_search_filter_class = 'ExactMatchMultiFilter';
function __construct($name, $object) { function __construct($name, $object) {
$this->object = $object; $this->object = $object;
parent::__construct($name); parent::__construct($name);
@ -21,7 +23,11 @@ class PrimaryKey extends Int {
public function scaffoldFormField($title = null) { public function scaffoldFormField($title = null) {
$objs = DataObject::get($this->object->class); $objs = DataObject::get($this->object->class);
$map = ($objs) ? $objs->toDropdownMap() : false;
$first = $objs->First();
$titleField = isset($first->Title) ? "Title" : "Name";
$map = ($objs) ? $objs->toDropdownMap("ID", $titleField) : false;
return new DropdownField($this->name, $title, $map, null, null, ' '); return new DropdownField($this->name, $title, $map, null, null, ' ');
} }

View File

@ -77,20 +77,22 @@ class CodeViewer extends Controller {
protected $classComment, $methodComment; protected $classComment, $methodComment;
function saveClassComment($token) { function saveClassComment($token) {
$this->classComment = $this->prettyComment($token); $this->classComment = $this->parseComment($token);
} }
function saveMethodComment($token) { function saveMethodComment($token) {
$this->methodComment = $this->prettyComment($token); $this->methodComment = $this->parseComment($token);
} }
function createClass($token) { function createClass($token) {
$this->currentClass = array( $this->currentClass = array(
"description" => $this->classComment "description" => $this->classComment['pretty'],
"heading" => isset($this->classComment['heading']) ? $this->classComment['heading'] : null,
); );
$ths->classComment = null; $ths->classComment = null;
} }
function setClassName($token) { function setClassName($token) {
$this->currentClass['name'] = $token[1]; $this->currentClass['name'] = $token[1];
if(!$this->currentClass['heading']) $this->currentClass['heading'] = $token[1];
} }
function completeClass($token) { function completeClass($token) {
$this->classes[] = $this->currentClass; $this->classes[] = $this->currentClass;
@ -99,12 +101,14 @@ class CodeViewer extends Controller {
function createMethod($token) { function createMethod($token) {
$this->currentMethod = array(); $this->currentMethod = array();
$this->currentMethod['content'] = "<pre>"; $this->currentMethod['content'] = "<pre>";
$this->currentMethod['description'] = $this->methodComment; $this->currentMethod['description'] = $this->methodComment['pretty'];
$this->currentMethod['heading'] = isset($this->methodComment['heading']) ? $this->methodComment['heading'] : null;
$this->methodComment = null; $this->methodComment = null;
} }
function setMethodName($token) { function setMethodName($token) {
$this->currentMethod['name'] = $token[1]; $this->currentMethod['name'] = $token[1];
if(!$this->currentMethod['heading']) $this->currentMethod['heading'] = $token[1];
} }
function appendMethodComment($token) { function appendMethodComment($token) {
if(substr($token[1],0,2) == '/*') { if(substr($token[1],0,2) == '/*') {
@ -124,6 +128,24 @@ class CodeViewer extends Controller {
return "<p>$comment</p>"; return "<p>$comment</p>";
} }
function parseComment($token) {
$parsed = array();
$comment = preg_replace('/^\/\*/','',$token[1]);
$comment = preg_replace('/\*\/$/','',$comment);
$comment = preg_replace('/(^|\n)[\t ]*\* */m',"\n",$comment);
foreach(array('heading','nav') as $var) {
if(preg_match('/@' . $var . '\s+([^\n]+)\n/', $comment, $matches)) {
$parsed[$var] = $matches[1];
$comment = preg_replace('/@' . $var . '\s+([^\n]+)\n/','', $comment);
}
}
$parsed['pretty'] = "<p>" . str_replace("\n\n", "</p><p>", htmlentities($comment)). "</p>";
return $parsed;
}
protected $isNewLine = true; protected $isNewLine = true;
function appendMethodContent($token) { function appendMethodContent($token) {
@ -271,12 +293,12 @@ class CodeViewer extends Controller {
$subclasses = ClassInfo::subclassesFor('SapphireTest'); $subclasses = ClassInfo::subclassesFor('SapphireTest');
foreach($this->classes as $classDef) { foreach($this->classes as $classDef) {
if(true ||in_array($classDef['name'], $subclasses)) { if(true ||in_array($classDef['name'], $subclasses)) {
echo "<h1>$classDef[name]</h1>"; echo "<h1>$classDef[heading]</h1>";
echo "<div style=\"font-weight: bold\">$classDef[description]</div>"; echo "<div style=\"font-weight: bold\">$classDef[description]</div>";
if(isset($classDef['methods'])) foreach($classDef['methods'] as $method) { if(isset($classDef['methods'])) foreach($classDef['methods'] as $method) {
if(true || substr($method['name'],0,4) == 'test') { if(true || substr($method['name'],0,4) == 'test') {
//$title = ucfirst(strtolower(preg_replace('/([a-z])([A-Z])/', '$1 $2', substr($method['name'], 4)))); //$title = ucfirst(strtolower(preg_replace('/([a-z])([A-Z])/', '$1 $2', substr($method['name'], 4))));
$title = $method['name']; $title = $method['heading'];
echo "<h2>$title</h2>"; echo "<h2>$title</h2>";
echo "<div style=\"font-weight: bold\">$method[description]</div>"; echo "<div style=\"font-weight: bold\">$method[description]</div>";

View File

@ -17,7 +17,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
* *
* @var string * @var string
*/ */
protected static $fixture_file = null; static $fixture_file = null;
protected $originalMailer; protected $originalMailer;

View File

@ -20,11 +20,15 @@ class TaskRunner extends Controller {
} }
function runTask($request) { function runTask($request) {
echo "<h1>Running task...</h1>";
$TaskName = $request->param('TaskName'); $TaskName = $request->param('TaskName');
if (class_exists($TaskName)) { if (class_exists($TaskName) && is_subclass_of($TaskName, 'BuildTask')) {
if(Director::is_cli()) echo "Running task '$TaskName'...\n\n";
else echo "<h1>Running task '$TaskName'...</h1>\n";
$task = new $TaskName(); $task = new $TaskName();
if (!$task->isDisabled()) $task->run($request); if (!$task->isDisabled()) $task->run($request);
} else {
echo "Build task '$TaskName' not found.";
} }
} }

View File

@ -321,6 +321,64 @@ class FieldSet extends DataObjectSet {
return $this->transform(new ReadonlyTransformation()); return $this->transform(new ReadonlyTransformation());
} }
/**
* Transform the named field into a readonly feld.
*/
function makeFieldReadonly($fieldName) {
// Iterate on items, looking for the applicable field
foreach($this->items as $i => $field) {
// Once it's found, use FormField::transform to turn the field into a readonly version of itself.
if($field->Name() == $fieldName) {
$this->items[$i] = $field->transform(new ReadonlyTransformation());
// Clear an internal cache
$this->sequentialSet = null;
// A true results indicates that the field was foudn
return true;
}
}
return false;
}
/**
* Change the order of fields in this FieldSet by specifying an ordered list of field names.
* This works well in conjunction with SilverStripe's scaffolding functions: take the scaffold, and
* shuffle the fields around to the order that you want.
*
* Please note that any tabs or other dataless fields will be clobbered by this operation.
*
* Field names can be given as an array, or just as a list of arguments.
*/
function changeFieldOrder($fieldNames) {
// Field names can be given as an array, or just as a list of arguments.
if(!is_array($fieldNames)) $fieldNames = func_get_args();
// Build a map of fields indexed by their name. This will make the 2nd step much easier.
$fieldMap = array();
foreach($this->dataFields() as $field) $fieldMap[$field->Name()] = $field;
// Iterate through the ordered list of names, building a new array to be put into $this->items.
// While we're doing this, empty out $fieldMap so that we can keep track of leftovers.
// Unrecognised field names are okay; just ignore them
$fields = array();
foreach($fieldNames as $fieldName) {
if(isset($fieldMap[$fieldName])) {
$fields[] = $fieldMap[$fieldName];
unset($fieldMap[$fieldName]);
}
}
// Add the leftover fields to the end of the list.
$fields = $fields + array_values($fieldMap);
// Update our internal $this->items parameter.
$this->items = $fields;
// Re-set an internal cache
$this->sequentialSet = null;
}
} }
?> ?>

View File

@ -220,6 +220,7 @@ JS
} }
function Headings() { function Headings() {
$headings = array();
foreach($this->fieldList as $fieldName => $fieldTitle) { foreach($this->fieldList as $fieldName => $fieldTitle) {
$isSorted = (isset($_REQUEST['ctf'][$this->Name()]['sort']) && $fieldName == $_REQUEST['ctf'][$this->Name()]['sort']); $isSorted = (isset($_REQUEST['ctf'][$this->Name()]['sort']) && $fieldName == $_REQUEST['ctf'][$this->Name()]['sort']);
// we can't allow sorting with partial summaries (groupByField) // we can't allow sorting with partial summaries (groupByField)
@ -313,10 +314,10 @@ JS
// we don't limit when doing certain actions // we don't limit when doing certain actions
if(!isset($_REQUEST['methodName']) || !in_array($_REQUEST['methodName'],array('printall','export'))) { if(!isset($_REQUEST['methodName']) || !in_array($_REQUEST['methodName'],array('printall','export'))) {
$dataQuery->limit = $SQL_limit; $dataQuery->limit(array(
if(isset($SQL_start)) { 'limit' => $SQL_limit,
$dataQuery->limit .= " OFFSET {$SQL_start}"; 'start' => (isset($SQL_start)) ? $SQL_start : null
} ));
} }
// get data // get data

View File

@ -113,7 +113,7 @@ TableListField.prototype = {
new Ajax.Request( new Ajax.Request(
el.href, el.href,
{ {
postBody: 'update=1&paginate=1', postBody: 'update=1',
onComplete: Ajax.Evaluator, onComplete: Ajax.Evaluator,
onFailure: this.ajaxErrorHandler.bind(this) onFailure: this.ajaxErrorHandler.bind(this)
} }

View File

@ -5,23 +5,28 @@
* *
* The main.php does a number of set-up activities for the request. * The main.php does a number of set-up activities for the request.
* *
* - Includes the first one of the following files that it finds: (root)/_ss_environment.php, (root)/../_ss_environment.php, or (root)/../../_ss_environment.php * - Includes the first one of the following files that it finds: (root)/_ss_environment.php,
* (root)/../_ss_environment.php, or (root)/../../_ss_environment.php
* - Gets an up-to-date manifest from {@link ManifestBuilder} * - Gets an up-to-date manifest from {@link ManifestBuilder}
* - Sets up error handlers with {@link Debug::loadErrorHandlers()} * - Sets up error handlers with {@link Debug::loadErrorHandlers()}
* - Calls {@link DB::connect()}, passing it the global variable $databaseConfig that should be defined in an _config.php * - Calls {@link DB::connect()}, passing it the global variable $databaseConfig that should
& be defined in an _config.php
* - Sets up the default director rules using {@link Director::addRules()} * - Sets up the default director rules using {@link Director::addRules()}
* *
* After that, it calls {@link Director::direct()}, which is responsible for doing most of the real work. * After that, it calls {@link Director::direct()}, which is responsible for doing most of the
* real work.
* *
* Finally, main.php will use {@link Profiler} to show a profile if the querystring variable "debug_profile" is set. * Finally, main.php will use {@link Profiler} to show a profile if the querystring variable
* "debug_profile" is set.
* *
* CONFIGURING THE WEBSERVER * CONFIGURING THE WEBSERVER
* *
* To use Sapphire, every request that doesn't point directly to a file should be rewritten to sapphire/main.php?url=(url). * To use Sapphire, every request that doesn't point directly to a file should be rewritten to
* For example, http://www.example.com/about-us/rss would be rewritten to http://www.example.com/sapphire/main.php?url=about-us/rss * sapphire/main.php?url=(url). For example, http://www.example.com/about-us/rss would be rewritten
* to http://www.example.com/sapphire/main.php?url=about-us/rss
* *
* It's important that requests that point directly to a file aren't rewritten; otherwise, visitors won't be able to download * It's important that requests that point directly to a file aren't rewritten; otherwise, visitors
* any CSS, JS, image files, or other downloads. * won't be able to download any CSS, JS, image files, or other downloads.
* *
* On Apache, RewriteEngine can be used to do this. * On Apache, RewriteEngine can be used to do this.
* *
@ -76,6 +81,7 @@ switch(strtolower(substr($memString,-1))) {
default: default:
$memory = round($memString); $memory = round($memString);
} }
// Check we have at least 32M // Check we have at least 32M
if ($memory < (32 * 1024 * 1024)) { if ($memory < (32 * 1024 * 1024)) {
// Increase memory limit // Increase memory limit
@ -146,5 +152,3 @@ if(isset($_GET['debug_profile'])) {
Profiler::unmark('all_execution'); Profiler::unmark('all_execution');
Profiler::show(isset($_GET['profile_trace'])); Profiler::show(isset($_GET['profile_trace']));
} }
?>

2
sake
View File

@ -15,7 +15,7 @@ if [ "$1" = "installsake" ]; then
fi fi
# Find the PHP binary # Find the PHP binary
for candidatephp in "php5 php"; do for candidatephp in php5 php; do
if [ -f `which $candidatephp` ]; then if [ -f `which $candidatephp` ]; then
php=`which $candidatephp` php=`which $candidatephp`
break break

View File

@ -68,9 +68,9 @@ class SearchContext extends Object {
* @return FieldSet * @return FieldSet
*/ */
public function getSearchFields() { public function getSearchFields() {
return ($this->fields) ? $this->fields : singleton($this->modelClass)->scaffoldSearchFields();
// $this->fields is causing weirdness, so we ignore for now, using the default scaffolding // $this->fields is causing weirdness, so we ignore for now, using the default scaffolding
//return ($this->fields) ? $this->fields : singleton($this->modelClass)->scaffoldSearchFields(); //return singleton($this->modelClass)->scaffoldSearchFields();
return singleton($this->modelClass)->scaffoldSearchFields();
} }
/** /**
@ -143,8 +143,6 @@ class SearchContext extends Object {
$query = $this->getQuery($searchParams, $sort, $limit); $query = $this->getQuery($searchParams, $sort, $limit);
$sql = $query->sql();
// use if a raw SQL query is needed // use if a raw SQL query is needed
$results = new DataObjectSet(); $results = new DataObjectSet();
foreach($query->execute() as $row) { foreach($query->execute() as $row) {

View File

@ -1,32 +0,0 @@
<?php
/**
* @package sapphire
* @subpackage search
*/
/**
* Checks if a value is in a given set.
* SQL syntax used: Column IN ('val1','val2')
*
* @todo Add negation (NOT IN)6
*/
class CollectionFilter extends SearchFilter {
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
$values = explode(',',$this->getValue());
if(!$values) return false;
for($i=0; $i<count($values); $i++) {
if(!is_numeric($values[$i])) {
// @todo Fix string replacement to only replace leading and tailing quotes
$values[$i] = str_replace("'", '', $values[$i]);
$values[$i] = Convert::raw2sql($values[$i]);
}
}
$SQL_valueStr = "'" . implode("','", $values) . "'";
return $query->where("{$this->getDbName()} IN ({$SQL_valueStr})");
}
}
?>

View File

@ -21,9 +21,11 @@ class ExactMatchFilter extends SearchFilter {
* @return unknown * @return unknown
*/ */
public function apply(SQLQuery $query) { public function apply(SQLQuery $query) {
if($this->getValue()) {
$query = $this->applyRelation($query); $query = $this->applyRelation($query);
return $query->where("{$this->getDbName()} = '{$this->getValue()}'"); return $query->where("{$this->getDbName()} = '{$this->getValue()}'");
} }
}
} }
?> ?>

View File

@ -0,0 +1,34 @@
<?php
/**
* @package sapphire
* @subpackage search
*/
/**
* Checks if a value is in a given set.
* SQL syntax used: Column IN ('val1','val2')
*
* @todo Add negation (NOT IN)6
*/
class ExactMatchMultiFilter extends SearchFilter {
public function apply(SQLQuery $query) {
if($this->getValue()) {
$query = $this->applyRelation($query);
$values = explode(',',$this->getValue());
if(!$values) return false;
for($i=0; $i<count($values); $i++) {
if(!is_numeric($values[$i])) {
// @todo Fix string replacement to only replace leading and tailing quotes
$values[$i] = str_replace("'", '', $values[$i]);
$values[$i] = Convert::raw2sql($values[$i]);
}
}
$SQL_valueStr = "'" . implode("','", $values) . "'";
return $query->where("{$this->getDbName()} IN ({$SQL_valueStr})");
}
}
}
?>

View File

@ -103,18 +103,20 @@ abstract class SearchFilter extends Object {
*/ */
protected function applyRelation($query) { protected function applyRelation($query) {
if (is_array($this->relation)) { if (is_array($this->relation)) {
$model = singleton($this->model);
foreach($this->relation as $rel) { foreach($this->relation as $rel) {
$model = singleton($this->model);
if ($component = $model->has_one($rel)) { if ($component = $model->has_one($rel)) {
if(!$query->isJoinedTo($component)) {
$foreignKey = $model->getReverseAssociation($component); $foreignKey = $model->getReverseAssociation($component);
$query->leftJoin($component, "`$component`.`ID` = `{$this->model}`.`{$foreignKey}ID`"); $query->leftJoin($component, "`$component`.`ID` = `{$this->model}`.`{$foreignKey}ID`");
}
$this->model = $component; $this->model = $component;
} elseif ($component = $model->has_many($rel)) { } elseif ($component = $model->has_many($rel)) {
if(!$query->isJoinedTo($component)) {
$ancestry = $model->getClassAncestry(); $ancestry = $model->getClassAncestry();
$model = singleton($component); $foreignKey = $model->getComponentJoinField($rel);
$foreignKey = $model->getReverseAssociation($ancestry[0]); $query->leftJoin($component, "`$component`.`{$foreignKey}` = `{$ancestry[0]}`.`ID`");
$foreignKey = ($foreignKey) ? $foreignKey : $ancestry[0]; }
$query->leftJoin($component, "`$component`.`{$foreignKey}ID` = `{$this->model}`.`ID`");
$this->model = $component; $this->model = $component;
} elseif ($component = $model->many_many($rel)) { } elseif ($component = $model->many_many($rel)) {
list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component; list($parentClass, $componentClass, $parentField, $componentField, $relationTable) = $component;

View File

@ -0,0 +1,29 @@
<?php
/**
* @package sapphire
* @subpackage search
*/
/**
* Checks if a value starts with one of the items of in a given set.
* SQL syntax used: Column IN ('val1','val2')
*
* @todo Add negation (NOT IN)6
*/
class StartsWithMultiFilter extends SearchFilter {
public function apply(SQLQuery $query) {
if($this->getValue()) {
$query = $this->applyRelation($query);
$values = explode(',',$this->getValue());
foreach($values as $value) {
$SQL_value = Convert::raw2sql(str_replace("'", '', $value));
$matches[] = "{$this->getDbName()} LIKE '$SQL_value%'";
}
return $query->where(implode(" OR ", $matches));
}
}
}
?>

View File

@ -1,12 +1,12 @@
<?php <?php
class SearchContextTest extends SapphireTest { class SearchContextTest extends SapphireTest {
static $fixture_file = 'sapphire/tests/SearchContextTest.yml'; static $fixture_file = 'sapphire/tests/SearchContextTest.yml';
function testResultSetFilterReturnsExpectedCount() { function testResultSetFilterReturnsExpectedCount() {
$person = singleton('SearchContextTest_Person'); $person = singleton('SearchContextTest_Person');
$context = $person->getDefaultSearchContext(); $context = $person->getDefaultSearchContext();
$results = $context->getResults(array('Name'=>'')); $results = $context->getResults(array('Name'=>''));
$this->assertEquals(5, $results->Count()); $this->assertEquals(5, $results->Count());
@ -70,6 +70,20 @@ class SearchContextTest extends SapphireTest {
); );
} }
function testUserDefinedFieldsAppearInSearchContext() {
$company = singleton('SearchContextTest_Company');
$context = $company->getDefaultSearchContext();
$fields = $context->getFields();
$this->assertEquals(
new FieldSet(
new TextField("Name", 'Name'),
new TextareaField("Industry", 'Industry'),
new NumericField("AnnualProfit", 'The Almighty Annual Profit')
),
$context->getFields()
);
}
function testRelationshipObjectsLinkedInSearch() { function testRelationshipObjectsLinkedInSearch() {
$project = singleton('SearchContextTest_Project'); $project = singleton('SearchContextTest_Project');
$context = $project->getDefaultSearchContext(); $context = $project->getDefaultSearchContext();
@ -80,16 +94,12 @@ class SearchContextTest extends SapphireTest {
$this->assertEquals(1, $results->Count()); $this->assertEquals(1, $results->Count());
//Debug::dump(DB::query("select * from SearchContextTest_Deadline")->next());
$project = $results->First(); $project = $results->First();
$this->assertType('SearchContextTest_Project', $project); $this->assertType('SearchContextTest_Project', $project);
$this->assertEquals("Blog Website", $project->Name); $this->assertEquals("Blog Website", $project->Name);
$this->assertEquals(2, $project->Actions()->Count()); $this->assertEquals(2, $project->Actions()->Count());
$this->assertEquals("Get RSS feeds working", $project->Actions()->First()->Description); $this->assertEquals("Get RSS feeds working", $project->Actions()->First()->Description);
//Debug::dump($project->Deadline()->CompletionDate);
//$this->assertEquals()
} }
function testCanGenerateQueryUsingAllFilterTypes() { function testCanGenerateQueryUsingAllFilterTypes() {
@ -151,8 +161,14 @@ class SearchContextTest_Company extends DataObject implements TestOnly {
static $searchable_fields = array( static $searchable_fields = array(
"Name" => "PartialMatchFilter", "Name" => "PartialMatchFilter",
"Industry" => "TextareaField", "Industry" => array(
"AnnualProfit" => array("NumericField" => "PartialMatchFilter") 'field' => "TextareaField"
),
"AnnualProfit" => array(
'field' => "NumericField",
'filter' => "PartialMatchFilter",
'title' => 'The Almighty Annual Profit'
)
); );
} }
@ -222,7 +238,7 @@ class SearchContextTest_AllFilterTypes extends DataObject implements TestOnly {
"PartialMatch" => "PartialMatchFilter", "PartialMatch" => "PartialMatchFilter",
"Negation" => "NegationFilter", "Negation" => "NegationFilter",
"SubstringMatch" => "SubstringFilter", "SubstringMatch" => "SubstringFilter",
"CollectionMatch" => "CollectionFilter", "CollectionMatch" => "ExactMatchMultiFilter",
"StartsWith" => "StartsWithFilter", "StartsWith" => "StartsWithFilter",
"EndsWith" => "EndsWithFilter" "EndsWith" => "EndsWithFilter"
); );