diff --git a/core/modules/block/tests/modules/block_test_views/test_views/views.view.test_view_block_with_context.yml b/core/modules/block/tests/modules/block_test_views/test_views/views.view.test_view_block_with_context.yml new file mode 100644 index 0000000000000000000000000000000000000000..4f8a84115a2abece07760e4fc943504c7ffd3912 --- /dev/null +++ b/core/modules/block/tests/modules/block_test_views/test_views/views.view.test_view_block_with_context.yml @@ -0,0 +1,433 @@ +langcode: en +status: true +dependencies: + module: + - node + - user +id: test_view_block_with_context +label: test_view_block_with_context +module: views +description: '' +tag: '' +base_table: node_field_data +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: some + options: + items_per_page: 5 + offset: 0 + style: + type: default + row: + type: fields + fields: + title: + id: title + table: node_field_data + field: title + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: true + text: 'Test view row: {{ title }}' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: + link_to_entity: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + plugin_id: field + filters: + status: + value: '1' + table: node_field_data + field: status + plugin_id: boolean + entity_type: node + entity_field: status + id: status + expose: + operator: '' + group: 1 + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + entity_type: node + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + title: test_view_block_with_context + header: { } + footer: { } + empty: + area_text_custom: + id: area_text_custom + table: views + field: area_text_custom + relationship: none + group_type: group + admin_label: '' + empty: true + tokenize: false + content: 'Test view: No results found.' + plugin_id: text_custom + relationships: { } + arguments: + 'null': + id: 'null' + table: views + field: 'null' + relationship: none + group_type: group + admin_label: '' + default_action: default + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: fixed + default_argument_options: + argument: foo + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + must_not_be: false + plugin_id: 'null' + null_1: + id: null_1 + table: views + field: 'null' + relationship: none + group_type: group + admin_label: '' + default_action: ignore + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: fixed + default_argument_options: + argument: '' + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + must_not_be: false + plugin_id: 'null' + nid: + id: nid + table: node_field_data + field: nid + relationship: none + group_type: group + admin_label: '' + default_action: empty + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: fixed + default_argument_options: + argument: '' + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: true + validate: + type: 'entity:node' + fail: 'not found' + validate_options: + operation: view + multiple: 0 + bundles: { } + access: false + break_phrase: false + not: false + entity_type: node + entity_field: nid + plugin_id: node_nid + display_extenders: { } + cache_metadata: + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - 'user.node_grants:view' + - user.permissions + cacheable: false + max-age: -1 + tags: { } + block_1: + display_plugin: block + id: block_1 + display_title: Block + position: 1 + display_options: + display_extenders: { } + cache_metadata: + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - 'user.node_grants:view' + - user.permissions + cacheable: false + max-age: -1 + tags: { } + block_2: + display_plugin: block + id: block_2 + display_title: 'Block 2' + position: 2 + display_options: + display_extenders: { } + arguments: + created: + id: created + table: node_field_data + field: created + relationship: none + group_type: group + admin_label: '' + default_action: ignore + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: fixed + default_argument_options: + argument: '' + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: true + validate: + type: numeric + fail: 'not found' + validate_options: { } + entity_type: node + entity_field: created + plugin_id: date + vid: + id: vid + table: node_field_data + field: vid + relationship: none + group_type: group + admin_label: '' + default_action: ignore + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: fixed + default_argument_options: + argument: '' + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + break_phrase: false + not: false + entity_type: node + entity_field: vid + plugin_id: numeric + title: + id: title + table: node_field_data + field: title + relationship: none + group_type: group + admin_label: '' + default_action: ignore + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: fixed + default_argument_options: + argument: '' + default_argument_skip_url: false + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + glossary: false + limit: 0 + case: none + path_case: none + transform_dash: false + break_phrase: false + entity_type: node + entity_field: title + plugin_id: string + defaults: + arguments: false + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - 'user.node_grants:view' + - user.permissions + tags: { } diff --git a/core/modules/views/src/Plugin/Block/ViewsBlock.php b/core/modules/views/src/Plugin/Block/ViewsBlock.php index ad8d7c9148e50ae9926a343e6c9b6737abeed7f5..34116fd55024b2b0e59d8fa2406395aefccc9baf 100644 --- a/core/modules/views/src/Plugin/Block/ViewsBlock.php +++ b/core/modules/views/src/Plugin/Block/ViewsBlock.php @@ -5,6 +5,7 @@ use Drupal\Component\Utility\Xss; use Drupal\Core\Form\FormStateInterface; use Drupal\views\Element\View; +use Drupal\Core\Entity\EntityInterface; /** * Provides a generic Views block. @@ -23,10 +24,31 @@ class ViewsBlock extends ViewsBlockBase { public function build() { $this->view->display_handler->preBlockBuild($this); + $args = []; + foreach ($this->view->display_handler->getHandlers('argument') as $argument_name => $argument) { + // Initialize the argument value. Work around a limitation in + // \Drupal\views\ViewExecutable::_buildArguments() that skips processing + // later arguments if an argument with default action "ignore" and no + // argument is provided. + $args[$argument_name] = $argument->options['default_action'] == 'ignore' ? 'all' : NULL; + + if (!empty($this->context[$argument_name])) { + if ($value = $this->context[$argument_name]->getContextValue()) { + + // Context values are often entities, but views arguments expect to + // receive just the entity ID, convert it. + if ($value instanceof EntityInterface) { + $value = $value->id(); + } + $args[$argument_name] = $value; + } + } + } + // We ask ViewExecutable::buildRenderable() to avoid creating a render cache // entry for the view output by passing FALSE, because we're going to cache // the whole block instead. - if ($output = $this->view->buildRenderable($this->displayID, [], FALSE)) { + if ($output = $this->view->buildRenderable($this->displayID, array_values($args), FALSE)) { // Before returning the block output, convert it to a renderable array // with contextual links. $this->addContextualLinks($output); diff --git a/core/modules/views/src/Plugin/Derivative/ViewsBlock.php b/core/modules/views/src/Plugin/Derivative/ViewsBlock.php index 2e0c1647996be0f7324d46f9aa99c7506d83ef23..c092a28ddac8285dc22f619795e31984bc85c897 100644 --- a/core/modules/views/src/Plugin/Derivative/ViewsBlock.php +++ b/core/modules/views/src/Plugin/Derivative/ViewsBlock.php @@ -82,6 +82,7 @@ public function getDerivativeDefinitions($base_plugin_definition) { $executable = $view->getExecutable(); $executable->initDisplay(); foreach ($executable->displayHandlers as $display) { + /** @var \Drupal\views\Plugin\views\display\DisplayPluginInterface $display */ // Add a block plugin definition for each block display. if (isset($display) && !empty($display->definition['uses_hook_block'])) { $delta = $view->id() . '-' . $display->display['id']; @@ -106,9 +107,18 @@ public function getDerivativeDefinitions($base_plugin_definition) { 'config_dependencies' => array( 'config' => array( $view->getConfigDependencyName(), - ) - ) + ), + ), ); + + // Look for arguments and expose them as context. + foreach ($display->getHandlers('argument') as $argument_name => $argument) { + /** @var \Drupal\views\Plugin\views\argument\ArgumentPluginBase $argument */ + if ($context_definition = $argument->getContextDefinition()) { + $this->derivatives[$delta]['context'][$argument_name] = $context_definition; + } + } + $this->derivatives[$delta] += $base_plugin_definition; } } diff --git a/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php b/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php index 398d874cbaf333eb30baa6b30a83b8adaf1a2544..6b8105b106c135bf32671cb8fe31db9aa87a2510 100644 --- a/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php +++ b/core/modules/views/src/Plugin/views/argument/ArgumentPluginBase.php @@ -1326,6 +1326,17 @@ public function calculateDependencies() { return $dependencies; } + /** + * Returns a context definition for this argument. + * + * @return \Drupal\Core\Plugin\Context\ContextDefinitionInterface|null + * A context definition that represents the argument or NULL if that is + * not possible. + */ + public function getContextDefinition() { + return $this->getPlugin('argument_validator')->getContextDefinition(); + } + } /** diff --git a/core/modules/views/src/Plugin/views/argument/NumericArgument.php b/core/modules/views/src/Plugin/views/argument/NumericArgument.php index b3148369a780a2f4aabd61ba6fca4c7495611cfc..c2a829366015e47eae50b7c6bb0261706d0e1b80 100644 --- a/core/modules/views/src/Plugin/views/argument/NumericArgument.php +++ b/core/modules/views/src/Plugin/views/argument/NumericArgument.php @@ -3,6 +3,7 @@ namespace Drupal\views\Plugin\views\argument; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\Context\ContextDefinition; /** * Basic argument handler for arguments that are numeric. Incorporates @@ -124,4 +125,17 @@ public function getSortName() { return $this->t('Numerical', array(), array('context' => 'Sort order')); } + /** + * {@inheritdoc} + */ + public function getContextDefinition() { + if ($context_definition = parent::getContextDefinition()) { + return $context_definition; + } + + // If the parent does not provide a context definition through the + // validation plugin, fall back to the integer type. + return new ContextDefinition('integer', $this->adminLabel(), FALSE); + } + } diff --git a/core/modules/views/src/Plugin/views/argument/StringArgument.php b/core/modules/views/src/Plugin/views/argument/StringArgument.php index 085589ab004995471aadfbbbd22968ff18b353ef..e138c1d2725518f0f031e5b4cb4f3c452818a4ef 100644 --- a/core/modules/views/src/Plugin/views/argument/StringArgument.php +++ b/core/modules/views/src/Plugin/views/argument/StringArgument.php @@ -5,6 +5,7 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Database\Database; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\Context\ContextDefinition; use Drupal\views\ViewExecutable; use Drupal\views\Plugin\views\display\DisplayPluginBase; use Drupal\views\ManyToOneHelper; @@ -319,4 +320,17 @@ public function summaryName($data) { return $this->caseTransform(parent::summaryName($data), $this->options['case']); } + /** + * {@inheritdoc} + */ + public function getContextDefinition() { + if ($context_definition = parent::getContextDefinition()) { + return $context_definition; + } + + // If the parent does not provide a context definition through the + // validation plugin, fall back to the string type. + return new ContextDefinition('string', $this->adminLabel(), FALSE); + } + } diff --git a/core/modules/views/src/Plugin/views/argument_validator/ArgumentValidatorPluginBase.php b/core/modules/views/src/Plugin/views/argument_validator/ArgumentValidatorPluginBase.php index 109af50952450c6e11f7ae578518361924ce0c1f..c1d41d494ab10dc43036712aa38aad9447481e4e 100644 --- a/core/modules/views/src/Plugin/views/argument_validator/ArgumentValidatorPluginBase.php +++ b/core/modules/views/src/Plugin/views/argument_validator/ArgumentValidatorPluginBase.php @@ -104,6 +104,15 @@ public function validateArgument($arg) { return TRUE; } */ public function processSummaryArguments(&$args) { } + /** + * Returns a context definition for this argument. + * + * @return \Drupal\Core\Plugin\Context\ContextDefinitionInterface|null + * A context definition that represents the argument or NULL if that is + * not possible. + */ + public function getContextDefinition() { } + } /** diff --git a/core/modules/views/src/Plugin/views/argument_validator/Entity.php b/core/modules/views/src/Plugin/views/argument_validator/Entity.php index 53e29d7ba3ffdf194736eb4903eef0c1b9e8d398..5a963bab35a878f45cc78a7e12014b23594834d3 100644 --- a/core/modules/views/src/Plugin/views/argument_validator/Entity.php +++ b/core/modules/views/src/Plugin/views/argument_validator/Entity.php @@ -5,6 +5,7 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Plugin\Context\ContextDefinition; use Drupal\views\Plugin\views\argument\ArgumentPluginBase; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -228,4 +229,11 @@ public function calculateDependencies() { return $dependencies; } + /** + * {@inheritdoc} + */ + public function getContextDefinition() { + return new ContextDefinition('entity:' . $this->definition['entity_type'], $this->argument->adminLabel(), FALSE); + } + } diff --git a/core/modules/views/src/Plugin/views/argument_validator/NumericArgumentValidator.php b/core/modules/views/src/Plugin/views/argument_validator/NumericArgumentValidator.php index a3184f4f93a78eaf565ced5953a03c5f5e8d591f..7368e06286dd3ede0cb65022ebbe3c636e7d2e96 100644 --- a/core/modules/views/src/Plugin/views/argument_validator/NumericArgumentValidator.php +++ b/core/modules/views/src/Plugin/views/argument_validator/NumericArgumentValidator.php @@ -2,6 +2,8 @@ namespace Drupal\views\Plugin\views\argument_validator; +use Drupal\Core\Plugin\Context\ContextDefinition; + /** * Validate whether an argument is numeric or not. * @@ -18,4 +20,11 @@ public function validateArgument($argument) { return is_numeric($argument); } + /** + * {@inheritdoc} + */ + public function getContextDefinition() { + return new ContextDefinition('integer', $this->argument->adminLabel(), FALSE); + } + } diff --git a/core/modules/views/src/Tests/Plugin/ContextualFiltersBlockContextTest.php b/core/modules/views/src/Tests/Plugin/ContextualFiltersBlockContextTest.php new file mode 100644 index 0000000000000000000000000000000000000000..7fb70580a274b3bce9aef52ddbbd235b179df8a3 --- /dev/null +++ b/core/modules/views/src/Tests/Plugin/ContextualFiltersBlockContextTest.php @@ -0,0 +1,153 @@ +<?php + +namespace Drupal\views\Tests\Plugin; + +use Drupal\Core\Plugin\Context\ContextDefinitionInterface; +use Drupal\views\Tests\ViewTestData; +use Drupal\views\Tests\ViewTestBase; +use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait; + +/** + * A test for contextual filters exposed as block context. + * + * @group views + */ +class ContextualFiltersBlockContextTest extends ViewTestBase { + + use AssertPageCacheContextsAndTagsTrait; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = ['block', 'block_test_views', 'views_ui', 'node']; + + /** + * Views used by this test. + * + * @var array + */ + public static $testViews = ['test_view_block_with_context']; + + /** + * Test node type. + * + * @var \Drupal\node\NodeTypeInterface + */ + protected $nodeType; + + /** + * Test nodes. + * + * @var \Drupal\node\NodeInterface[] + */ + protected $nodes; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + + ViewTestData::createTestViews(get_class($this), ['block_test_views']); + $this->enableViewsTestModule(); + + $this->nodeType = $this->container->get('entity_type.manager') + ->getStorage('node_type') + ->create([ + 'name' => 'Test node type', + 'type' => 'test', + ]); + $this->nodeType->save(); + + $this->nodes[0] = $this->container->get('entity_type.manager') + ->getStorage('node') + ->create(['type' => $this->nodeType->id(), 'title' => 'First test node']); + $this->nodes[0]->save(); + + $this->nodes[1] = $this->container->get('entity_type.manager') + ->getStorage('node') + ->create(['type' => $this->nodeType->id(), 'title' => 'Second test node']); + $this->nodes[1]->save(); + } + + /** + * Tests exposed context. + */ + public function testBlockContext() { + $this->drupalLogin($this->drupalCreateUser(['administer views', 'administer blocks'])); + + // Check if context was correctly propagated to the block. + $definition = $this->container->get('plugin.manager.block') + ->getDefinition('views_block:test_view_block_with_context-block_1'); + $this->assertTrue($definition['context']['nid'] instanceof ContextDefinitionInterface); + /** @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context */ + $context = $definition['context']['nid']; + $this->assertEqual($context->getDataType(), 'entity:node', 'Context definition data type is correct.'); + $this->assertEqual($context->getLabel(), 'Content: ID', 'Context definition label is correct.'); + $this->assertFalse($context->isRequired(), 'Context is not required.'); + + // Place test block via block UI to check if contexts are correctly exposed. + $this->drupalGet( + 'admin/structure/block/add/views_block:test_view_block_with_context-block_1/classy', + ['query' => ['region' => 'content']] + ); + $edit = [ + 'settings[context_mapping][nid]' => '@node.node_route_context:node', + ]; + $this->drupalPostForm(NULL, $edit, 'Save block'); + + // Check if mapping saved correctly. + /** @var \Drupal\block\BlockInterface $block */ + $block = $this->container->get('entity_type.manager') + ->getStorage('block') + ->load('views_block__test_view_block_with_context_block_1'); + $expected_settings = [ + 'id' => 'views_block:test_view_block_with_context-block_1', + 'label' => '', + 'provider' => 'views', + 'label_display' => 'visible', + 'views_label' => '', + 'items_per_page' => 'none', + 'context_mapping' => ['nid' => '@node.node_route_context:node'] + ]; + $this->assertEqual($block->getPlugin()->getConfiguration(), $expected_settings, 'Block settings are correct.'); + + // Make sure view behaves as expected. + $this->drupalGet('<front>'); + $this->assertText('Test view: No results found.'); + + $this->drupalGet($this->nodes[0]->toUrl()); + $this->assertText('Test view row: First test node'); + + $this->drupalGet($this->nodes[1]->toUrl()); + $this->assertText('Test view row: Second test node'); + + // Check the second block which should expose two integer contexts, one + // based on the numeric plugin and the other based on numeric validation. + $definition = $this->container->get('plugin.manager.block') + ->getDefinition('views_block:test_view_block_with_context-block_2'); + $this->assertTrue($definition['context']['created'] instanceof ContextDefinitionInterface); + /** @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context */ + $context = $definition['context']['created']; + $this->assertEqual($context->getDataType(), 'integer', 'Context definition data type is correct.'); + $this->assertEqual($context->getLabel(), 'Content: Authored on', 'Context definition label is correct.'); + $this->assertFalse($context->isRequired(), 'Context is not required.'); + + $this->assertTrue($definition['context']['vid'] instanceof ContextDefinitionInterface); + /** @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context */ + $context = $definition['context']['vid']; + $this->assertEqual($context->getDataType(), 'integer', 'Context definition data type is correct.'); + $this->assertEqual($context->getLabel(), 'Content: Revision ID', 'Context definition label is correct.'); + $this->assertFalse($context->isRequired(), 'Context is not required.'); + + $this->assertTrue($definition['context']['title'] instanceof ContextDefinitionInterface); + /** @var \Drupal\Core\Plugin\Context\ContextDefinitionInterface $context */ + $context = $definition['context']['title']; + $this->assertEqual($context->getDataType(), 'string', 'Context definition data type is correct.'); + $this->assertEqual($context->getLabel(), 'Content: Title', 'Context definition label is correct.'); + $this->assertFalse($context->isRequired(), 'Context is not required.'); + } + +} diff --git a/core/modules/views/tests/src/Unit/Plugin/Block/ViewsBlockTest.php b/core/modules/views/tests/src/Unit/Plugin/Block/ViewsBlockTest.php index ef67a71876d62b7f83d9f5b03b3260b2eb7271ea..b269f9b292731e55a0f0a1f49c7400ee9739775b 100644 --- a/core/modules/views/tests/src/Unit/Plugin/Block/ViewsBlockTest.php +++ b/core/modules/views/tests/src/Unit/Plugin/Block/ViewsBlockTest.php @@ -111,6 +111,11 @@ protected function setUp() { $this->displayHandler->expects($this->any()) ->method('getPluginId') ->willReturn('block'); + + $this->displayHandler->expects($this->any()) + ->method('getHandlers') + ->willReturn([]); + $this->executable->display_handler = $this->displayHandler; $this->storage = $this->getMockBuilder('Drupal\Core\Config\Entity\ConfigEntityStorage')