Commit 10a19315 authored by ekes's avatar ekes Committed by borisson_

Issue #2796569 by borisson_, slasher13, ChristianAdamski, ekes: New Dependent Facet processor

parent 859139dc
<?php
namespace Drupal\facets\Plugin\Condition;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\Condition\ConditionPluginBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\facets\FacetManager\DefaultFacetManager;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides an 'other facet' condition.
*
* This adds a condition plugin to make sure that facets can depend on other
* facet's or their values. The facet value is a freeform textfield and works on
* both raw and display values of the results.
*
* @Condition(
* id = "other_facet",
* label = @Translation("Other facet"),
* )
*/
class OtherFacet extends ConditionPluginBase implements ContainerFactoryPluginInterface {
/**
* The facet entity storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $facetStorage;
/**
* The block plugin manager.
*
* @var \Drupal\Core\Block\BlockManagerInterface
*/
protected $blockManager;
/**
* The user that's currently logged in.
*
* @var \Drupal\Core\Session\AccountProxyInterface
*/
protected $currentUser;
/**
* The facet manager service.
*
* @var \Drupal\facets\FacetManager\DefaultFacetManager
*/
protected $facetManager;
/**
* Creates a new instance of the condition.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
* The entity storage.
* @param \Drupal\Core\Block\BlockManagerInterface $block_manager
* The block plugin manager.
* @param \Drupal\Core\Session\AccountProxyInterface $current_user
* The currently logged in user.
* @param \Drupal\facets\FacetManager\DefaultFacetManager $facet_manager
* The default facet manager class.
* @param array $configuration
* The plugin configuration, an array with configuration values keyed by
* configuration option name. The special key 'context' may be used to
* initialize the defined contexts by setting it to an array of context
* values keyed by context names.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
*/
public function __construct(EntityStorageInterface $entity_storage, BlockManagerInterface $block_manager, AccountProxyInterface $current_user, DefaultFacetManager $facet_manager, array $configuration, $plugin_id, $plugin_definition) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->facetStorage = $entity_storage;
$this->blockManager = $block_manager;
$this->currentUser = $current_user;
$this->facetManager = $facet_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$container->get('entity_type.manager')->getStorage('facets_facet'),
$container->get('plugin.manager.block'),
$container->get('current_user'),
$container->get('facets.manager'),
$configuration,
$plugin_id,
$plugin_definition
);
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$options = [];
// Loop over all defined blocks and filter them by provider, this builds an
// array of blocks that are provided by the facets module.
foreach ($this->blockManager->getDefinitions() as $definition) {
if ($definition['provider'] == 'facets') {
$options[$definition['id']] = $definition['label'];
}
}
$form['facets'] = [
'#title' => $this->t('Other facet blocks'),
'#type' => 'radios',
'#options' => $options,
'#default_value' => $this->configuration['facets'],
];
$form['facet_value'] = [
'#title' => $this->t('Facet value'),
'#description' => $this->t('Only applies when a facet is already selected.'),
'#type' => 'textfield',
'#default_value' => $this->configuration['facet_value'],
];
return parent::buildConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['facets'] = $form_state->getValue('facets');
$this->configuration['facet_value'] = $form_state->getValue('facet_value');
parent::submitConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function summary() {
return $this->t(
'The facet is @facet also rendered on the same page.',
['@facet' => $this->configuration['facets']]
);
}
/**
* {@inheritdoc}
*/
public function evaluate() {
$allowed_facet_value = $this->configuration['facet_value'];
$allowed_facets = $this->configuration['facets'];
// Return as early as possible when there are no settings for allowed
// facets.
if (empty($allowed_facets)) {
return TRUE;
}
/** @var \Drupal\facets\Plugin\Block\FacetBlock $block_plugin */
$block_plugin = $this->blockManager->createInstance($allowed_facets);
// Allowed facet value is not set, so we only have to check if the block is
// shown here by running the access method on the block plugin with the
// currently logged in user.
if (empty($allowed_facet_value)) {
return $block_plugin->access($this->currentUser);
}
// The block plugin id is saved in the schema: BasePluginID:FacetID. This
// means we can explode the ID on ':' and the facet id is in the last part
// of that result.
$block_plugin_id = $block_plugin->getPluginId();
$facet_id = explode(PluginBase::DERIVATIVE_SEPARATOR, $block_plugin_id)[1];
/** @var \Drupal\facets\FacetInterface $facet */
$facet = $this->facetStorage->load($facet_id);
$facet = $this->facetManager->returnProcessedFacet($facet);
foreach ($facet->getResults() as $result) {
$is_value = $result->getRawValue() == $allowed_facet_value || $result->getDisplayValue() == $allowed_facet_value;
if ($is_value && $result->isActive()) {
return TRUE;
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
$config = [
'facets' => '',
'facet_value' => '',
];
return $config + parent::defaultConfiguration();
}
}
<?php
namespace Drupal\facets\Plugin\facets\processor;
use Drupal\Core\Form\FormStateInterface;
use Drupal\facets\FacetInterface;
use Drupal\facets\Processor\BuildProcessorInterface;
use Drupal\facets\Processor\ProcessorPluginBase;
use Drupal\facets\FacetManager\DefaultFacetManager;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a processor that makes a facet depend on the state of another facet.
*
* @FacetsProcessor(
* id = "dependent_processor",
* label = @Translation("Dependent Facet"),
* description = @Translation("Make this facet depend on the state of another facet."),
* stages = {
* "build" = 5
* }
* )
*/
class DependentFacetProcessor extends ProcessorPluginBase implements BuildProcessorInterface, ContainerFactoryPluginInterface {
/**
* The language manager.
*
* @var \Drupal\facets\FacetManager\DefaultFacetManager
*/
protected $facetsManager;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $facetStorage;
/**
* Constructs a new object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\facets\FacetManager\DefaultFacetManager $facets_manager
* The language manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, DefaultFacetManager $facets_manager, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->facetsManager = $facets_manager;
$this->facetStorage = $entity_type_manager->getStorage('facets_facet');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('facets.manager'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state, FacetInterface $current_facet) {
$build = [];
$processors = $current_facet->getProcessors();
$config = isset($processors[$this->getPluginId()]) ? $processors[$this->getPluginId()]->getConfiguration() : NULL;
// Loop over all defined blocks and filter them by provider, this builds an
// array of blocks that are provided by the facets module.
/** @var \Drupal\facets\Entity\Facet[] $facets */
$facets = $this->facetStorage->loadMultiple();
foreach ($facets as $facet) {
if ($facet->getFacetSourceId() !== $current_facet->getFacetSourceId()) {
continue;
}
if ($facet->id() === $current_facet->id()) {
continue;
}
$build[$facet->id()]['label'] = [
'#title' => $facet->getName(),
'#type' => 'label',
];
$build[$facet->id()]['enable'] = [
'#title' => $this->t('Enable condition'),
'#type' => 'checkbox',
'#default_value' => !empty($config[$facet->id()]['enable']),
];
$build[$facet->id()]['condition'] = [
'#title' => $this->t('Condition mode'),
'#type' => 'radios',
'#options' => [
'presence' => $this->t('Check for facet being present'),
'not_empty' => $this->t('Check for facet being selected / not empty'),
'values' => $this->t('Check for facet being set to specific values'),
],
'#default_value' => empty($config[$facet->id()]['condition']) ? NULL : $config[$facet->id()]['condition'],
'#states' => [
'visible' => [
':input[name="facet_settings[' . $this->getPluginId() . '][settings][' . $facet->id() . '][enable]"]' => ['checked' => TRUE],
],
],
];
$build[$facet->id()]['values'] = [
'#title' => $this->t('Values'),
'#type' => 'textfield',
'#default_value' => empty($config[$facet->id()]['values']) ? '' : $config[$facet->id()]['values'],
'#states' => [
'visible' => [
':input[name="facet_settings[' . $this->getPluginId() . '][settings][' . $facet->id() . '][enable]"]' => ['checked' => TRUE],
':input[name="facet_settings[' . $this->getPluginId() . '][settings][' . $facet->id() . '][condition]"]' => ['value' => 'values'],
],
],
];
$build[$facet->id()]['negate'] = [
'#title' => $this->t('Negate condition'),
'#type' => 'checkbox',
'#default_value' => !empty($config[$facet->id()]['negate']),
'#states' => [
'visible' => [
':input[name="facet_settings[' . $this->getPluginId() . '][settings][' . $facet->id() . '][enable]"]' => ['checked' => TRUE],
],
],
];
}
return parent::buildConfigurationForm($form, $form_state, $current_facet) + $build;
}
/**
* {@inheritdoc}
*/
public function build(FacetInterface $facet, array $results) {
$conditions = $this->getConfiguration();
foreach ($conditions as $facet_id => $condition) {
if (empty($condition['enable'])) {
continue;
}
$enabled_conditions[$facet_id] = $condition;
}
// Return as early as possible when there are no settings for allowed
// facets.
if (empty($enabled_conditions)) {
return $results;
}
$return = TRUE;
foreach ($enabled_conditions as $facet_id => $condition_settings) {
/** @var \Drupal\facets\Entity\Facet $current_facet */
$current_facet = $this->facetStorage->load($facet_id);
$current_facet = $this->facetsManager->returnProcessedFacet($current_facet);
if ($condition_settings['condition'] == 'not_empty') {
$return = !empty($current_facet->getActiveItems());
}
if ($condition_settings['condition'] == 'values') {
$return = FALSE;
$values = explode(',', $condition_settings['values']);
foreach ($current_facet->getResults() as $result) {
$isActive = $result->isActive();
$raw_value_in_expected = in_array($result->getRawValue(), $values);
$display_value_in_expected = in_array($result->getDisplayValue(), $values);
if ($isActive && ($raw_value_in_expected || $display_value_in_expected)) {
$return = TRUE;
}
}
}
if (!empty($condition_settings['negate'])) {
$return = !$return;
}
}
return $return ? $results : [];
}
}
......@@ -238,13 +238,14 @@ class IntegrationTest extends FacetsTestBase {
$this->assertFacetBlocksAppear();
// Change the visiblity settings of the DependingFacet.
$this->drupalGet('admin/structure/block/manage/dependingfacet');
$this->drupalGet('admin/config/search/facets/' . $depending_facet_id . '/edit');
$edit = [
'visibility[other_facet][facets]' => 'facet_block:dependablefacet',
'visibility[other_facet][facet_value]' => 'item',
'facet_settings[dependent_processor][status]' => TRUE,
'facet_settings[dependent_processor][settings][' . $facet_id . '][enable]' => TRUE,
'facet_settings[dependent_processor][settings][' . $facet_id . '][condition]' => 'values',
'facet_settings[dependent_processor][settings][' . $facet_id . '][values]' => 'item',
];
$this->drupalPostForm(NULL, $edit, 'Save block');
$this->assertText('The block configuration has been saved.');
$this->drupalPostForm(NULL, $edit, 'Save');
// Go to the view and test that only the types are shown.
$this->drupalGet('search-api-test-fulltext');
......@@ -266,13 +267,15 @@ class IntegrationTest extends FacetsTestBase {
$this->assertNoLink('orange');
// Change the visibility settings to negate the previous settings.
$this->drupalGet('admin/structure/block/manage/dependingfacet');
$this->drupalGet('admin/config/search/facets/' . $depending_facet_id . '/edit');
$edit = [
'visibility[other_facet][facets]' => 'facet_block:dependablefacet',
'visibility[other_facet][facet_value]' => 'item',
'visibility[other_facet][negate]' => TRUE,
'facet_settings[dependent_processor][status]' => TRUE,
'facet_settings[dependent_processor][settings][' . $facet_id . '][enable]' => TRUE,
'facet_settings[dependent_processor][settings][' . $facet_id . '][condition]' => 'values',
'facet_settings[dependent_processor][settings][' . $facet_id . '][values]' => 'item',
'facet_settings[dependent_processor][settings][' . $facet_id . '][negate]' => TRUE,
];
$this->drupalPostForm(NULL, $edit, 'Save block');
$this->drupalPostForm(NULL, $edit, 'Save');
// Go the the view and test only the type facet is shown.
$this->drupalGet('search-api-test-fulltext');
......
<?php
namespace Drupal\Tests\facets\Unit\Plugin\Condition;
use Drupal\facets\Entity\Facet;
use Drupal\facets\Plugin\Condition\OtherFacet;
use Drupal\facets\Result\Result;
use Drupal\Tests\UnitTestCase;
/**
* Unit test for the 'other facet' condition plugin.
*
* @group facets
*/
class OtherFacetTest extends UnitTestCase {
/**
* Tests what happens when no values are passed on to the plugin.
*/
public function testNoValue() {
$storage = $this->getMock('\Drupal\Core\Entity\EntityStorageInterface');
$manager = $this->getMockBuilder('\Drupal\Core\Block\BlockManager')
->disableOriginalConstructor()
->getMock();
$user = $this->getMock('\Drupal\Core\Session\AccountProxyInterface');
$facet_manager = $this->getMockBuilder('\Drupal\facets\FacetManager\DefaultFacetManager')
->disableOriginalConstructor()
->getMock();
$sut = new OtherFacet($storage, $manager, $user, $facet_manager, [], 'other_facet', '');
$evaluation = $sut->evaluate();
$this->assertTrue($evaluation);
}
/**
* Tests the return value of the plugin for a displayed facet.
*/
public function testDisplayedFacet() {
$block = $this->getMockBuilder('\Drupal\facets\Plugin\Block\FacetBlock')
->disableOriginalConstructor()
->getMock();
$block->expects($this->exactly(1))
->method('access')
->willReturn(TRUE);
$storage = $this->getMock('\Drupal\Core\Entity\EntityStorageInterface');
$manager = $this->getMockBuilder('\Drupal\Core\Block\BlockManager')
->disableOriginalConstructor()
->getMock();
$manager->expects($this->exactly(1))
->method('createInstance')
->willReturn($block);
$user = $this->getMock('\Drupal\Core\Session\AccountProxyInterface');
$facet_manager = $this->getMockBuilder('\Drupal\facets\FacetManager\DefaultFacetManager')
->disableOriginalConstructor()
->getMock();
$sut = new OtherFacet($storage, $manager, $user, $facet_manager, ['facets' => 'test'], 'other_facet', '');
$evaluation = $sut->evaluate();
$this->assertTrue($evaluation);
}
/**
* Tests the return value of the plugin for a hidden facet.
*/
public function testHiddenFacet() {
$block = $this->getMockBuilder('\Drupal\facets\Plugin\Block\FacetBlock')
->disableOriginalConstructor()
->getMock();
$block->expects($this->exactly(1))
->method('access')
->willReturn(FALSE);
$storage = $this->getMock('\Drupal\Core\Entity\EntityStorageInterface');
$manager = $this->getMockBuilder('\Drupal\Core\Block\BlockManager')
->disableOriginalConstructor()
->getMock();
$manager->expects($this->exactly(1))
->method('createInstance')
->willReturn($block);
$user = $this->getMock('\Drupal\Core\Session\AccountProxyInterface');
$facet_manager = $this->getMockBuilder('\Drupal\facets\FacetManager\DefaultFacetManager')
->disableOriginalConstructor()
->getMock();
$sut = new OtherFacet($storage, $manager, $user, $facet_manager, ['facets' => 'test'], 'other_facet', '');
$evaluation = $sut->evaluate();
$this->assertFalse($evaluation);
}
/**
* Tests the return value of the plugin for an active facet value.
*/
public function testActiveFacetValue() {
$facet = new Facet([], 'facets_facet');
/** @var \Drupal\facets\Result\ResultInterface[] $results */
$results = [
new Result('llama', 'Llama', 1),
new Result('kitten', 'Kitten', 5),
new Result('puppy', 'Puppy', 3),
];
$results[0]->setActiveState(TRUE);
$facet->setResults($results);
$block = $this->getMockBuilder('\Drupal\facets\Plugin\Block\FacetBlock')
->disableOriginalConstructor()
->getMock();
$block->expects($this->exactly(0))->method('access');
$block->expects($this->exactly(1))
->method('getPluginId')
->willReturn('block:id');
$storage = $this->getMock('\Drupal\Core\Entity\EntityStorageInterface');
$storage->expects($this->exactly(1))
->method('load')
->with('id')
->willReturn($facet);
$manager = $this->getMockBuilder('\Drupal\Core\Block\BlockManager')
->disableOriginalConstructor()
->getMock();
$manager->expects($this->exactly(1))
->method('createInstance')
->willReturn($block);
$user = $this->getMock('\Drupal\Core\Session\AccountProxyInterface');
$facet_manager = $this->getMockBuilder('\Drupal\facets\FacetManager\DefaultFacetManager')
->disableOriginalConstructor()
->getMock();
$facet_manager->expects($this->exactly(1))
->method('returnProcessedFacet')
->with($facet)
->willReturn($facet);
$configuration = ['facets' => 'test', 'facet_value' => 'llama'];
$sut = new OtherFacet($storage, $manager, $user, $facet_manager, $configuration, 'other_facet', '');
$evaluation = $sut->evaluate();
$this->assertTrue($evaluation);
}
/**
* Tests the return value of the plugin for an inactive facet value.
*/
public function testInactiveFacetValue() {
$facet = new Facet([], 'facets_facet');
/** @var \Drupal\facets\Result\ResultInterface[] $results */
$results = [
new Result('llama', 'Llama', 1),
new Result('kitten', 'Kitten', 5),
new Result('puppy', 'Puppy', 3),
];
$results[1]->setActiveState(TRUE);
$facet->setResults($results);
$block = $this->getMockBuilder('\Drupal\facets\Plugin\Block\FacetBlock')
->disableOriginalConstructor()
->getMock();
$block->expects($this->exactly(0))->method('access');
$block->expects($this->exactly(1))
->method('getPluginId')
->willReturn('block:id');
$storage = $this->getMock('\Drupal\Core\Entity\EntityStorageInterface');
$storage->expects($this->exactly(1))
->method('load')
->with('id')
->willReturn($facet);
$manager = $this->getMockBuilder('\Drupal\Core\Block\BlockManager')
->disableOriginalConstructor()
->getMock();
$manager->expects($this->exactly(1))
->method('createInstance')
->willReturn($block);
$user = $this->getMock('\Drupal\Core\Session\AccountProxyInterface');
$facet_manager = $this->getMockBuilder('\Drupal\facets\FacetManager\DefaultFacetManager')
->disableOriginalConstructor()
->getMock();