diff --git a/core/modules/views/src/Plugin/views/field/Field.php b/core/modules/views/src/Plugin/views/field/Field.php index 73e03a4..db62ab2 100644 --- a/core/modules/views/src/Plugin/views/field/Field.php +++ b/core/modules/views/src/Plugin/views/field/Field.php @@ -977,11 +977,15 @@ protected function getTableMapping() { * {@inheritdoc} */ public function getValue(ResultRow $values, $field = NULL) { + $entity = $this->getEntity($values); + $langcode = $this->getFieldLangcode($entity, $values); + $entity = $entity->getTranslation($langcode); + if ($field === NULL) { - return $this->getEntity($values)->{$this->definition['field_name']}->value; + return $entity->{$this->definition['field_name']}->value; } - return $this->getEntity($values)->{$this->definition['field_name']}->$field; + return $entity->{$this->definition['field_name']}->$field; } } diff --git a/core/modules/views/src/Plugin/views/join/FieldOrLanguageJoin.php b/core/modules/views/src/Plugin/views/join/FieldOrLanguageJoin.php new file mode 100644 index 0000000..f679b52 --- /dev/null +++ b/core/modules/views/src/Plugin/views/join/FieldOrLanguageJoin.php @@ -0,0 +1,59 @@ +extra)) { + $extras = array(); + foreach ($this->extra as $extra) { + $extras[] = $this->buildExtra($extra, $arguments, $table, $select_query, $left_table); + } + + if ($extras) { + + // 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 ' . array_shift($extras); + } + else { + $condition .= ' AND (' . implode(' ' . $this->extraOperator . ' ', $extras) . ')'; + } + + // Tack on the langcode OR bundle join condition extra. + if (!empty($language_bundle_conditions)) { + $condition .= ' AND (' . implode(' OR ', $language_bundle_conditions) . ')'; + } + } + } + elseif ($this->extra && is_string($this->extra)) { + $condition .= " AND ($this->extra)"; + } + } +} diff --git a/core/modules/views/src/Plugin/views/join/JoinPluginBase.php b/core/modules/views/src/Plugin/views/join/JoinPluginBase.php index fdcc37f..b525120 100644 --- a/core/modules/views/src/Plugin/views/join/JoinPluginBase.php +++ b/core/modules/views/src/Plugin/views/join/JoinPluginBase.php @@ -259,7 +259,8 @@ public function buildJoin($select_query, $table, $view_query) { $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. + // 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; } diff --git a/core/modules/views/src/Tests/FieldApiDataTest.php b/core/modules/views/src/Tests/FieldApiDataTest.php index 96ff642..b538aed 100644 --- a/core/modules/views/src/Tests/FieldApiDataTest.php +++ b/core/modules/views/src/Tests/FieldApiDataTest.php @@ -7,7 +7,12 @@ namespace Drupal\views\Tests; +use Drupal\field\Entity\FieldConfig; use Drupal\field\Tests\Views\FieldTestBase; +use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\node\Entity\Node; +use Drupal\node\Entity\NodeType; +use Drupal\views\Views; /** * Tests the Field Views data. @@ -16,10 +21,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 = array( @@ -36,6 +58,96 @@ 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(); + + $node = Node::create([ + '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->getTranslation('fr'); + $translation->{$field_names[1]}->value = 'field name 1: fr'; + $translation->{$field_names[3]}->value = 'field name 3: fr'; + $translation->save(); + + $node = Node::create([ + '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->getTranslation('fr'); + $translation->{$field_names[1]}->value = 'field name 1: fr'; + $translation->save(); + } /** @@ -43,7 +155,7 @@ protected function setUp() { * * We check data structure for both node and node revision tables. */ - function testViewsData() { + function ptestViewsData() { $views_data = $this->container->get('views.views_data'); $data = array(); @@ -88,4 +200,119 @@ function testViewsData() { $this->assertTrue(empty($data[$revision_table][$field_storage->getName()]['field']['click sortable']), 'Non-primary fields are not click sortable'); } + /** + * 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(); + } + } diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_config_translation_filter.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_config_translation_filter.yml new file mode 100644 index 0000000..146f280 --- /dev/null +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_field_config_translation_filter.yml @@ -0,0 +1,173 @@ +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_type: 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 + style: + type: html_list + row: + type: fields + display_plugin: default + display_title: Master + id: default + position: 0 + embed_1: + display_options: + defaults: + fields: true + filters: false + filters: + field_name_1_value: + id: field_name_1_value + table: node__field_name_1 + field: field_name_1_value + value: 'field name 1: es' + plugin_id: string + entity_type: node + entity_field: field_name_1 + display_plugin: embed + display_title: Embed 1 + id: embed_1 + position: 1 + embed_2: + display_options: + defaults: + filters: false + filters: + field_name_1_value: + id: field_name_1_value + table: node__field_name_1 + field: field_name_1_value + value: 'field name 1: fr' + plugin_id: string + entity_type: node + entity_field: field_name_1 + display_plugin: embed + display_title: Embed 2 + id: embed_2 + position: 2 + embed_3: + display_options: + defaults: + filters: false + filters: + field_name_2_value: + id: field_name_2_value + table: node__field_name_2 + field: field_name_2_value + value: 'field name 2: es' + plugin_id: string + entity_type: node + entity_field: field_name_2 + display_plugin: embed + display_title: Embed 3 + id: embed_3 + position: 3 + embed_4: + display_options: + defaults: + filters: false + filters: + field_name_2_value: + id: field_name_2_value + table: node__field_name_2 + field: field_name_2_value + value: 'field name 2: fr' + plugin_id: string + entity_type: node + entity_field: field_name_2 + display_plugin: embed + display_title: Embed 4 + id: embed_4 + position: 4 + embed_5: + display_options: + defaults: + filters: false + filters: + field_name_3_value: + id: field_name_3_value + table: node__field_name_3 + field: field_name_3_value + value: 'field name 3: es' + plugin_id: string + entity_type: node + entity_field: field_name_3 + display_plugin: embed + display_title: Embed 5 + id: embed_5 + position: 5 + embed_6: + display_options: + defaults: + filters: false + filters: + field_name_3_value: + id: field_name_3_value + table: node__field_name_3 + field: field_name_3_value + value: 'field name 3: fr' + plugin_id: string + entity_type: node + entity_field: field_name_3 + display_plugin: embed + display_title: Embed 6 + id: embed_6 + position: 6 diff --git a/core/modules/views/views.views.inc b/core/modules/views/views.views.inc index ea865e2..ad5e281 100644 --- a/core/modules/views/views.views.inc +++ b/core/modules/views/views.views.inc @@ -8,6 +8,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\Sql\SqlContentEntityStorage; +use Drupal\field\Entity\FieldConfig; use Drupal\field\FieldConfigInterface; use Drupal\field\FieldStorageConfigInterface; use Drupal\system\ActionConfigEntityInterface; @@ -338,6 +339,40 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora ); } + // Determine if the field instances are translatable. + $bundles_names = $field_storage->getBundles(); + $translation_join_type = FALSE; + $field_configs = []; + $translatable_configs = []; + $untranslatable_configs = []; + $untranslatable_config_bundles = []; + + foreach ($bundles_names as $bundle) { + $field_configs[$bundle] = FieldConfig::loadByName($entity_type->id(), $bundle, $field_name); + } + foreach ($field_configs as $bundle => $config) { + if ($config->isTranslatable()) { + $translatable_configs[$bundle] = $config; + } + else { + $untranslatable_configs[$bundle] = $config; + } + } + + // If the field is translatable on all the bundles, there will be a join on + // the langcode. + if (!empty($translatable_configs) && empty($untranslatable_configs)) { + $translation_join_type = 'language'; + } + // If the field is translatable only on certain bundles, there will be a join + // on langcode OR bundle name. + elseif (!empty($translatable_configs) && !empty($untranslatable_configs)) { + foreach ($untranslatable_configs as $config) { + $untranslatable_config_bundles[] = $config->getTargetBundle(); + } + $translation_join_type = 'language_bundle'; + } + // Build the relationships between the field table and the entity tables. $table_alias = $field_tables[EntityStorageInterface::FIELD_LOAD_CURRENT]['alias']; if ($data_table) { @@ -347,7 +382,6 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora 'field' => 'entity_id', 'extra' => array( array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), - array('left_field' => 'langcode', 'field' => 'langcode'), ), ); } @@ -362,6 +396,24 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora ); } + if ($translation_join_type === 'language_bundle') { + $data[$table_alias]['table']['join'][$data_table]['join_id'] = 'field_or_language_join'; + $data[$table_alias]['table']['join'][$data_table]['extra'][] = array( + 'left_field' => 'langcode', + 'field' => 'langcode', + ); + $data[$table_alias]['table']['join'][$data_table]['extra'][] = array( + 'value' => $untranslatable_config_bundles, + 'field' => 'bundle', + ); + } + elseif ($translation_join_type === 'language') { + $data[$table_alias]['table']['join'][$data_table]['extra'][] = array( + 'left_field' => 'langcode', + 'field' => 'langcode', + ); + } + if ($supports_revisions) { $table_alias = $field_tables[EntityStorageInterface::FIELD_LOAD_REVISION]['alias']; if ($entity_revision_data_table) { @@ -371,7 +423,6 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora 'field' => 'revision_id', 'extra' => array( array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), - array('left_field' => 'langcode', 'field' => 'langcode'), ), ); } @@ -385,11 +436,26 @@ function views_field_default_views_data(FieldStorageConfigInterface $field_stora ), ); } + if ($translation_join_type === 'language_bundle') { + $data[$table_alias]['table']['join'][$entity_revision_data_table]['join_id'] = 'field_or_language_join'; + $data[$table_alias]['table']['join'][$entity_revision_data_table]['extra'][] = array( + 'left_field' => 'langcode', + 'field' => 'langcode', + ); + $data[$table_alias]['table']['join'][$entity_revision_data_table]['extra'][] = array( + 'value' => $untranslatable_config_bundles, + 'field' => 'bundle', + ); + } + elseif ($translation_join_type === 'language') { + $data[$table_alias]['table']['join'][$entity_revision_data_table]['extra'][] = array( + 'left_field' => 'langcode', + 'field' => 'langcode', + ); + } } $group_name = $entity_type->getLabel(); - // Get the list of bundles the field appears in. - $bundles_names = $field_storage->getBundles(); // Build the list of additional fields to add to queries. $add_fields = array('delta', 'langcode', 'bundle'); foreach (array_keys($field_columns) as $column) {