Skip to content
Commits on Source (9)
......@@ -4,9 +4,7 @@ CONTENTS OF THIS FILE
* Requirements
* Installation
* Configuration
* Troubleshooting
* FAQ
* Maintainers
INTRODUCTION
------------
......@@ -14,8 +12,9 @@ Todo
REQUIREMENTS
------------
No other modules required, though the module is useless without an implementing
module like search api.
No other modules required, we're supporting drupal core as a source for creating
facets. Though we recommend using Search API, as that integration is better
tested.
INSTALLATION
------------
......@@ -25,17 +24,14 @@ INSTALLATION
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
---------------
Todo
After adding one of those, you can add a facet on the facets configuration page:
/admin/config/search/facets
FAQ
---
Todo
MAINTAINERS
-----------
Current maintainers:
* Todo
......@@ -29,6 +29,9 @@ facets.facet.*:
query_operator:
type: string
label: 'Query Operator'
exclude:
type: boolean
label: 'Exclude'
widget:
type: string
label: 'Widget identifier'
......
......@@ -4,3 +4,6 @@ description: 'Faceted search interfaces that can be used on Search API searchers
core: 8.x
package: Search
configure: facets.overview
test_dependencies:
- search_api:search_api
- drupal:views
......@@ -44,6 +44,7 @@ use Drupal\facets\FacetInterface;
* "widget",
* "widget_configs",
* "query_operator",
* "exclude",
* "only_visible_when_facet_source_is_visible",
* "processor_configs",
* "empty_behavior",
......@@ -109,6 +110,13 @@ class Facet extends ConfigEntityBase implements FacetInterface {
*/
protected $query_operator;
/**
* A boolean flag indicating if search should exclude selected facets.
*
* @var bool
*/
protected $exclude;
/**
* The field identifier.
*
......@@ -363,6 +371,20 @@ class Facet extends ConfigEntityBase implements FacetInterface {
return $this->query_operator ?: 'OR';
}
/**
* {@inheritdoc}
*/
public function setExclude($exclude) {
return $this->exclude = $exclude;
}
/**
* {@inheritdoc}
*/
public function getExclude() {
return $this->exclude;
}
/**
* {@inheritdoc}
*/
......
......@@ -164,6 +164,18 @@ interface FacetInterface extends ConfigEntityInterface {
*/
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.
*
......@@ -193,6 +205,14 @@ interface FacetInterface extends ConfigEntityInterface {
*/
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.
*
......
......@@ -225,16 +225,13 @@ class DefaultFacetManager {
$this->facets = $this->getEnabledFacets();
foreach ($this->facets as $facet) {
foreach ($facet->getProcessors() as $processor) {
$processor_definition = $processor->getPluginDefinition();
if (is_array($processor_definition['stages']) && array_key_exists(ProcessorInterface::STAGE_PRE_QUERY, $processor_definition['stages'])) {
/** @var PreQueryProcessorInterface $pre_query_processor */
$pre_query_processor = $this->processorPluginManager->createInstance($processor->getPluginDefinition()['id'], ['facet' => $facet]);
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);
foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_PRE_QUERY) as $processor) {
/** @var PreQueryProcessorInterface $pre_query_processor */
$pre_query_processor = $this->processorPluginManager->createInstance($processor->getPluginDefinition()['id'], ['facet' => $facet]);
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);
}
}
}
......@@ -289,16 +286,13 @@ class DefaultFacetManager {
// @see \Drupal\facets\Processor\WidgetOrderProcessorInterface.
$results = $facet->getResults();
foreach ($facet->getProcessors() as $processor) {
$processor_definition = $this->processorPluginManager->getDefinition($processor->getPluginDefinition()['id']);
if (is_array($processor_definition['stages']) && array_key_exists(ProcessorInterface::STAGE_BUILD, $processor_definition['stages'])) {
/** @var BuildProcessorInterface $build_processor */
$build_processor = $this->processorPluginManager->createInstance($processor->getPluginDefinition()['id'], ['facet' => $facet]);
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);
foreach ($facet->getProcessorsByStage(ProcessorInterface::STAGE_BUILD) as $processor) {
/** @var BuildProcessorInterface $build_processor */
$build_processor = $this->processorPluginManager->createInstance($processor->getPluginDefinition()['id'], ['facet' => $facet]);
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);
}
$facet->setResults($results);
......
......@@ -359,6 +359,13 @@ class FacetDisplayForm extends EntityForm {
'#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(
'#type' => 'details',
'#title' => t('Advanced settings'),
......@@ -537,6 +544,8 @@ class FacetDisplayForm extends EntityForm {
$facet->setQueryOperator($form_state->getValue(['facet_settings', 'query_operator']));
$facet->setExclude($form_state->getValue(['facet_settings', 'exclude']));
$facet->save();
drupal_set_message(t('Facet %name has been updated.', ['%name' => $facet->getName()]));
}
......
......@@ -221,6 +221,7 @@ class FacetForm extends EntityForm {
* Handles form submissions for the facet source subform.
*/
public function submitAjaxFacetSourceConfigForm($form, FormStateInterface $form_state) {
$form_state->setValue('id', NULL);
$form_state->setRebuild();
}
......@@ -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}
*/
......
......@@ -98,7 +98,7 @@ abstract class SearchApiBaseFacetSource extends FacetSourcePluginBase {
// identifier.
$field_id = $facet->getFieldIdentifier();
// Get the Search API Server.
$server = $this->index->getServer();
$server = $this->index->getServerInstance();
// Get the Search API Backend.
$backend = $server->getBackend();
......
......@@ -47,6 +47,7 @@ class SearchApiString extends QueryTypePluginBase {
if (!empty($query)) {
$operator = $this->facet->getQueryOperator();
$field_identifier = $this->facet->getFieldIdentifier();
$exclude = $this->facet->getExclude();
// 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
......@@ -81,7 +82,7 @@ class SearchApiString extends QueryTypePluginBase {
if (count($active_items)) {
$filter = $query->createConditionGroup($operator);
foreach ($active_items as $value) {
$filter->addCondition($this->facet->getFieldIdentifier(), $value);
$filter->addCondition($this->facet->getFieldIdentifier(), $value, $exclude ? '<>' : '=');
}
$query->addConditionGroup($filter);
}
......
......@@ -71,9 +71,10 @@ class IntegrationTest extends FacetWebTestBase {
// Check if the overview is empty.
$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->editFacet($facet_name);
$this->addFacetDuplicate($facet_name);
// By default, the view should show all entities.
$this->drupalGet('search-api-test-fulltext');
......@@ -440,6 +441,52 @@ class IntegrationTest extends FacetWebTestBase {
$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.
*
......@@ -602,6 +649,40 @@ class IntegrationTest extends FacetWebTestBase {
$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.
*
......
......@@ -19,8 +19,8 @@ display:
query:
type: search_api_query
options:
search_api_bypass_access: false
entity_access: false
bypass_access: true
skip_access: true
parse_mode: terms
exposed_form:
type: basic
......