From 7b1b0e8ddca5a1299c97b753837039e4aefd9d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Perej=C3=B3n?= Date: Tue, 28 Feb 2017 11:06:11 +0100 Subject: [PATCH] Runs searches against fields with the Translate behavior If the table has the Translate behavior configured for the searched fields, search in the translated table field content. --- src/Model/Behavior/SearchableBehavior.php | 40 +- tests/Fixture/ColorsFixture.php | 31 + .../Model/Behavior/SearchableBehaviorTest.php | 1295 +++++++++-------- 3 files changed, 758 insertions(+), 608 deletions(-) create mode 100644 tests/Fixture/ColorsFixture.php diff --git a/src/Model/Behavior/SearchableBehavior.php b/src/Model/Behavior/SearchableBehavior.php index 962337a..ee1eaa4 100755 --- a/src/Model/Behavior/SearchableBehavior.php +++ b/src/Model/Behavior/SearchableBehavior.php @@ -11,6 +11,7 @@ namespace Search\Model\Behavior; use Cake\Core\Configure; +use Cake\I18n\I18n; use Cake\ORM\Behavior; use Cake\ORM\Query; use Cake\ORM\Table; @@ -240,9 +241,8 @@ protected function _addCondLike($data, $field) { $cond = []; foreach ($fieldNames as $fieldName) { - if (strpos($fieldName, '.') === false) { - $fieldName = $this->_table->alias() . '.' . $fieldName; - } + + $fieldName = $this->_getFullFieldName($fieldName); if ($field['before'] === true) { $field['before'] = '%'; @@ -325,9 +325,9 @@ protected function _addCondValue($data, $field) { $cond = []; foreach ($fieldNames as $fieldName) { - if (strpos($fieldName, '.') === false) { - $fieldName = $this->_table->alias() . '.' . $fieldName; - } + + $fieldName = $this->_getFullFieldName($fieldName); + if (is_array($fieldValue) && empty($fieldValue)) { continue; } @@ -375,4 +375,32 @@ protected function _addCondFinder(Query $query, $data, $field) { } return $query; } + + + /** + * Returns the full field name including the table alias. + * If the table has the translate behavior and the field is configured to be + * translated, returns the corresponding field to filter by the translated + * content. + * + * @param $fieldName Field name + * @return string + */ + protected function _getFullFieldName($fieldName) + { + // If the fieldName already contains a table alias, do nothing. + if (strpos($fieldName, '.') === false) { + // If this is a translated field, search in the translated table instead of the original one + if ($this->_table->hasBehavior('Translate') && + $this->_table->locale() != I18n::defaultLocale() && + in_array($fieldName, $this->_table->behaviors()->get('Translate')->config('fields'))) { + $fieldName = $this->_table->alias() . '_' . $fieldName . '_translation.content'; + } else { + $fieldName = $this->_table->alias() . '.' . $fieldName; + } + } + + return $fieldName; + } + } diff --git a/tests/Fixture/ColorsFixture.php b/tests/Fixture/ColorsFixture.php new file mode 100644 index 0000000..3a20978 --- /dev/null +++ b/tests/Fixture/ColorsFixture.php @@ -0,0 +1,31 @@ + ['type' => 'string', 'null' => false, 'default' => null, 'length' => 36], + 'name' => ['type' => 'string', 'null' => false, 'default' => null, 'length' => 30], + ]; + +} diff --git a/tests/TestCase/Model/Behavior/SearchableBehaviorTest.php b/tests/TestCase/Model/Behavior/SearchableBehaviorTest.php index a90f312..0ce3671 100755 --- a/tests/TestCase/Model/Behavior/SearchableBehaviorTest.php +++ b/tests/TestCase/Model/Behavior/SearchableBehaviorTest.php @@ -11,6 +11,7 @@ namespace Search\Test\TestCase\Model\Behavior; use Cake\Datasource\ConnectionManager; +use Cake\I18n\I18n; use Cake\ORM\Query; use Cake\ORM\Table; use Cake\TestSuite\TestCase; @@ -18,24 +19,39 @@ /** * Tag test model */ -class TagsTable extends Table { +class TagsTable extends Table +{ } /** - * Tagged test model + * Colors test model */ -class TaggedTable extends Table { +class ColorsTable extends Table +{ + public function initialize(array $options) + { + $this->addBehavior('Search.Searchable'); + $this->addBehavior('Translate', ['fields' => ['name']]); + } +} /** - * Table to use - * - * @var string + * Tagged test model */ - public $useTable = 'tagged'; - - public function initialize(array $options) { - $this->belongsTo('Tags'); - } +class TaggedTable extends Table +{ + + /** + * Table to use + * + * @var string + */ + public $useTable = 'tagged'; + + public function initialize(array $options) + { + $this->belongsTo('Tags'); + } } /** @@ -43,602 +59,677 @@ public function initialize(array $options) { * * Contains various find and condition methods used by the tests below. */ -class ArticlesTable extends Table { - - public function initialize(array $options) { - $this->belongsTo('Tagged'); - $this->belongsToMany('Tags', [ - 'through' => $this->association('Tagged')->target() - ]); - $this->addBehavior('Search.Searchable'); - } - -/** - * Makes an array of range numbers that matches the ones on the interface. - * - * @param $data - * @param null $field - * @return array - */ - public function makeRangeCondition($data, $field = null) { - if (is_string($data)) { - $input = $data; - } - if (is_array($data)) { - if (!empty($field['name'])) { - $input = $data[$field['name']]; - } else { - $input = $data['range']; - } - } - switch ($input) { - case '10': - return [0, 10]; - case '100': - return [11, 100]; - case '1000': - return [101, 1000]; - default: - return [0, 0]; - } - } - -/** - * orConditions - * - * @param array $data - * @return array - */ - public function findOrConditions(Query $query, $data = []) { - $filter = $data['filter']; - $cond = [ - 'OR' => [ - $this->alias() . '.title LIKE' => '%' . $filter . '%', - $this->alias() . '.body LIKE' => '%' . $filter . '%', - ]]; - $query->orWhere($cond); - return $query; - } - - public function findOr2Conditions(Query $query, $data = []) { - $filter = $data['filter2']; - $cond = [ - 'OR' => [ - $this->alias() . '.field1 LIKE' => '%' . $filter . '%', - $this->alias() . '.field2 LIKE' => '%' . $filter . '%', - ]]; - $query->orWhere($cond); - return $query; - } +class ArticlesTable extends Table +{ + + public function initialize(array $options) + { + $this->belongsTo('Tagged'); + $this->belongsToMany('Tags', [ + 'through' => $this->association('Tagged')->target() + ]); + $this->addBehavior('Search.Searchable'); + } + + /** + * Makes an array of range numbers that matches the ones on the interface. + * + * @param $data + * @param null $field + * @return array + */ + public function makeRangeCondition($data, $field = null) + { + if (is_string($data)) { + $input = $data; + } + if (is_array($data)) { + if (!empty($field['name'])) { + $input = $data[$field['name']]; + } else { + $input = $data['range']; + } + } + switch ($input) { + case '10': + return [0, 10]; + case '100': + return [11, 100]; + case '1000': + return [101, 1000]; + default: + return [0, 0]; + } + } + + /** + * orConditions + * + * @param array $data + * @return array + */ + public function findOrConditions(Query $query, $data = []) + { + $filter = $data['filter']; + $cond = [ + 'OR' => [ + $this->alias() . '.title LIKE' => '%' . $filter . '%', + $this->alias() . '.body LIKE' => '%' . $filter . '%', + ]]; + $query->orWhere($cond); + return $query; + } + + public function findOr2Conditions(Query $query, $data = []) + { + $filter = $data['filter2']; + $cond = [ + 'OR' => [ + $this->alias() . '.field1 LIKE' => '%' . $filter . '%', + $this->alias() . '.field2 LIKE' => '%' . $filter . '%', + ]]; + $query->orWhere($cond); + return $query; + } } -/** - * SearchableTestCase - */ -class SearchableBehaviorTest extends TestCase { - -/** - * Article test model - * - * @var ArticlesTable - */ - public $Articles; - -/** - * Load relevant fixtures - * - * @var array - */ - public $fixtures = [ - 'plugin.search.articles', - 'plugin.search.tags', - 'plugin.search.tagged', - //'core.users' - ]; - -/** - * Load Article test model - * - * @return void - */ - public function setUp() { - parent::setUp(); - $this->connection = ConnectionManager::get('test'); - - $this->Articles = new ArticlesTable([ - 'alias' => 'Articles', - 'table' => 'articles', - 'connection' => $this->connection - ]); - } - -/** - * Release Article test model - * - * @return void - */ - public function tearDown() { - parent::tearDown(); - - unset($this->Articles); - } - -/** - * Test getWildcards() - * - * @return void - */ - public function testGetWildcards() { - $result = $this->Articles->getWildcards(); - $expected = ['any' => '*', 'one' => '?']; - $this->assertSame($expected, $result); - - $this->Articles->behaviors()->Searchable->config('wildcardAny', false); - $this->Articles->behaviors()->Searchable->config('wildcardOne', false); - $result = $this->Articles->getWildcards(); - $expected = ['any' => false, 'one' => false]; - $this->assertSame($expected, $result); - - $this->Articles->behaviors()->Searchable->config('wildcardAny', '%'); - $this->Articles->behaviors()->Searchable->config('wildcardOne', '_'); - $result = $this->Articles->getWildcards(); - $expected = ['any' => '%', 'one' => '_']; - $this->assertSame($expected, $result); - } - -/** - * Test 'value' filter type - * - * @return void - * @link http://github.com/CakeDC/Search/issues#issue/3 - */ - public function testValueCondition() { - $this->Articles->filterArgs = [ - ['name' => 'slug', 'type' => 'value']]; - $data = []; - $result = $this->Articles->find('searchable', $data); - $expected = $this->Articles->find('all'); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - $data = ['slug' => 'first_article']; - $result = $this->Articles->find('searchable', $data); - $expected = $this->Articles->find('all')->where(['Articles.slug' => 'first_article']); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - $this->Articles->filterArgs = [ - ['name' => 'fakeslug', 'type' => 'value', 'field' => 'Article2.slug']]; - $data = ['fakeslug' => 'first_article']; - $result = $this->Articles->find('searchable', $data); - $expected = $this->Articles->find('all')->where(['Article2.slug' => 'first_article']); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - // Testing http://github.com/CakeDC/Search/issues#issue/3 - $this->Articles->filterArgs = [ - ['name' => 'views', 'type' => 'value']]; - $data = ['views' => '0']; - $result = $this->Articles->find('searchable', $data); - $expected = $this->Articles->find('all')->where(['Articles.views' => 0]); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - $this->Articles->filterArgs = [ - ['name' => 'views', 'type' => 'value']]; - $data = ['views' => 0]; - $result = $this->Articles->find('searchable', $data); - $expected = $this->Articles->find('all')->where(['Articles.views' => 0]); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - $this->Articles->filterArgs = [ - ['name' => 'views', 'type' => 'value']]; - $data = ['views' => '']; - $result = $this->Articles->find('searchable', $data); - $expected = $this->Articles->find('all'); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - // Multiple fields + cross model searches - $this->Articles->filterArgs = [ - 'faketitle' => ['type' => 'value', 'field' => ['title', 'User.name']] - ]; - $data = ['faketitle' => 'First']; - $result = $this->Articles->find('searchable', $data); - $expected = $this->Articles->find('all')->where(['OR' => ['Articles.title' => 'First', 'User.name' => 'First']]); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - // Multiple select dropdown - $this->Articles->filterArgs = [ - 'fakesource' => ['type' => 'value'] - ]; - $data = ['fakesource' => [5, 9]]; - $result = $this->Articles->find('searchable', $data); - $expected = $this->Articles->find('all')->where(['Articles.fakesource' => [5, 9]]); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - } - -/** - * Test 'like' filter type - * - * @return void - */ - public function testLikeCondition() { - $this->Articles->filterArgs = [ - ['name' => 'title', 'type' => 'like']]; - - $data = []; - $result = $this->Articles->find('searchable', $data); - $expected = $this->Articles->find('all'); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - $data = ['title' => 'First']; - $result = $this->Articles->find('searchable', $data); - $expected = ['Articles.title LIKE' => '%First%']; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - $this->Articles->filterArgs = [ - ['name' => 'faketitle', 'type' => 'like', 'field' => 'Articles.title']]; - - $data = ['faketitle' => 'First']; - $result = $this->Articles->find('searchable', $data); - $expected = ['Articles.title LIKE' => '%First%']; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - // Wildcards should be treated as normal text - $this->Articles->filterArgs = [ - ['name' => 'faketitle', 'type' => 'like', 'field' => 'Articles.title'] - ]; - $data = ['faketitle' => '%First_']; - $result = $this->Articles->find('searchable', $data); - $expected = ['Articles.title LIKE' => '%\%First\_%']; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - // Working with like settings - $this->Articles->behaviors()->Searchable->config('like.before', false); - $result = $this->Articles->find('searchable', $data); - $expected = ['Articles.title LIKE' => '\%First\_%']; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - $this->Articles->behaviors()->Searchable->config('like.after', false); - $result = $this->Articles->find('searchable', $data); - $expected = ['Articles.title LIKE' => '\%First\_']; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - // Now custom like should be possible - $data = ['faketitle' => '*First?']; - $this->Articles->behaviors()->Searchable->config('like.after', false); - $result = $this->Articles->find('searchable', $data); - $expected = ['Articles.title LIKE' => '%First_']; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - $data = ['faketitle' => 'F?rst']; - $this->Articles->behaviors()->Searchable->config('like.before', true); - $this->Articles->behaviors()->Searchable->config('like.after', true); - $result = $this->Articles->find('searchable', $data); - $expected = ['Articles.title LIKE' => '%F_rst%']; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - $data = ['faketitle' => 'F*t']; - $this->Articles->behaviors()->Searchable->config('like.before', true); - $this->Articles->behaviors()->Searchable->config('like.after', true); - $result = $this->Articles->find('searchable', $data); - $expected = ['Articles.title LIKE' => '%F%t%']; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - // now we try the default wildcards % and _ - $data = ['faketitle' => '*First?']; - $this->Articles->behaviors()->Searchable->config('like.before', false); - $this->Articles->behaviors()->Searchable->config('like.after', false); - $this->Articles->behaviors()->Searchable->config('wildcardAny', '%'); - $this->Articles->behaviors()->Searchable->config('wildcardOne', '_'); - $result = $this->Articles->find('searchable', $data); - $expected = ['Articles.title LIKE' => '*First?']; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - // Now it is possible and makes sense to allow wildcards in between (custom wildcard use case) - $data = ['faketitle' => '%Fi_st_']; - $result = $this->Articles->find('searchable', $data); - $expected = ['Articles.title LIKE' => '%Fi_st_']; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - // Shortcut disable/enable like before/after - $data = ['faketitle' => '%First_']; - $this->Articles->behaviors()->Searchable->config('like', false); - $result = $this->Articles->find('searchable', $data); - $expected = ['Articles.title LIKE' => '%First_']; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - // Multiple OR fields per field - $this->Articles->filterArgs = [ - ['name' => 'faketitle', 'type' => 'like', 'field' => ['title', 'descr']] - ]; - - $data = ['faketitle' => 'First']; - $this->Articles->behaviors()->Searchable->config('like', true); - $result = $this->Articles->find('searchable', $data); - $expected = [ - 'OR' => [ - 'Articles.title LIKE' => '%First%', - 'Articles.descr LIKE' => '%First%' - ] - ]; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - // Set before => false dynamically - $this->Articles->filterArgs = [ - ['name' => 'faketitle', - 'type' => 'like', - 'field' => ['title', 'descr'], - 'before' => false - ] - ]; - $data = ['faketitle' => 'First']; - $result = $this->Articles->find('searchable', $data); - $expected = [ - 'OR' => [ - 'Articles.title LIKE' => 'First%', - 'Articles.descr LIKE' => 'First%' - ] - ]; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - // Manually define the before/after type - $this->Articles->filterArgs = [ - ['name' => 'faketitle', 'type' => 'like', 'field' => ['title'], - 'before' => '_', 'after' => '_'] - ]; - $data = ['faketitle' => 'First']; - $result = $this->Articles->find('searchable', $data); - $expected = ['Articles.title LIKE' => '_First_']; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - // Cross model searches + named keys (shorthand) - $this->Articles->belongsTo('Users'); - $this->Articles->filterArgs = [ - 'faketitle' => ['type' => 'like', 'field' => ['title', 'Users.name'], - 'before' => false, 'after' => true] - ]; - $data = ['faketitle' => 'First']; - $result = $this->Articles->find('searchable', $data); - $expected = ['OR' => ['Articles.title LIKE' => 'First%', 'Users.name LIKE' => 'First%']]; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - // With already existing or conditions + named keys (shorthand) - $this->Articles->filterArgs = [ - 'faketitle' => ['type' => 'like', 'field' => ['title', 'Users.name'], - 'before' => false, 'after' => true], - 'otherfaketitle' => ['type' => 'like', 'field' => ['descr', 'comment'], - 'before' => false, 'after' => true] - ]; - - $data = ['faketitle' => 'First', 'otherfaketitle' => 'Second']; - $result = $this->Articles->find('searchable', $data); - $expected = $this->Articles->find('all') - ->where(['OR' => [ - 'Articles.title LIKE' => 'First%', - 'Users.name LIKE' => 'First%' - ]]) - ->where([ - 'OR' => [ - 'Articles.descr LIKE' => 'Second%', - 'Articles.comment LIKE' => 'Second%'] - ]); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - // Wildcards and and/or connectors - $this->Articles->removeBehavior('Searchable'); - $this->Articles->addBehavior('Search.Searchable'); - $this->Articles->filterArgs = [ - ['name' => 'faketitle', 'type' => 'like', 'field' => 'Articles.title', - 'connectorAnd' => '+', 'connectorOr' => ',', 'before' => true, 'after' => true] - ]; - $data = ['faketitle' => 'First%+Second%, Third%']; - $result = $this->Articles->find('searchable', $data); - $expected = [ - 0 => [ - 'OR' => [ - ['AND' => [ - ['Articles.title LIKE' => '%First\%%'], - ['Articles.title LIKE' => '%Second\%%'], - ]], - ['AND' => [ - ['Articles.title LIKE' => '%Third\%%'] - ]], - ] - ] - ]; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - } - -/** - * Test 'query' filter type with one orConditions method - * - * Uses ``Article::orConditions()``. - * - * @return void - */ - public function testQueryOneOrConditions() { - $this->Articles->filterArgs = [ - ['name' => 'filter', 'type' => 'finder', 'finder' => 'orConditions']]; - - $data = []; - $result = $this->Articles->find('searchable', $data); - $expected = $this->Articles->find('all'); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - $data = ['filter' => 'ticl']; - $result = $this->Articles->find('searchable', $data); - $expected = ['OR' => [ - 'Articles.title LIKE' => '%ticl%', - 'Articles.body LIKE' => '%ticl%']]; - $expected = $this->Articles->find('all')->orWhere($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - } - -/** - * Test 'query' filter type with two orConditions methods - * - * Uses ``Article::orConditions()`` and ``Article::or2Conditions()``. - * - * @return void - */ - public function testQueryOrTwoOrConditions() { - $this->Articles->filterArgs = [ - ['name' => 'filter', 'type' => 'finder', 'finder' => 'orConditions'], - ['name' => 'filter2', 'type' => 'finder', 'finder' => 'or2Conditions']]; - - $data = []; - $result = $this->Articles->find('searchable', $data); - $expected = $this->Articles->find('all'); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - $data = ['filter' => 'ticl', 'filter2' => 'test']; - $result = $this->Articles->find('searchable', $data); - $expected = $this->Articles->find('all') - ->orWhere([ - 'OR' => [ - 'Articles.title LIKE' => '%ticl%', - 'Articles.body LIKE' => '%ticl%' - ] - ]) - ->orWhere([ - 'OR' => [ - 'Articles.field1 LIKE' => '%test%', - 'Articles.field2 LIKE' => '%test%' - ] - ]); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - } /** - * Test 'query' filter type with behavior condition method - * - * Uses ``FilterBehavior::FilterBehavior::mostFilterConditions()``. - * - * @return void - */ - public function testQueryWithBehaviorCondition() { - $this->Articles->addBehavior('Filter', [ - 'className' => 'Search\Test\TestClass\Model\Behavior\FilterBehavior' - ]); - $this->Articles->filterArgs = [ - ['name' => 'filter', 'type' => 'finder', 'finder' => 'mostFilterConditions'] - ]; - - $data = []; - $result = $this->Articles->find('searchable', $data); - $expected = $this->Articles->find('all'); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - - $data = ['filter' => 'views']; - $result = $this->Articles->find('searchable', $data); - $expected = ['Articles.views > 10']; - $expected = $this->Articles->find('all')->where($expected); - - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - } - -/** - * Test 'query' filter type with 'defaultValue' set - * - * @return void - */ - public function testDefaultValue() { - $this->Articles->filterArgs = [ - 'faketitle' => ['type' => 'value', 'defaultValue' => '100'] - ]; - - $data = []; - $result = $this->Articles->find('searchable', $data); - $expected = ['Articles.faketitle' => 100]; - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($expected, $result); - } - -/** - * Test passedArgs() - * - * @return void - */ - public function testPassedArgs() { - $this->Articles->filterArgs = [ - ['name' => 'slug', 'type' => 'value']]; - $data = ['slug' => 'first_article', 'filter' => 'myfilter']; - $result = $this->Articles->passedArgs($data); - $expected = ['slug' => 'first_article']; - $this->assertEquals($expected, $result); - } - -/** - * Test whether 'allowEmpty' will be respected - * - * @return void - */ - public function testAllowEmptyWithNullValues() { - // Author is just empty, created will be mapped against schema default (NULL) - // and slug omitted as its NULL already - $this->Articles->filterArgs = [ - 'title' => [ - 'name' => 'title', - 'type' => 'like', - 'field' => 'Articles.title', - 'allowEmpty' => true - ], - 'author' => [ - 'name' => 'author', - 'type' => 'value', - 'field' => 'Articles.author', - 'allowEmpty' => true - ], - 'created' => [ - 'name' => 'created', - 'type' => 'value', - 'field' => 'Articles.created', - 'allowEmpty' => true - ], - 'slug' => [ - 'name' => 'slug', - 'type' => 'value', - 'field' => 'Articles.slug', - 'allowEmpty' => true - ], - ]; - $data = ['title' => 'first', 'author' => '', 'created' => '', 'slug' => null]; - $expected = [ - 'Articles.title LIKE' => '%first%', - 'Articles.author' => '', - 'Articles.created' => null, - ]; - $result = $this->Articles->find('searchable', $data); - $expected = $this->Articles->find('all')->where($expected); - $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); - } - -/** - * Gets the 'where' part of a query - * - * @param Query $query Query to extract the part from - * - * @return mixed + * SearchableTestCase */ - protected function _getWhere(Query $query) { - $query->traverse(function ($part) use (&$where) { - $where = $part; - }, ['where']); - return $where; - } +class SearchableBehaviorTest extends TestCase +{ + + /** + * Article test model + * + * @var ArticlesTable + */ + public $Articles; + + /** + * Color test model + * + * @var ColorsTable + */ + public $Colors; + + /** + * Load relevant fixtures + * + * @var array + */ + public $fixtures = [ + 'plugin.search.articles', + 'plugin.search.tags', + 'plugin.search.tagged', + 'plugin.search.colors', + //'core.users' + ]; + + /** + * Load Article test model + * + * @return void + */ + public function setUp() + { + parent::setUp(); + $this->connection = ConnectionManager::get('test'); + + $this->Articles = new ArticlesTable([ + 'alias' => 'Articles', + 'table' => 'articles', + 'connection' => $this->connection + ]); + + $this->Colors = new ColorsTable([ + 'alias' => 'Colors', + 'table' => 'colors', + 'connection' => $this->connection + ]); + } + + /** + * Release Article test model + * + * @return void + */ + public function tearDown() + { + parent::tearDown(); + + unset($this->Articles); + } + + /** + * Test getWildcards() + * + * @return void + */ + public function testGetWildcards() + { + $result = $this->Articles->getWildcards(); + $expected = ['any' => '*', 'one' => '?']; + $this->assertSame($expected, $result); + + $this->Articles->behaviors()->Searchable->config('wildcardAny', false); + $this->Articles->behaviors()->Searchable->config('wildcardOne', false); + $result = $this->Articles->getWildcards(); + $expected = ['any' => false, 'one' => false]; + $this->assertSame($expected, $result); + + $this->Articles->behaviors()->Searchable->config('wildcardAny', '%'); + $this->Articles->behaviors()->Searchable->config('wildcardOne', '_'); + $result = $this->Articles->getWildcards(); + $expected = ['any' => '%', 'one' => '_']; + $this->assertSame($expected, $result); + } + + /** + * Test 'value' filter type + * + * @return void + * @link http://github.com/CakeDC/Search/issues#issue/3 + */ + public function testValueCondition() + { + $this->Articles->filterArgs = [ + ['name' => 'slug', 'type' => 'value']]; + $data = []; + $result = $this->Articles->find('searchable', $data); + $expected = $this->Articles->find('all'); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + $data = ['slug' => 'first_article']; + $result = $this->Articles->find('searchable', $data); + $expected = $this->Articles->find('all')->where(['Articles.slug' => 'first_article']); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + $this->Articles->filterArgs = [ + ['name' => 'fakeslug', 'type' => 'value', 'field' => 'Article2.slug']]; + $data = ['fakeslug' => 'first_article']; + $result = $this->Articles->find('searchable', $data); + $expected = $this->Articles->find('all')->where(['Article2.slug' => 'first_article']); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // Testing http://github.com/CakeDC/Search/issues#issue/3 + $this->Articles->filterArgs = [ + ['name' => 'views', 'type' => 'value']]; + $data = ['views' => '0']; + $result = $this->Articles->find('searchable', $data); + $expected = $this->Articles->find('all')->where(['Articles.views' => 0]); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + $this->Articles->filterArgs = [ + ['name' => 'views', 'type' => 'value']]; + $data = ['views' => 0]; + $result = $this->Articles->find('searchable', $data); + $expected = $this->Articles->find('all')->where(['Articles.views' => 0]); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + $this->Articles->filterArgs = [ + ['name' => 'views', 'type' => 'value']]; + $data = ['views' => '']; + $result = $this->Articles->find('searchable', $data); + $expected = $this->Articles->find('all'); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // Multiple fields + cross model searches + $this->Articles->filterArgs = [ + 'faketitle' => ['type' => 'value', 'field' => ['title', 'User.name']] + ]; + $data = ['faketitle' => 'First']; + $result = $this->Articles->find('searchable', $data); + $expected = $this->Articles->find('all')->where(['OR' => ['Articles.title' => 'First', 'User.name' => 'First']]); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // Multiple select dropdown + $this->Articles->filterArgs = [ + 'fakesource' => ['type' => 'value'] + ]; + $data = ['fakesource' => [5, 9]]; + $result = $this->Articles->find('searchable', $data); + $expected = $this->Articles->find('all')->where(['Articles.fakesource' => [5, 9]]); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + } + + /** + * Test 'like' filter type + * + * @return void + */ + public function testLikeCondition() + { + $this->Articles->filterArgs = [ + ['name' => 'title', 'type' => 'like']]; + + $data = []; + $result = $this->Articles->find('searchable', $data); + $expected = $this->Articles->find('all'); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + $data = ['title' => 'First']; + $result = $this->Articles->find('searchable', $data); + $expected = ['Articles.title LIKE' => '%First%']; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + $this->Articles->filterArgs = [ + ['name' => 'faketitle', 'type' => 'like', 'field' => 'Articles.title']]; + + $data = ['faketitle' => 'First']; + $result = $this->Articles->find('searchable', $data); + $expected = ['Articles.title LIKE' => '%First%']; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // Wildcards should be treated as normal text + $this->Articles->filterArgs = [ + ['name' => 'faketitle', 'type' => 'like', 'field' => 'Articles.title'] + ]; + $data = ['faketitle' => '%First_']; + $result = $this->Articles->find('searchable', $data); + $expected = ['Articles.title LIKE' => '%\%First\_%']; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // Working with like settings + $this->Articles->behaviors()->Searchable->config('like.before', false); + $result = $this->Articles->find('searchable', $data); + $expected = ['Articles.title LIKE' => '\%First\_%']; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + $this->Articles->behaviors()->Searchable->config('like.after', false); + $result = $this->Articles->find('searchable', $data); + $expected = ['Articles.title LIKE' => '\%First\_']; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // Now custom like should be possible + $data = ['faketitle' => '*First?']; + $this->Articles->behaviors()->Searchable->config('like.after', false); + $result = $this->Articles->find('searchable', $data); + $expected = ['Articles.title LIKE' => '%First_']; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + $data = ['faketitle' => 'F?rst']; + $this->Articles->behaviors()->Searchable->config('like.before', true); + $this->Articles->behaviors()->Searchable->config('like.after', true); + $result = $this->Articles->find('searchable', $data); + $expected = ['Articles.title LIKE' => '%F_rst%']; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + $data = ['faketitle' => 'F*t']; + $this->Articles->behaviors()->Searchable->config('like.before', true); + $this->Articles->behaviors()->Searchable->config('like.after', true); + $result = $this->Articles->find('searchable', $data); + $expected = ['Articles.title LIKE' => '%F%t%']; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // now we try the default wildcards % and _ + $data = ['faketitle' => '*First?']; + $this->Articles->behaviors()->Searchable->config('like.before', false); + $this->Articles->behaviors()->Searchable->config('like.after', false); + $this->Articles->behaviors()->Searchable->config('wildcardAny', '%'); + $this->Articles->behaviors()->Searchable->config('wildcardOne', '_'); + $result = $this->Articles->find('searchable', $data); + $expected = ['Articles.title LIKE' => '*First?']; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // Now it is possible and makes sense to allow wildcards in between (custom wildcard use case) + $data = ['faketitle' => '%Fi_st_']; + $result = $this->Articles->find('searchable', $data); + $expected = ['Articles.title LIKE' => '%Fi_st_']; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // Shortcut disable/enable like before/after + $data = ['faketitle' => '%First_']; + $this->Articles->behaviors()->Searchable->config('like', false); + $result = $this->Articles->find('searchable', $data); + $expected = ['Articles.title LIKE' => '%First_']; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // Multiple OR fields per field + $this->Articles->filterArgs = [ + ['name' => 'faketitle', 'type' => 'like', 'field' => ['title', 'descr']] + ]; + + $data = ['faketitle' => 'First']; + $this->Articles->behaviors()->Searchable->config('like', true); + $result = $this->Articles->find('searchable', $data); + $expected = [ + 'OR' => [ + 'Articles.title LIKE' => '%First%', + 'Articles.descr LIKE' => '%First%' + ] + ]; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // Set before => false dynamically + $this->Articles->filterArgs = [ + ['name' => 'faketitle', + 'type' => 'like', + 'field' => ['title', 'descr'], + 'before' => false + ] + ]; + $data = ['faketitle' => 'First']; + $result = $this->Articles->find('searchable', $data); + $expected = [ + 'OR' => [ + 'Articles.title LIKE' => 'First%', + 'Articles.descr LIKE' => 'First%' + ] + ]; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // Manually define the before/after type + $this->Articles->filterArgs = [ + ['name' => 'faketitle', 'type' => 'like', 'field' => ['title'], + 'before' => '_', 'after' => '_'] + ]; + $data = ['faketitle' => 'First']; + $result = $this->Articles->find('searchable', $data); + $expected = ['Articles.title LIKE' => '_First_']; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // Cross model searches + named keys (shorthand) + $this->Articles->belongsTo('Users'); + $this->Articles->filterArgs = [ + 'faketitle' => ['type' => 'like', 'field' => ['title', 'Users.name'], + 'before' => false, 'after' => true] + ]; + $data = ['faketitle' => 'First']; + $result = $this->Articles->find('searchable', $data); + $expected = ['OR' => ['Articles.title LIKE' => 'First%', 'Users.name LIKE' => 'First%']]; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // With already existing or conditions + named keys (shorthand) + $this->Articles->filterArgs = [ + 'faketitle' => ['type' => 'like', 'field' => ['title', 'Users.name'], + 'before' => false, 'after' => true], + 'otherfaketitle' => ['type' => 'like', 'field' => ['descr', 'comment'], + 'before' => false, 'after' => true] + ]; + + $data = ['faketitle' => 'First', 'otherfaketitle' => 'Second']; + $result = $this->Articles->find('searchable', $data); + $expected = $this->Articles->find('all') + ->where(['OR' => [ + 'Articles.title LIKE' => 'First%', + 'Users.name LIKE' => 'First%' + ]]) + ->where([ + 'OR' => [ + 'Articles.descr LIKE' => 'Second%', + 'Articles.comment LIKE' => 'Second%'] + ]); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // Wildcards and and/or connectors + $this->Articles->removeBehavior('Searchable'); + $this->Articles->addBehavior('Search.Searchable'); + $this->Articles->filterArgs = [ + ['name' => 'faketitle', 'type' => 'like', 'field' => 'Articles.title', + 'connectorAnd' => '+', 'connectorOr' => ',', 'before' => true, 'after' => true] + ]; + $data = ['faketitle' => 'First%+Second%, Third%']; + $result = $this->Articles->find('searchable', $data); + $expected = [ + 0 => [ + 'OR' => [ + ['AND' => [ + ['Articles.title LIKE' => '%First\%%'], + ['Articles.title LIKE' => '%Second\%%'], + ]], + ['AND' => [ + ['Articles.title LIKE' => '%Third\%%'] + ]], + ] + ] + ]; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + } + + /** + * Test 'query' filter type with one orConditions method + * + * Uses ``Article::orConditions()``. + * + * @return void + */ + public function testQueryOneOrConditions() + { + $this->Articles->filterArgs = [ + ['name' => 'filter', 'type' => 'finder', 'finder' => 'orConditions']]; + + $data = []; + $result = $this->Articles->find('searchable', $data); + $expected = $this->Articles->find('all'); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + $data = ['filter' => 'ticl']; + $result = $this->Articles->find('searchable', $data); + $expected = ['OR' => [ + 'Articles.title LIKE' => '%ticl%', + 'Articles.body LIKE' => '%ticl%']]; + $expected = $this->Articles->find('all')->orWhere($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + } + + /** + * Test 'query' filter type with two orConditions methods + * + * Uses ``Article::orConditions()`` and ``Article::or2Conditions()``. + * + * @return void + */ + public function testQueryOrTwoOrConditions() + { + $this->Articles->filterArgs = [ + ['name' => 'filter', 'type' => 'finder', 'finder' => 'orConditions'], + ['name' => 'filter2', 'type' => 'finder', 'finder' => 'or2Conditions']]; + + $data = []; + $result = $this->Articles->find('searchable', $data); + $expected = $this->Articles->find('all'); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + $data = ['filter' => 'ticl', 'filter2' => 'test']; + $result = $this->Articles->find('searchable', $data); + $expected = $this->Articles->find('all') + ->orWhere([ + 'OR' => [ + 'Articles.title LIKE' => '%ticl%', + 'Articles.body LIKE' => '%ticl%' + ] + ]) + ->orWhere([ + 'OR' => [ + 'Articles.field1 LIKE' => '%test%', + 'Articles.field2 LIKE' => '%test%' + ] + ]); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + } + + /** + * Test 'query' filter type with behavior condition method + * + * Uses ``FilterBehavior::FilterBehavior::mostFilterConditions()``. + * + * @return void + */ + public function testQueryWithBehaviorCondition() + { + $this->Articles->addBehavior('Filter', [ + 'className' => 'Search\Test\TestClass\Model\Behavior\FilterBehavior' + ]); + $this->Articles->filterArgs = [ + ['name' => 'filter', 'type' => 'finder', 'finder' => 'mostFilterConditions'] + ]; + + $data = []; + $result = $this->Articles->find('searchable', $data); + $expected = $this->Articles->find('all'); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + $data = ['filter' => 'views']; + $result = $this->Articles->find('searchable', $data); + $expected = ['Articles.views > 10']; + $expected = $this->Articles->find('all')->where($expected); + + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + } + + /** + * Test 'query' filter type with 'defaultValue' set + * + * @return void + */ + public function testDefaultValue() + { + $this->Articles->filterArgs = [ + 'faketitle' => ['type' => 'value', 'defaultValue' => '100'] + ]; + + $data = []; + $result = $this->Articles->find('searchable', $data); + $expected = ['Articles.faketitle' => 100]; + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($expected, $result); + } + + /** + * Test passedArgs() + * + * @return void + */ + public function testPassedArgs() + { + $this->Articles->filterArgs = [ + ['name' => 'slug', 'type' => 'value']]; + $data = ['slug' => 'first_article', 'filter' => 'myfilter']; + $result = $this->Articles->passedArgs($data); + $expected = ['slug' => 'first_article']; + $this->assertEquals($expected, $result); + } + + /** + * Test whether 'allowEmpty' will be respected + * + * @return void + */ + public function testAllowEmptyWithNullValues() + { + // Author is just empty, created will be mapped against schema default (NULL) + // and slug omitted as its NULL already + $this->Articles->filterArgs = [ + 'title' => [ + 'name' => 'title', + 'type' => 'like', + 'field' => 'Articles.title', + 'allowEmpty' => true + ], + 'author' => [ + 'name' => 'author', + 'type' => 'value', + 'field' => 'Articles.author', + 'allowEmpty' => true + ], + 'created' => [ + 'name' => 'created', + 'type' => 'value', + 'field' => 'Articles.created', + 'allowEmpty' => true + ], + 'slug' => [ + 'name' => 'slug', + 'type' => 'value', + 'field' => 'Articles.slug', + 'allowEmpty' => true + ], + ]; + $data = ['title' => 'first', 'author' => '', 'created' => '', 'slug' => null]; + $expected = [ + 'Articles.title LIKE' => '%first%', + 'Articles.author' => '', + 'Articles.created' => null, + ]; + $result = $this->Articles->find('searchable', $data); + $expected = $this->Articles->find('all')->where($expected); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + } + + + + /** + * Test translated searches + * + * @return void + */ + public function testTranslatedDefaultLocale() + { + // Sets the locale to the default, to test the table without translations + I18n::locale(I18n::defaultLocale()); + + // Test like + $this->Colors->filterArgs = ['name' => ['type' => 'like']]; + $result = $this->Colors->find('searchable', ['name' => 'Red']); + $expected = $this->Colors->find('all')->where(['Colors.name LIKE' => '%Red%']); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // Test value + $this->Colors->filterArgs = ['name' => ['type' => 'value']]; + $result = $this->Colors->find('searchable', ['name' => 'Red']); + $expected = $this->Colors->find('all')->where(['Colors.name' => 'Red']); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + + // Tests for the other locales searches + I18n::locale('testing-locale'); + + // Test like + $this->Colors->filterArgs = ['name' => ['type' => 'like']]; + $result = $this->Colors->find('searchable', ['name' => 'Red']); + $expected = $this->Colors->find('all')->where(['Colors_name_translation.content LIKE' => '%Red%']); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + + // Test value + $this->Colors->filterArgs = ['name' => ['type' => 'value']]; + $result = $this->Colors->find('searchable', ['name' => 'Red']); + $expected = $this->Colors->find('all')->where(['Colors_name_translation.content' => 'Red']); + $this->assertEquals($this->_getWhere($expected), $this->_getWhere($result)); + } + + + /** + * Gets the 'where' part of a query + * + * @param Query $query Query to extract the part from + * + * @return mixed + */ + protected function _getWhere(Query $query) + { + $query->traverse(function ($part) use (&$where) { + $where = $part; + }, ['where']); + return $where; + } }