From 62bb5c4d533a9f8fef99012114754afb4a3edd14 Mon Sep 17 00:00:00 2001 From: Alex Pott Date: Mon, 18 Aug 2014 17:41:42 +0100 Subject: [PATCH] Issue #2218065 by jhodgdon, dawehner: Fixed Need to join fields to the entity field data tables, not entity tables, or filtering increases number of results. --- .../Tests/Views/CommentFieldFilterTest.php | 138 ++++++ .../views.view.test_field_filters.yml | 358 ++++++++++++++ core/modules/field/field.views.inc | 72 ++- .../field/src/Tests/Views/ApiDataTest.php | 4 + .../src/Tests/Views/NodeFieldFilterTest.php | 126 +++++ .../views.view.test_field_filters.yml | 407 +++++++++++++++ .../Tests/Views/TaxonomyFieldFilterTest.php | 186 +++++++ .../views.view.test_field_filters.yml | 466 ++++++++++++++++++ .../src/Plugin/views/join/JoinPluginBase.php | 15 +- 9 files changed, 1755 insertions(+), 17 deletions(-) create mode 100644 core/modules/comment/src/Tests/Views/CommentFieldFilterTest.php create mode 100644 core/modules/comment/tests/modules/comment_test_views/test_views/views.view.test_field_filters.yml create mode 100644 core/modules/node/src/Tests/Views/NodeFieldFilterTest.php create mode 100644 core/modules/node/tests/modules/node_test_views/test_views/views.view.test_field_filters.yml create mode 100644 core/modules/taxonomy/src/Tests/Views/TaxonomyFieldFilterTest.php create mode 100644 core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_field_filters.yml diff --git a/core/modules/comment/src/Tests/Views/CommentFieldFilterTest.php b/core/modules/comment/src/Tests/Views/CommentFieldFilterTest.php new file mode 100644 index 0000000000..9632449294 --- /dev/null +++ b/core/modules/comment/src/Tests/Views/CommentFieldFilterTest.php @@ -0,0 +1,138 @@ + 'fr', + 'name' => 'French', + )); + language_save($language); + + $language = new Language(array( + 'id' => 'es', + 'name' => 'Spanish', + )); + language_save($language); + + // Make the comment body field translatable. The title is already + // translatable by definition. + $field = FieldStorageConfig::loadByName('comment', 'comment_body'); + $field->translatable = TRUE; + $field->save(); + + // Set up comment titles. + $this->comment_titles = array( + 'en' => 'Food in Paris', + 'es' => 'Comida en Paris', + 'fr' => 'Nouriture en Paris', + ); + + // Create a new comment. Using the one created earlier will not work, + // as it predates the language set-up. + $comment = array( + 'uid' => $this->loggedInUser->id(), + 'entity_id' => $this->node_user_commented->id(), + 'entity_type' => 'node', + 'field_name' => 'comment', + 'cid' => '', + 'pid' => '', + 'node_type' => '', + ); + $this->comment = entity_create('comment', $comment); + + // Add field values and translate the comment. + $this->comment->subject->value = $this->comment_titles['en']; + $this->comment->comment_body->value = $this->comment_titles['en']; + $this->comment->langcode = 'en'; + $this->comment->save(); + foreach (array('es', 'fr') as $langcode) { + $translation = $this->comment->addTranslation($langcode, array()); + $translation->comment_body->value = $this->comment_titles[$langcode]; + $translation->subject->value = $this->comment_titles[$langcode]; + } + $this->comment->save(); + } + + /** + * Tests body and title filters. + */ + public function testFilters() { + // Test the title filter page, which filters for title contains 'Comida'. + // Should show just the Spanish translation, once. + $this->assertPageCounts('test-title-filter', array('es' => 1, 'fr' => 0, 'en' => 0), 'Comida title filter'); + + // Test the body filter page, which filters for body contains 'Comida'. + // Should show just the Spanish translation, once. + $this->assertPageCounts('test-body-filter', array('es' => 1, 'fr' => 0, 'en' => 0), 'Comida body filter'); + + // Test the title Paris filter page, which filters for title contains + // 'Paris'. Should show each translation once. + $this->assertPageCounts('test-title-paris', array('es' => 1, 'fr' => 1, 'en' => 1), 'Paris title filter'); + + // Test the body Paris filter page, which filters for body contains + // 'Paris'. Should show each translation once. + $this->assertPageCounts('test-body-paris', array('es' => 1, 'fr' => 1, 'en' => 1), 'Paris body filter'); + } + + /** + * Asserts that the given comment translation counts are correct. + * + * @param string $path + * Path of the page to test. + * @param array $counts + * Array whose keys are languages, and values are the number of times + * that translation should be shown on the given page. + * @param string $message + * Message suffix to display. + */ + protected function assertPageCounts($path, $counts, $message) { + // Get the text of the page. + $this->drupalGet($path); + $text = $this->getTextContent(); + + // Check the counts. Note that the title and body are both shown on the + // page, and they are the same. So the title/body string should appear on + // the page twice as many times as the input count. + foreach ($counts as $langcode => $count) { + $this->assertEqual(substr_count($text, $this->comment_titles[$langcode]), 2 * $count, 'Translation ' . $langcode . ' has count ' . $count . ' with ' . $message); + } + } +} diff --git a/core/modules/comment/tests/modules/comment_test_views/test_views/views.view.test_field_filters.yml b/core/modules/comment/tests/modules/comment_test_views/test_views/views.view.test_field_filters.yml new file mode 100644 index 0000000000..692c5d5db8 --- /dev/null +++ b/core/modules/comment/tests/modules/comment_test_views/test_views/views.view.test_field_filters.yml @@ -0,0 +1,358 @@ +uuid: b9f5216c-231d-477b-b312-18020246cbb0 +langcode: en +status: true +dependencies: + module: + - comment +id: test_field_filters +label: 'Test field filters' +module: views +description: '' +tag: '' +base_table: comment +base_field: cid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + provider: views + display_options: + access: + type: perm + options: + perm: 'access content' + provider: user + dependencies: { } + cache: + type: none + options: { } + provider: views + dependencies: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: false + query_tags: { } + provider: views + dependencies: { } + 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 + provider: views + dependencies: { } + pager: + type: none + options: + items_per_page: 0 + offset: 0 + style: + type: default + row: + type: 'entity:comment' + options: + links: true + view_mode: full + rendering_language: translation_language_renderer + relationships: + node: + id: node + table: comment_field_data + field: node + required: true + plugin_id: standard + provider: views + relationship: none + group_type: group + admin_label: Content + dependencies: { } + fields: + subject: + id: subject + table: comment_field_data + field: subject + provider: comment + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + link_to_comment: 1 + relationship: none + group_type: group + admin_label: '' + dependencies: { } + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + link_to_entity: false + filters: + subject: + id: subject + table: comment_field_data + field: subject + relationship: none + group_type: group + admin_label: '' + dependencies: + module: + - views + operator: contains + value: Comida + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + provider: views + sorts: { } + title: 'Title filter page' + header: { } + footer: { } + empty: { } + arguments: { } + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + page_tc: + display_plugin: page + id: page_tc + display_title: 'Title Comida' + position: 1 + provider: views + display_options: + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + path: test-title-filter + display_description: '' + page_bp: + display_plugin: page + id: page_bp + display_title: 'Body Paris' + position: 1 + provider: views + display_options: + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + path: test-body-paris + display_description: '' + filters: + comment_body_value: + id: comment_body_value + table: comment__comment_body + field: comment_body_value + relationship: none + group_type: group + admin_label: '' + dependencies: + module: + - views + operator: contains + value: Paris + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + provider: views + defaults: + filters: false + filter_groups: false + title: false + filter_groups: + operator: AND + groups: + 1: AND + title: 'Body filter page' + page_tp: + display_plugin: page + id: page_tp + display_title: 'Title Paris' + position: 1 + provider: views + display_options: + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + path: test-title-paris + display_description: '' + filters: + subject: + id: subject + table: comment_field_data + field: subject + relationship: none + group_type: group + admin_label: '' + dependencies: + module: + - views + - views + operator: contains + value: Paris + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + provider: views + defaults: + filters: false + filter_groups: false + filter_groups: + operator: AND + groups: + 1: AND + page_bf: + display_plugin: page + id: page_bf + display_title: 'Body Comida' + position: 1 + provider: views + display_options: + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + path: test-body-filter + display_description: '' + filters: + comment_body_value: + id: comment_body_value + table: comment__comment_body + field: comment_body_value + relationship: none + group_type: group + admin_label: '' + dependencies: + module: + - views + - views + operator: contains + value: Comida + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + provider: views + defaults: + filters: false + filter_groups: false + title: false + filter_groups: + operator: AND + groups: + 1: AND + title: 'Body filter page' diff --git a/core/modules/field/field.views.inc b/core/modules/field/field.views.inc index 6968bec082..3f684199e9 100644 --- a/core/modules/field/field.views.inc +++ b/core/modules/field/field.views.inc @@ -125,17 +125,28 @@ function field_views_field_default_views_data(FieldStorageConfigInterface $field $field_columns = $field_storage->getColumns(); // Grab information about the entity type tables. + // We need to join to both the base table and the data table, if available. $entity_manager = \Drupal::entityManager(); $entity_type_id = $field_storage->entity_type; $entity_type = $entity_manager->getDefinition($entity_type_id); - if (!$entity_table = $entity_type->getBaseTable()) { + if (!$base_table = $entity_type->getBaseTable()) { + // We cannot do anything if for some reason there is no base table. return $data; } - $entity_tables = array($entity_table => $entity_type_id); + $entity_tables = array($base_table => $entity_type_id); + // Some entities may not have a data table. + $data_table = $entity_type->getDataTable(); + if ($data_table) { + $entity_tables[$data_table] = $entity_type_id; + } $entity_revision_table = $entity_type->getRevisionTable(); $supports_revisions = $entity_type->hasKey('revision') && $entity_revision_table; if ($supports_revisions) { $entity_tables[$entity_revision_table] = $entity_type_id; + $entity_revision_data_table = $entity_type->getRevisionDataTable(); + if ($entity_revision_data_table) { + $entity_tables[$entity_revision_data_table] = $entity_type_id; + } } // Description of the field tables. @@ -154,24 +165,55 @@ function field_views_field_default_views_data(FieldStorageConfigInterface $field // Build the relationships between the field table and the entity tables. $table_alias = $field_tables[EntityStorageInterface::FIELD_LOAD_CURRENT]['alias']; - $data[$table_alias]['table']['join'][$entity_table] = array( - 'left_field' => $entity_type->getKey('id'), - 'field' => 'entity_id', - 'extra' => array( - array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), - ), - ); - if ($supports_revisions) { - $table_alias = $field_tables[EntityStorageInterface::FIELD_LOAD_REVISION]['alias']; - $data[$table_alias]['table']['join'][$entity_revision_table] = array( - 'left_field' => $entity_type->getKey('revision'), - 'field' => 'revision_id', + if ($data_table) { + // Tell Views how to join to the base table, via the data table. + $data[$table_alias]['table']['join'][$base_table] = array( + 'left_table' => $data_table, + 'left_field' => $entity_type->getKey('id'), + 'field' => 'entity_id', + 'extra' => array( + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + array('left_field' => 'langcode', 'field' => 'langcode'), + ), + ); + } + else { + // If there is no data table, just join directly. + $data[$table_alias]['table']['join'][$base_table] = array( + 'left_field' => $entity_type->getKey('id'), + 'field' => 'entity_id', 'extra' => array( array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), ), ); } + if ($supports_revisions) { + $table_alias = $field_tables[EntityStorageInterface::FIELD_LOAD_REVISION]['alias']; + if ($entity_revision_data_table) { + // Tell Views how to join to the revision table, via the data table. + $data[$table_alias]['table']['join'][$entity_revision_table] = array( + 'left_table' => $entity_revision_data_table, + 'left_field' => $entity_type->getKey('revision'), + 'field' => 'revision_id', + 'extra' => array( + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + array('left_field' => 'langcode', 'field' => 'langcode'), + ), + ); + } + else { + // If there is no data table, just join directly. + $data[$table_alias]['table']['join'][$entity_revision_table] = array( + 'left_field' => $entity_type->getKey('revision'), + 'field' => 'revision_id', + 'extra' => array( + array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + ), + ); + } + } + $group_name = $entity_type->getLabel(); // Get the list of bundles the field appears in. $bundles_names = $field_storage->getBundles(); @@ -214,7 +256,7 @@ function field_views_field_default_views_data(FieldStorageConfigInterface $field if ($type == EntityStorageInterface::FIELD_LOAD_CURRENT) { if ($label != $label_name) { $aliases[] = array( - 'base' => $entity_table, + 'base' => $base_table, 'group' => $group_name, 'title' => $label_name, 'help' => t('This is an alias of @group: @field.', array('@group' => $group_name, '@field' => $label)), diff --git a/core/modules/field/src/Tests/Views/ApiDataTest.php b/core/modules/field/src/Tests/Views/ApiDataTest.php index 7f0cdd195f..7631e2949f 100644 --- a/core/modules/field/src/Tests/Views/ApiDataTest.php +++ b/core/modules/field/src/Tests/Views/ApiDataTest.php @@ -63,18 +63,22 @@ function testViewsData() { $this->assertTrue(isset($data[$revision_table]['table']['join']['node_revision'])); $expected_join = array( + 'left_table' => 'node_field_data', 'left_field' => 'nid', 'field' => 'entity_id', 'extra' => array( array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + array('left_field' => 'langcode', 'field' => 'langcode'), ), ); $this->assertEqual($expected_join, $data[$current_table]['table']['join']['node']); $expected_join = array( + 'left_table' => 'node_field_revision', 'left_field' => 'vid', 'field' => 'revision_id', 'extra' => array( array('field' => 'deleted', 'value' => 0, 'numeric' => TRUE), + array('left_field' => 'langcode', 'field' => 'langcode'), ), ); $this->assertEqual($expected_join, $data[$revision_table]['table']['join']['node_revision']); diff --git a/core/modules/node/src/Tests/Views/NodeFieldFilterTest.php b/core/modules/node/src/Tests/Views/NodeFieldFilterTest.php new file mode 100644 index 0000000000..d7ffc0374e --- /dev/null +++ b/core/modules/node/src/Tests/Views/NodeFieldFilterTest.php @@ -0,0 +1,126 @@ +profile != 'standard') { + $this->drupalCreateContentType(array('type' => 'page', 'name' => 'Basic page')); + } + + // Add two new languages. + $language = new Language(array( + 'id' => 'fr', + 'name' => 'French', + )); + language_save($language); + + $language = new Language(array( + 'id' => 'es', + 'name' => 'Spanish', + )); + language_save($language); + + // Make the body field translatable. The title is already translatable by + // definition. + $field = FieldStorageConfig::loadByName('node', 'body'); + $field->translatable = TRUE; + $field->save(); + + // Set up node titles. + $this->node_titles = array( + 'en' => 'Food in Paris', + 'es' => 'Comida en Paris', + 'fr' => 'Nouriture en Paris', + ); + + // Create node with translations. + $node = $this->drupalCreateNode(array('title' => $this->node_titles['en'], 'langcode' => 'en', 'type' => 'page', 'body' => array(array('value' => $this->node_titles['en'])))); + foreach (array('es', 'fr') as $langcode) { + $translation = $node->addTranslation($langcode, array('title' => $this->node_titles[$langcode])); + $translation->body->value = $this->node_titles[$langcode]; + } + $node->save(); + } + + /** + * Tests body and title filters. + */ + public function testFilters() { + // Test the title filter page, which filters for title contains 'Comida'. + // Should show just the Spanish translation, once. + $this->assertPageCounts('test-title-filter', array('es' => 1, 'fr' => 0, 'en' => 0), 'Comida title filter'); + + // Test the body filter page, which filters for body contains 'Comida'. + // Should show just the Spanish translation, once. + $this->assertPageCounts('test-body-filter', array('es' => 1, 'fr' => 0, 'en' => 0), 'Comida body filter'); + + // Test the title Paris filter page, which filters for title contains + // 'Paris'. Should show each translation once. + $this->assertPageCounts('test-title-paris', array('es' => 1, 'fr' => 1, 'en' => 1), 'Paris title filter'); + + // Test the body Paris filter page, which filters for body contains + // 'Paris'. Should show each translation once. + $this->assertPageCounts('test-body-paris', array('es' => 1, 'fr' => 1, 'en' => 1), 'Paris body filter'); + } + + /** + * Asserts that the given node translation counts are correct. + * + * @param string $path + * Path of the page to test. + * @param array $counts + * Array whose keys are languages, and values are the number of times + * that translation should be shown on the given page. + * @param string $message + * Message suffix to display. + */ + protected function assertPageCounts($path, $counts, $message) { + // Get the text of the page. + $this->drupalGet($path); + $text = $this->getTextContent(); + + // Check the counts. Note that the title and body are both shown on the + // page, and they are the same. So the title/body string should appear on + // the page twice as many times as the input count. + foreach ($counts as $langcode => $count) { + $this->assertEqual(substr_count($text, $this->node_titles[$langcode]), 2 * $count, 'Translation ' . $langcode . ' has count ' . $count . ' with ' . $message); + } + } +} diff --git a/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_field_filters.yml b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_field_filters.yml new file mode 100644 index 0000000000..410622a58f --- /dev/null +++ b/core/modules/node/tests/modules/node_test_views/test_views/views.view.test_field_filters.yml @@ -0,0 +1,407 @@ +uuid: bd845a9c-6c2e-4d53-965c-92b170bb79e4 +langcode: en +status: true +dependencies: + module: + - node +id: test_field_filters +label: 'Test field filters' +module: views +description: '' +tag: '' +base_table: node +base_field: nid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + provider: views + display_options: + access: + type: perm + options: + perm: 'access content' + provider: user + dependencies: { } + cache: + type: none + options: { } + provider: views + dependencies: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: false + query_tags: { } + provider: views + dependencies: { } + 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 + provider: views + dependencies: { } + pager: + type: none + options: + items_per_page: 0 + offset: 0 + style: + type: default + row: + type: 'entity:node' + options: + build_mode: teaser + links: false + view_mode: teaser + rendering_language: translation_language_renderer + fields: + title: + id: title + table: node_field_data + field: title + provider: node + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + link_to_node: 1 + relationship: none + group_type: group + admin_label: '' + dependencies: { } + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + filters: + status: + value: true + table: node_field_data + field: status + provider: node + id: status + expose: + operator: '' + group: 1 + title: + id: title + table: node_field_data + field: title + relationship: none + group_type: group + admin_label: '' + dependencies: + module: + - views + - views + - views + operator: contains + value: Paris + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + provider: views + sorts: + created: + id: created + table: node_field_data + field: created + order: DESC + relationship: none + group_type: group + admin_label: '' + dependencies: { } + exposed: false + expose: + label: '' + granularity: second + title: 'Test field filters' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + page_tf: + display_plugin: page + id: page_tf + display_title: 'Title filter page' + position: 1 + provider: views + display_options: + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + path: test-title-filter + display_description: '' + title: 'Test title filter' + defaults: + title: false + filters: false + filter_groups: false + filters: + status: + value: true + table: node_field_data + field: status + provider: node + id: status + expose: + operator: '' + group: 1 + title: + id: title + table: node_field_data + field: title + relationship: none + group_type: group + admin_label: '' + dependencies: + module: + - views + - views + - views + - views + operator: contains + value: Comida + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + provider: views + filter_groups: + operator: AND + groups: + 1: AND + page_bf: + display_plugin: page + id: page_bf + display_title: 'Body filter page' + position: 1 + provider: views + display_options: + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + path: test-body-filter + display_description: '' + title: 'Test body filters' + defaults: + title: false + filters: false + filter_groups: false + filters: + status: + value: true + table: node_field_data + field: status + provider: node + id: status + expose: + operator: '' + group: 1 + body_value: + id: body_value + table: node__body + field: body_value + relationship: none + group_type: group + admin_label: '' + dependencies: + module: + - views + - views + operator: contains + value: Comida + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + provider: views + filter_groups: + operator: AND + groups: + 1: AND + page_bfp: + display_plugin: page + id: page_bfp + display_title: 'Body filter page Paris' + position: 1 + provider: views + display_options: + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + path: test-body-paris + display_description: '' + title: 'Test body filters' + defaults: + title: false + filters: false + filter_groups: false + filters: + status: + value: true + table: node_field_data + field: status + provider: node + id: status + expose: + operator: '' + group: 1 + body_value: + id: body_value + table: node__body + field: body_value + relationship: none + group_type: group + admin_label: '' + dependencies: + module: + - views + - views + - views + operator: contains + value: Paris + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + provider: views + filter_groups: + operator: AND + groups: + 1: AND + page_tfp: + display_plugin: page + id: page_tfp + display_title: 'Title filter page Paris' + position: 1 + provider: views + display_options: + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + path: test-title-paris + display_description: '' + title: 'Test title filter' + defaults: + title: false diff --git a/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldFilterTest.php b/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldFilterTest.php new file mode 100644 index 0000000000..c1d7065d4d --- /dev/null +++ b/core/modules/taxonomy/src/Tests/Views/TaxonomyFieldFilterTest.php @@ -0,0 +1,186 @@ + 'fr', + 'name' => 'French', + )); + language_save($language); + + $language = new Language(array( + 'id' => 'es', + 'name' => 'Spanish', + )); + language_save($language); + + // Set up term names. + $this->term_names = array( + 'en' => 'Food in Paris', + 'es' => 'Comida en Paris', + 'fr' => 'Nouriture en Paris', + ); + + // Create a vocabulary. + $this->vocabulary = entity_create('taxonomy_vocabulary', array( + 'name' => 'Views testing tags', + 'vid' => 'views_testing_tags', + )); + $this->vocabulary->save(); + + // Add a translatable field to the vocabulary. + $field = entity_create('field_storage_config', array( + 'name' => 'field_foo', + 'entity_type' => 'taxonomy_term', + 'type' => 'text', + )); + $field->translatable = TRUE; + $field->save(); + entity_create('field_instance_config', array( + 'field_name' => 'field_foo', + 'entity_type' => 'taxonomy_term', + 'label' => 'Foo', + 'bundle' => 'views_testing_tags', + ))->save(); + + // Create term with translations. + $taxonomy = $this->createTermWithProperties(array('name' => $this->term_names['en'], 'langcode' => 'en', 'description' => $this->term_names['en'], 'field_foo' => $this->term_names['en'])); + foreach (array('es', 'fr') as $langcode) { + $translation = $taxonomy->addTranslation($langcode, array('name' => $this->term_names[$langcode])); + $translation->description->value = $this->term_names[$langcode]; + $translation->field_foo->value = $this->term_names[$langcode]; + } + $taxonomy->save(); + + ViewTestData::createTestViews(get_class($this), array('taxonomy_test_views')); + + } + + /** + * Tests description and term name filters. + */ + public function testFilters() { + // Test the name filter page, which filters for name contains 'Comida'. + // Should show just the Spanish translation, once. + $this->assertPageCounts('test-name-filter', array('es' => 1, 'fr' => 0, 'en' => 0), 'Comida name filter'); + + // Test the description filter page, which filters for description contains + // 'Comida'. Should show just the Spanish translation, once. + $this->assertPageCounts('test-desc-filter', array('es' => 1, 'fr' => 0, 'en' => 0), 'Comida description filter'); + + // Test the field filter page, which filters for field_foo contains + // 'Comida'. Should show just the Spanish translation, once. + $this->assertPageCounts('test-field-filter', array('es' => 1, 'fr' => 0, 'en' => 0), 'Comida field filter'); + + // Test the name Paris filter page, which filters for name contains + // 'Paris'. Should show each translation once. + $this->assertPageCounts('test-name-paris', array('es' => 1, 'fr' => 1, 'en' => 1), 'Paris name filter'); + + // Test the description Paris page, which filters for description contains + // 'Paris'. Should show each translation, once. + $this->assertPageCounts('test-desc-paris', array('es' => 1, 'fr' => 1, 'en' => 1), 'Paris description filter'); + + // Test the field Paris filter page, which filters for field_foo contains + // 'Paris'. Should show each translation once. + $this->assertPageCounts('test-field-paris', array('es' => 1, 'fr' => 1, 'en' => 1), 'Paris field filter'); + + } + + /** + * Asserts that the given taxonomy translation counts are correct. + * + * @param string $path + * Path of the page to test. + * @param array $counts + * Array whose keys are languages, and values are the number of times + * that translation should be shown on the given page. + * @param string $message + * Message suffix to display. + */ + protected function assertPageCounts($path, $counts, $message) { + // Get the text of the page. + $this->drupalGet($path); + $text = $this->getTextContent(); + + // Check the counts. Note that the title and body are both shown on the + // page, and they are the same. So the title/body string should appear on + // the page twice as many times as the input count. + foreach ($counts as $langcode => $count) { + $this->assertEqual(substr_count($text, $this->term_names[$langcode]), 2 * $count, 'Translation ' . $langcode . ' has count ' . $count . ' with ' . $message); + } + } + + /** + * Creates a taxonomy term with specified name and other properties. + * + * @param array $properties + * Array of properties and field values to set. + * + * @return \Drupal\taxonomy\Term + * The created taxonomy term. + */ + protected function createTermWithProperties($properties) { + // Use the first available text format. + $filter_formats = filter_formats(); + $format = array_pop($filter_formats); + + $properties += array( + 'name' => $this->randomMachineName(), + 'description' => $this->randomMachineName(), + 'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED, + 'field_foo' => $this->randomMachineName(), + ); + + $term = entity_create('taxonomy_term', array( + 'name' => $properties['name'], + 'description' => $properties['description'], + 'format' => $format->format, + 'vid' => $this->vocabulary->id(), + 'langcode' => $properties['langcode'], + )); + $term->field_foo->value = $properties['field_foo']; + $term->save(); + return $term; + } + +} diff --git a/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_field_filters.yml b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_field_filters.yml new file mode 100644 index 0000000000..372c0f5d70 --- /dev/null +++ b/core/modules/taxonomy/tests/modules/taxonomy_test_views/test_views/views.view.test_field_filters.yml @@ -0,0 +1,466 @@ +uuid: 2a772a88-315e-4320-973d-61c59393d2c8 +langcode: en +status: true +dependencies: + module: + - taxonomy +id: test_field_filters +label: 'Test field filters' +module: views +description: '' +tag: '' +base_table: taxonomy_term_data +base_field: tid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + provider: views + display_options: + access: + type: perm + options: + perm: 'access content' + provider: user + dependencies: { } + cache: + type: none + options: { } + provider: views + dependencies: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: false + query_tags: { } + provider: views + dependencies: { } + 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 + provider: views + dependencies: { } + pager: + type: none + options: + items_per_page: 0 + offset: 0 + style: + type: default + row: + type: 'entity:taxonomy_term' + fields: + name: + id: name + table: taxonomy_term_field_data + field: name + provider: taxonomy + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + link_to_taxonomy: 1 + relationship: none + group_type: group + admin_label: '' + dependencies: { } + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + convert_spaces: false + filters: + name: + id: name + table: taxonomy_term_field_data + field: name + relationship: none + group_type: group + admin_label: '' + dependencies: + module: + - views + operator: contains + value: Comida + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + provider: views + sorts: { } + title: 'Name filter page' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + page_dc: + display_plugin: page + id: page_dc + display_title: 'Description Comida' + position: 3 + provider: views + display_options: + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + display_description: '' + path: test-desc-filter + filters: + description__value: + id: description__value + table: taxonomy_term_field_data + field: description__value + relationship: none + group_type: group + admin_label: '' + dependencies: + module: + - views + operator: contains + value: Comida + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + provider: views + defaults: + filters: false + filter_groups: false + title: false + filter_groups: + operator: AND + groups: + 1: AND + title: 'Description filter page' + page_dp: + display_plugin: page + id: page_dp + display_title: 'Description Comida' + position: 3 + provider: views + display_options: + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + display_description: '' + path: test-desc-paris + filters: + description__value: + id: description__value + table: taxonomy_term_field_data + field: description__value + relationship: none + group_type: group + admin_label: '' + dependencies: + module: + - views + - views + operator: contains + value: Paris + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + provider: views + defaults: + filters: false + filter_groups: false + title: false + filter_groups: + operator: AND + groups: + 1: AND + title: 'Description filter page' + page_nc: + display_plugin: page + id: page_nc + display_title: 'Name Comida' + position: 1 + provider: views + display_options: + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + path: test-name-filter + display_description: '' + page_np: + display_plugin: page + id: page_np + display_title: 'Name Paris' + position: 1 + provider: views + display_options: + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + path: test-name-paris + display_description: '' + filters: + name: + id: name + table: taxonomy_term_field_data + field: name + relationship: none + group_type: group + admin_label: '' + dependencies: + module: + - views + - views + operator: contains + value: Paris + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + provider: views + defaults: + filters: false + filter_groups: false + filter_groups: + operator: AND + groups: + 1: AND + page_fp: + display_plugin: page + id: page_fp + display_title: 'Field Paris' + position: 3 + provider: views + display_options: + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + display_description: '' + path: test-field-paris + filters: + field_foo_value: + id: field_foo_value + table: taxonomy_term__field_foo + field: field_foo_value + relationship: none + group_type: group + admin_label: '' + dependencies: + module: + - views + - views + operator: contains + value: Paris + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + provider: views + defaults: + filters: false + filter_groups: false + title: false + filter_groups: + operator: AND + groups: + 1: AND + title: 'Field filter page' + page_fc: + display_plugin: page + id: page_fc + display_title: 'Field Comida' + position: 3 + provider: views + display_options: + field_langcode: '***CURRENT_LANGUAGE***' + field_langcode_add_to_query: null + display_description: '' + path: test-field-filter + filters: + field_foo_value: + id: field_foo_value + table: taxonomy_term__field_foo + field: field_foo_value + relationship: none + group_type: group + admin_label: '' + dependencies: + module: + - views + operator: contains + value: Comida + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + provider: views + defaults: + filters: false + filter_groups: false + title: false + filter_groups: + operator: AND + groups: + 1: AND + title: 'Field filter page' diff --git a/core/modules/views/src/Plugin/views/join/JoinPluginBase.php b/core/modules/views/src/Plugin/views/join/JoinPluginBase.php index c91cf1d5f5..cd1dda3e5d 100644 --- a/core/modules/views/src/Plugin/views/join/JoinPluginBase.php +++ b/core/modules/views/src/Plugin/views/join/JoinPluginBase.php @@ -217,6 +217,10 @@ public function buildJoin($select_query, $table, $view_query) { if (is_array($this->extra)) { $extras = array(); foreach ($this->extra as $info) { + // Do not require 'value' to be set; allow for field syntax instead. + $info += array( + 'value' => NULL, + ); // Figure out the table name. Remember, only use aliases provided // if at all possible. $join_table = ''; @@ -260,8 +264,15 @@ public function buildJoin($select_query, $table, $view_query) { else { // With a single value, the '=' operator is implicit. $operator = !empty($info['operator']) ? $info['operator'] : '='; - $placeholder = ':views_join_condition_' . $select_query->nextPlaceholder(); - $arguments[$placeholder] = $info['value']; + // Allow the value to be set either with the 'value' element or + // with 'left_field'. + if (isset($info['left_field'])) { + $placeholder = "$left[alias].$info[left_field]"; + } + else { + $placeholder = ':views_join_condition_' . $select_query->nextPlaceholder(); + $arguments[$placeholder] = $info['value']; + } } $extras[] = "$join_table$info[field] $operator $placeholder"; -- GitLab