From 8210ebe0ae25d4b2107808d691b9c487bcbbb009 Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Mon, 4 Sep 2017 15:13:49 +0100
Subject: [PATCH] Issue #2687773 by mikeker, sukanya.ramakrishnan, dawehner,
 amateescu, mroycroft, Lukas von Blarer, Lendude: Using checkboxes with an is

---
 .../src/Plugin/views/filter/ManyToOne.php     |   3 +
 ...iews.view.test_exposed_form_checkboxes.yml | 155 ++++++++++++++++
 .../Plugin/ExposedFormCheckboxesTest.php      | 168 ++++++++++++++++++
 .../src/Functional/Plugin/ExposedFormTest.php |  42 -----
 .../views_test_checkboxes_theme.theme         |   7 +-
 5 files changed, 332 insertions(+), 43 deletions(-)
 create mode 100644 core/modules/views/tests/modules/views_test_config/test_views/views.view.test_exposed_form_checkboxes.yml
 create mode 100644 core/modules/views/tests/src/Functional/Plugin/ExposedFormCheckboxesTest.php

diff --git a/core/modules/views/src/Plugin/views/filter/ManyToOne.php b/core/modules/views/src/Plugin/views/filter/ManyToOne.php
index e678dee82f3f..86eb1f3361ba 100644
--- a/core/modules/views/src/Plugin/views/filter/ManyToOne.php
+++ b/core/modules/views/src/Plugin/views/filter/ManyToOne.php
@@ -129,6 +129,9 @@ protected function opHelper() {
     if (empty($this->value)) {
       return;
     }
+    // Form API returns unchecked options in the form of option_id => 0. This
+    // breaks the generated query for "is all of" filters so we remove them.
+    $this->value = array_filter($this->value, 'static::arrayFilterZero');
     $this->helper->addFilter();
   }
 
diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_exposed_form_checkboxes.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_exposed_form_checkboxes.yml
new file mode 100644
index 000000000000..26b1d3259ebd
--- /dev/null
+++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_exposed_form_checkboxes.yml
@@ -0,0 +1,155 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - taxonomy.vocabulary.test_exposed_checkboxes
+  module:
+    - node
+    - taxonomy
+id: test_exposed_form_checkboxes
+label: ''
+module: views
+description: ''
+tag: ''
+base_table: node_field_data
+base_field: nid
+core: '8'
+display:
+  default:
+    display_options:
+      access:
+        type: none
+      cache:
+        type: tag
+      exposed_form:
+        options:
+          reset_button: true
+        type: basic
+      filters:
+        type:
+          id: type
+          table: node_field_data
+          field: type
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: in
+          value: {  }
+          group: 1
+          exposed: true
+          expose:
+            operator_id: type_op
+            label: 'Content: Type'
+            description: 'Exposed description'
+            use_operator: false
+            operator: ''
+            identifier: type
+            required: false
+            remember: false
+            multiple: true
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+            reduce: false
+          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: in_operator
+          entity_type: node
+          entity_field: type
+        tid:
+          id: tid
+          table: taxonomy_index
+          field: tid
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: and
+          value: {  }
+          group: 1
+          exposed: true
+          expose:
+            operator_id: tid_op
+            label: 'Has taxonomy term'
+            description: ''
+            use_operator: false
+            operator: tid_op
+            identifier: tid
+            required: false
+            remember: false
+            multiple: true
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+            reduce: false
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          reduce_duplicates: false
+          type: select
+          limit: true
+          vid: test_exposed_checkboxes
+          hierarchy: false
+          error_message: true
+          plugin_id: taxonomy_index_tid
+      pager:
+        type: full
+      query:
+        options:
+          query_comment: ''
+        type: views_query
+      style:
+        type: default
+      row:
+        type: 'entity:node'
+      display_extenders: {  }
+    display_plugin: default
+    display_title: Master
+    id: default
+    position: 0
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - user
+        - 'user.node_grants:view'
+      tags: {  }
+  page_1:
+    display_options:
+      path: test_exposed_form_checkboxes
+      display_extenders: {  }
+    display_plugin: page
+    display_title: Page
+    id: page_1
+    position: 0
+    cache_metadata:
+      max-age: -1
+      contexts:
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+        - user
+        - 'user.node_grants:view'
+      tags: {  }
diff --git a/core/modules/views/tests/src/Functional/Plugin/ExposedFormCheckboxesTest.php b/core/modules/views/tests/src/Functional/Plugin/ExposedFormCheckboxesTest.php
new file mode 100644
index 000000000000..80584d796e2b
--- /dev/null
+++ b/core/modules/views/tests/src/Functional/Plugin/ExposedFormCheckboxesTest.php
@@ -0,0 +1,168 @@
+<?php
+
+namespace Drupal\Tests\views\Functional\Plugin;
+
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait;
+use Drupal\taxonomy\Entity\Term;
+use Drupal\taxonomy\Entity\Vocabulary;
+use Drupal\Tests\views\Functional\ViewTestBase;
+use Drupal\views\Tests\ViewTestData;
+use Drupal\views\Views;
+
+/**
+ * Tests exposed forms functionality.
+ *
+ * @group views
+ */
+class ExposedFormCheckboxesTest extends ViewTestBase {
+
+  use EntityReferenceTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $testViews = ['test_exposed_form_checkboxes'];
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = ['node', 'views_ui', 'taxonomy'];
+
+  /**
+   * Test terms.
+   *
+   * @var array
+   */
+  public $terms = [];
+
+  /**
+   * Vocabulary for testing checkbox options.
+   *
+   * @var \Drupal\taxonomy\Entity\Vocabulary
+   */
+  public $vocabulary;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp($import_test_views = TRUE) {
+    parent::setUp(FALSE);
+
+    // Create a vocabulary and entity reference field so we can test the "is all
+    // of" filter operator. Must be done ahead of the view import so the
+    // vocabulary is in place to meet the view dependencies.
+    $vocabulary = Vocabulary::create([
+      'name' => 'test_exposed_checkboxes',
+      'vid' => 'test_exposed_checkboxes',
+      'nodes' => ['article' => 'article'],
+    ]);
+    $vocabulary->save();
+    $this->vocabulary = $vocabulary;
+
+    ViewTestData::createTestViews(self::class, ['views_test_config']);
+    $this->enableViewsTestModule();
+
+    // Create two content types.
+    $this->drupalCreateContentType(['type' => 'article']);
+    $this->drupalCreateContentType(['type' => 'page']);
+
+    // Create some random nodes: 5 articles, one page.
+    for ($i = 0; $i < 5; $i++) {
+      $this->drupalCreateNode(['type' => 'article']);
+    }
+    $this->drupalCreateNode(['type' => 'page']);
+  }
+
+  /**
+   * Tests overriding the default render option with checkboxes.
+   */
+  public function testExposedFormRenderCheckboxes() {
+    // Use a test theme to convert multi-select elements into checkboxes.
+    \Drupal::service('theme_handler')->install(['views_test_checkboxes_theme']);
+    $this->config('system.theme')
+      ->set('default', 'views_test_checkboxes_theme')
+      ->save();
+
+    // Only display 5 items per page so we can test that paging works.
+    $view = Views::getView('test_exposed_form_checkboxes');
+    $display = &$view->storage->getDisplay('default');
+    $display['display_options']['pager']['options']['items_per_page'] = 5;
+
+    $view->save();
+    $this->drupalGet('test_exposed_form_checkboxes');
+
+    $actual = $this->xpath('//form//input[@type="checkbox" and @name="type[article]"]');
+    $this->assertEqual(count($actual), 1, 'Article option renders as a checkbox.');
+    $actual = $this->xpath('//form//input[@type="checkbox" and @name="type[page]"]');
+    $this->assertEqual(count($actual), 1, 'Page option renders as a checkbox');
+
+    // Ensure that all results are displayed.
+    $rows = $this->xpath("//div[contains(@class, 'views-row')]");
+    $this->assertEqual(count($rows), 5, '5 rows are displayed by default on the first page when no options are checked.');
+
+    $this->clickLink('Page 2');
+    $rows = $this->xpath("//div[contains(@class, 'views-row')]");
+    $this->assertEqual(count($rows), 1, '1 row is displayed by default on the second page when no options are checked.');
+    $this->assertNoText('An illegal choice has been detected. Please contact the site administrator.');
+  }
+
+  /**
+   * Tests that "is all of" filters work with checkboxes.
+   */
+  public function testExposedIsAllOfFilter() {
+    foreach (['Term 1', 'Term 2', 'Term 3'] as $term_name) {
+      // Add a few terms to the new vocabulary.
+      $term = Term::create([
+        'name' => $term_name,
+        'vid' => $this->vocabulary->id(),
+      ]);
+      $term->save();
+      $this->terms[] = $term;
+    }
+
+    // Create a field.
+    $field_name = Unicode::strtolower($this->randomMachineName());
+    $handler_settings = [
+      'target_bundles' => [
+        $this->vocabulary->id() => $this->vocabulary->id(),
+      ],
+      'auto_create' => FALSE,
+    ];
+    $this->createEntityReferenceField('node', 'article', $field_name, NULL, 'taxonomy_term', 'default', $handler_settings, FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
+
+    // Add some test nodes.
+    $this->createNode([
+      'type' => 'article',
+      $field_name => [$this->terms[0]->id(), $this->terms[1]->id()],
+    ]);
+    $this->createNode([
+      'type' => 'article',
+      $field_name => [$this->terms[0]->id(), $this->terms[2]->id()],
+    ]);
+
+    // Use a test theme to convert multi-select elements into checkboxes.
+    \Drupal::service('theme_handler')->install(['views_test_checkboxes_theme']);
+    $this->config('system.theme')
+      ->set('default', 'views_test_checkboxes_theme')
+      ->save();
+
+    $this->drupalGet('test_exposed_form_checkboxes');
+
+    // Ensure that all results are displayed.
+    $rows = $this->xpath("//div[contains(@class, 'views-row')]");
+    $this->assertEqual(count($rows), 8, 'All rows are displayed by default on the first page when no options are checked.');
+    $this->assertNoText('An illegal choice has been detected. Please contact the site administrator.');
+
+    // Select one option and ensure we still have results.
+    $tid = $this->terms[0]->id();
+    $this->drupalPostForm(NULL, ["tid[$tid]" => $tid], t('Apply'));
+
+    // Ensure only nodes tagged with $tid are displayed.
+    $rows = $this->xpath("//div[contains(@class, 'views-row')]");
+    $this->assertEqual(count($rows), 2, 'Correct rows are displayed when a tid is selected.');
+    $this->assertNoText('An illegal choice has been detected. Please contact the site administrator.');
+  }
+
+}
diff --git a/core/modules/views/tests/src/Functional/Plugin/ExposedFormTest.php b/core/modules/views/tests/src/Functional/Plugin/ExposedFormTest.php
index 134f7b545860..220dc243533b 100644
--- a/core/modules/views/tests/src/Functional/Plugin/ExposedFormTest.php
+++ b/core/modules/views/tests/src/Functional/Plugin/ExposedFormTest.php
@@ -191,48 +191,6 @@ public function testResetButton() {
     $this->helperButtonHasLabel('edit-reset', $expected_label);
   }
 
-  /**
-   * Tests overriding the default render option with checkboxes.
-   */
-  public function testExposedFormRenderCheckboxes() {
-    // Make sure we have at least two options for node type.
-    $this->drupalCreateContentType(['type' => 'page']);
-    $this->drupalCreateNode(['type' => 'page']);
-
-    // Use a test theme to convert multi-select elements into checkboxes.
-    \Drupal::service('theme_handler')->install(['views_test_checkboxes_theme']);
-    $this->config('system.theme')
-      ->set('default', 'views_test_checkboxes_theme')
-      ->save();
-
-    // Set the "type" filter to multi-select.
-    $view = Views::getView('test_exposed_form_buttons');
-    $filter = $view->getHandler('page_1', 'filter', 'type');
-    $filter['expose']['multiple'] = TRUE;
-    $view->setHandler('page_1', 'filter', 'type', $filter);
-
-    // Only display 5 items per page so we can test that paging works.
-    $display = &$view->storage->getDisplay('default');
-    $display['display_options']['pager']['options']['items_per_page'] = 5;
-
-    $view->save();
-    $this->drupalGet('test_exposed_form_buttons');
-
-    $actual = $this->xpath('//form//input[@type="checkbox" and @name="type[article]"]');
-    $this->assertEqual(count($actual), 1, 'Article option renders as a checkbox.');
-    $actual = $this->xpath('//form//input[@type="checkbox" and @name="type[page]"]');
-    $this->assertEqual(count($actual), 1, 'Page option renders as a checkbox');
-
-    // Ensure that all results are displayed.
-    $rows = $this->xpath("//div[contains(@class, 'views-row')]");
-    $this->assertEqual(count($rows), 5, '5 rows are displayed by default on the first page when no options are checked.');
-
-    $this->clickLink('Page 2');
-    $rows = $this->xpath("//div[contains(@class, 'views-row')]");
-    $this->assertEqual(count($rows), 1, '1 row is displayed by default on the second page when no options are checked.');
-    $this->assertNoText('An illegal choice has been detected. Please contact the site administrator.');
-  }
-
   /**
    * Tests the exposed block functionality.
    */
diff --git a/core/modules/views/tests/themes/views_test_checkboxes_theme/views_test_checkboxes_theme.theme b/core/modules/views/tests/themes/views_test_checkboxes_theme/views_test_checkboxes_theme.theme
index cab52ac713a2..393c6cbd1c5a 100644
--- a/core/modules/views/tests/themes/views_test_checkboxes_theme/views_test_checkboxes_theme.theme
+++ b/core/modules/views/tests/themes/views_test_checkboxes_theme/views_test_checkboxes_theme.theme
@@ -11,5 +11,10 @@
  * Changes an exposed "type" filter from a multi-select to checkboxes.
  */
 function views_test_checkboxes_theme_form_views_exposed_form_alter(&$form, FormStateInterface $form_state) {
-  $form['type']['#type'] = 'checkboxes';
+  if (isset($form['type'])) {
+    $form['type']['#type'] = 'checkboxes';
+  }
+  if (isset($form['tid'])) {
+    $form['tid']['#type'] = 'checkboxes';
+  }
 }
-- 
GitLab