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@60208 467b73ca-7a2a-4603-9d3b-597d59a354a9
This commit is contained in:
parent
b5776e0438
commit
a599df309c
@ -28,6 +28,7 @@ Director::addRules(10, array(
|
|||||||
'' => 'RootURLController',
|
'' => 'RootURLController',
|
||||||
'sitemap.xml' => 'GoogleSitemap',
|
'sitemap.xml' => 'GoogleSitemap',
|
||||||
'api/v1' => 'RestfulServer',
|
'api/v1' => 'RestfulServer',
|
||||||
|
'dev' => 'DevelopmentAdmin'
|
||||||
));
|
));
|
||||||
|
|
||||||
Director::addRules(1, array(
|
Director::addRules(1, array(
|
||||||
|
@ -169,7 +169,7 @@ class RestfulServer extends Controller {
|
|||||||
* @param $includeHeader Include <?xml ...?> header (Default: true)
|
* @param $includeHeader Include <?xml ...?> header (Default: true)
|
||||||
* @return String XML
|
* @return String XML
|
||||||
*/
|
*/
|
||||||
protected function dataObjectAsXML(DataObject $obj, $includeHeader = true) {
|
public function dataObjectAsXML(DataObject $obj, $includeHeader = true) {
|
||||||
$className = $obj->class;
|
$className = $obj->class;
|
||||||
$id = $obj->ID;
|
$id = $obj->ID;
|
||||||
$objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID");
|
$objHref = Director::absoluteURL(self::$api_base . "$obj->class/$obj->ID");
|
||||||
@ -177,7 +177,8 @@ class RestfulServer extends Controller {
|
|||||||
$json = "";
|
$json = "";
|
||||||
if($includeHeader) $json .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
if($includeHeader) $json .= "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
||||||
$json .= "<$className href=\"$objHref.xml\">\n";
|
$json .= "<$className href=\"$objHref.xml\">\n";
|
||||||
foreach($obj->db() as $fieldName => $fieldType) {
|
$dbFields = array_merge($obj->databaseFields(), array('ID'=>'Int'));
|
||||||
|
foreach($dbFields as $fieldName => $fieldType) {
|
||||||
if(is_object($obj->$fieldName)) {
|
if(is_object($obj->$fieldName)) {
|
||||||
$json .= $obj->$fieldName->toXML();
|
$json .= $obj->$fieldName->toXML();
|
||||||
} else {
|
} else {
|
||||||
@ -228,7 +229,7 @@ class RestfulServer extends Controller {
|
|||||||
* @param DataObjectSet $set
|
* @param DataObjectSet $set
|
||||||
* @return String XML
|
* @return String XML
|
||||||
*/
|
*/
|
||||||
protected function dataObjectSetAsXML(DataObjectSet $set) {
|
public function dataObjectSetAsXML(DataObjectSet $set) {
|
||||||
$className = $set->class;
|
$className = $set->class;
|
||||||
|
|
||||||
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<$className>\n";
|
$xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<$className>\n";
|
||||||
@ -248,12 +249,13 @@ class RestfulServer extends Controller {
|
|||||||
* @param DataObject $obj
|
* @param DataObject $obj
|
||||||
* @return String JSON
|
* @return String JSON
|
||||||
*/
|
*/
|
||||||
protected function dataObjectAsJSON(DataObject $obj) {
|
public function dataObjectAsJSON(DataObject $obj) {
|
||||||
$className = $obj->class;
|
$className = $obj->class;
|
||||||
$id = $obj->ID;
|
$id = $obj->ID;
|
||||||
|
|
||||||
$json = "{\n className : \"$className\",\n";
|
$json = "{\n className : \"$className\",\n";
|
||||||
foreach($obj->db() as $fieldName => $fieldType) {
|
$dbFields = array_merge($obj->databaseFields(), array('ID'=>'Int'));
|
||||||
|
foreach($dbFields as $fieldName => $fieldType) {
|
||||||
if(is_object($obj->$fieldName)) {
|
if(is_object($obj->$fieldName)) {
|
||||||
$jsonParts[] = "$fieldName : " . $obj->$fieldName->toJSON();
|
$jsonParts[] = "$fieldName : " . $obj->$fieldName->toJSON();
|
||||||
} else {
|
} else {
|
||||||
@ -302,7 +304,7 @@ class RestfulServer extends Controller {
|
|||||||
* @param DataObjectSet $set
|
* @param DataObjectSet $set
|
||||||
* @return String JSON
|
* @return String JSON
|
||||||
*/
|
*/
|
||||||
protected function dataObjectSetAsJSON(DataObjectSet $set) {
|
public function dataObjectSetAsJSON(DataObjectSet $set) {
|
||||||
$jsonParts = array();
|
$jsonParts = array();
|
||||||
foreach($set as $item) {
|
foreach($set as $item) {
|
||||||
if($item->canView()) $jsonParts[] = $this->dataObjectAsJSON($item);
|
if($item->canView()) $jsonParts[] = $this->dataObjectAsJSON($item);
|
||||||
|
@ -1158,33 +1158,31 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
|||||||
* @return SearchContext
|
* @return SearchContext
|
||||||
*/
|
*/
|
||||||
public function getDefaultSearchContext() {
|
public function getDefaultSearchContext() {
|
||||||
$c = new SearchContext($this->class);
|
return new SearchContext($this->class, $this->searchable_fields(), $this->defaultSearchFilters());
|
||||||
|
|
||||||
return $c;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine which properties on the DataObject are
|
* Determine which properties on the DataObject are
|
||||||
* searchable, and map them to their default {@link FormField}
|
* searchable, and map them to their default {@link FormField}
|
||||||
* representations. Useful for scaffolding a searchform for {@link ModelAdmin}.
|
* representations. Used for scaffolding a searchform for {@link ModelAdmin}.
|
||||||
*
|
*
|
||||||
* @usedby {@link SearchContext}
|
* @usedby {@link SearchContext}
|
||||||
* @return FieldSet
|
* @return FieldSet
|
||||||
*/
|
*/
|
||||||
public function scaffoldSearchFields() {
|
public function scaffoldSearchFields() {
|
||||||
$fields = new FieldSet();
|
$fields = new FieldSet();
|
||||||
foreach($this->searchableFields() as $fieldName => $fieldType) {
|
foreach($this->searchable_fields() as $fieldName => $fieldType) {
|
||||||
// @todo Pass localized title
|
// @todo Pass localized title
|
||||||
$fields->push($this->dbObject($fieldName)->scaffoldSearchField());
|
$fields->push($this->dbObject($fieldName)->scaffoldSearchField());
|
||||||
}
|
}
|
||||||
$extras = $this->invokeWithExtensions('extraSearchFields');
|
/*$extras = $this->invokeWithExtensions('extraSearchFields');
|
||||||
if ($extras) {
|
if ($extras) {
|
||||||
foreach($extras as $result) {
|
foreach($extras as $result) {
|
||||||
foreach($result as $fieldName => $fieldType) {
|
foreach($result as $fieldName => $fieldType) {
|
||||||
$fields->push(new $fieldType($fieldName));
|
$fields->push(new $fieldType($fieldName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
return $fields;
|
return $fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1203,6 +1201,13 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
|||||||
// commented out, to be less of a pain in the ass
|
// commented out, to be less of a pain in the ass
|
||||||
//$fields->addFieldToTab('Root.Main', $this->dbObject($fieldName)->scaffoldFormField());
|
//$fields->addFieldToTab('Root.Main', $this->dbObject($fieldName)->scaffoldFormField());
|
||||||
$fields->push($this->dbObject($fieldName)->scaffoldFormField());
|
$fields->push($this->dbObject($fieldName)->scaffoldFormField());
|
||||||
|
}
|
||||||
|
foreach($this->has_one() as $relationship => $component) {
|
||||||
|
$model = singleton($component);
|
||||||
|
$records = DataObject::get($component);
|
||||||
|
$collect = ($model->hasMethod('customSelectOption')) ? 'customSelectOption' : current($model->summary_fields());
|
||||||
|
$options = $records->filter_map('ID', $collect);
|
||||||
|
$fields->push(new DropdownField($relationship.'ID', $relationship, $options));
|
||||||
}
|
}
|
||||||
return $fields;
|
return $fields;
|
||||||
}
|
}
|
||||||
@ -1212,13 +1217,11 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
|||||||
*/
|
*/
|
||||||
protected function addScaffoldRelationFields($fieldSet) {
|
protected function addScaffoldRelationFields($fieldSet) {
|
||||||
foreach($this->has_many() as $relationship => $component) {
|
foreach($this->has_many() as $relationship => $component) {
|
||||||
$relationshipFields = array_keys($this->searchableFields());
|
$relationshipFields = array_keys($this->searchable_fields());
|
||||||
$fieldSet->push(new ComplexTableField($this, $relationship, $component, $relationshipFields));
|
$fieldSet->push(new ComplexTableField($this, $relationship, $component, $relationshipFields));
|
||||||
}
|
}
|
||||||
return $fieldSet;
|
return $fieldSet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Centerpiece of every data administration interface in Silverstripe,
|
* Centerpiece of every data administration interface in Silverstripe,
|
||||||
@ -2059,7 +2062,7 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
|||||||
* but still needs to know the properties of its parent. This should be merged into databaseFields or
|
* but still needs to know the properties of its parent. This should be merged into databaseFields or
|
||||||
* customDatabaseFields.
|
* customDatabaseFields.
|
||||||
*
|
*
|
||||||
* @todo integrate with pre-existing crap
|
* @todo review whether this is still needed after recent API changes
|
||||||
*/
|
*/
|
||||||
public function inheritedDatabaseFields() {
|
public function inheritedDatabaseFields() {
|
||||||
$fields = array();
|
$fields = array();
|
||||||
@ -2073,32 +2076,69 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the default searchable fields for this object,
|
* Get the default searchable fields for this object,
|
||||||
* excluding any fields that are specifically overriden
|
* as defined in the $searchable_fields list. If searchable
|
||||||
* in the data object itself.
|
* fields are not defined on the data object, uses a default
|
||||||
|
* selection of summary fields.
|
||||||
*
|
*
|
||||||
* @todo rename $searchable to $excluded
|
* @return array
|
||||||
* @todo overcomplicated, should be simpler way of looking up whether specific fields are supposed to be searchable or not
|
|
||||||
*/
|
*/
|
||||||
public function searchableFields() {
|
public function searchable_fields() {
|
||||||
$parents = ClassInfo::dataClassesFor($this);
|
$fields = $this->stat('searchable_fields');
|
||||||
$fields = array();
|
if (!$fields) {
|
||||||
$searchable = array();
|
$fields = array_fill_keys($this->summary_fields(), 'TextField');
|
||||||
foreach($parents as $class) {
|
}
|
||||||
$fields = array_merge($fields, singleton($class)->stat('db'));
|
return $fields;
|
||||||
$obj = singleton($class);
|
}
|
||||||
$results = $obj->invokeWithExtensions('excludeFromSearch');
|
|
||||||
if ($results) {
|
/**
|
||||||
foreach($results as $result) {
|
* Get the default summary fields for this object.
|
||||||
if (is_array($result)) {
|
*
|
||||||
$searchable = array_merge($searchable, $result);
|
* @todo use the translation apparatus to return a default field selection for the language
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function summary_fields() {
|
||||||
|
$fields = $this->stat('summary_fields');
|
||||||
|
if (!$fields) {
|
||||||
|
$fields = array();
|
||||||
|
if ($this->hasField('Name')) $fields[] = 'Name';
|
||||||
|
if ($this->hasField('Title')) $fields[] = 'Title';
|
||||||
|
if ($this->hasField('Description')) $fields[] = 'Description';
|
||||||
|
if ($this->hasField('Firstname')) $fields[] = 'Firstname';
|
||||||
|
}
|
||||||
|
return $fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines a default list of filters for the search context.
|
||||||
|
*
|
||||||
|
* If a filter class mapping is defined on the data object,
|
||||||
|
* it is constructed here. Otherwise, the default filter specified in
|
||||||
|
* {@link DBField} is used.
|
||||||
|
*
|
||||||
|
* @todo error handling/type checking for valid FormField and SearchFilter subclasses?
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function defaultSearchFilters() {
|
||||||
|
$filters = array();
|
||||||
|
foreach($this->searchable_fields() as $name => $type) {
|
||||||
|
if (is_int($name)) {
|
||||||
|
$filters[$type] = $this->dbObject($type)->defaultSearchFilter();
|
||||||
|
} else {
|
||||||
|
if (is_array($type)) {
|
||||||
|
$filter = current($type);
|
||||||
|
$filters[$name] = new $filter();
|
||||||
|
} else {
|
||||||
|
if (is_subclass_of($type, 'SearchFilter')) {
|
||||||
|
$filters[$name] = new $type($name);
|
||||||
|
} else {
|
||||||
|
$filters[$name] = $this->dbObject($name)->defaultSearchFilter();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
foreach($searchable as $field) {
|
return $filters;
|
||||||
unset($fields[$field]);
|
|
||||||
}
|
|
||||||
return $fields;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2126,6 +2166,9 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
|||||||
return self::$context_obj;
|
return self::$context_obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ignore
|
||||||
|
*/
|
||||||
protected static $context_obj = null;
|
protected static $context_obj = null;
|
||||||
|
|
||||||
|
|
||||||
@ -2231,6 +2274,44 @@ class DataObject extends ViewableData implements DataObjectInterface {
|
|||||||
*/
|
*/
|
||||||
public static $default_sort = null;
|
public static $default_sort = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
||||||
|
* "Name" => "PartialMatchFilter"
|
||||||
|
* );
|
||||||
|
* </code>
|
||||||
|
*
|
||||||
|
* Overriding the default form field and filter:
|
||||||
|
* <code>
|
||||||
|
* static $searchable_fields = array(
|
||||||
|
* "Name" => array("TextField" => "PartialMatchFilter")
|
||||||
|
* );
|
||||||
|
* </code>
|
||||||
|
*/
|
||||||
|
public static $searchable_fields = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a default list of fields to be used by a 'summary'
|
||||||
|
* view of this object.
|
||||||
|
*/
|
||||||
|
public static $summary_fields = null;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
@ -596,6 +596,21 @@ class DataObjectSet extends ViewableData implements IteratorAggregate {
|
|||||||
}
|
}
|
||||||
return $map;
|
return $map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Temporary filter method for filtering a list based on multiple fields of the DataObject.
|
||||||
|
*
|
||||||
|
* Question: should any args be passed to the filter function?
|
||||||
|
*
|
||||||
|
* @todo deprecate toDropdownMap() and map_multiple(), rename this method to map()
|
||||||
|
*/
|
||||||
|
public function filter_map($key, $value) {
|
||||||
|
$map = array();
|
||||||
|
foreach($this->items as $object) {
|
||||||
|
$map[$object->$key] = ($object->hasMethod($value)) ? $object->$value() : $object->$value;
|
||||||
|
}
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find an item in this list where the field $key is equal to $value
|
* Find an item in this list where the field $key is equal to $value
|
||||||
|
@ -96,7 +96,7 @@ class ErrorPage extends Page {
|
|||||||
// Run the page
|
// Run the page
|
||||||
Requirements::clear();
|
Requirements::clear();
|
||||||
$controller = new ErrorPage_Controller($this);
|
$controller = new ErrorPage_Controller($this);
|
||||||
$errorContent = $controller->run( array() )->getBody();
|
$errorContent = $controller->handleRequest(new HTTPRequest('GET',''))->getBody();
|
||||||
|
|
||||||
if(!file_exists("../assets")) {
|
if(!file_exists("../assets")) {
|
||||||
mkdir("../assets", 02775);
|
mkdir("../assets", 02775);
|
||||||
|
@ -60,6 +60,12 @@ class SQLQuery extends Object {
|
|||||||
*/
|
*/
|
||||||
public $delete;
|
public $delete;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The logical connective used to join WHERE clauses. Defaults to AND.
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $connective = 'AND';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a new SQLQuery.
|
* Construct a new SQLQuery.
|
||||||
* @param array $select An array of fields to select.
|
* @param array $select An array of fields to select.
|
||||||
@ -81,6 +87,20 @@ class SQLQuery extends Object {
|
|||||||
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the disjunctive operator 'OR' to join filter expressions in the WHERE clause.
|
||||||
|
*/
|
||||||
|
public function useDisjunction() {
|
||||||
|
$this->connective = 'OR';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the conjunctive operator 'AND' to join filter expressions in the WHERE clause.
|
||||||
|
*/
|
||||||
|
public function useConjunction() {
|
||||||
|
$this->connective = 'AND';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Swap the use of one table with another.
|
* Swap the use of one table with another.
|
||||||
@ -121,6 +141,15 @@ class SQLQuery extends Object {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an SQL WHERE clause to filter a SELECT query.
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
function getFilter() {
|
||||||
|
return implode(") {$this->connective} (" , $this->where);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate the SQL statement for this query.
|
* Generate the SQL statement for this query.
|
||||||
* @return string
|
* @return string
|
||||||
@ -134,7 +163,7 @@ class SQLQuery extends Object {
|
|||||||
}
|
}
|
||||||
$text .= " FROM " . implode(" ", $this->from);
|
$text .= " FROM " . implode(" ", $this->from);
|
||||||
|
|
||||||
if($this->where) $text .= " WHERE (" . implode(") AND (" , $this->where) . ")";
|
if($this->where) $text .= " WHERE (" . $this->getFilter(). ")";
|
||||||
if($this->groupby) $text .= " GROUP BY " . implode(", ", $this->groupby);
|
if($this->groupby) $text .= " GROUP BY " . implode(", ", $this->groupby);
|
||||||
if($this->having) $text .= " HAVING ( " . implode(" ) AND ( ", $this->having) . " )";
|
if($this->having) $text .= " HAVING ( " . implode(" ) AND ( ", $this->having) . " )";
|
||||||
if($this->orderby) $text .= " ORDER BY " . $this->orderby;
|
if($this->orderby) $text .= " ORDER BY " . $this->orderby;
|
||||||
|
@ -222,7 +222,7 @@ abstract class DBField extends ViewableData {
|
|||||||
* @return SearchFilter
|
* @return SearchFilter
|
||||||
*/
|
*/
|
||||||
public function defaultSearchFilter() {
|
public function defaultSearchFilter() {
|
||||||
return new ExactMatchSearchFilter();
|
return new ExactMatchFilter($this->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -12,7 +12,8 @@ class DevelopmentAdmin extends Controller {
|
|||||||
|
|
||||||
static $url_handlers = array(
|
static $url_handlers = array(
|
||||||
'' => 'index',
|
'' => 'index',
|
||||||
'$Action' => '$Action'
|
'$Action' => '$Action',
|
||||||
|
'$Action//$Action/$ID' => 'handleAction',
|
||||||
);
|
);
|
||||||
|
|
||||||
function index() {
|
function index() {
|
||||||
@ -31,16 +32,14 @@ HTML;
|
|||||||
$renderer->writeFooter();
|
$renderer->writeFooter();
|
||||||
}
|
}
|
||||||
|
|
||||||
function tests() {
|
function tests($request) {
|
||||||
if(isset($this->urlParams['NestedAction'])) {
|
$controller = new TestRunner();
|
||||||
Director::redirect("TestRunner/only/" . $this->urlParams['NestedAction']);
|
return $controller->handleRequest($request);
|
||||||
} else {
|
|
||||||
Director::redirect("TestRunner/");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function tasks($request) {
|
function tasks() {
|
||||||
return new TaskRunner();
|
$controller = new TaskRunner();
|
||||||
|
return $controller->handleRequest($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,11 @@ class TestRunner extends Controller {
|
|||||||
/** @ignore */
|
/** @ignore */
|
||||||
private static $default_reporter;
|
private static $default_reporter;
|
||||||
|
|
||||||
|
static $url_handlers = array(
|
||||||
|
'' => 'index',
|
||||||
|
'$TestCase' => 'only'
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override the default reporter with a custom configured subclass.
|
* Override the default reporter with a custom configured subclass.
|
||||||
*
|
*
|
||||||
@ -76,15 +81,13 @@ class TestRunner extends Controller {
|
|||||||
/**
|
/**
|
||||||
* Run only a single test class
|
* Run only a single test class
|
||||||
*/
|
*/
|
||||||
function only() {
|
function only($request) {
|
||||||
$className = $this->urlParams['ID'];
|
$className = $request->param('TestCase');
|
||||||
if(class_exists($className)) {
|
if(class_exists($className)) {
|
||||||
$this->runTests(array($className));
|
$this->runTests(array($className));
|
||||||
} else {
|
} else {
|
||||||
echo "Class '$className' not found";
|
echo "Class '$className' not found";
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function runTests($classList, $coverage = false) {
|
function runTests($classList, $coverage = false) {
|
||||||
|
@ -56,17 +56,7 @@ class SearchContext extends Object {
|
|||||||
* @var array
|
* @var array
|
||||||
protected $params;
|
protected $params;
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
|
||||||
* Require either all search filters to be evaluated to true,
|
|
||||||
* or just a single one.
|
|
||||||
*
|
|
||||||
* @todo Not Implemented
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $booleanSearchType = 'AND';
|
|
||||||
|
|
||||||
|
|
||||||
function __construct($modelClass, $fields = null, $filters = null) {
|
function __construct($modelClass, $fields = null, $filters = null) {
|
||||||
$this->modelClass = $modelClass;
|
$this->modelClass = $modelClass;
|
||||||
$this->fields = $fields;
|
$this->fields = $fields;
|
||||||
@ -74,7 +64,7 @@ class SearchContext extends Object {
|
|||||||
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns scaffolded search fields for UI.
|
* Returns scaffolded search fields for UI.
|
||||||
*
|
*
|
||||||
@ -82,43 +72,53 @@ 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
|
||||||
|
//return ($this->fields) ? $this->fields : singleton($this->modelClass)->scaffoldSearchFields();
|
||||||
|
return singleton($this->modelClass)->scaffoldSearchFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the query object augumented with all clauses from
|
* Returns a SQL object representing the search context for the given
|
||||||
* the connected {@link SearchFilter}s
|
* list of query parameters.
|
||||||
*
|
|
||||||
* @todo query generation
|
|
||||||
*
|
*
|
||||||
* @param array $searchParams
|
* @param array $searchParams
|
||||||
* @return SQLQuery
|
* @return SQLQuery
|
||||||
*/
|
*/
|
||||||
public function getQuery($searchParams) {
|
public function getQuery($searchParams, $start = false, $limit = false) {
|
||||||
$q = new SQLQuery("*", $this->modelClass);
|
$model = singleton($this->modelClass);
|
||||||
$this->processFilters($q);
|
$fields = array_keys($model->db());
|
||||||
return $q;
|
$query = new SQLQuery($fields, $this->modelClass);
|
||||||
|
foreach($searchParams as $key => $value) {
|
||||||
|
$filter = $this->getFilter($key);
|
||||||
|
if ($filter) {
|
||||||
|
$query->where[] = $filter->apply($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Light wrapper around {@link getQuery()}.
|
* Returns a result set from the given search parameters.
|
||||||
*
|
*
|
||||||
|
* @todo rearrange start and limit params to reflect DataObject
|
||||||
|
*
|
||||||
* @param array $searchParams
|
* @param array $searchParams
|
||||||
* @param int $start
|
* @param int $start
|
||||||
* @param int $limit
|
* @param int $limit
|
||||||
* @return DataObjectSet
|
* @return DataObjectSet
|
||||||
*/
|
*/
|
||||||
public function getResults($searchParams, $start = false, $limit = false) {
|
public function getResults($searchParams, $start = false, $limit = false) {
|
||||||
$q = $this->getQuery($searchParams);
|
$query = $this->getQuery($searchParams, $start, $limit);
|
||||||
//$q->limit = $start ? "$start, $limit" : $limit;
|
//
|
||||||
$output = new DataObjectSet();
|
// use if a raw SQL query is needed
|
||||||
foreach($q->execute() as $row) {
|
//$results = new DataObjectSet();
|
||||||
$className = $row['RecordClassName'];
|
//foreach($query->execute() as $row) {
|
||||||
$output->push(new $className($row));
|
// $className = $row['ClassName'];
|
||||||
}
|
// $results->push(new $className($row));
|
||||||
|
//}
|
||||||
// do the setting of start/limit on the dataobjectset
|
//return $results;
|
||||||
return $output;
|
//
|
||||||
|
return DataObject::get($this->modelClass, $query->getFilter(), "", "", $limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -128,14 +128,25 @@ class SearchContext extends Object {
|
|||||||
* @param array $searchFilters
|
* @param array $searchFilters
|
||||||
* @param SQLQuery $query
|
* @param SQLQuery $query
|
||||||
*/
|
*/
|
||||||
protected function processFilters($searchFilters, SQLQuery &$query) {
|
protected function processFilters(SQLQuery $query, $searchParams) {
|
||||||
foreach($this->filters as $filter) {
|
$conditions = array();
|
||||||
$filter->updateQuery($searchFilters, $tableName, $query);
|
foreach($this->filters as $field => $filter) {
|
||||||
|
if (strstr($field, '.')) {
|
||||||
|
$path = explode('.', $field);
|
||||||
|
} else {
|
||||||
|
$conditions[] = $filter->apply($searchParams[$field]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
$query->where = $conditions;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ############ Getters/Setters ###########
|
public function getFilter($name) {
|
||||||
|
if (isset($this->filters[$name])) {
|
||||||
|
return $this->filters[$name];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function getFields() {
|
public function getFields() {
|
||||||
return $this->fields;
|
return $this->fields;
|
||||||
@ -146,7 +157,7 @@ class SearchContext extends Object {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function getFilters() {
|
public function getFilters() {
|
||||||
return $this->fields;
|
return $this->filters;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setFilters($filters) {
|
public function setFilters($filters) {
|
||||||
|
@ -10,5 +10,14 @@
|
|||||||
*/
|
*/
|
||||||
class ExactMatchFilter extends SearchFilter {
|
class ExactMatchFilter extends SearchFilter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies an exact match (equals) on a field value.
|
||||||
|
*
|
||||||
|
* @return unknown
|
||||||
|
*/
|
||||||
|
public function apply($value) {
|
||||||
|
return "{$this->name}='$value'";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
@ -6,6 +6,10 @@
|
|||||||
* @subpackage search
|
* @subpackage search
|
||||||
*/
|
*/
|
||||||
class FulltextFilter extends SearchFilter {
|
class FulltextFilter extends SearchFilter {
|
||||||
|
|
||||||
|
public function apply($value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
21
search/filters/NegationFilter.php
Normal file
21
search/filters/NegationFilter.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* @package search
|
||||||
|
* @subpackage filters
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches on rows where the field is not equal to the given value.
|
||||||
|
*
|
||||||
|
* @package search
|
||||||
|
* @subpackage filters
|
||||||
|
*/
|
||||||
|
class NegationFilter extends SearchFilter {
|
||||||
|
|
||||||
|
public function apply($value) {
|
||||||
|
return "{$this->name} != '$value'";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
@ -7,5 +7,9 @@
|
|||||||
*/
|
*/
|
||||||
class PartialMatchFilter extends SearchFilter {
|
class PartialMatchFilter extends SearchFilter {
|
||||||
|
|
||||||
|
public function apply($value) {
|
||||||
|
return "{$this->name} LIKE '%$value%'";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
@ -7,5 +7,13 @@
|
|||||||
*/
|
*/
|
||||||
abstract class SearchFilter extends Object {
|
abstract class SearchFilter extends Object {
|
||||||
|
|
||||||
|
protected $name;
|
||||||
|
|
||||||
|
function __construct($name) {
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract public function apply($value);
|
||||||
|
|
||||||
}
|
}
|
||||||
?>
|
?>
|
@ -6,7 +6,10 @@
|
|||||||
* @subpackage search
|
* @subpackage search
|
||||||
*/
|
*/
|
||||||
class SubstringMatchFilter extends SearchFilter {
|
class SubstringMatchFilter extends SearchFilter {
|
||||||
|
|
||||||
|
public function apply($value) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,7 @@ class SearchContextTest extends SapphireTest {
|
|||||||
static $fixture_file = 'sapphire/tests/SearchContextTest.yml';
|
static $fixture_file = 'sapphire/tests/SearchContextTest.yml';
|
||||||
|
|
||||||
function testResultSetFilterReturnsExpectedCount() {
|
function testResultSetFilterReturnsExpectedCount() {
|
||||||
$person = singleton('PersonBubble');
|
$person = singleton('SearchContextTest_Person');
|
||||||
$context = $person->getDefaultSearchContext();
|
$context = $person->getDefaultSearchContext();
|
||||||
|
|
||||||
$results = $context->getResultSet(array('Name'=>''));
|
$results = $context->getResultSet(array('Name'=>''));
|
||||||
@ -15,13 +15,90 @@ class SearchContextTest extends SapphireTest {
|
|||||||
|
|
||||||
$results = $context->getResultSet(array('EyeColor'=>'green', 'HairColor'=>'black'));
|
$results = $context->getResultSet(array('EyeColor'=>'green', 'HairColor'=>'black'));
|
||||||
$this->assertEquals(1, $results->Count());
|
$this->assertEquals(1, $results->Count());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//function
|
function testSummaryIncludesDefaultFieldsIfNotDefined() {
|
||||||
|
$person = singleton('SearchContextTest_Person');
|
||||||
|
$this->assertContains('Name', $person->summary_fields());
|
||||||
|
|
||||||
|
$book = singleton('SearchContextTest_Book');
|
||||||
|
$this->assertContains('Title', $book->summary_fields());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testAccessDefinedSummaryFields() {
|
||||||
|
$company = singleton('SearchContextTest_Company');
|
||||||
|
$this->assertContains('Industry', $company->summary_fields());
|
||||||
|
}
|
||||||
|
|
||||||
|
function testExactMatchUsedByDefaultWhenNotExplicitlySet() {
|
||||||
|
$person = singleton('SearchContextTest_Person');
|
||||||
|
$context = $person->getDefaultSearchContext();
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
"Name" => new ExactMatchFilter("Name"),
|
||||||
|
"HairColor" => new ExactMatchFilter("HairColor"),
|
||||||
|
"EyeColor" => new ExactMatchFilter("EyeColor")
|
||||||
|
),
|
||||||
|
$context->getFilters()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testDefaultFiltersDefinedWhenNotSetInDataObject() {
|
||||||
|
$book = singleton('SearchContextTest_Book');
|
||||||
|
$context = $book->getDefaultSearchContext();
|
||||||
|
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
"Title" => new ExactMatchFilter("Title")
|
||||||
|
),
|
||||||
|
$context->getFilters()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testUserDefinedFiltersAppearInSearchContext() {
|
||||||
|
//$company = singleton('SearchContextTest_Company');
|
||||||
|
//$context = $company->getDefaultSearchContext();
|
||||||
|
|
||||||
|
/*$this->assertEquals(
|
||||||
|
array(
|
||||||
|
"Name" => new PartialMatchFilter("Name"),
|
||||||
|
"Industry" => new ExactMatchFilter("Industry"),
|
||||||
|
"AnnualProfit" => new PartialMatchFilter("AnnualProfit")
|
||||||
|
),
|
||||||
|
$context->getFilters()
|
||||||
|
);*/
|
||||||
|
}
|
||||||
|
|
||||||
|
function testRelationshipObjectsLinkedInSearch() {
|
||||||
|
//$project = singleton('SearchContextTest_Project');
|
||||||
|
//$context = $project->getDefaultSearchContext();
|
||||||
|
|
||||||
|
//$query = array("Name"=>"Blog Website");
|
||||||
|
|
||||||
|
//$results = $context->getQuery($query);
|
||||||
|
}
|
||||||
|
|
||||||
|
function testCanGenerateQueryUsingAllFilterTypes() {
|
||||||
|
$all = singleton("SearchContextTest_AllFilterTypes");
|
||||||
|
$context = $all->getDefaultSearchContext();
|
||||||
|
|
||||||
|
$params = array(
|
||||||
|
"ExactMatch" => "Match Me Exactly",
|
||||||
|
"PartialMatch" => "partially",
|
||||||
|
"Negation" => "undisclosed"
|
||||||
|
);
|
||||||
|
|
||||||
|
$results = $context->getResults($params);
|
||||||
|
|
||||||
|
$this->assertEquals(1, $results->Count());
|
||||||
|
$this->assertEquals("Filtered value", $results->First()->HiddenValue);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PersonBubble extends DataObject {
|
class SearchContextTest_Person extends DataObject implements TestOnly {
|
||||||
|
|
||||||
static $db = array(
|
static $db = array(
|
||||||
"Name" => "Text",
|
"Name" => "Text",
|
||||||
@ -30,6 +107,102 @@ class PersonBubble extends DataObject {
|
|||||||
"EyeColor" => "Text"
|
"EyeColor" => "Text"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static $searchable_fields = array(
|
||||||
|
"Name", "HairColor", "EyeColor"
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchContextTest_Book extends DataObject implements TestOnly {
|
||||||
|
|
||||||
|
static $db = array(
|
||||||
|
"Title" => "Text",
|
||||||
|
"Summary" => "Text"
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchContextTest_Company extends DataObject implements TestOnly {
|
||||||
|
|
||||||
|
static $db = array(
|
||||||
|
"Name" => "Text",
|
||||||
|
"Industry" => "Text",
|
||||||
|
"AnnualProfit" => "Int"
|
||||||
|
);
|
||||||
|
|
||||||
|
static $summary_fields = array(
|
||||||
|
"Industry"
|
||||||
|
);
|
||||||
|
|
||||||
|
static $searchable_fields = array(
|
||||||
|
"Name" => "PartialMatchFilter",
|
||||||
|
"Industry" => "TextareaField",
|
||||||
|
"AnnualProfit" => array("NumericField" => "PartialMatchFilter")
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchContextTest_Project extends DataObject implements TestOnly {
|
||||||
|
|
||||||
|
static $db = array(
|
||||||
|
"Name" => "Text"
|
||||||
|
);
|
||||||
|
|
||||||
|
static $has_one = array(
|
||||||
|
"Deadline" => "SearchContextTest_Deadline"
|
||||||
|
);
|
||||||
|
|
||||||
|
static $has_many = array(
|
||||||
|
"Actions" => "SearchContextTest_Action"
|
||||||
|
);
|
||||||
|
|
||||||
|
static $searchable_fields = array(
|
||||||
|
"Name" => "PartialMatchFilter",
|
||||||
|
"Actions.SolutionArea" => "ExactMatchFilter"
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchContextTest_Deadline extends DataObject implements TestOnly {
|
||||||
|
|
||||||
|
static $db = array(
|
||||||
|
"CompletionDate" => "Datetime"
|
||||||
|
);
|
||||||
|
|
||||||
|
static $has_one = array(
|
||||||
|
"Project" => "SearchContextTest_Project"
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchContextTest_Action extends DataObject implements TestOnly {
|
||||||
|
|
||||||
|
static $db = array(
|
||||||
|
"Description" => "Text",
|
||||||
|
"SolutionArea" => "Text"
|
||||||
|
);
|
||||||
|
|
||||||
|
static $has_one = array(
|
||||||
|
"Project" => "SearchContextTest_Project"
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchContextTest_AllFilterTypes extends DataObject implements TestOnly {
|
||||||
|
|
||||||
|
static $db = array(
|
||||||
|
"ExactMatch" => "Text",
|
||||||
|
"PartialMatch" => "Text",
|
||||||
|
"Negation" => "Text",
|
||||||
|
"HiddenValue" => "Text"
|
||||||
|
);
|
||||||
|
|
||||||
|
static $searchable_fields = array(
|
||||||
|
"ExactMatch" => "ExactMatchFilter",
|
||||||
|
"PartialMatch" => "PartialMatchFilter",
|
||||||
|
"Negation" => "NegationFilter"
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
?>
|
?>
|
@ -1,4 +1,4 @@
|
|||||||
PersonBubble:
|
SearchContextTest_Person:
|
||||||
person1:
|
person1:
|
||||||
Name: James
|
Name: James
|
||||||
Email: james@example.com
|
Email: james@example.com
|
||||||
@ -25,4 +25,36 @@ PersonBubble:
|
|||||||
HairColor: black
|
HairColor: black
|
||||||
EyeColor: green
|
EyeColor: green
|
||||||
|
|
||||||
|
SearchContextTest_Deadline:
|
||||||
|
deadline1:
|
||||||
|
CompletionDate: 2008-05-29 09:00:00
|
||||||
|
deadline2:
|
||||||
|
CompletionDate: 2008-05-29 09:00:00
|
||||||
|
|
||||||
|
SearchContextTest_Action:
|
||||||
|
action1:
|
||||||
|
Description: Get search context working
|
||||||
|
SolutionArea: backend
|
||||||
|
action2:
|
||||||
|
Description: Get relationship editor working
|
||||||
|
SolutionArea: frontend
|
||||||
|
action3:
|
||||||
|
Description: Get RSS feeds working
|
||||||
|
SolutionArea: technical
|
||||||
|
|
||||||
|
SearchContextTest_Project:
|
||||||
|
project1:
|
||||||
|
Name: CRM Application
|
||||||
|
Deadline: =>SearchContextTest_Deadline.deadline1
|
||||||
|
Actions: =>SearchContextTest_Action.action1,=>SearchContextTest_Action.action2
|
||||||
|
project2:
|
||||||
|
Name: Blog Website
|
||||||
|
Deadline: =>SearchContextTest_Deadline.deadline2
|
||||||
|
Actions: =>SearchContextTest_Action.action3
|
||||||
|
|
||||||
|
SearchContextTest_AllFilterTypes:
|
||||||
|
filter1:
|
||||||
|
ExactMatch: Match me exactly
|
||||||
|
PartialMatch: Match me partially
|
||||||
|
Negation: Shouldnt match me
|
||||||
|
HiddenValue: Filtered value
|
Loading…
Reference in New Issue
Block a user