diff --git a/search_api_db/search_api_db_defaults/config/optional/views.view.search_content.yml b/search_api_db/search_api_db_defaults/config/optional/views.view.search_content.yml index 252e71b..9404bd0 100644 --- a/search_api_db/search_api_db_defaults/config/optional/views.view.search_content.yml +++ b/search_api_db/search_api_db_defaults/config/optional/views.view.search_content.yml @@ -72,66 +72,6 @@ display: 'entity:node': article: search_result page: search_result - fields: - search_api_id: - table: search_api_index_default_index - field: search_api_id - id: search_api_id - entity_type: null - entity_field: null - plugin_id: numeric - relationship: none - group_type: group - admin_label: '' - label: '' - exclude: false - alter: - alter_text: false - text: '' - make_link: false - path: '' - absolute: false - external: false - replace_spaces: false - path_case: none - trim_whitespace: false - alt: '' - rel: '' - link_class: '' - prefix: '' - suffix: '' - target: '' - nl2br: false - max_length: 0 - word_boundary: true - ellipsis: true - more_link: false - more_link_text: '' - more_link_path: '' - strip_tags: false - trim: false - preserve_tags: '' - html: false - element_type: '' - element_class: '' - element_label_type: '' - element_label_class: '' - element_label_colon: true - element_wrapper_type: '' - element_wrapper_class: '' - element_default_classes: true - empty: '' - hide_empty: false - empty_zero: false - hide_alter_empty: true - set_precision: false - precision: 0 - decimal: . - separator: ',' - format_plural: false - format_plural_string: "1\x03@count" - prefix: '' - suffix: '' filters: search_api_fulltext: id: search_api_fulltext diff --git a/src/Plugin/views/filter/SearchApiFilterEntityBase.php b/src/Plugin/views/filter/SearchApiFilterEntityBase.php index b6e40f8..928041e 100644 --- a/src/Plugin/views/filter/SearchApiFilterEntityBase.php +++ b/src/Plugin/views/filter/SearchApiFilterEntityBase.php @@ -16,6 +16,13 @@ use Drupal\Core\Form\FormStateInterface; abstract class SearchApiFilterEntityBase extends SearchApiFilterString { /** + * Where the $query object will reside: + * + * @var \Drupal\search_api\Plugin\views\query\SearchApiQuery + */ + public $query = NULL; + + /** * If exposed form input was successfully validated, the entered entity IDs. * * @var array diff --git a/src/Plugin/views/filter/SearchApiFilterNumeric.php b/src/Plugin/views/filter/SearchApiFilterNumeric.php index e5b6a37..c6b5f6f 100644 --- a/src/Plugin/views/filter/SearchApiFilterNumeric.php +++ b/src/Plugin/views/filter/SearchApiFilterNumeric.php @@ -35,7 +35,7 @@ class SearchApiFilterNumeric extends NumericFilter { * {@inheritdoc} */ protected function opEmpty($field) { - $this->getQuery()->condition($this->realField, NULL, $this->operator == 'empty' ? '=' : '<>', $this->options['group']); + $this->getQuery()->addCondition($this->realField, NULL, $this->operator == 'empty' ? '=' : '<>', $this->options['group']); } } diff --git a/src/Plugin/views/query/SearchApiQuery.php b/src/Plugin/views/query/SearchApiQuery.php index 7759c6a..da6e85f 100644 --- a/src/Plugin/views/query/SearchApiQuery.php +++ b/src/Plugin/views/query/SearchApiQuery.php @@ -271,7 +271,7 @@ class SearchApiQuery extends QueryPluginBase { } // Setup the nested filter structure for this query. - if (!empty($this->where)) { + if (!empty($this->conditions)) { // If the different groups are combined with the OR operator, we have to // add a new OR filter to the query to which the filters for the groups // will be added. @@ -283,8 +283,8 @@ class SearchApiQuery extends QueryPluginBase { $base = $this->query; } // Add a nested filter for each filter group, with its set conjunction. - foreach ($this->where as $group_id => $group) { - if (!empty($group['conditions']) || !empty($group['filters'])) { + foreach ($this->conditions as $group_id => $group) { + if (!empty($group['conditions']) || !empty($group['condition_groups'])) { $group += array('type' => 'AND'); // For filters without a group, we want to always add them directly to // the query. @@ -295,8 +295,8 @@ class SearchApiQuery extends QueryPluginBase { $conditions->addCondition($field, $value, $operator); } } - if (!empty($group['filters'])) { - foreach ($group['filters'] as $nested_conditions) { + if (!empty($group['condition_groups'])) { + foreach ($group['condition_groups'] as $nested_conditions) { $conditions->addConditionGroup($nested_conditions); } } @@ -681,7 +681,7 @@ class SearchApiQuery extends QueryPluginBase { if (empty($group)) { $group = 0; } - $this->conditions[$group]['filters'][] = $condition_group; + $this->conditions[$group]['condition_groups'][] = $condition_group; } return $this; } @@ -776,7 +776,7 @@ class SearchApiQuery extends QueryPluginBase { $field = $this->transformDbCondition($field); } if ($field instanceof ConditionGroupInterface) { - $this->conditions[$group]['filters'][] = $field; + $this->conditions[$group]['condition_groups'][] = $field; } elseif (!$this->shouldAbort()) { // We only need to abort if that wasn't done by transformDbCondition() diff --git a/src/Query/Query.php b/src/Query/Query.php index eb22371..c8d0bcb 100644 --- a/src/Query/Query.php +++ b/src/Query/Query.php @@ -476,7 +476,7 @@ class Query implements QueryInterface { } // @todo Fix for entities contained in options (which might kill // var_export() due to circular references). - $ret .= 'Options: ' . str_replace("\n", "\n ", var_export($this->options, TRUE)) . "\n"; +// $ret .= 'Options: ' . str_replace("\n", "\n ", var_export($this->options, TRUE)) . "\n"; return $ret; } diff --git a/src/Tests/ViewsTest.php b/src/Tests/ViewsTest.php index e84e258..e89c56c 100644 --- a/src/Tests/ViewsTest.php +++ b/src/Tests/ViewsTest.php @@ -7,6 +7,7 @@ namespace Drupal\search_api\Tests; +use Drupal\Component\Utility\Html; use Drupal\search_api\Entity\Index; /** @@ -39,36 +40,145 @@ class ViewsTest extends WebTestBase { parent::setUp(); $this->setUpExampleStructure(); - - \Drupal::getContainer()->get('search_api.index_task_manager')->addItemsAll(Index::load($this->indexId)); + \Drupal::getContainer() + ->get('search_api.index_task_manager') + ->addItemsAll(Index::load($this->indexId)); + $this->insertExampleContent(); + $this->indexItems($this->indexId); } /** - * Tests a view with a fulltext search field. + * Tests a view with exposed filters. */ - public function testFulltextSearch() { - $this->insertExampleContent(); - $this->assertEqual($this->indexItems($this->indexId), 5, '5 items were indexed.'); + public function testView() { + $this->checkResults(array(), array_keys($this->entities), 'Unfiltered search'); - $this->drupalGet('search-api-test-fulltext'); - // By default, the view should show all entities. - $this->assertText('Displaying 5 search results', 'The search view displays the correct number of results.'); - foreach ($this->entities as $id => $entity) { - $this->assertText($entity->label(), "Entity #$id found in the results."); - } + $this->checkResults(array('search_api_fulltext' => 'foobar'), array(3), 'Search for a single word'); + $this->checkResults(array('search_api_fulltext' => 'foo test'), array(1, 2, 4), 'Search for multiple words'); + $query = array( + 'search_api_fulltext' => 'foo test', + 'search_api_fulltext_op' => 'or', + ); + $this->checkResults($query, array(1, 2, 3, 4, 5), 'OR search for multiple words'); + $query = array( + 'search_api_fulltext' => 'foobar', + 'search_api_fulltext_op' => 'not', + ); + $this->checkResults($query, array(1, 2, 4, 5), 'Negated search'); + $query = array( + 'search_api_fulltext' => 'foo test', + 'search_api_fulltext_op' => 'not', + ); + $this->checkResults($query, array(), 'Negated search for multiple words'); + $query = array( + 'search_api_fulltext' => 'fo', + ); + $label = 'Search for short word'; + $this->checkResults($query, array(), $label); + $this->assertText('You must include at least one positive keyword with 3 characters or more', "$label displayed the correct warning."); + $query = array( + 'search_api_fulltext' => 'foo to test', + ); + $label = 'Fulltext search including short word'; + $this->checkResults($query, array(1, 2, 4), $label); + $this->assertNoText('You must include at least one positive keyword with 3 characters or more', "$label didn't display a warning."); - // Search for something. - $this->drupalGet('search-api-test-fulltext', array('query' => array('search_api_fulltext' => 'foobar'))); + $this->checkResults(array('id[value]' => 2), array(2), 'Search with ID filter'); + // @todo Enable "between" again. See #2624870. +// $query = array( +// 'id[min]' => 2, +// 'id[max]' => 4, +// 'id_op' => 'between', +// ); +// $this->checkResults($query, array(2, 3, 4), 'Search with ID "in between" filter'); + $query = array( + 'id[value]' => 2, + 'id_op' => '>', + ); + $this->checkResults($query, array(3, 4, 5), 'Search with ID "greater than" filter'); + $query = array( + 'id[value]' => 2, + 'id_op' => '!=', + ); + $this->checkResults($query, array(1, 3, 4, 5), 'Search with ID "not equal" filter'); + $query = array( + 'id_op' => 'empty', + ); + $this->checkResults($query, array(), 'Search with ID "empty" filter'); + $query = array( + 'id_op' => 'not empty', + ); + $this->checkResults($query, array(1, 2, 3, 4, 5), 'Search with ID "not empty" filter'); + + $this->checkResults(array('keywords[value]' => 'apple'), array(2, 4), 'Search with Keywords filter'); + // @todo Enable "between" again. See #2624870. +// $query = array( +// 'keywords[min]' => 'aardvark', +// 'keywords[max]' => 'calypso', +// 'keywords_op' => 'between', +// ); +// $this->checkResults($query, array(2, 4, 5), 'Search with Keywords "in between" filter'); + $query = array( + 'keywords[value]' => 'radish', + 'keywords_op' => '>=', + ); + $this->checkResults($query, array(1, 4, 5), 'Search with Keywords "greater than or equal" filter'); + $query = array( + 'keywords[value]' => 'orange', + 'keywords_op' => '!=', + ); + $this->checkResults($query, array(3, 4), 'Search with Keywords "not equal" filter'); + $query = array( + 'keywords_op' => 'empty', + ); + $this->checkResults($query, array(3), 'Search with Keywords "empty" filter'); + $query = array( + 'keywords_op' => 'not empty', + ); + $this->checkResults($query, array(1, 2, 4, 5), 'Search with Keywords "not empty" filter'); + + $query = array( + 'search_api_fulltext' => 'foo to test', + 'id[value]' => 2, + 'id_op' => '>', + 'keywords_op' => 'not empty', + ); + $this->checkResults($query, array(4), 'Search with multiple filters'); + } - // Now it should only find one entity. - $this->assertText('Displaying 1 search results', 'The search view displays the correct number of results.'); - foreach ($this->entities as $id => $entity) { - if ($id == 3) { - $this->assertText($entity->label(), "Entity #$id found in the results."); + /** + * Checks the Views results for a certain set of parameters. + * + * @param array $query + * The GET parameters to set for the view. + * @param int[]|null $expected_results + * (optional) The IDs of the expected results; or NULL to skip checking the + * results. + * @param string $label + * (optional) A label for this search, to include in assert messages. + */ + protected function checkResults(array $query, array $expected_results = NULL, $label = 'Search') { + $this->drupalGet('search-api-test-fulltext', array('query' => $query)); + + if (isset($expected_results)) { + $count = count($expected_results); + $count_assert_message = "$label returned correct number of results."; + if ($count) { + $this->assertText("Displaying $count search results", $count_assert_message); } else { - $this->assertNoText($entity->label(), "Entity #$id not found in the results."); + $this->assertNoText('search results', $count_assert_message); + } + + $expected_results = array_combine($expected_results, $expected_results); + $actual_results = array(); + foreach ($this->entities as $id => $entity) { + $entity_label = Html::escape($entity->label()); + if (strpos($this->getRawContent(), ">$entity_label<") !== FALSE) { + $actual_results[$id] = $id; + } } + $this->assertEqual($expected_results, $actual_results, "$label returned correct results."); } } diff --git a/tests/search_api_test_views/config/install/views.view.search_api_test_view.yml b/tests/search_api_test_views/config/install/views.view.search_api_test_view.yml new file mode 100644 index 0000000..150de86 --- /dev/null +++ b/tests/search_api_test_views/config/install/views.view.search_api_test_view.yml @@ -0,0 +1,189 @@ +base_field: search_api_id +base_table: search_api_index_database_search_index +core: 8.x +description: '' +status: true +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + options: { } + cache: + type: none + options: { } + query: + type: search_api_query + options: + search_api_bypass_access: false + entity_access: false + parse_mode: terms + exposed_form: + type: basic + options: + submit_button: Search + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 20, 40, 60' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: '‹ previous' + next: 'next ›' + first: '« first' + last: 'last »' + quantity: 9 + style: + type: default + row: + type: search_api + options: + view_modes: + bundle: + 'article': default + 'page': default + datasource: + 'entity:entity_test': default + filters: + search_api_fulltext: + id: search_api_fulltext + table: search_api_index_database_search_index + field: search_api_fulltext + relationship: none + group_type: group + admin_label: '' + operator: and + value: '' + group: 1 + exposed: true + expose: + operator_id: search_api_fulltext_op + label: '' + description: '' + use_operator: true + operator: search_api_fulltext_op + identifier: search_api_fulltext + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + min_length: 3 + fields: { } + plugin_id: search_api_fulltext + id: + plugin_id: search_api_numeric + id: id + table: search_api_index_database_search_index + field: id + relationship: none + admin_label: '' + operator: '=' + group: 1 + exposed: true + expose: + operator_id: id_op + label: '' + description: '' + use_operator: true + operator: id_op + identifier: id + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + keywords: + plugin_id: search_api_string + id: keywords + table: search_api_index_database_search_index + field: keywords + relationship: none + admin_label: '' + operator: '=' + group: 1 + exposed: true + expose: + operator_id: keywords_op + label: '' + description: '' + use_operator: true + operator: keywords_op + identifier: keywords + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + sorts: { } + title: 'Fulltext test index' + header: + result: + id: result + table: views + field: result + relationship: none + group_type: group + admin_label: '' + content: 'Displaying @total search results' + plugin_id: result + footer: { } + empty: { } + relationships: { } + arguments: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + path: search-api-test-fulltext +label: 'Search API Test Fulltext search view' +module: views +id: search_api_test_view +tag: '' +langcode: en +dependencies: + module: + - search_api + - search_api_test_views