Skip to content
Commits on Source (77)
......@@ -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
......@@ -14,6 +14,9 @@ facets.facet.*:
status:
type: boolean
label: 'Status'
url_alias:
type: string
label: 'Name of facet as used in the URL'
facet_source_id:
type: string
label: 'Facet source id'
......@@ -23,9 +26,28 @@ facets.facet.*:
query_type_name:
type: string
label: 'Query Type Name'
query_operator:
type: string
label: 'Query Operator'
exclude:
type: boolean
label: 'Exclude'
widget:
type: string
label: 'Field identifier'
label: 'Widget identifier'
empty_behavior:
type: mapping
label: 'Empty behavior'
mapping:
behavior:
type: string
label: 'The empty behavior identifier'
text_format:
type: string
label: 'Text format'
text:
type: string
label: 'Text'
widget_configs:
type: sequence
label: 'Widget plugin configurations'
......@@ -52,4 +74,18 @@ facets.facet.*:
type: integer
label: 'The processor''s weight for this stage'
settings:
type: facets.processor.plugin.[%parent.processor_id]
type: plugin.plugin_configuration.facets_processor.[%parent.processor_id]
facet_configs:
type: sequence
label: 'Facet plugin-specific options'
sequence:
type: plugin.plugin_configuration.facets_facet_options.[%key]
label: 'Facet plugin options'
condition.plugin.other_facet:
type: condition.plugin
mapping:
facet_value:
type: string
facets:
type: string
facets.facet_source.*:
type: config_entity
label : 'Facet Source'
mapping:
uuid:
type: string
label: 'UUID'
id:
type: string
label: 'ID'
name:
type: label
label: Name'
filter_key:
type: string
label: 'Filter key'
url_processor:
type: string
label: 'Url processor'
facets.processor.plugin.count_widget_order:
plugin.plugin_configuration.facets_processor.count_widget_widget_order:
type: mapping
label: 'Count widget order'
mapping:
......@@ -6,7 +6,7 @@ facets.processor.plugin.count_widget_order:
type: string
label: sort order
facets.processor.plugin.display_value_widget_order:
plugin.plugin_configuration.facets_processor.display_value_widget_order:
type: mapping
label: 'Display value widget order'
mapping:
......@@ -14,7 +14,7 @@ facets.processor.plugin.display_value_widget_order:
type: string
label: sort order
facets.processor.plugin.raw_value_widget_order:
plugin.plugin_configuration.facets_processor.raw_value_widget_order:
type: mapping
label: 'Raw value widget order'
mapping:
......@@ -22,7 +22,7 @@ facets.processor.plugin.raw_value_widget_order:
type: string
label: sort order
facets.processor.plugin.active_widget_order:
plugin.plugin_configuration.facets_processor.active_widget_order:
type: mapping
label: 'Active widget order'
mapping:
......@@ -30,7 +30,7 @@ facets.processor.plugin.active_widget_order:
type: string
label: sort order
facets.processor.plugin.count_limit:
plugin.plugin_configuration.facets_processor.count_limit:
type: mapping
label: 'Count limit widget'
mapping:
......@@ -40,3 +40,9 @@ facets.processor.plugin.count_limit:
maximum_items:
type: integer
label: 'Maximum amount of items to show.'
plugin.plugin_configuration.facets_processor.url_processor_handler:
type: config_object
plugin.plugin_configuration.facets_processor.hide_non_narrowing_result_processor:
type: config_object
<?php
/**
* @file
* Hooks provided by the core_search_facets module.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Adds field types as possible options for facets.
*
* @param array $allowed_field_types
* The field types.
*
* @return array
* Array that contains the field types.
*/
function hook_facets_core_allowed_field_types(array $allowed_field_types) {
$allowed_field_types[] = 'float';
return $allowed_field_types;
}
/**
* @} End of "addtogroup hooks".
*/
......@@ -45,3 +45,13 @@ function core_search_facets_search_plugin_alter(array &$definitions) {
//$definitions['node_search']['class'] = 'Drupal\core_search_facets\Plugin\Search\NodeSearchFacets';
}
}
/**
* Implements hook_facets_core_allowed_field_types().
*/
function core_search_facets_facets_core_allowed_field_types(array $allowed_field_types) {
$allowed_field_types[] = 'taxonomy_term';
$allowed_field_types[] = 'integer';
return $allowed_field_types;
}
......@@ -72,22 +72,20 @@ class FacetsQuery extends SearchQuery {
$this->join('search_total', 't', 'i.word = t.word');
$this
->condition('i.type', $this->type)
// @TODO needs review. Adding n.uid,n.type and others to avoid "Syntax error or access violation: 1055"
->groupBy('i.langcode')
->groupBy('n.uid')
->groupBy('n.type')
->groupBy('value')
->groupBy('i.type')
->groupBy('i.sid')
->having('COUNT(*) >= :matches', array(':matches' => $this->matches));
// For complex search queries, add the LIKE conditions.
if (!$this->simple) {
$this->join('search_dataset', 'd', 'i.sid = d.sid AND i.type = d.type');
$this->condition($this->conditions);
}
/*if (!$this->simple) {
$this->join('search_dataset', 'd', 'i.sid = d.sid AND i.type = d.type');
$this->condition($this->conditions);
}*/
// Add conditions to query.
$this->join('search_dataset', 'd', 'i.sid = d.sid AND i.type = d.type');
$this->join('search_dataset', 'd', 'i.sid = d.sid AND i.type = d.type AND i.langcode = d.langcode');
$this->condition($this->conditions);
// Add tag and useful metadata.
......
......@@ -2,7 +2,7 @@
/**
* @file
* Contains \Drupal\core_search_facets\Plugin\FacetSourceInterface.
* Contains \Drupal\core_search_facets\Plugin\CoreSearchFacetSourceInterface.
*/
namespace Drupal\core_search_facets\Plugin;
......@@ -14,7 +14,7 @@ use Drupal\facets\FacetInterface;
*
* A facet source is used to abstract the data source where facets can be added
* to. A good example of this is a search api view. There are other possible
* facet data sources, these all implement the FacetSourceInterface.
* facet data sources, these all implement the FacetSourcePluginInterface.
*/
interface CoreSearchFacetSourceInterface {
......
......@@ -9,15 +9,17 @@ namespace Drupal\core_search_facets\Plugin\Search;
use Drupal\Core\Config\Config;
use Drupal\Core\Database\Driver\mysql\Connection;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\facets\FacetSource\FacetSourcePluginManager;
use Drupal\node\Plugin\Search\NodeSearch;
use Drupal\Core\Render\RendererInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Handles searching for node entities using the Search module index.
......@@ -34,13 +36,13 @@ class NodeSearchFacets extends NodeSearch {
$plugin_id,
$plugin_definition,
Connection $database,
EntityManagerInterface $entity_manager,
EntityTypeManagerInterface $entity_manager,
ModuleHandlerInterface $module_handler,
Config $search_settings,
LanguageManagerInterface $language_manager,
RendererInterface $renderer,
$facet_source_plugin_manager,
$request_stack,
FacetSourcePluginManager $facet_source_plugin_manager,
RequestStack $request_stack,
AccountInterface $account = NULL) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $database, $entity_manager, $module_handler, $search_settings, $language_manager, $renderer, $account);
......@@ -60,7 +62,7 @@ class NodeSearchFacets extends NodeSearch {
$plugin_id,
$plugin_definition,
$container->get('database'),
$container->get('entity.manager'),
$container->get('entity_type.manager'),
$container->get('module_handler'),
$container->get('config.factory')->get('search.settings'),
$container->get('language_manager'),
......@@ -96,6 +98,7 @@ class NodeSearchFacets extends NodeSearch {
'#access' => $this->account && $this->account->hasPermission('use advanced search'),
'#open' => $used_advanced,
);
$form['advanced']['keywords-fieldset'] = array(
'#type' => 'fieldset',
'#title' => t('Keywords'),
......
......@@ -12,9 +12,12 @@ use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\core_search_facets\Plugin\CoreSearchFacetSourceInterface;
use Drupal\facets\FacetInterface;
use Drupal\facets\FacetSource\FacetSourceInterface;
use Drupal\facets\FacetSource\FacetSourcePluginBase;
use Drupal\facets\QueryType\QueryTypePluginManager;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\search\SearchPageInterface;
use Drupal\search\SearchPluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
......@@ -52,6 +55,11 @@ class CoreNodeSearchFacetSource extends FacetSourcePluginBase implements CoreSea
*/
protected $configFactory;
/**
* The plugin manager for core search plugins.
*
* @var \Drupal\search\SearchPluginManager
*/
protected $searchManager;
/**
......@@ -62,7 +70,7 @@ class CoreNodeSearchFacetSource extends FacetSourcePluginBase implements CoreSea
/**
* {@inheritdoc}
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, $query_type_plugin_manager, $search_manager, RequestStack $request_stack) {
public function __construct(array $configuration, $plugin_id, array $plugin_definition, QueryTypePluginManager $query_type_plugin_manager, SearchPluginManager $search_manager, RequestStack $request_stack) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $query_type_plugin_manager);
$this->searchManager = $search_manager;
$this->setSearchKeys($request_stack->getMasterRequest()->query->get('keys'));
......@@ -119,27 +127,35 @@ class CoreNodeSearchFacetSource extends FacetSourcePluginBase implements CoreSea
* {@inheritdoc}
*/
public function getQueryTypesForFacet(FacetInterface $facet) {
// Get our Facets Field Identifier.
$field_id = $facet->getFieldIdentifier();
// Verify if the field exists. Otherwise the type will be a column
// (type,uid...) from the node and we can use the field identifier directly.
if ($field = FieldStorageConfig::loadByName('node', $facet->getFieldIdentifier())) {
$field_type = $field->getType();
}
else {
$field_type = $facet->getFieldIdentifier();
}
return $this->getQueryTypesForDataType($field_id);
return $this->getQueryTypesForFieldType($field_type);
}
/**
* Get the query types for a data type.
* Get the query types for a field type.
*
* @param string $field_id
* The field id.
* @param string $field_type
* The field type.
*
* @return array
* An array of query types.
*/
public function getQueryTypesForDataType($field_id) {
protected function getQueryTypesForFieldType($field_type) {
$query_types = [];
switch ($field_id) {
switch ($field_type) {
case 'type':
case 'uid':
case 'langcode':
case 'integer':
case 'entity_reference':
$query_types['string'] = 'core_node_search_string';
break;
}
......@@ -151,7 +167,6 @@ class CoreNodeSearchFacetSource extends FacetSourcePluginBase implements CoreSea
* {@inheritdoc}
*/
public function isRenderedInCurrentRequest() {
// @TODO Avoid the use of \Duupal so maybe inject?
$request = \Drupal::requestStack()->getMasterRequest();
$search_page = $request->attributes->get('entity');
if ($search_page instanceof SearchPageInterface) {
......@@ -168,7 +183,7 @@ class CoreNodeSearchFacetSource extends FacetSourcePluginBase implements CoreSea
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $facet, FacetSourceInterface $facet_source) {
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['field_identifier'] = [
'#type' => 'select',
......@@ -176,7 +191,7 @@ class CoreNodeSearchFacetSource extends FacetSourcePluginBase implements CoreSea
'#title' => $this->t('Facet field'),
'#description' => $this->t('Choose the indexed field.'),
'#required' => TRUE,
'#default_value' => $facet->getFieldIdentifier(),
'#default_value' => $this->facet->getFieldIdentifier(),
];
return $form;
......@@ -186,33 +201,57 @@ class CoreNodeSearchFacetSource extends FacetSourcePluginBase implements CoreSea
* {@inheritdoc}
*/
public function getFields() {
$default_fields = [
// Default fields.
$facet_fields = $this->getDefaultFields();
// Get the allowed field types.
$allowed_field_types = \Drupal::moduleHandler()->invokeAll('facets_core_allowed_field_types', array($field_types = []));
// Get the current field instances and detect if the field type is allowed.
$fields = FieldConfig::loadMultiple();
/** @var \Drupal\Field\FieldConfigInterface $field */
foreach ($fields as $field) {
// Verify if the target type is allowed for entity reference fields,
// otherwise verify the field type(i.e. integer, float...).
$target_is_allowed = in_array($field->getFieldStorageDefinition()->getSetting('target_type'), $allowed_field_types);
$field_is_allowed = in_array($field->getFieldStorageDefinition()->getType(), $allowed_field_types);
if ($target_is_allowed || $field_is_allowed) {
/** @var \Drupal\field\Entity\FieldConfig $field */
if (!array_key_exists($field->getName(), $facet_fields)) {
$facet_fields[$field->getName()] = $this->t('@label', ['@label' => $field->getLabel()]);
}
}
}
return $facet_fields;
}
/**
* Getter for default node fields.
*
* @return array
* An array containing the default fields enabled on a node.
*/
protected function getDefaultFields() {
return [
'type' => $this->t('Content Type'),
'uid' => $this->t('Author'),
'langcode' => $this->t('Language'),
];
return $default_fields;
}
/**
* {@inheritdoc}
*/
public function getFacetQueryExtender() {
// If (!$this->facetQueryExtender) {
// $this->facetQueryExtender = db_select('search_index',
// 'i',
// array('target' => 'replica'))
// ->extend('Drupal\search\ViewsSearchQuery');
// $this->searchQuery->searchExpression($input, $this->searchType);
// $this->searchQuery->publicParseSearchExpression();
$this->facetQueryExtender = db_select('search_index', 'i', array('target' => 'replica'))->extend('Drupal\core_search_facets\FacetsQuery');
$this->facetQueryExtender->join('node_field_data', 'n', 'n.nid = i.sid');
$this->facetQueryExtender
// ->condition('n.status', 1).
->addTag('node_access')
->searchExpression($this->keys, 'node_search');
// }.
if (!$this->facetQueryExtender) {
$this->facetQueryExtender = db_select('search_index', 'i', array('target' => 'replica'))->extend('Drupal\core_search_facets\FacetsQuery');
$this->facetQueryExtender->join('node_field_data', 'n', 'n.nid = i.sid');
$this->facetQueryExtender
// ->condition('n.status', 1).
->addTag('node_access')
->searchExpression($this->keys, 'node_search');
}
return $this->facetQueryExtender;
}
......@@ -220,46 +259,54 @@ class CoreNodeSearchFacetSource extends FacetSourcePluginBase implements CoreSea
* {@inheritdoc}
*/
public function getQueryInfo(FacetInterface $facet) {
// If (!$facet['field api name']) {
// We add the language code of the indexed item to the result of the query.
// So in this case we need to use the search_index table alias (i) for the
// langcode field. Otherwise we will have same nid for multiple languages
// as result. For more details see NodeSearch::findResults().
$table_alias = $facet->getFieldIdentifier() == 'langcode' ? 'i' : 'n';
$query_info = [
'fields' => [
$table_alias . '.' . $facet->getFieldIdentifier() => [
'table_alias' => $table_alias,
'field' => $facet->getFieldIdentifier(),
$query_info = [];
$field_name = $facet->getFieldIdentifier();
$default_fields = $this->getDefaultFields();
if (array_key_exists($facet->getFieldIdentifier(), $default_fields)) {
// We add the language code of the indexed item to the result of the
// query. So in this case we need to use the search_index table alias (i)
// for the langcode field. Otherwise we will have same nid for multiple
// languages as result. For more details see NodeSearch::findResults().
// @TODO review if I can refactor this.
$table_alias = $facet->getFieldIdentifier() == 'langcode' ? 'i' : 'n';
$query_info = [
'fields' => [
$table_alias . '.' . $facet->getFieldIdentifier() => [
'table_alias' => $table_alias,
'field' => $facet->getFieldIdentifier(),
],
],
],
];
// }
/*else {
$query_info = array();
// Gets field info, finds table name and field name.
$field = field_info_field($facet['field api name']);
$table = _field_sql_storage_tablename($field);
// Iterates over columns, adds fields to query info.
foreach ($field['columns'] as $column_name => $attributes) {
$column = _field_sql_storage_columnname($field['field_name'], $column_name);
$query_info['fields'][$table . '.' . $column] = array(
'table_alias' => $table,
'field' => $column,
);
];
}
else {
// Gets field info, finds table name and field name.
$table = "node__{$field_name}";
// The column name will be different depending on the field type, it's
// always the fields machine name, suffixed with '_value'. Entity
// reference fields change that suffix into '_target_id'.
$field_config = FieldStorageConfig::loadByName('node', $facet->getFieldIdentifier());
$field_type = $field_config->getType();
if ($field_type == 'entity_reference') {
$column = $facet->getFieldIdentifier() . '_target_id';
}
else {
$column = $facet->getFieldIdentifier() . '_value';
}
// Adds the join on the node table.
$query_info['joins'] = array(
$table => array(
'table' => $table,
'alias' => $table,
'condition' => "n.vid = $table.revision_id",
),
);
}*/
$query_info['fields'][$field_name . '.' . $column] = array(
'table_alias' => $table,
'field' => $column,
);
// Adds the join on the node table.
$query_info['joins'] = array(
$table => array(
'table' => $table,
'alias' => $table,
'condition' => "n.vid = $table.revision_id AND i.langcode = $table.langcode",
),
);
}
// Returns query info, makes sure all keys are present.
return $query_info + [
......
......@@ -8,7 +8,9 @@
namespace Drupal\core_search_facets\Plugin\facets\facet_source;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\facets\FacetSource\FacetSourceDeriverBase;
use Drupal\search\SearchPluginManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -18,12 +20,24 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*/
class CoreNodeSearchFacetSourceDeriver extends FacetSourceDeriverBase {
/**
* The plugin manager for core search plugins.
*
* @var \Drupal\search\SearchPluginManager
*/
protected $searchManager;
/**
* Create an instance of the deriver.
* Creates an instance of the deriver.
*
* @param string $base_plugin_id
* The plugin ID.
* @param \Drupal\search\SearchPluginManager $search_manager
* The plugin manager for core search plugins.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity manager.
*/
public function __construct(ContainerInterface $container, $base_plugin_id, $search_manager, $entity_type_manager) {
public function __construct($base_plugin_id, SearchPluginManager $search_manager, EntityTypeManagerInterface $entity_type_manager) {
$this->searchManager = $search_manager;
$this->entityTypeManager = $entity_type_manager;
}
......@@ -33,11 +47,10 @@ class CoreNodeSearchFacetSourceDeriver extends FacetSourceDeriverBase {
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
return new static(
$container,
$base_plugin_id,
$container->get('plugin.manager.search'),
$container->get('entity_type.manager')
);
);
}
/**
......
......@@ -21,7 +21,7 @@ use Drupal\facets\Result\Result;
class CoreNodeSearchString extends QueryTypePluginBase {
/**
* Holds the backend's native query object.
* The backend's native query object.
*
* @var \Drupal\search_api\Query\QueryInterface
*/
......@@ -31,7 +31,6 @@ class CoreNodeSearchString extends QueryTypePluginBase {
* {@inheritdoc}
*/
public function execute() {
/** @var \Drupal\core_search_facets\Plugin\CoreSearchFacetSourceInterface $facet_source */
$facet_source = $this->facet->getFacetSource();
$query_info = $facet_source->getQueryInfo($this->facet);
......@@ -46,17 +45,16 @@ class CoreNodeSearchString extends QueryTypePluginBase {
foreach ($query_info['fields'] as $field_info) {
// Adds join to the facet query.
/*$facet_query->addFacetJoin($query_info, $field_info['table_alias']);
$facet_query->addFacetJoin($query_info, $field_info['table_alias']);
// Adds adds join to search query, makes sure it is only added once.
// Adds join to search query, makes sure it is only added once.
if (isset($query_info['joins'][$field_info['table_alias']])) {
if (!isset($tables_joined[$field_info['table_alias']])) {
$tables_joined[$field_info['table_alias']] = TRUE;
$join_info = $query_info['joins'][$field_info['table_alias']];
$this->query->join($join_info['table'], $join_info['alias'],
$join_info['condition']);
if (!isset($tables_joined[$field_info['table_alias']])) {
$tables_joined[$field_info['table_alias']] = TRUE;
$join_info = $query_info['joins'][$field_info['table_alias']];
$this->query->join($join_info['table'], $join_info['alias'], $join_info['condition']);
}
}
}*/
// Adds facet conditions to the queries.
$field = $field_info['table_alias'] . '.' . $field_info['field'];
......@@ -78,6 +76,9 @@ class CoreNodeSearchString extends QueryTypePluginBase {
$facet_query = $facet_source->getFacetQueryExtender();
$facet_query->addFacetField($query_info);
foreach ($query_info['joins'] as $table_alias => $join_info) {
$facet_query->addFacetJoin($query_info, $table_alias);
}
// Only build results if a search is executed.
if ($facet_query->getSearchExpression()) {
......
<?php
/**
* @file
* Contains \Drupal\core_search_facets\Tests\HooksTest.
*/
namespace Drupal\core_search_facets\Tests;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
/**
* Tests integration of hooks.
*
* @group core_search_facets
*/
class HooksTest extends WebTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'node',
'search',
'core_search_facets_test_hooks',
'field',
];
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Create a field of type float.
FieldStorageConfig::create(
[
'field_name' => 'float',
'entity_type' => 'node',
'type' => 'float',
]
)->save();
// Create an instance of the float field on the "page" content type.
FieldConfig::create(
[
'field_name' => 'float',
'entity_type' => 'node',
'bundle' => 'page',
'label' => 'Float Field Label',
]
)->save();
// Log in, so we can test all the things.
$this->drupalLogin($this->adminUser);
}
/**
* Tests various that all hooks fire correctly.
*/
public function testHooks() {
// Verify that hook_facets_core_allowed_field_types was triggered.
$facet_add_page = 'admin/config/search/facets/add-facet';
$this->drupalGet($facet_add_page);
$this->assertResponse(200);
// Select the node_search facet source.
$this->drupalGet($facet_add_page);
$this->drupalPostForm(
NULL,
['facet_source_id' => 'core_node_search:node_search'],
$this->t('Configure facet source')
);
// The field appears as expected.
$this->assertText('Float Field Label', 'Float Field appears as expected');
}
}
<?php
/**
* @file
* Contains \Drupal\core_search_facets\Tests\IntegrationTest.
*/
namespace Drupal\core_search_facets\Tests;
use Drupal\core_search_facets\Tests\WebTestBase as CoreSearchFacetsWebTestBase;
/**
* Tests the admin UI with the core search facet source.
*
* @group core_search_facets
*/
class IntegrationTest extends CoreSearchFacetsWebTestBase {
/**
* The block entities used by this test.
*
* @var \Drupal\block\BlockInterface[]
*/
protected $blocks;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->drupalLogin($this->adminUser);
// Index the content.
\Drupal::service('plugin.manager.search')->createInstance('node_search')->updateIndex();
search_update_totals();
// Make absolutely sure the ::$blocks variable doesn't pass information
// along between tests.
$this->blocks = NULL;
}
/**
* Tests various operations via the Facets' admin UI.
*/
public function testFramework() {
$facet_name = "Test Facet name";
$facet_id = 'test_facet_name';
// Check if the overview is empty.
$this->checkEmptyOverview();
// Add a new facet and edit it.
$this->addFacet($facet_name);
$this->editFacet($facet_name);
// Create and place a block for "Test Facet name" facet.
$this->createFacetBlock($facet_id);
// Verify that the facet results are correct.
$this->drupalGet('search/node', ['query' => ['keys' => 'test']]);
$this->assertLink('page');
$this->assertLink('article');
// Verify that facet blocks appear as expected.
$this->assertFacetBlocksAppear();
$this->setShowAmountOfResults($facet_name, TRUE);
// Verify that the number of results per item.
$this->drupalGet('search/node', ['query' => ['keys' => 'test']]);
$this->assertLink('page (10)');
$this->assertLink('article (10)');
// Do not show the block on empty behaviors.
// Truncate the search_index table because, for the moment, we don't have
// the possibility to clear the index from the API.
// See https://www.drupal.org/node/326062
\Drupal::database()->truncate('search_index')->execute();
// Verify that no facet blocks appear. Empty behavior "None" is selected by
// default.
$this->drupalGet('search/node', ['query' => ['keys' => 'test']]);
$this->assertNoFacetBlocksAppear();
// Verify that the "empty_text" appears as expected.
$this->setEmptyBehaviorFacetText($facet_name);
$this->drupalGet('search/node', ['query' => ['keys' => 'test']]);
$this->assertRaw('block-test-facet-name');
$this->assertRaw('No results found for this block!');
// Delete the block.
$this->deleteBlock($facet_id);
// Delete the facet and make sure the overview is empty again.
$this->deleteUnusedFacet($facet_name);
$this->checkEmptyOverview();
}
/**
* Configures the possibility to show the amount of results for facet blocks.
*
* @param string $facet_name
* The name of the facet.
* @param bool|TRUE $show
* Boolean to determine if we want to show the amount of results.
*/
protected function setShowAmountOfResults($facet_name, $show = TRUE) {
$facet_id = $this->convertNameToMachineName($facet_name);
$facet_display_page = '/admin/config/search/facets/' . $facet_id . '/display';
// Go to the facet edit page and make sure "edit facet %facet" is present.
$this->drupalGet($facet_display_page);
$this->assertResponse(200);
// Configure the text for empty results behavior.
$edit = [
'widget_configs[show_numbers]' => $show,
];
$this->drupalPostForm(NULL, $edit, $this->t('Save'));
}
/**
* Deletes a facet block by id.
*
* @param string $id
* The id of the block.
*/
protected function deleteBlock($id) {
$this->drupalGet('admin/structure/block/manage/' . $this->blocks[$id]->id(), array('query' => array('destination' => 'admin')));
$this->clickLink(t('Delete'));
$this->drupalPostForm(NULL, array(), t('Delete'));
$this->assertRaw(t('The block %name has been deleted.', array('%name' => $this->blocks[$id]->label())));
}
/**
* Asserts that a facet block does not appear.
*/
protected function assertNoFacetBlocksAppear() {
foreach ($this->blocks as $block) {
$this->assertNoBlockAppears($block);
}
}
/**
* Asserts that a facet block appears.
*/
protected function assertFacetBlocksAppear() {
foreach ($this->blocks as $block) {
$this->assertBlockAppears($block);
}
}
/**
* Creates a facet block by id.
*
* @param string $id
* The id of the block.
*/
protected function createFacetBlock($id) {
$block = [
'plugin_id' => 'facet_block:' . $id,
'settings' => [
'region' => 'footer',
'id' => str_replace('_', '-', $id),
],
];
$this->blocks[$id] = $this->drupalPlaceBlock($block['plugin_id'], $block['settings']);
}
/**
* Configures empty behavior option to show a text on empty results.
*
* @param string $facet_name
* The name of the facet.
*/
protected function setEmptyBehaviorFacetText($facet_name) {
$facet_id = $this->convertNameToMachineName($facet_name);
$facet_display_page = '/admin/config/search/facets/' . $facet_id . '/display';
// Go to the facet edit page and make sure "edit facet %facet" is present.
$this->drupalGet($facet_display_page);
$this->assertResponse(200);
// Configure the text for empty results behavior.
$edit = [
'facet_settings[empty_behavior]' => 'text',
'facet_settings[empty_behavior_container][empty_behavior_text][value]' => 'No results found for this block!',
];
$this->drupalPostForm(NULL, $edit, $this->t('Save'));
}
/**
* Configures a facet to only be visible when accessing to the facet source.
*
* @param string $facet_name
* The name of the facet.
*/
protected function setOptionShowOnlyWhenFacetSourceVisible($facet_name) {
$facet_id = $this->convertNameToMachineName($facet_name);
$facet_display_page = '/admin/config/search/facets/' . $facet_id . '/display';
$this->drupalGet($facet_display_page);
$this->assertResponse(200);
$edit = [
'facet_settings[only_visible_when_facet_source_is_visible]' => TRUE,
'widget' => 'links',
'widget_configs[show_numbers]' => '0',
];
$this->drupalPostForm(NULL, $edit, $this->t('Save'));
}
/**
* Get the facet overview page and make sure the overview is empty.
*/
protected function checkEmptyOverview() {
$facet_overview = '/admin/config/search/facets';
$this->drupalGet($facet_overview);
$this->assertResponse(200);
// The list overview has Field: field_name as description. This tests on the
// absence of that.
$this->assertNoText('Field:');
}
/**
* Tests adding a facet trough the interface.
*
* @param string $facet_name
* The name of the facet.
*/
protected function addFacet($facet_name) {
$facet_id = $this->convertNameToMachineName($facet_name);
// Go to the Add facet page and make sure that returns a 200.
$facet_add_page = '/admin/config/search/facets/add-facet';
$this->drupalGet($facet_add_page);
$this->assertResponse(200);
$form_values = [
'name' => '',
'id' => $facet_id,
'status' => 1,
'url_alias' => $facet_id,
];
// Try filling out the form, but without having filled in a name for the
// facet to test for form errors.
$this->drupalPostForm($facet_add_page, $form_values, $this->t('Save'));
$this->assertText($this->t('Facet name field is required.'));
$this->assertText($this->t('Facet source field is required.'));
// Make sure that when filling out the name, the form error disappears.
$form_values['name'] = $facet_name;
$this->drupalPostForm(NULL, $form_values, $this->t('Save'));
$this->assertNoText($this->t('Facet name field is required.'));
// Configure the facet source by selecting one of the search api views.
$this->drupalGet($facet_add_page);
$this->drupalPostForm(NULL, ['facet_source_id' => 'core_node_search:node_search'], $this->t('Configure facet source'));
// The facet field is still required.
$this->drupalPostForm(NULL, $form_values, $this->t('Save'));
$this->assertText($this->t('Facet field field is required.'));
// Fill in all fields and make sure the 'field is required' message is no
// longer shown.
$facet_source_form = [
'facet_source_configs[core_node_search:node_search][field_identifier]' => 'type',
];
$this->drupalPostForm(NULL, $form_values + $facet_source_form, $this->t('Save'));
$this->assertNoText('field is required.');
// Make sure that the redirection to the display page is correct.
$this->assertRaw(t('Facet %name has been created.', ['%name' => $facet_name]));
$this->assertUrl('admin/config/search/facets/' . $facet_id . '/display');
$this->drupalGet('admin/config/search/facets');
}
/**
* Tests editing of a facet through the UI.
*
* @param string $facet_name
* The name of the facet.
*/
public function editFacet($facet_name) {
$facet_id = $this->convertNameToMachineName($facet_name);
$facet_edit_page = '/admin/config/search/facets/' . $facet_id . '/edit';
// Go to the facet edit page and make sure "edit facet %facet" is present.
$this->drupalGet($facet_edit_page);
$this->assertResponse(200);
$this->assertRaw($this->t('Edit facet @facet', ['@facet' => $facet_name]));
// Change the facet name to add in "-2" to test editing of a facet works.
$form_values = ['name' => $facet_name . ' - 2'];
$this->drupalPostForm($facet_edit_page, $form_values, $this->t('Save'));
// Make sure that the redirection back to the overview was successful and
// the edited facet is shown on the overview page.
$this->assertRaw(t('Facet %name has been updated.', ['%name' => $facet_name . ' - 2']));
// Make sure the "-2" suffix is still on the facet when editing a facet.
$this->drupalGet($facet_edit_page);
$this->assertRaw($this->t('Edit facet @facet', ['@facet' => $facet_name . ' - 2']));
// Edit the form and change the facet's name back to the initial name.
$form_values = ['name' => $facet_name];
$this->drupalPostForm($facet_edit_page, $form_values, $this->t('Save'));
// Make sure that the redirection back to the overview was successful and
// the edited facet is shown on the overview page.
$this->assertRaw(t('Facet %name has been updated.', ['%name' => $facet_name]));
}
/**
* This deletes an unused facet through the UI.
*
* @param string $facet_name
* The name of the facet.
*/
protected function deleteUsedFacet($facet_name) {
$facet_id = $this->convertNameToMachineName($facet_name);
$facet_delete_page = '/admin/config/search/facets/' . $facet_id . '/delete';
// Go to the facet delete page and make the warning is shown.
$this->drupalGet($facet_delete_page);
$this->assertResponse(200);
// Check that the facet by testing for the message and the absence of the
// facet name on the overview.
$this->assertRaw($this->t('The facet is currently used in a block and thus can\'t be removed. Remove the block first.'));
}
/**
* This deletes a facet through the UI.
*
* @param string $facet_name
* The name of the facet.
*/
protected function deleteUnusedFacet($facet_name) {
$facet_id = $this->convertNameToMachineName($facet_name);
$facet_delete_page = '/admin/config/search/facets/' . $facet_id . '/delete';
// Go to the facet delete page and make the warning is shown.
$this->drupalGet($facet_delete_page);
$this->assertResponse(200);
$this->assertText($this->t('This action cannot be undone'));
// Actually submit the confirmation form.
$this->drupalPostForm(NULL, [], $this->t('Delete'));
// Check that the facet by testing for the message and the absence of the
// facet name on the overview.
$this->assertRaw($this->t('The facet %facet has been deleted.', ['%facet' => $facet_name]));
// Refresh the page because on the previous page the $facet_name is still
// visible (in the message).
$facet_overview = '/admin/config/search/facets';
$this->drupalGet($facet_overview);
$this->assertResponse(200);
$this->assertNoText($facet_name);
}
/**
* Convert facet name to machine name.
*
* @param string $facet_name
* The name of the facet.
*
* @return string
* The facet name changed to a machine name.
*/
protected function convertNameToMachineName($facet_name) {
return preg_replace('@[^a-zA-Z0-9_]+@', '_', strtolower($facet_name));
}
/**
* Go to the Delete Facet Page using the facet name.
*
* @param string $facet_name
* The name of the facet.
*/
protected function goToDeleteFacetPage($facet_name) {
$facet_id = $this->convertNameToMachineName($facet_name);
$facet_delete_page = '/admin/config/search/facets/' . $facet_id . '/delete';
// Go to the facet delete page and make the warning is shown.
$this->drupalGet($facet_delete_page);
$this->assertResponse(200);
}
}
<?php
/**
* @file
* Contains \Drupal\core_search_facets\Tests\WebTestBase.
*/
namespace Drupal\core_search_facets\Tests;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\simpletest\WebTestBase as SimpletestWebTestBase;
/**
* Provides the base class for web tests for Core Search Facets.
*/
abstract class WebTestBase extends SimpletestWebTestBase {
use StringTranslationTrait;
/**
* Modules to enable for this test.
*
* @var string[]
*/
public static $modules = [
'field',
'search',
'entity_test',
'views',
'node',
'facets',
'block',
'core_search_facets',
];
/**
* An admin user used for this test.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $adminUser;
/**
* A user without Search / Facet admin permission.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $unauthorizedUser;
/**
* The anonymous user used for this test.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $anonymousUser;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
// Create content types.
$this->drupalCreateContentType(['type' => 'page']);
$this->drupalCreateContentType(['type' => 'article']);
// Adding 10 pages.
for ($i = 0; $i < 10; $i++) {
$this->drupalCreateNode(array(
'title' => 'foo bar' . $i,
'body' => 'test page' . $i,
'type' => 'page',
));
}
// Adding 10 articles.
for ($i = 0; $i < 10; $i++) {
$this->drupalCreateNode(array(
'title' => 'foo baz' . $i,
'body' => 'test article' . $i,
'type' => 'article',
));
}
// Create the users used for the tests.
$this->adminUser = $this->drupalCreateUser([
'administer search',
'administer facets',
'access administration pages',
'administer nodes',
'access content overview',
'administer content types',
'administer blocks',
'search content',
]);
}
}
name: 'Core Search Facets Hooks Test'
type: module
description: 'Support module for core_search_facets tests, tests all the hooks.'
package: Testing
dependencies:
- facets
- core_search_facets
core: 8.x
hidden: true
<?php
/**
* @file
* Tests all the hooks defined by the core_search_facets module.
*/
/**
* Implements hook_facets_core_allowed_field_types().
*/
function core_search_facets_test_hooks_facets_core_allowed_field_types(array $allowed_field_types) {
$allowed_field_types[] = 'float';
return $allowed_field_types;
}
<?php
/**
* @file
* Hooks provided by the Facet API module.
* Hooks provided by the Facets module.
*/
/**
......@@ -10,7 +10,7 @@
*/
/**
* Alter the Facet API Query Type mapping.
* Alter the Facets Query Type mapping.
*
* Modules may implement this hook to alter the mapping that defines how a
* certain data type should be handled in Search API based Facets.
......
......@@ -3,3 +3,7 @@ type: module
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
drupal.facets.index-active-formatters:
version: VERSION
js:
js/index-active-formatters.js: {}
dependencies:
- core/jquery
- core/drupal
- core/jquery.once
drupal.facets.admin_css:
version: VERSION
css:
......