Skip to content
Commits on Source (9)
...@@ -4,9 +4,7 @@ CONTENTS OF THIS FILE ...@@ -4,9 +4,7 @@ CONTENTS OF THIS FILE
* Requirements * Requirements
* Installation * Installation
* Configuration * Configuration
* Troubleshooting
* FAQ * FAQ
* Maintainers
INTRODUCTION INTRODUCTION
------------ ------------
...@@ -14,8 +12,9 @@ Todo ...@@ -14,8 +12,9 @@ Todo
REQUIREMENTS REQUIREMENTS
------------ ------------
No other modules required, though the module is useless without an implementing No other modules required, we're supporting drupal core as a source for creating
module like search api. facets. Though we recommend using Search API, as that integration is better
tested.
INSTALLATION INSTALLATION
------------ ------------
...@@ -25,17 +24,14 @@ INSTALLATION ...@@ -25,17 +24,14 @@ INSTALLATION
CONFIGURATION CONFIGURATION
------------- -------------
Todo Before adding a facet, there should be a facet source. Facet sources can be:
- Drupal core's search.
- A view based on a Search API index with a page display.
- A page from the search_api_page module.
TROUBLESHOOTING After adding one of those, you can add a facet on the facets configuration page:
--------------- /admin/config/search/facets
Todo
FAQ FAQ
--- ---
Todo Todo
MAINTAINERS
-----------
Current maintainers:
* Todo
...@@ -29,6 +29,9 @@ facets.facet.*: ...@@ -29,6 +29,9 @@ facets.facet.*:
query_operator: query_operator:
type: string type: string
label: 'Query Operator' label: 'Query Operator'
exclude:
type: boolean
label: 'Exclude'
widget: widget:
type: string type: string
label: 'Widget identifier' label: 'Widget identifier'
......
...@@ -4,3 +4,6 @@ description: 'Faceted search interfaces that can be used on Search API searchers ...@@ -4,3 +4,6 @@ description: 'Faceted search interfaces that can be used on Search API searchers
core: 8.x core: 8.x
package: Search package: Search
configure: facets.overview configure: facets.overview
test_dependencies:
- search_api:search_api
- drupal:views
...@@ -44,6 +44,7 @@ use Drupal\facets\FacetInterface; ...@@ -44,6 +44,7 @@ use Drupal\facets\FacetInterface;
* "widget", * "widget",
* "widget_configs", * "widget_configs",
* "query_operator", * "query_operator",
* "exclude",
* "only_visible_when_facet_source_is_visible", * "only_visible_when_facet_source_is_visible",
* "processor_configs", * "processor_configs",
* "empty_behavior", * "empty_behavior",
...@@ -109,6 +110,13 @@ class Facet extends ConfigEntityBase implements FacetInterface { ...@@ -109,6 +110,13 @@ class Facet extends ConfigEntityBase implements FacetInterface {
*/ */
protected $query_operator; protected $query_operator;
/**
* A boolean flag indicating if search should exclude selected facets.
*
* @var bool
*/
protected $exclude;
/** /**
* The field identifier. * The field identifier.
* *
...@@ -363,6 +371,20 @@ class Facet extends ConfigEntityBase implements FacetInterface { ...@@ -363,6 +371,20 @@ class Facet extends ConfigEntityBase implements FacetInterface {
return $this->query_operator ?: 'OR'; return $this->query_operator ?: 'OR';
} }
/**
* {@inheritdoc}
*/
public function setExclude($exclude) {
return $this->exclude = $exclude;
}
/**
* {@inheritdoc}
*/
public function getExclude() {
return $this->exclude;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
......
...@@ -164,6 +164,18 @@ interface FacetInterface extends ConfigEntityInterface { ...@@ -164,6 +164,18 @@ interface FacetInterface extends ConfigEntityInterface {
*/ */
public function getQueryOperator(); public function getQueryOperator();
/**
* Returns the value of the exclude boolean.
*
* This will return true when the current facet's value should be exclusive
* from the search rather than inclusive.
* When this returns TRUE, the operator will be "<>" instead of "=".
*
* @return bool
* A boolean flag indicating if search should exlude selected facets
*/
public function getExclude();
/** /**
* Returns the plugin name for the url processor. * Returns the plugin name for the url processor.
* *
...@@ -193,6 +205,14 @@ interface FacetInterface extends ConfigEntityInterface { ...@@ -193,6 +205,14 @@ interface FacetInterface extends ConfigEntityInterface {
*/ */
public function setQueryOperator($operator); public function setQueryOperator($operator);
/**
* Sets the exclude.
*
* @param bool $exclude
* A boolean flag indicating if search should exclude selected facets
*/
public function setExclude($exclude);
/** /**
* Returns the Facet source id. * Returns the Facet source id.
* *
......
...@@ -225,16 +225,13 @@ class DefaultFacetManager { ...@@ -225,16 +225,13 @@ class DefaultFacetManager {
$this->facets = $this->getEnabledFacets(); $this->facets = $this->getEnabledFacets();
foreach ($this->facets as $facet) { foreach ($this->facets as $facet) {
foreach ($facet->getProcessors() as $processor) { foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_PRE_QUERY) as $processor) {
$processor_definition = $processor->getPluginDefinition(); /** @var PreQueryProcessorInterface $pre_query_processor */
if (is_array($processor_definition['stages']) && array_key_exists(ProcessorInterface::STAGE_PRE_QUERY, $processor_definition['stages'])) { $pre_query_processor = $this->processorPluginManager->createInstance($processor->getPluginDefinition()['id'], ['facet' => $facet]);
/** @var PreQueryProcessorInterface $pre_query_processor */ if (!$pre_query_processor instanceof PreQueryProcessorInterface) {
$pre_query_processor = $this->processorPluginManager->createInstance($processor->getPluginDefinition()['id'], ['facet' => $facet]); throw new InvalidProcessorException(new FormattableMarkup("The processor @processor has a pre_query definition but doesn't implement the required PreQueryProcessorInterface interface", ['@processor' => $processor_configuration['processor_id']]));
if (!$pre_query_processor instanceof PreQueryProcessorInterface) {
throw new InvalidProcessorException(new FormattableMarkup("The processor @processor has a pre_query definition but doesn't implement the required PreQueryProcessorInterface interface", ['@processor' => $processor_configuration['processor_id']]));
}
$pre_query_processor->preQuery($facet);
} }
$pre_query_processor->preQuery($facet);
} }
} }
} }
...@@ -289,16 +286,13 @@ class DefaultFacetManager { ...@@ -289,16 +286,13 @@ class DefaultFacetManager {
// @see \Drupal\facets\Processor\WidgetOrderProcessorInterface. // @see \Drupal\facets\Processor\WidgetOrderProcessorInterface.
$results = $facet->getResults(); $results = $facet->getResults();
foreach ($facet->getProcessors() as $processor) { foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_BUILD) as $processor) {
$processor_definition = $this->processorPluginManager->getDefinition($processor->getPluginDefinition()['id']); /** @var BuildProcessorInterface $build_processor */
if (is_array($processor_definition['stages']) && array_key_exists(ProcessorInterface::STAGE_BUILD, $processor_definition['stages'])) { $build_processor = $this->processorPluginManager->createInstance($processor->getPluginDefinition()['id'], ['facet' => $facet]);
/** @var BuildProcessorInterface $build_processor */ if (!$build_processor instanceof BuildProcessorInterface) {
$build_processor = $this->processorPluginManager->createInstance($processor->getPluginDefinition()['id'], ['facet' => $facet]); throw new InvalidProcessorException(new FormattableMarkup("The processor @processor has a build definition but doesn't implement the required BuildProcessorInterface interface", ['@processor' => $processor['processor_id']]));
if (!$build_processor instanceof BuildProcessorInterface) {
throw new InvalidProcessorException(new FormattableMarkup("The processor @processor has a build definition but doesn't implement the required BuildProcessorInterface interface", ['@processor' => $processor['processor_id']]));
}
$results = $build_processor->build($facet, $results);
} }
$results = $build_processor->build($facet, $results);
} }
$facet->setResults($results); $facet->setResults($results);
......
...@@ -359,6 +359,13 @@ class FacetDisplayForm extends EntityForm { ...@@ -359,6 +359,13 @@ class FacetDisplayForm extends EntityForm {
'#default_value' => $facet->getQueryOperator(), '#default_value' => $facet->getQueryOperator(),
]; ];
$form['facet_settings']['exclude'] = [
'#type' => 'checkbox',
'#title' => $this->t('Exclude'),
'#description' => $this->t('Make the search exclude selected facets, instead of restricting it to them.'),
'#default_value' => $facet->getExclude(),
];
$form['weights'] = array( $form['weights'] = array(
'#type' => 'details', '#type' => 'details',
'#title' => t('Advanced settings'), '#title' => t('Advanced settings'),
...@@ -537,6 +544,8 @@ class FacetDisplayForm extends EntityForm { ...@@ -537,6 +544,8 @@ class FacetDisplayForm extends EntityForm {
$facet->setQueryOperator($form_state->getValue(['facet_settings', 'query_operator'])); $facet->setQueryOperator($form_state->getValue(['facet_settings', 'query_operator']));
$facet->setExclude($form_state->getValue(['facet_settings', 'exclude']));
$facet->save(); $facet->save();
drupal_set_message(t('Facet %name has been updated.', ['%name' => $facet->getName()])); drupal_set_message(t('Facet %name has been updated.', ['%name' => $facet->getName()]));
} }
......
...@@ -221,6 +221,7 @@ class FacetForm extends EntityForm { ...@@ -221,6 +221,7 @@ class FacetForm extends EntityForm {
* Handles form submissions for the facet source subform. * Handles form submissions for the facet source subform.
*/ */
public function submitAjaxFacetSourceConfigForm($form, FormStateInterface $form_state) { public function submitAjaxFacetSourceConfigForm($form, FormStateInterface $form_state) {
$form_state->setValue('id', NULL);
$form_state->setRebuild(); $form_state->setRebuild();
} }
...@@ -254,6 +255,20 @@ class FacetForm extends EntityForm { ...@@ -254,6 +255,20 @@ class FacetForm extends EntityForm {
} }
} }
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
$facet_source_id = $form_state->getValue('facet_source_id');
if (!is_null($facet_source_id) && $facet_source_id !== '') {
/** @var \Drupal\facets\FacetSource\FacetSourcePluginInterface $facet_source */
$facet_source = $this->getFacetSourcePluginManager()->createInstance($facet_source_id, ['facet' => $this->getEntity()]);
$facet_source->validateConfigurationForm($form, $form_state);
}
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
......
...@@ -98,7 +98,7 @@ abstract class SearchApiBaseFacetSource extends FacetSourcePluginBase { ...@@ -98,7 +98,7 @@ abstract class SearchApiBaseFacetSource extends FacetSourcePluginBase {
// identifier. // identifier.
$field_id = $facet->getFieldIdentifier(); $field_id = $facet->getFieldIdentifier();
// Get the Search API Server. // Get the Search API Server.
$server = $this->index->getServer(); $server = $this->index->getServerInstance();
// Get the Search API Backend. // Get the Search API Backend.
$backend = $server->getBackend(); $backend = $server->getBackend();
......
...@@ -47,6 +47,7 @@ class SearchApiString extends QueryTypePluginBase { ...@@ -47,6 +47,7 @@ class SearchApiString extends QueryTypePluginBase {
if (!empty($query)) { if (!empty($query)) {
$operator = $this->facet->getQueryOperator(); $operator = $this->facet->getQueryOperator();
$field_identifier = $this->facet->getFieldIdentifier(); $field_identifier = $this->facet->getFieldIdentifier();
$exclude = $this->facet->getExclude();
// Copy the query object so we can do an unfiltered query. We need to have // Copy the query object so we can do an unfiltered query. We need to have
// this unfiltered results to make sure that the count of a facet is // this unfiltered results to make sure that the count of a facet is
...@@ -81,7 +82,7 @@ class SearchApiString extends QueryTypePluginBase { ...@@ -81,7 +82,7 @@ class SearchApiString extends QueryTypePluginBase {
if (count($active_items)) { if (count($active_items)) {
$filter = $query->createConditionGroup($operator); $filter = $query->createConditionGroup($operator);
foreach ($active_items as $value) { foreach ($active_items as $value) {
$filter->addCondition($this->facet->getFieldIdentifier(), $value); $filter->addCondition($this->facet->getFieldIdentifier(), $value, $exclude ? '<>' : '=');
} }
$query->addConditionGroup($filter); $query->addConditionGroup($filter);
} }
......
...@@ -71,9 +71,10 @@ class IntegrationTest extends FacetWebTestBase { ...@@ -71,9 +71,10 @@ class IntegrationTest extends FacetWebTestBase {
// Check if the overview is empty. // Check if the overview is empty.
$this->checkEmptyOverview(); $this->checkEmptyOverview();
// Add a new facet and edit it. // Add a new facet and edit it. Check adding a duplicate.
$this->addFacet($facet_name); $this->addFacet($facet_name);
$this->editFacet($facet_name); $this->editFacet($facet_name);
$this->addFacetDuplicate($facet_name);
// By default, the view should show all entities. // By default, the view should show all entities.
$this->drupalGet('search-api-test-fulltext'); $this->drupalGet('search-api-test-fulltext');
...@@ -440,6 +441,52 @@ class IntegrationTest extends FacetWebTestBase { ...@@ -440,6 +441,52 @@ class IntegrationTest extends FacetWebTestBase {
$this->assertOptionSelected('edit-processors-hide-non-narrowing-result-processor-weights-build', 5); $this->assertOptionSelected('edit-processors-hide-non-narrowing-result-processor-weights-build', 5);
} }
/**
* Tests the facet's exclude functionality.
*/
public function testExcludeFacet() {
$facet_name = 'test & facet';
$facet_id = 'test_facet';
$facet_edit_page = 'admin/config/search/facets/' . $facet_id . '/display';
$this->addFacet($facet_name);
$this->createFacetBlock($facet_id);
$this->drupalGet($facet_edit_page);
$this->assertNoFieldChecked('edit-facet-settings-exclude');
$this->drupalPostForm(NULL, ['facet_settings[exclude]' => TRUE], $this->t('Save'));
$this->assertResponse(200);
$this->assertFieldChecked('edit-facet-settings-exclude');
$this->drupalGet('search-api-test-fulltext');
$this->assertText('foo bar baz');
$this->assertText('foo baz');
$this->assertLink('item');
$this->clickLink('item');
$this->assertLink('(-) item');
$this->assertText('foo baz');
$this->assertText('bar baz');
$this->assertNoText('foo bar baz');
$this->drupalGet($facet_edit_page);
$this->drupalPostForm(NULL, ['facet_settings[exclude]' => FALSE], $this->t('Save'));
$this->assertResponse(200);
$this->assertNoFieldChecked('edit-facet-settings-exclude');
$this->drupalGet('search-api-test-fulltext');
$this->assertText('foo bar baz');
$this->assertText('foo baz');
$this->assertLink('item');
$this->clickLink('item');
$this->assertLink('(-) item');
$this->assertText('foo bar baz');
$this->assertText('foo test');
$this->assertText('bar');
$this->assertNoText('foo baz');
}
/** /**
* Deletes a facet block by id. * Deletes a facet block by id.
* *
...@@ -602,6 +649,40 @@ class IntegrationTest extends FacetWebTestBase { ...@@ -602,6 +649,40 @@ class IntegrationTest extends FacetWebTestBase {
$this->drupalGet('admin/config/search/facets'); $this->drupalGet('admin/config/search/facets');
} }
/**
* Tests creating a facet with an existing machine name.
*
* @param string $facet_name
* The name of the facet.
*/
protected function addFacetDuplicate($facet_name, $facet_type = 'type') {
$facet_id = $this->convertNameToMachineName($facet_name);
$facet_add_page = '/admin/config/search/facets/add-facet';
$this->drupalGet($facet_add_page);
$form_values = [
'name' => $facet_name,
'id' => $facet_id,
'url_alias' => $facet_id,
'facet_source_id' => 'search_api_views:search_api_test_view:page_1',
];
$facet_source_configs['facet_source_configs[search_api_views:search_api_test_view:page_1][field_identifier]'] = $facet_type;
// Try to submit a facet with a duplicate machine name after form rebuilding
// via facet source submit.
$this->drupalPostForm(NULL, $form_values, $this->t('Configure facet source'));
$this->drupalPostForm(NULL, $form_values + $facet_source_configs, $this->t('Save'));
$this->assertText($this->t('The machine-readable name is already in use. It must be unique.'));
// Try to submit a facet with a duplicate machine name after form rebuilding
// via facet source submit using AJAX.
$this->drupalPostAjaxForm(NULL, $form_values, array('facet_source_configure' => t('Configure facet source')));
$this->drupalPostForm(NULL, $form_values + $facet_source_configs, $this->t('Save'));
$this->assertText($this->t('The machine-readable name is already in use. It must be unique.'));
}
/** /**
* Tests editing of a facet through the UI. * Tests editing of a facet through the UI.
* *
......
...@@ -19,8 +19,8 @@ display: ...@@ -19,8 +19,8 @@ display:
query: query:
type: search_api_query type: search_api_query
options: options:
search_api_bypass_access: false bypass_access: true
entity_access: false skip_access: true
parse_mode: terms parse_mode: terms
exposed_form: exposed_form:
type: basic type: basic
......