(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;
/**
* 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
* returned from the convert*() methods should be used,
@ -155,6 +164,20 @@ abstract class DataFormatter extends Object {
return $this->customAddFields;
}
/**
* @param array $fields
*/
public function setRemoveFields($fields) {
$this->removeFields = $fields;
}
/**
* @return array
*/
public function getRemoveFields() {
return $this->removeFields;
}
public function getOutputContentType() {
return $this->outputContentType;
}
@ -193,6 +216,11 @@ abstract class DataFormatter extends Object {
// add default required fields
$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;
}

View File

@ -38,6 +38,12 @@
* @todo Make SearchContext specification customizeable for each class
* @todo Allow for range-searches (e.g. on Created column)
* @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 {
static $url_handlers = array(

View File

@ -1299,7 +1299,11 @@ class DataObject extends ViewableData implements DataObjectInterface {
* @return SearchContext
*/
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() {
$fields = new FieldSet();
foreach($this->searchable_fields() as $fieldName => $fieldType) {
$field = $this->relObject($fieldName)->scaffoldSearchField();
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();
}
if (strstr($fieldName, '.')) {
$field->setName(str_replace('.', '__', $fieldName));
}
$field->setTitle($this->searchable_fields_labels($fieldName));
$field->setTitle($spec['title']);
$fields->push($field);
}
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,
* 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 DataObject::fieldLabels()}
* @param array $fieldClasses Optional mapping of fieldnames to subclasses of {@link DBField}
* @return FieldSet
*/
@ -1356,6 +1364,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
} else {
$fieldObject = $this->dbObject($fieldName)->scaffoldFormField();
}
$fieldObject->setTitle($this->fieldLabels($fieldName));
$fields->push($fieldObject);
}
foreach($this->has_one() as $relationship => $component) {
@ -1769,7 +1778,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
$object = $component->dbObject($fieldName);
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;
}
@ -2291,26 +2300,46 @@ class DataObject extends ViewableData implements DataObjectInterface {
*
* @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');
$labels = $this->fieldLabels();
// if fields were passed in numeric array,
// convert to an associative array
if($fields && array_key_exists(0, $fields)) {
$fields = array_fill_keys(array_values($fields), 'TextField');
}
// fallback to summary fields
if(!$fields) $fields = array_keys($this->summaryFields());
if (!$fields) {
$fields = array_fill_keys(array_keys($this->summaryFields()), 'TextField');
} else {
// rewrite array, if it is using shorthand syntax
$rewrite = array();
foreach($fields as $name => $type) {
if (is_int($name)) $rewrite[$type] = 'TextField';
else $rewrite[$name] = $type;
// rewrite array, if it is using shorthand syntax
$rewrite = array();
foreach($fields as $name => $specOrName) {
$identifer = (is_int($name)) ? $specOrName : $name;
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;
}
@ -2329,28 +2358,20 @@ class DataObject extends ViewableData implements DataObjectInterface {
* Generates labels based on name of the field itself, if no static property
* {@link self::searchable_fields_labels} exists.
*
* @todo fix bad code
*
* @param $fieldName name of the field to retrieve
* @return array of all element labels if no argument given
* @return string of label if field
*/
public function searchable_fields_labels($fieldName=false) {
$custom_labels = $this->stat('searchable_fields_labels');
public function fieldLabels($fieldName = false) {
$customLabels = $this->stat('field_labels');
$autoLabels = array();
foreach($this->databaseFields() as $name => $type) {
$autoLabels[$name] = FormField::name_to_label($name);
}
$labels = array_merge((array)$autoLabels, (array)$customLabels);
$fields = array_keys($this->searchable_fields());
$labels = array_combine($fields, $fields);
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);
}
if($fieldName) {
return (isset($labels[$fieldName])) ? $labels[$fieldName] : FormField::name_to_label($fieldName);
} else {
return $labels;
}
@ -2404,21 +2425,9 @@ class DataObject extends ViewableData implements DataObjectInterface {
*/
public function defaultSearchFilters() {
$filters = array();
foreach($this->searchable_fields() as $name => $type) {
if (is_int($name)) {
$filters[$type] = $this->relObject($type)->defaultSearchFilter();
} 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);
}
}
}
foreach($this->searchableFields() as $name => $spec) {
$filterClass = $spec['filter'];
$filters[$name] = new $filterClass($name);
}
return $filters;
}
@ -2574,18 +2583,6 @@ class DataObject extends ViewableData implements DataObjectInterface {
* Default list of fields that can be scaffolded by the ModelAdmin
* 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:
* <code>
* static $searchable_fields = array(
@ -2593,10 +2590,25 @@ class DataObject extends ViewableData implements DataObjectInterface {
* );
* </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>
* 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>
*/
@ -2606,7 +2618,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
* User defined labels for searchable_fields, used to override
* 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'

View File

@ -141,7 +141,7 @@ class SQLQuery extends Object {
* @return SQLQuery This instance
*/
public function leftJoin($table, $onPredicate) {
$this->from[] = "LEFT JOIN $table ON $onPredicate";
$this->from[$table] = "LEFT JOIN $table ON $onPredicate";
return $this;
}
@ -151,10 +151,17 @@ class SQLQuery extends Object {
* @return SQLQuery This instance
*/
public function innerJoin($table, $onPredicate) {
$this->from[] = "INNER JOIN $table ON $onPredicate";
$this->from[$table] = "INNER JOIN $table ON $onPredicate";
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.
*

View File

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

View File

@ -14,6 +14,8 @@ class PrimaryKey extends Int {
*/
protected $object;
protected static $default_search_filter_class = 'ExactMatchMultiFilter';
function __construct($name, $object) {
$this->object = $object;
parent::__construct($name);
@ -21,7 +23,11 @@ class PrimaryKey extends Int {
public function scaffoldFormField($title = null) {
$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, ' ');
}

View File

@ -77,20 +77,22 @@ class CodeViewer extends Controller {
protected $classComment, $methodComment;
function saveClassComment($token) {
$this->classComment = $this->prettyComment($token);
$this->classComment = $this->parseComment($token);
}
function saveMethodComment($token) {
$this->methodComment = $this->prettyComment($token);
$this->methodComment = $this->parseComment($token);
}
function createClass($token) {
$this->currentClass = array(
"description" => $this->classComment
"description" => $this->classComment['pretty'],
"heading" => isset($this->classComment['heading']) ? $this->classComment['heading'] : null,
);
$ths->classComment = null;
}
function setClassName($token) {
$this->currentClass['name'] = $token[1];
if(!$this->currentClass['heading']) $this->currentClass['heading'] = $token[1];
}
function completeClass($token) {
$this->classes[] = $this->currentClass;
@ -99,12 +101,14 @@ class CodeViewer extends Controller {
function createMethod($token) {
$this->currentMethod = array();
$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;
}
function setMethodName($token) {
$this->currentMethod['name'] = $token[1];
if(!$this->currentMethod['heading']) $this->currentMethod['heading'] = $token[1];
}
function appendMethodComment($token) {
if(substr($token[1],0,2) == '/*') {
@ -124,6 +128,24 @@ class CodeViewer extends Controller {
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;
function appendMethodContent($token) {
@ -271,12 +293,12 @@ class CodeViewer extends Controller {
$subclasses = ClassInfo::subclassesFor('SapphireTest');
foreach($this->classes as $classDef) {
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>";
if(isset($classDef['methods'])) foreach($classDef['methods'] as $method) {
if(true || substr($method['name'],0,4) == 'test') {
//$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 "<div style=\"font-weight: bold\">$method[description]</div>";

View File

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

View File

@ -20,11 +20,15 @@ class TaskRunner extends Controller {
}
function runTask($request) {
echo "<h1>Running task...</h1>";
$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();
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());
}
/**
* 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() {
$headings = array();
foreach($this->fieldList as $fieldName => $fieldTitle) {
$isSorted = (isset($_REQUEST['ctf'][$this->Name()]['sort']) && $fieldName == $_REQUEST['ctf'][$this->Name()]['sort']);
// we can't allow sorting with partial summaries (groupByField)
@ -313,10 +314,10 @@ JS
// we don't limit when doing certain actions
if(!isset($_REQUEST['methodName']) || !in_array($_REQUEST['methodName'],array('printall','export'))) {
$dataQuery->limit = $SQL_limit;
if(isset($SQL_start)) {
$dataQuery->limit .= " OFFSET {$SQL_start}";
}
$dataQuery->limit(array(
'limit' => $SQL_limit,
'start' => (isset($SQL_start)) ? $SQL_start : null
));
}
// get data

View File

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

View File

@ -5,23 +5,28 @@
*
* 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}
* - 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()}
*
* 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
*
* To use Sapphire, every request that doesn't point directly to a file should be rewritten to 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
* To use Sapphire, every request that doesn't point directly to a file should be rewritten to
* 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
* any CSS, JS, image files, or other downloads.
* It's important that requests that point directly to a file aren't rewritten; otherwise, visitors
* won't be able to download any CSS, JS, image files, or other downloads.
*
* On Apache, RewriteEngine can be used to do this.
*
@ -34,8 +39,8 @@
* Include _ss_environment.php file
*/
$envFiles = array('../_ss_environment.php', '../../_ss_environment.php', '../../../_ss_environment.php');
foreach($envFiles as $envFile) {
if(@file_exists($envFile)) {
foreach ($envFiles as $envFile) {
if (@file_exists($envFile)) {
include($envFile);
break;
}
@ -47,37 +52,38 @@ foreach($envFiles as $envFile) {
require_once("core/Core.php");
header("Content-type: text/html; charset=\"utf-8\"");
if(function_exists('mb_http_output')) {
if (function_exists('mb_http_output')) {
mb_http_output('UTF-8');
mb_internal_encoding('UTF-8');
}
if(get_magic_quotes_gpc()) {
if (get_magic_quotes_gpc()) {
if($_REQUEST) stripslashes_recursively($_REQUEST);
if($_GET) stripslashes_recursively($_GET);
if($_POST) stripslashes_recursively($_POST);
}
if(isset($_REQUEST['trace'])) {
if (isset($_REQUEST['trace'])) {
apd_set_pprof_trace();
}
// Ensure we have enough memory
$memString = ini_get("memory_limit");
switch(strtolower(substr($memString,-1))) {
case "k":
$memory = round(substr($memString,0,-1)*1024);
break;
case "m":
$memory = round(substr($memString,0,-1)*1024*1024);
break;
case "g":
$memory = round(substr($memString,0,-1)*1024*1024*1024);
break;
default:
$memory = round($memString);
switch(strtolower(substr($memString, -1))) {
case "k":
$memory = round(substr($memString, 0, -1)*1024);
break;
case "m":
$memory = round(substr($memString, 0, -1)*1024*1024);
break;
case "g":
$memory = round(substr($memString, 0, -1)*1024*1024*1024);
break;
default:
$memory = round($memString);
}
// Check we have at least 32M
if($memory < (32 * 1024 * 1024)) {
if ($memory < (32 * 1024 * 1024)) {
// Increase memory limit
ini_set('memory_limit', '32M');
}
@ -91,34 +97,34 @@ require_once('filesystem/Filesystem.php');
require_once("core/Session.php");
// If this is a dev site, enable php error reporting
if(Director::isDev()) {
if (Director::isDev()) {
error_reporting(E_ALL);
}
Session::start();
if(isset($_GET['url'])) {
if (isset($_GET['url'])) {
$url = $_GET['url'];
// Lighttpd uses this
} else {
list($url, $query) = explode('?', $_SERVER['REQUEST_URI'], 2);
parse_str($query, $_GET);
if($_GET) $_REQUEST = array_merge((array)$_REQUEST, (array)$_GET);
if ($_GET) $_REQUEST = array_merge((array)$_REQUEST, (array)$_GET);
}
if(ManifestBuilder::staleManifest()){
if (ManifestBuilder::staleManifest()) {
ManifestBuilder::compileManifest();
}
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($_GET['debug_profile'])) Profiler::init();
if(isset($_GET['debug_profile'])) Profiler::mark('all_execution');
if (isset($_GET['debug_profile'])) Profiler::init();
if (isset($_GET['debug_profile'])) Profiler::mark('all_execution');
if(isset($_GET['debug_profile'])) Profiler::mark('main.php init');
if (isset($_GET['debug_profile'])) Profiler::mark('main.php init');
// Load error handlers
Debug::loadErrorHandlers();
@ -126,9 +132,9 @@ Debug::loadErrorHandlers();
// Connect to database
require_once("core/model/DB.php");
if(isset($_GET['debug_profile'])) Profiler::mark('DB::connect');
if (isset($_GET['debug_profile'])) Profiler::mark('DB::connect');
DB::connect($databaseConfig);
if(isset($_GET['debug_profile'])) Profiler::unmark('DB::connect');
if (isset($_GET['debug_profile'])) Profiler::unmark('DB::connect');
// Get the request URL
@ -136,15 +142,13 @@ $baseURL = dirname(dirname($_SERVER['SCRIPT_NAME']));
if(substr($url,0,strlen($baseURL)) == $baseURL) $url = substr($url,strlen($baseURL));
if (substr($url, 0, strlen($baseURL)) == $baseURL) $url = substr($url, strlen($baseURL));
// Direct away - this is the "main" function, that hands control to the appropriate controller
if(isset($_GET['debug_profile'])) Profiler::unmark('main.php init');
if (isset($_GET['debug_profile'])) Profiler::unmark('main.php init');
Director::direct($url);
if(isset($_GET['debug_profile'])) {
if (isset($_GET['debug_profile'])) {
Profiler::unmark('all_execution');
Profiler::show(isset($_GET['profile_trace']));
}
?>

2
sake
View File

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

View File

@ -68,9 +68,9 @@ class SearchContext extends Object {
* @return FieldSet
*/
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
//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);
$sql = $query->sql();
// use if a raw SQL query is needed
$results = new DataObjectSet();
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,8 +21,10 @@ class ExactMatchFilter extends SearchFilter {
* @return unknown
*/
public function apply(SQLQuery $query) {
$query = $this->applyRelation($query);
return $query->where("{$this->getDbName()} = '{$this->getValue()}'");
if($this->getValue()) {
$query = $this->applyRelation($query);
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) {
if (is_array($this->relation)) {
$model = singleton($this->model);
foreach($this->relation as $rel) {
$model = singleton($this->model);
if ($component = $model->has_one($rel)) {
$foreignKey = $model->getReverseAssociation($component);
$query->leftJoin($component, "`$component`.`ID` = `{$this->model}`.`{$foreignKey}ID`");
if(!$query->isJoinedTo($component)) {
$foreignKey = $model->getReverseAssociation($component);
$query->leftJoin($component, "`$component`.`ID` = `{$this->model}`.`{$foreignKey}ID`");
}
$this->model = $component;
} elseif ($component = $model->has_many($rel)) {
$ancestry = $model->getClassAncestry();
$model = singleton($component);
$foreignKey = $model->getReverseAssociation($ancestry[0]);
$foreignKey = ($foreignKey) ? $foreignKey : $ancestry[0];
$query->leftJoin($component, "`$component`.`{$foreignKey}ID` = `{$this->model}`.`ID`");
if(!$query->isJoinedTo($component)) {
$ancestry = $model->getClassAncestry();
$foreignKey = $model->getComponentJoinField($rel);
$query->leftJoin($component, "`$component`.`{$foreignKey}` = `{$ancestry[0]}`.`ID`");
}
$this->model = $component;
} elseif ($component = $model->many_many($rel)) {
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
class SearchContextTest extends SapphireTest {
static $fixture_file = 'sapphire/tests/SearchContextTest.yml';
function testResultSetFilterReturnsExpectedCount() {
$person = singleton('SearchContextTest_Person');
$context = $person->getDefaultSearchContext();
$results = $context->getResults(array('Name'=>''));
$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() {
$project = singleton('SearchContextTest_Project');
$context = $project->getDefaultSearchContext();
@ -80,16 +94,12 @@ class SearchContextTest extends SapphireTest {
$this->assertEquals(1, $results->Count());
//Debug::dump(DB::query("select * from SearchContextTest_Deadline")->next());
$project = $results->First();
$this->assertType('SearchContextTest_Project', $project);
$this->assertEquals("Blog Website", $project->Name);
$this->assertEquals(2, $project->Actions()->Count());
$this->assertEquals("Get RSS feeds working", $project->Actions()->First()->Description);
//Debug::dump($project->Deadline()->CompletionDate);
//$this->assertEquals()
}
function testCanGenerateQueryUsingAllFilterTypes() {
@ -151,8 +161,14 @@ class SearchContextTest_Company extends DataObject implements TestOnly {
static $searchable_fields = array(
"Name" => "PartialMatchFilter",
"Industry" => "TextareaField",
"AnnualProfit" => array("NumericField" => "PartialMatchFilter")
"Industry" => array(
'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",
"Negation" => "NegationFilter",
"SubstringMatch" => "SubstringFilter",
"CollectionMatch" => "CollectionFilter",
"CollectionMatch" => "ExactMatchMultiFilter",
"StartsWith" => "StartsWithFilter",
"EndsWith" => "EndsWithFilter"
);