Skip to content
Snippets Groups Projects
Commit 73312ef0 authored by Alex Pott's avatar Alex Pott
Browse files

Issue #2287073 by Berdir, slashrsm, Denchev, jibran, Thew, dasjo, andypost,...

Issue #2287073 by Berdir, slashrsm, Denchev, jibran, Thew, dasjo, andypost, Arla, zaporylie, marvin_B8, piyuesh23, EclipseGc: Allow views contextual filters to expose the context using argument validation plugins
parent 24a4d4b6
No related branches found
No related tags found
No related merge requests found
Showing
with 691 additions and 3 deletions
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: { }
......@@ -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);
......
......@@ -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;
}
}
......
......@@ -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();
}
}
/**
......
......@@ -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);
}
}
......@@ -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);
}
}
......@@ -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() { }
}
/**
......
......@@ -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);
}
}
......@@ -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);
}
}
<?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.');
}
}
......@@ -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')
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment