Commit 34698633 authored by catch's avatar catch

Issue #2451657 by Upchuk, tstoeckler, k4v, lokapujya, dawehner, willwh,...

Issue #2451657 by Upchuk, tstoeckler, k4v, lokapujya, dawehner, willwh, holist, yobottehg, Peacog, Sharique, plach, Berdir, deepakaryan1988, Xano, geertvd, pminf, alexpott, AkshayKalose, lauriii, mkernel, pritish.kumar, jibran: Views should not condition joins on the langcode of fields that are not translatable
parent f3f18a23
......@@ -1043,10 +1043,13 @@ protected function getTableMapping() {
*/
public function getValue(ResultRow $values, $field = NULL) {
$entity = $this->getEntity($values);
// Retrieve the translated object.
$translated_entity = $this->getEntityFieldRenderer()->getEntityTranslation($entity, $values);
// Some bundles might not have a specific field, in which case the entity
// (potentially a fake one) doesn't have it either.
/** @var \Drupal\Core\Field\FieldItemListInterface $field_item_list */
$field_item_list = isset($entity->{$this->definition['field_name']}) ? $entity->{$this->definition['field_name']} : NULL;
$field_item_list = isset($translated_entity->{$this->definition['field_name']}) ? $translated_entity->{$this->definition['field_name']} : NULL;
if (!isset($field_item_list)) {
// There isn't anything we can do without a valid field.
......
<?php
namespace Drupal\views\Plugin\views\join;
use Drupal\Core\Database\Query\SelectInterface;
/**
* Implementation for the "field OR language" join.
*
* If the extra conditions contain either ".langcode" or ".bundle", they will be
* grouped and joined with OR instead of AND. The entire group will then be
* joined to the other conditions with AND.
*
* This is needed for configurable fields that are translatable on some bundles
* and untranslatable on others. The correct field values to fetch in this case
* have a langcode that matches the entity record *or* have a bundle on which
* the field is untranslatable. Thus, the entity base table (or data table, or
* revision data table, respectively) must join the field data table (or field
* revision table) on a matching langcode *or* a bundle where the field is
* untranslatable. The following example views data achieves this for a node
* field named 'field_tags' which is translatable on an 'article' node type, but
* not on the 'news' and 'page' node types:
*
* @code
* $data['node__field_tags']['table']['join']['node_field_data'] = [
* 'join_id' => 'field_or_language_join',
* 'table' => 'node__field_tags',
* 'left_field' => 'nid',
* 'field' => 'entity_id',
* 'extra' => [
* [
* 'field' => 'deleted',
* 'value' => 0,
* 'numeric' => TRUE,
* ],
* [
* 'left_field' => 'langcode',
* 'field' => 'langcode',
* ],
* [
* 'field' => 'bundle',
* 'value' => ['news', 'page'],
* ],
* ],
* ];
* @endcode
*
* The resulting join condition for this example would be the following:
*
* @code
* ON node__field_tags.deleted = 0
* AND (
* node_field_data.langcode = node__field_tags.langcode
* OR node__field.tags.bundle IN ['news', 'page']
* )
* @endcode
*
* @see views_field_default_views_data()
*
* @ingroup views_join_handlers
*
* @ViewsJoin("field_or_language_join")
*/
class FieldOrLanguageJoin extends JoinPluginBase {
/**
* {@inheritdoc}
*/
protected function joinAddExtra(&$arguments, &$condition, $table, SelectInterface $select_query, $left_table = NULL) {
if (empty($this->extra)) {
return;
}
if (is_array($this->extra)) {
$extras = [];
foreach ($this->extra as $extra) {
$extras[] = $this->buildExtra($extra, $arguments, $table, $select_query, $left_table);
}
// Remove and store the langcode OR bundle join condition extra.
$language_bundle_conditions = [];
foreach ($extras as $key => $extra) {
if (strpos($extra, '.langcode') !== FALSE || strpos($extra, '.bundle') !== FALSE) {
$language_bundle_conditions[] = $extra;
unset($extras[$key]);
}
}
if (count($extras) > 1) {
$condition .= ' AND (' . implode(' ' . $this->extraOperator . ' ', $extras) . ')';
}
elseif ($extras) {
$condition .= ' AND ' . array_shift($extras);
}
// Tack on the langcode OR bundle join condition extra.
if (!empty($language_bundle_conditions)) {
$condition .= ' AND (' . implode(' OR ', $language_bundle_conditions) . ')';
}
}
elseif (is_string($this->extra)) {
$condition .= " AND ($this->extra)";
}
}
}
......@@ -2,6 +2,7 @@
namespace Drupal\views\Plugin\views\join;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Plugin\PluginBase;
/**
......@@ -261,12 +262,13 @@ public function buildJoin($select_query, $table, $view_query) {
}
if ($this->leftTable) {
$left = $view_query->getTableInfo($this->leftTable);
$left_field = "$left[alias].$this->leftField";
$left_table = $view_query->getTableInfo($this->leftTable);
$left_field = "$left_table[alias].$this->leftField";
}
else {
// This can be used if left_field is a formula or something. It should be used only *very* rarely.
$left_field = $this->leftField;
$left_table = NULL;
}
$condition = "$left_field = $table[alias].$this->field";
......@@ -274,89 +276,123 @@ public function buildJoin($select_query, $table, $view_query) {
// Tack on the extra.
if (isset($this->extra)) {
if (is_array($this->extra)) {
$extras = [];
foreach ($this->extra as $info) {
// Do not require 'value' to be set; allow for field syntax instead.
$info += [
'value' => NULL,
];
// Figure out the table name. Remember, only use aliases provided
// if at all possible.
$join_table = '';
if (!array_key_exists('table', $info)) {
$join_table = $table['alias'] . '.';
}
elseif (isset($info['table'])) {
// If we're aware of a table alias for this table, use the table
// alias instead of the table name.
if (isset($left) && $left['table'] == $info['table']) {
$join_table = $left['alias'] . '.';
}
else {
$join_table = $info['table'] . '.';
}
}
$this->joinAddExtra($arguments, $condition, $table, $select_query, $left_table);
}
// Convert a single-valued array of values to the single-value case,
// and transform from IN() notation to = notation
if (is_array($info['value']) && count($info['value']) == 1) {
$info['value'] = array_shift($info['value']);
}
if (is_array($info['value'])) {
// We use an SA-CORE-2014-005 conformant placeholder for our array
// of values. Also, note that the 'IN' operator is implicit.
// @see https://www.drupal.org/node/2401615.
$operator = !empty($info['operator']) ? $info['operator'] : 'IN';
$placeholder = ':views_join_condition_' . $select_query->nextPlaceholder() . '[]';
$placeholder_sql = "( $placeholder )";
}
else {
// With a single value, the '=' operator is implicit.
$operator = !empty($info['operator']) ? $info['operator'] : '=';
$placeholder = $placeholder_sql = ':views_join_condition_' . $select_query->nextPlaceholder();
}
// Set 'field' as join table field if available or set 'left field' as
// join table field is not set.
if (isset($info['field'])) {
$join_table_field = "$join_table$info[field]";
// Allow the value to be set either with the 'value' element or
// with 'left_field'.
if (isset($info['left_field'])) {
$placeholder_sql = "$left[alias].$info[left_field]";
}
else {
$arguments[$placeholder] = $info['value'];
}
}
// Set 'left field' as join table field is not set.
else {
$join_table_field = "$left[alias].$info[left_field]";
$arguments[$placeholder] = $info['value'];
}
// Render out the SQL fragment with parameters.
$extras[] = "$join_table_field $operator $placeholder_sql";
}
$select_query->addJoin($this->type, $right_table, $table['alias'], $condition, $arguments);
}
/**
* Adds the extras to the join condition.
*
* @param array $arguments
* Array of query arguments.
* @param string $condition
* The condition to be built.
* @param array $table
* The right table.
* @param \Drupal\Core\Database\Query\SelectInterface $select_query
* The current select query being built.
* @param array $left_table
* The left table.
*/
protected function joinAddExtra(&$arguments, &$condition, $table, SelectInterface $select_query, $left_table = NULL) {
if (is_array($this->extra)) {
$extras = [];
foreach ($this->extra as $info) {
$extras[] = $this->buildExtra($info, $arguments, $table, $select_query, $left_table);
}
if ($extras) {
if (count($extras) == 1) {
$condition .= ' AND ' . array_shift($extras);
}
else {
$condition .= ' AND (' . implode(' ' . $this->extraOperator . ' ', $extras) . ')';
}
if ($extras) {
if (count($extras) == 1) {
$condition .= ' AND ' . array_shift($extras);
}
else {
$condition .= ' AND (' . implode(' ' . $this->extraOperator . ' ', $extras) . ')';
}
}
elseif ($this->extra && is_string($this->extra)) {
$condition .= " AND ($this->extra)";
}
elseif ($this->extra && is_string($this->extra)) {
$condition .= " AND ($this->extra)";
}
}
/**
* Builds a single extra condition.
*
* @param array $info
* The extra information. See JoinPluginBase::$extra for details.
* @param array $arguments
* Array of query arguments.
* @param array $table
* The right table.
* @param \Drupal\Core\Database\Query\SelectInterface $select_query
* The current select query being built.
* @param array $left
* The left table.
*
* @return string
* The extra condition
*/
protected function buildExtra($info, &$arguments, $table, SelectInterface $select_query, $left) {
// Do not require 'value' to be set; allow for field syntax instead.
$info += [
'value' => NULL,
];
// Figure out the table name. Remember, only use aliases provided
// if at all possible.
$join_table = '';
if (!array_key_exists('table', $info)) {
$join_table = $table['alias'] . '.';
}
elseif (isset($info['table'])) {
// If we're aware of a table alias for this table, use the table
// alias instead of the table name.
if (isset($left) && $left['table'] == $info['table']) {
$join_table = $left['alias'] . '.';
}
else {
$join_table = $info['table'] . '.';
}
}
$select_query->addJoin($this->type, $right_table, $table['alias'], $condition, $arguments);
// Convert a single-valued array of values to the single-value case,
// and transform from IN() notation to = notation
if (is_array($info['value']) && count($info['value']) == 1) {
$info['value'] = array_shift($info['value']);
}
if (is_array($info['value'])) {
// We use an SA-CORE-2014-005 conformant placeholder for our array
// of values. Also, note that the 'IN' operator is implicit.
// @see https://www.drupal.org/node/2401615.
$operator = !empty($info['operator']) ? $info['operator'] : 'IN';
$placeholder = ':views_join_condition_' . $select_query->nextPlaceholder() . '[]';
$placeholder_sql = "( $placeholder )";
}
else {
// With a single value, the '=' operator is implicit.
$operator = !empty($info['operator']) ? $info['operator'] : '=';
$placeholder = $placeholder_sql = ':views_join_condition_' . $select_query->nextPlaceholder();
}
// Set 'field' as join table field if available or set 'left field' as
// join table field is not set.
if (isset($info['field'])) {
$join_table_field = "$join_table$info[field]";
// Allow the value to be set either with the 'value' element or
// with 'left_field'.
if (isset($info['left_field'])) {
$placeholder_sql = "$left[alias].$info[left_field]";
}
else {
$arguments[$placeholder] = $info['value'];
}
}
// Set 'left field' as join table field is not set.
else {
$join_table_field = "$left[alias].$info[left_field]";
$arguments[$placeholder] = $info['value'];
}
// Render out the SQL fragment with parameters.
return "$join_table_field $operator $placeholder_sql";
}
}
/**
* @}
*/
......@@ -5,6 +5,11 @@
use Drupal\Component\Render\MarkupInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Tests\Views\FieldTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\language\Entity\ContentLanguageSettings;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
use Drupal\views\Views;
/**
* Tests the Field Views data.
......@@ -13,10 +18,27 @@
*/
class FieldApiDataTest extends FieldTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['language'];
/**
* {@inheritdoc}
*/
public static $testViews = ['test_field_config_translation_filter'];
/**
* The nodes used by the translation filter tests.
*
* @var \Drupal\node\NodeInterface[]
*/
protected $translationNodes;
protected function setUp() {
parent::setUp();
parent::setUp(FALSE);
$field_names = $this->setUpFieldStorages(1);
$field_names = $this->setUpFieldStorages(4);
// Attach the field to nodes only.
$field = [
......@@ -43,6 +65,109 @@ protected function setUp() {
];
$nodes[] = $this->drupalCreateNode($edit);
}
$bundles = [];
$bundles[] = $bundle = NodeType::create(['type' => 'bundle1']);
$bundle->save();
$bundles[] = $bundle = NodeType::create(['type' => 'bundle2']);
$bundle->save();
// Make the first field translatable on all bundles.
$field = FieldConfig::create([
'field_name' => $field_names[1],
'entity_type' => 'node',
'bundle' => $bundles[0]->id(),
'translatable' => TRUE,
]);
$field->save();
$field = FieldConfig::create([
'field_name' => $field_names[1],
'entity_type' => 'node',
'bundle' => $bundles[1]->id(),
'translatable' => TRUE,
]);
$field->save();
// Make the second field not translatable on any bundle.
$field = FieldConfig::create([
'field_name' => $field_names[2],
'entity_type' => 'node',
'bundle' => $bundles[0]->id(),
'translatable' => FALSE,
]);
$field->save();
$field = FieldConfig::create([
'field_name' => $field_names[2],
'entity_type' => 'node',
'bundle' => $bundles[1]->id(),
'translatable' => FALSE,
]);
$field->save();
// Make the last field translatable on some bundles.
$field = FieldConfig::create([
'field_name' => $field_names[3],
'entity_type' => 'node',
'bundle' => $bundles[0]->id(),
'translatable' => TRUE,
]);
$field->save();
$field = FieldConfig::create([
'field_name' => $field_names[3],
'entity_type' => 'node',
'bundle' => $bundles[1]->id(),
'translatable' => FALSE,
]);
$field->save();
// Create some example content.
ConfigurableLanguage::create([
'id' => 'es',
])->save();
ConfigurableLanguage::create([
'id' => 'fr',
])->save();
$config = ContentLanguageSettings::loadByEntityTypeBundle('node', $bundles[0]->id());
$config->setDefaultLangcode('es')
->setLanguageAlterable(TRUE)
->save();
$config = ContentLanguageSettings::loadByEntityTypeBundle('node', $bundles[1]->id());
$config->setDefaultLangcode('es')
->setLanguageAlterable(TRUE)
->save();
$node = Node::create([
'title' => 'Test title ' . $bundles[0]->id(),
'type' => $bundles[0]->id(),
'langcode' => 'es',
$field_names[1] => 'field name 1: es',
$field_names[2] => 'field name 2: es',
$field_names[3] => 'field name 3: es',
]);
$node->save();
$this->translationNodes[] = $node;
$translation = $node->addTranslation('fr');
$translation->{$field_names[1]}->value = 'field name 1: fr';
$translation->{$field_names[3]}->value = 'field name 3: fr';
$translation->title->value = $node->title->value;
$translation->save();
$node = Node::create([
'title' => 'Test title ' . $bundles[1]->id(),
'type' => $bundles[1]->id(),
'langcode' => 'es',
$field_names[1] => 'field name 1: es',
$field_names[2] => 'field name 2: es',
$field_names[3] => 'field name 3: es',
]);
$node->save();
$this->translationNodes[] = $node;
$translation = $node->addTranslation('fr');
$translation->{$field_names[1]}->value = 'field name 1: fr';
$translation->title->value = $node->title->value;
$translation->save();
}
/**
......@@ -137,4 +262,119 @@ protected function getViewsData() {
return $data;
}
/**
* Tests filtering entries with different translatabilty.
*/
public function testEntityFieldFilter() {
$map = [
'nid' => 'nid',
'langcode' => 'langcode',
];
$view = Views::getView('test_field_config_translation_filter');
// Filter by 'field name 1: es'.
$view->setDisplay('embed_1');
$this->executeView($view);
$expected = [
[
'nid' => $this->translationNodes[0]->id(),
'langcode' => 'es',
],
[
'nid' => $this->translationNodes[1]->id(),
'langcode' => 'es',
],
];
$this->assertIdenticalResultset($view, $expected, $map);
$view->destroy();
// Filter by 'field name 1: fr'.
$view->setDisplay('embed_2');
$this->executeView($view);
$expected = [
[
'nid' => $this->translationNodes[0]->id(),
'langcode' => 'fr',
],
[
'nid' => $this->translationNodes[1]->id(),
'langcode' => 'fr',
],
];
$this->assertIdenticalResultset($view, $expected, $map);
$view->destroy();
// Filter by 'field name 2: es'.
$view->setDisplay('embed_3');
$this->executeView($view);
$expected = [
[
'nid' => $this->translationNodes[0]->id(),
'langcode' => 'es',
],
[
'nid' => $this->translationNodes[0]->id(),
'langcode' => 'fr',
],
[
'nid' => $this->translationNodes[1]->id(),
'langcode' => 'es',
],
[
'nid' => $this->translationNodes[1]->id(),
'langcode' => 'fr',
],
];
$this->assertIdenticalResultset($view, $expected, $map);
$view->destroy();
// Filter by 'field name 2: fr', which doesn't exist.
$view->setDisplay('embed_4');
$this->executeView($view);
$expected = [
];
$this->assertIdenticalResultset($view, $expected, $map);
$view->destroy();
// Filter by 'field name 3: es'.
$view->setDisplay('embed_5');
$this->executeView($view);
$expected = [
[
'nid' => $this->translationNodes[0]->id(),
'langcode' => 'es',
],
[
'nid' => $this->translationNodes[1]->id(),
'langcode' => 'es',
],
// Why is this one returned?
[
'nid' => $this->translationNodes[1]->id(),
'langcode' => 'fr',
],
];
$this->assertIdenticalResultset($view, $expected, $map);
$view->destroy();
// Filter by 'field name 3: fr'.
$view->setDisplay('embed_6');
$this->executeView($view);
$expected = [
[
'nid' => $this->translationNodes[0]->id(),
'langcode' => 'fr',
],
];
$this->assertIdenticalResultset($view, $expected, $map);
$view->destroy();
}
}
langcode: en
status: true
dependencies: { }
id: test_field_config_translation_filter
module: views
description: ''
tag: ''
base_table: node_field_data
base_field: id
core: '8'
display:
default:
display_options:
access:
type: none
cache:
type: none
fields:
nid:
id: nid
field: nid
table: node_field_data
plugin_id: field
entity_type: node
entity_field: nid
langcode:
id: langcode
field: langcode
table: node_field_data
plugin_id: field
entity_type: node
entity_field: langcode
field_name_1:
id: field_name_1
table: node__field_name_1
field: field_name_1
plugin_id: field
entity_type: node
entity_field: field_name_1
field_name_2:
id: field_name_2
table: node__field_name_2
field: field_name_2
plugin_id: field
entity_type: node
entity_field: field_name_2
field_name_3:
id: field_name_3
table: node__field_name_3
field: field_name_3
plugin_id: field
entity_type: node
entity_field: field_name_3
sorts:
nid:
id: nid
table: node_field_data
field: nid
order: ASC
plugin_id: standard
entity_type: node
entity_field: nid
langcode:
id: langcode
table: node_field_data
field: langcode
relationship: none
group_type: group
admin_label: ''
order: ASC
exposed: false
expose:
label: ''
entity_type: node
entity_field: langcode
plugin_id: standard
style:
type: html_list
row:
type: fields
display_plugin: default
display_title: Master