Verified Commit b6c02433 authored by Dave Long's avatar Dave Long
Browse files

Issue #3322402 by idebr, smustgrave, Lendude, lind101, akalata, quietone,...

Issue #3322402 by idebr, smustgrave, Lendude, lind101, akalata, quietone, alexpott: Add 'Is empty (NULL)' and 'Is not empty (NOT NULL)' operators to boolean field filtering
parent 99e76680
Loading
Loading
Loading
Loading
Loading
+100 −15
Original line number Diff line number Diff line
@@ -82,7 +82,7 @@ public function operatorOptions($which = 'title') {
   * {@inheritdoc}
   */
  public function operators() {
    return [
    $operators = [
      '=' => [
        'title' => $this->t('Is equal to'),
        'method' => 'queryOpBoolean',
@@ -98,6 +98,25 @@ public function operators() {
        'query_operator' => self::NOT_EQUAL,
      ],
    ];

    // If the definition allows for the empty operator, add it.
    if (!empty($this->definition['allow empty'])) {
      $operators += [
        'empty' => [
          'title' => $this->t('Is empty (NULL)'),
          'method' => 'opEmpty',
          'short' => $this->t('empty'),
          'values' => 0,
        ],
        'not empty' => [
          'title' => $this->t('Is not empty (NOT NULL)'),
          'method' => 'opEmpty',
          'short' => $this->t('not empty'),
          'values' => 0,
        ],
      ];
    }
    return $operators;
  }

  /**
@@ -122,6 +141,8 @@ public function init(ViewExecutable $view, DisplayPluginBase $display, ?array &$
      $this->accept_null = (bool) $this->definition['accept_null'];
    }
    $this->valueOptions = NULL;

    $this->definition['allow empty'] = TRUE;
  }

  /**
@@ -166,6 +187,8 @@ protected function defineOptions() {
  }

  protected function valueForm(&$form, FormStateInterface $form_state) {
    $form['value'] = [];

    if (empty($this->valueOptions)) {
      // Initialize the array of possible values for this filter.
      $this->getValueOptions();
@@ -178,16 +201,28 @@ protected function valueForm(&$form, FormStateInterface $form_state) {
      // Configuring a filter: use radios for clarity.
      $filter_form_type = 'radios';
    }
    $display_options = 'all';
    $source = ':input[name="options[operator]"]';
    if ($exposed) {
      $identifier = $this->options['expose']['identifier'];
      if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) {
        $display_options = in_array($this->operator, $this->operatorValues(1)) ? 'value' : 'none';
      }
      else {
        $source = ':input[name="' . $this->options['expose']['operator_id'] . '"]';
      }
    }

    if ($display_options === 'all' || $display_options === 'value') {
      $form['value'] = [
        '#type' => $filter_form_type,
        '#title' => $this->value_value,
        '#options' => $this->valueOptions,
        '#default_value' => $this->value,
      ];
    if (!empty($this->options['exposed'])) {
      $identifier = $this->options['expose']['identifier'];
      $user_input = $form_state->getUserInput();
      if ($exposed && !isset($user_input[$identifier])) {
      if ($exposed && isset($identifier) && !isset($user_input[$identifier])) {
        $user_input[$identifier] = $this->value;
        $form_state->setUserInput($user_input);
      }
@@ -195,6 +230,15 @@ protected function valueForm(&$form, FormStateInterface $form_state) {
      if (!$exposed || empty($this->options['expose']['required'])) {
        $form['value']['#options'] = ['All' => $this->t('- Any -')] + $form['value']['#options'];
      }

      if ($display_options === 'all') {
        // Setup #states for operators with a value.
        foreach ($this->operatorValues(1) as $operator) {
          $form['value']['#states']['visible'][] = [
            $source => ['value' => $operator],
          ];
        }
      }
    }
  }

@@ -214,6 +258,8 @@ public function adminSummary() {
    if (empty($this->valueOptions)) {
      $this->getValueOptions();
    }
    if (in_array($this->operator, $this->operatorValues(1), TRUE)) {
      $this->getValueOptions();
      // Now that we have the valid options for this filter, just return the
      // human-readable label based on the current value.  The valueOptions
      // array is keyed with either 0 or 1, so if the current value is not
@@ -221,6 +267,9 @@ public function adminSummary() {
      return $this->operator . ' ' . $this->valueOptions[!empty($this->value)];
    }

    return $this->operator;
  }

  public function defaultExposeOptions() {
    parent::defaultExposeOptions();
    $this->options['expose']['operator_id'] = '';
@@ -237,7 +286,7 @@ public function query() {

    $info = $this->operators();
    if (!empty($info[$this->operator]['method'])) {
      call_user_func([$this, $info[$this->operator]['method']], $field, $info[$this->operator]['query_operator']);
      $this->{$info[$this->operator]['method']}($field, $info[$this->operator]['query_operator'] ?? NULL);
    }
  }

@@ -286,4 +335,40 @@ protected function queryOpBoolean($field, $query_operator = self::EQUAL) {
    }
  }

  /**
   * Filters by operator empty.
   *
   * @param string $field
   *   The views field.
   */
  protected function opEmpty(string $field): void {
    if ($this->operator === 'empty') {
      $operator = "IS NULL";
    }
    else {
      $operator = "IS NOT NULL";
    }

    $this->query->addWhere($this->options['group'], $field, NULL, $operator);
  }

  /**
   * Returns operators for values.
   *
   * @param int $values
   *   The values filter value.
   *
   * @return string[]
   *   A filtered list of operators.
   */
  protected function operatorValues(int $values = 1): array {
    $options = [];
    foreach ($this->operators() as $id => $info) {
      if (isset($info['values']) && $info['values'] === $values) {
        $options[] = $id;
      }
    }
    return $options;
  }

}
+77 −0
Original line number Diff line number Diff line
@@ -38,6 +38,30 @@ class FilterBooleanOperatorTest extends ViewsKernelTestBase {
    'views_test_data_id' => 'id',
  ];

  /**
   * {@inheritdoc}
   */
  protected function dataSet() {
    $dataset = parent::dataSet();
    $dataset[] = [
      'name' => 'Null',
      'age' => 0,
      'job' => 'Null',
      'created' => 0,
      'status' => NULL,
    ];
    return $dataset;
  }

  /**
   * {@inheritdoc}
   */
  protected function schemaDefinition() {
    $schema = parent::schemaDefinition();
    $schema['views_test_data']['fields']['status']['not null'] = FALSE;
    return $schema;
  }

  /**
   * Tests the BooleanOperator filter.
   */
@@ -112,6 +136,59 @@ public function testFilterBooleanOperator(): void {
    $this->assertIdenticalResultset($view, $expected_result, $this->columnMap);
  }

  /**
   * Tests the BooleanOperator empty/not empty filters.
   */
  public function testEmptyFilterBooleanOperator(): void {
    $view = Views::getView('test_view');
    $view->setDisplay();

    // Add an "empty" boolean filter on status.
    $view->displayHandlers->get('default')->overrideOption('filters', [
      'status' => [
        'id' => 'status',
        'field' => 'status',
        'table' => 'views_test_data',
        'operator' => 'empty',
      ],
    ]);
    $this->executeView($view);

    $expected_result = [
      ['id' => 6],
    ];

    $this->assertCount(1, $view->result);
    $this->assertIdenticalResultset($view, $expected_result, $this->columnMap);

    $view->destroy();
    $view->setDisplay();

    // Add a "not empty" boolean filter on status.
    $view->displayHandlers->get('default')->overrideOption('filters', [
      'status' => [
        'id' => 'status',
        'field' => 'status',
        'table' => 'views_test_data',
        'operator' => 'not empty',
      ],
    ]);
    $this->executeView($view);

    $expected_result = [
      ['id' => 1],
      ['id' => 2],
      ['id' => 3],
      ['id' => 4],
      ['id' => 5],
    ];

    $this->assertCount(5, $view->result);
    $this->assertIdenticalResultset($view, $expected_result, $this->columnMap);

    $view->destroy();
  }

  /**
   * Tests the boolean filter with grouped exposed form enabled.
   */