Merge pull request #7209 from open-sausages/pulls/4.0/model-admin-search-party

FEATURE: New getSummary() API for SearchContext
This commit is contained in:
Damian Mooyman 2017-07-27 12:51:50 +12:00 committed by GitHub
commit 7bcfde871b
3 changed files with 190 additions and 41 deletions

View File

@ -10,30 +10,34 @@ use SilverStripe\Forms\FormField;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\Filters\SearchFilter;
use SilverStripe\ORM\ArrayList;
use SilverStripe\View\ArrayData;
use SilverStripe\Forms\SelectField;
use SilverStripe\Forms\CheckboxField;
use InvalidArgumentException;
use Exception;
/**
* Manages searching of properties on one or more {@link DataObject}
* types, based on a given set of input parameters.
* SearchContext is intentionally decoupled from any controller-logic,
* it just receives a set of search parameters and an object class it acts on.
*
* The default output of a SearchContext is either a {@link SQLSelect} object
* for further refinement, or a {@link SS_List} that can be used to display
* search results, e.g. in a {@link TableListField} instance.
*
* In case you need multiple contexts, consider namespacing your request parameters
* by using {@link FieldList->namespace()} on the $fields constructor parameter.
*
* Each DataObject subclass can have multiple search contexts for different cases,
* e.g. for a limited frontend search and a fully featured backend search.
* By default, you can use {@link DataObject->getDefaultSearchContext()} which is automatically
* scaffolded. It uses {@link DataObject::$searchable_fields} to determine which fields
* to include.
*
* @see http://doc.silverstripe.com/doku.php?id=searchcontext
*/
* Manages searching of properties on one or more {@link DataObject}
* types, based on a given set of input parameters.
* SearchContext is intentionally decoupled from any controller-logic,
* it just receives a set of search parameters and an object class it acts on.
*
* The default output of a SearchContext is either a {@link SQLSelect} object
* for further refinement, or a {@link SS_List} that can be used to display
* search results, e.g. in a {@link TableListField} instance.
*
* In case you need multiple contexts, consider namespacing your request parameters
* by using {@link FieldList->namespace()} on the $fields constructor parameter.
*
* Each DataObject subclass can have multiple search contexts for different cases,
* e.g. for a limited frontend search and a fully featured backend search.
* By default, you can use {@link DataObject->getDefaultSearchContext()} which is automatically
* scaffolded. It uses {@link DataObject::$searchable_fields} to determine which fields
* to include.
*
* @see http://doc.silverstripe.com/doku.php?id=searchcontext
*/
class SearchContext
{
use Injectable;
@ -61,6 +65,13 @@ class SearchContext
*/
protected $filters;
/**
* Key/value pairs of search fields to search terms
*
* @var array
*/
protected $searchParams = [];
/**
* The logical connective used to join WHERE clauses. Defaults to AND.
* @var string
@ -110,10 +121,10 @@ class SearchContext
$baseTable = DataObject::getSchema()->baseDataTable($this->modelClass);
$fields = array("\"{$baseTable}\".*");
if ($this->modelClass != $classes[0]) {
$fields[] = '"'.$classes[0].'".*';
$fields[] = '"' . $classes[0] . '".*';
}
//$fields = array_keys($model->db());
$fields[] = '"'.$classes[0].'".\"ClassName\" AS "RecordClassName"';
$fields[] = '"' . $classes[0] . '".\"ClassName\" AS "RecordClassName"';
return $fields;
}
@ -121,7 +132,7 @@ class SearchContext
* Returns a SQL object representing the search context for the given
* list of query parameters.
*
* @param array $searchParams Map of search criteria, mostly taked from $_REQUEST.
* @param array $searchParams Map of search criteria, mostly taken from $_REQUEST.
* If a filter is applied to a relationship in dot notation,
* the parameter name should have the dots replaced with double underscores,
* for example "Comments__Name" instead of the filter name "Comments.Name".
@ -160,20 +171,14 @@ class SearchContext
/** @var DataList $query */
$query = $query->sort($sort);
$this->setSearchParams($searchParams);
// hack to work with $searchParems when it's an Object
if ($searchParams instanceof HTTPRequest) {
$searchParamArray = $searchParams->getVars();
} else {
$searchParamArray = $searchParams;
}
foreach ($searchParamArray as $key => $value) {
foreach ($this->searchParams as $key => $value) {
$key = str_replace('__', '.', $key);
if ($filter = $this->getFilter($key)) {
$filter->setModel($this->modelClass);
$filter->setValue($value);
if (! $filter->isEmpty()) {
if (!$filter->isEmpty()) {
$query = $query->alterDataQuery(array($filter, 'apply'));
}
}
@ -199,7 +204,7 @@ class SearchContext
*/
public function getResults($searchParams, $sort = false, $limit = false)
{
$searchParams = array_filter((array)$searchParams, array($this,'clearEmptySearchFields'));
$searchParams = array_filter((array)$searchParams, array($this, 'clearEmptySearchFields'));
// getQuery actually returns a DataList
return $this->getQuery($searchParams, $sort, $limit);
@ -311,4 +316,76 @@ class SearchContext
{
$this->fields->removeByName($fieldName);
}
/**
* Set search param values
*
* @param array|HTTPRequest $searchParams
* @return $this
*/
public function setSearchParams($searchParams)
{
// hack to work with $searchParams when it's an Object
if ($searchParams instanceof HTTPRequest) {
$this->searchParams = $searchParams->getVars();
} else {
$this->searchParams = $searchParams;
}
return $this;
}
/**
* @return array
*/
public function getSearchParams()
{
return $this->searchParams;
}
/**
* Gets a list of what fields were searched and the values provided
* for each field. Returns an ArrayList of ArrayData, suitable for
* rendering on a template.
*
* @return ArrayList
*/
public function getSummary()
{
$list = ArrayList::create();
foreach ($this->searchParams as $searchField => $searchValue) {
if (empty($searchValue)) {
continue;
}
$filter = $this->getFilter($searchField);
if (!$filter) {
continue;
}
$field = $this->fields->fieldByName($filter->getFullName());
if (!$field) {
continue;
}
// For dropdowns, checkboxes, etc, get the value that was presented to the user
// e.g. not an ID
if ($field instanceof SelectField) {
$source = $field->getSource();
if (isset($source[$searchValue])) {
$searchValue = $source[$searchValue];
}
} else {
// For checkboxes, it suffices to simply include the field in the list, since it's binary
if ($field instanceof CheckboxField) {
$searchValue = null;
}
}
$list->push(ArrayData::create([
'Field' => $field->Title(),
'Value' => $searchValue,
]));
}
return $list;
}
}

View File

@ -6,8 +6,12 @@ use SilverStripe\Dev\SapphireTest;
use SilverStripe\Forms\TextField;
use SilverStripe\Forms\TextareaField;
use SilverStripe\Forms\NumericField;
use SilverStripe\Forms\DropdownField;
use SilverStripe\Forms\CheckboxField;
use SilverStripe\Forms\FieldList;
use SilverStripe\ORM\DataList;
use SilverStripe\ORM\Filters\PartialMatchFilter;
use SilverStripe\ORM\Search\SearchContext;
class SearchContextTest extends SapphireTest
{
@ -28,13 +32,13 @@ class SearchContextTest extends SapphireTest
{
$person = SearchContextTest\Person::singleton();
$context = $person->getDefaultSearchContext();
$results = $context->getResults(array('Name'=>''));
$results = $context->getResults(array('Name' => ''));
$this->assertEquals(5, $results->Count());
$results = $context->getResults(array('EyeColor'=>'green'));
$results = $context->getResults(array('EyeColor' => 'green'));
$this->assertEquals(2, $results->Count());
$results = $context->getResults(array('EyeColor'=>'green', 'HairColor'=>'black'));
$results = $context->getResults(array('EyeColor' => 'green', 'HairColor' => 'black'));
$this->assertEquals(1, $results->Count());
}
@ -100,7 +104,6 @@ class SearchContextTest extends SapphireTest
{
$company = SearchContextTest\Company::singleton();
$context = $company->getDefaultSearchContext();
$fields = $context->getFields();
$this->assertEquals(
new FieldList(
new TextField("Name", 'Name'),
@ -115,16 +118,18 @@ class SearchContextTest extends SapphireTest
{
$action3 = $this->objFromFixture(SearchContextTest\Action::class, 'action3');
$project = singleton(SearchContextTest\Project::class);
$project = SearchContextTest\Project::singleton();
$context = $project->getDefaultSearchContext();
$params = array("Name"=>"Blog Website", "Actions__SolutionArea"=>"technical");
$params = array("Name" => "Blog Website", "Actions__SolutionArea" => "technical");
/** @var DataList $results */
$results = $context->getResults($params);
$this->assertEquals(1, $results->Count());
$this->assertEquals(1, $results->count());
$project = $results->First();
/** @var SearchContextTest\Project $project */
$project = $results->first();
$this->assertInstanceOf(SearchContextTest\Project::class, $project);
$this->assertEquals("Blog Website", $project->Name);
@ -184,4 +189,65 @@ class SearchContextTest extends SapphireTest
$this->assertEquals(1, $results->Count());
$this->assertEquals("Filtered value", $results->First()->HiddenValue);
}
public function testSearchContextSummary()
{
$filters = [
'KeywordSearch' => PartialMatchFilter::create('KeywordSearch'),
'Country' => PartialMatchFilter::create('Country'),
'CategoryID' => PartialMatchFilter::create('CategoryID'),
'Featured' => PartialMatchFilter::create('Featured'),
'Nothing' => PartialMatchFilter::create('Nothing'),
];
$fields = FieldList::create(
TextField::create('KeywordSearch', 'Keywords'),
TextField::create('Country', 'Country'),
DropdownField::create('CategoryID', 'Category', [
1 => 'Category one',
2 => 'Category two',
]),
CheckboxField::create('Featured', 'Featured')
);
$context = SearchContext::create(
SearchContextTest\Person::class,
$fields,
$filters
);
$context->setSearchParams([
'KeywordSearch' => 'tester',
'Country' => null,
'CategoryID' => 2,
'Featured' => 1,
'Nothing' => 'empty',
]);
$list = $context->getSummary();
$this->assertEquals(3, $list->count());
// KeywordSearch should be in the summary
$keyword = $list->find('Field', 'Keywords');
$this->assertNotNull($keyword);
$this->assertEquals('tester', $keyword->Value);
// Country should be skipped over
$country = $list->find('Field', 'Country');
$this->assertNull($country);
// Category should be expressed as the label
$category = $list->find('Field', 'Category');
$this->assertNotNull($category);
$this->assertEquals('Category two', $category->Value);
// Featured should have no value, since it's binary
$featured = $list->find('Field', 'Featured');
$this->assertNotNull($featured);
$this->assertNull($featured->Value);
// "Nothing" should come back null since there's no field for it
$nothing = $list->find('Field', 'Nothing');
$this->assertNull($nothing);
}
}

View File

@ -4,7 +4,13 @@ namespace SilverStripe\ORM\Tests\Search\SearchContextTest;
use SilverStripe\Dev\TestOnly;
use SilverStripe\ORM\DataObject;
use SilverStripe\ORM\HasManyList;
/**
* @property string $Name
* @method Deadline Deadline()
* @method HasManyList Actions()
*/
class Project extends DataObject implements TestOnly
{
private static $table_name = 'SearchContextTest_Project';