diff --git a/src/FacetManager/DefaultFacetManager.php b/src/FacetManager/DefaultFacetManager.php index 688f9ad..0718e96 100644 --- a/src/FacetManager/DefaultFacetManager.php +++ b/src/FacetManager/DefaultFacetManager.php @@ -320,4 +320,18 @@ class DefaultFacetManager { $facet_source_plugin->fillFacetsWithResults($this->facets); } + + /** + * Makes sure facets are processed and returns the updated facet entity. + * + * @param string $facet_id + * The id of the facet. + * @return \Drupal\facets\FacetInterface|NULL + * The updated facet if it exists, NULL otherwise. + */ + public function returnProcessedFacet($facet_id) { + $this->processFacets(); + return $this->facets[$facet_id]; + } + } diff --git a/src/Plugin/Condition/OtherFacet.php b/src/Plugin/Condition/OtherFacet.php new file mode 100644 index 0000000..b9f1440 --- /dev/null +++ b/src/Plugin/Condition/OtherFacet.php @@ -0,0 +1,207 @@ +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 = []; + + 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() { + $facet = reset($this->configuration['facets']); + return $this->t('The facet is @facet also rendered on the same page.', ['@facet' => $facet]); + } + + /** + * {@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) && !$this->isNegated()) { + return TRUE; + } + if (empty($allowed_facets) && $this->isNegated()) { + return FALSE; + } + + /** @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. + if (empty($allowed_facet_value)) { + $access_result = $block_plugin->access($this->currentUser); + if ($this->isNegated()) { + // A negated configuration should return the inverse of the normal + // result. + return !($access_result); + } + return $access_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); + $this->facetManager->setFacetSourceId($facet->getFacetSourceId()); + + $facet = $this->facetManager->returnProcessedFacet($facet_id); + + if (!$this->isNegated()) { + 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' => FALSE, 'facet_value' => FALSE]; + return $config + parent::defaultConfiguration(); + } + +} diff --git a/src/Tests/IntegrationTest.php b/src/Tests/IntegrationTest.php index 2bf9636..7f4a6ec 100644 --- a/src/Tests/IntegrationTest.php +++ b/src/Tests/IntegrationTest.php @@ -34,6 +34,7 @@ class IntegrationTest extends FacetWebTestBase { $this->setUpExampleStructure(); $this->insertExampleContent(); $this->assertEqual($this->indexItems($this->indexId), 5, '5 items were indexed.'); + $this->blocks = NULL; } /** @@ -201,6 +202,88 @@ class IntegrationTest extends FacetWebTestBase { } /** + * Tests facet dependencies. + */ + public function testFacetDependencies() { + $facet_name = "DependableFacet"; + $facet_id = 'dependablefacet'; + $this->addFacet($facet_name); + + $depending_facet_name = "DependingFacet"; + $depending_facet_id = "dependingfacet"; + $this->addFacet($depending_facet_name, 'keywords'); + + // Create both facets as blocks and add them on the page. + $this->createFacetBlock($facet_id); + $this->createFacetBlock($depending_facet_id); + + // Go the the view and test that both facets are shown. Item and article + // come from the DependableFacet, orange and grape come from DependingFacet. + $this->drupalGet('search-api-test-fulltext'); + $this->assertLink('grape'); + $this->assertLink('orange'); + $this->assertLink('item'); + $this->assertLink('article'); + $this->assertFacetBlocksAppear(); + + // Change the visiblity settings of the DependingFacet. + $this->drupalGet('admin/structure/block/manage/dependingfacet'); + $edit = [ + 'visibility[other_facet][facets]' => 'facet_block:dependablefacet', + 'visibility[other_facet][facet_value]' => 'item', + ]; + $this->drupalPostForm(NULL, $edit, $this->t('Save block')); + $this->assertText('The block configuration has been saved.'); + + // Go to the view and test that only the types are shown. + $this->drupalGet('search-api-test-fulltext'); + $this->assertNoLink('grape'); + $this->assertNoLink('orange'); + $this->assertLink('item'); + $this->assertLink('article'); + + // Click on the item, and test that this shows the keywords. + $this->clickLink('item'); + $this->assertLink('grape'); + $this->assertLink('orange'); + + // Go back to the view, click on article and test that the keywords are + // hidden. + $this->drupalGet('search-api-test-fulltext'); + $this->clickLink('article'); + $this->assertNoLink('grape'); + $this->assertNoLink('orange'); + + // Change the visibility settings to negate the previous settings. + $this->drupalGet('admin/structure/block/manage/dependingfacet'); + $edit = [ + 'visibility[other_facet][facets]' => 'facet_block:dependablefacet', + 'visibility[other_facet][facet_value]' => 'item', + 'visibility[other_facet][negate]' => TRUE, + ]; + $this->drupalPostForm(NULL, $edit, $this->t('Save block')); + + // Go the the view and test only the type facet is shown. + $this->drupalGet('search-api-test-fulltext'); + $this->assertLink('item'); + $this->assertLink('article'); + $this->assertNoLink('grape'); + $this->assertNoLink('orange'); + + // Click on the article, and test that this shows the keywords. + $this->clickLink('article'); + $this->assertLink('grape'); + $this->assertLink('orange'); + + // Go back to the view, click on item and test that the keywords are + // hidden. + $this->drupalGet('search-api-test-fulltext'); + $this->clickLink('article'); + $this->assertNoLink('grape'); + $this->assertNoLink('orange'); + } + + /** * Deletes a facet block by id. * * @param string $id @@ -312,7 +395,7 @@ class IntegrationTest extends FacetWebTestBase { * @param string $facet_name * The name of the facet. */ - protected function addFacet($facet_name) { + protected function addFacet($facet_name, $facet_type = 'type') { $facet_id = $this->convertNameToMachineName($facet_name); // Go to the Add facet page and make sure that returns a 200. @@ -350,7 +433,7 @@ class IntegrationTest extends FacetWebTestBase { // longer shown. $facet_source_form = [ 'facet_source_id' => 'search_api_views:search_api_test_view:page_1', - 'facet_source_configs[search_api_views:search_api_test_view:page_1][field_identifier]' => 'type', + 'facet_source_configs[search_api_views:search_api_test_view:page_1][field_identifier]' => $facet_type, ]; $this->drupalPostForm(NULL, $form_values + $facet_source_form, $this->t('Save')); $this->assertNoText('field is required.'); @@ -362,7 +445,6 @@ class IntegrationTest extends FacetWebTestBase { $this->drupalGet('admin/config/search/facets'); } - /** * Tests editing of a facet through the UI. * diff --git a/tests/src/Unit/Plugin/Condition/OtherFacetTest.php b/tests/src/Unit/Plugin/Condition/OtherFacetTest.php new file mode 100644 index 0000000..b25edbb --- /dev/null +++ b/tests/src/Unit/Plugin/Condition/OtherFacetTest.php @@ -0,0 +1,179 @@ +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, ['negate' => $negated], 'other_facet', ''); + + $evaluation = $sut->evaluate(); + if ($negated) { + $this->assertFalse($evaluation); + } + else { + $this->assertTrue($evaluation); + } + } + + /** + * Displayed facet. + * + * @dataProvider provideNegated + */ + public function testDisplayedFacet($negated) { + $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', 'negate' => $negated], 'other_facet', ''); + + $evaluation = $sut->evaluate(); + if ($negated) { + $this->assertFalse($evaluation); + } + else { + $this->assertTrue($evaluation); + } + } + + /** + * Hidden facet. + * + * @dataProvider provideNegated + */ + public function testHiddenFacet($negated) { + $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', 'negate' => $negated], 'other_facet', ''); + + $evaluation = $sut->evaluate(); + if ($negated) { + $this->assertTrue($evaluation); + } + else { + $this->assertFalse($evaluation); + } + } + + /** + * Active facet value. + * + * @dataProvider provideNegated + */ + public function testActiveFacetValue($negated) { + $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('id') + ->willReturn($facet); + + $configuration = ['facets' => 'test', 'facet_value' => 'llama', 'negate' => $negated]; + $sut = new OtherFacet($storage, $manager, $user, $facet_manager, $configuration, 'other_facet', ''); + + $evaluation = $sut->evaluate(); + if ($negated) { + $this->assertFalse($evaluation); + } + else { + $this->assertTrue($evaluation); + } + } + + /** + * @return array + */ + public function provideNegated() { + return [ + 'normal' => [FALSE], + 'negated' => [TRUE], + ]; + } +}