mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
(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:
parent
95d4634e2e
commit
8d0166e298
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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'
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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();
|
||||
|
@ -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, ' ');
|
||||
}
|
||||
|
@ -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>";
|
||||
|
@ -17,7 +17,7 @@ class SapphireTest extends PHPUnit_Framework_TestCase {
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $fixture_file = null;
|
||||
static $fixture_file = null;
|
||||
|
||||
protected $originalMailer;
|
||||
|
||||
|
@ -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.";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
86
main.php
86
main.php
@ -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
2
sake
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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})");
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
@ -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()}'");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
34
search/filters/ExactMatchMultiFilter.php
Normal file
34
search/filters/ExactMatchMultiFilter.php
Normal 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})");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
@ -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;
|
||||
|
29
search/filters/StartsWithMultiFilter.php
Normal file
29
search/filters/StartsWithMultiFilter.php
Normal 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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
@ -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"
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user