From d5c44d4ae1a1326da52dc2eb4da9300098326282 Mon Sep 17 00:00:00 2001
From: Peter Philipp <peter.philipp@cando-image.com>
Date: Thu, 13 Sep 2012 18:05:11 +0200
Subject: [PATCH] Issue #1783746 by das-peter: Add support for "between"
 operator

---
 .../search_api_views/includes/handler_filter.inc   |  166 +++++++++++++++++---
 .../includes/handler_filter_boolean.inc            |    8 +-
 .../includes/handler_filter_date.inc               |   48 ++++--
 .../includes/handler_filter_fulltext.inc           |    6 +-
 .../includes/handler_filter_language.inc           |   10 +-
 .../includes/handler_filter_options.inc            |   34 ++--
 includes/query.inc                                 |   24 +--
 7 files changed, 217 insertions(+), 79 deletions(-)

diff --git a/contrib/search_api_views/includes/handler_filter.inc b/contrib/search_api_views/includes/handler_filter.inc
index cc965da..87b69af 100644
--- a/contrib/search_api_views/includes/handler_filter.inc
+++ b/contrib/search_api_views/includes/handler_filter.inc
@@ -1,30 +1,33 @@
 <?php
 
 /**
+ * @file
+ * Views filter handler base class for handling all "normal" cases.
+ */
+
+/**
  * Views filter handler base class for handling all "normal" cases.
  */
 class SearchApiViewsHandlerFilter extends views_handler_filter {
 
   /**
-   * The value to filter for.
+   * The associated views query object.
    *
-   * @var mixed
+   * @var SearchApiViewsQuery
    */
-  public $value;
+  public $query;
 
-  /**
-   * The operator used for filtering.
-   *
-   * @var string
-   */
-  public $operator;
 
   /**
-   * The associated views query object.
-   *
-   * @var SearchApiViewsQuery
+   * Provide some extra help to get the operator/value easier to use.
    */
-  public $query;
+  public function init(&$view, &$options) {
+    // Maintain backward compatibility of the options format.
+    if (!is_array($options['value']) || (!isset($options['value']['value']) && !isset($options['value']['min']))) {
+      $options['value'] = array( 'value' => $options['value']);
+    }
+    parent::init($view, $options);
+  }
 
   /**
    * Provide a list of options for the operator form.
@@ -37,34 +40,147 @@ class SearchApiViewsHandlerFilter extends views_handler_filter {
       '<>' => t('Is not equal to'),
       '>=' => t('Is greater than or equal to'),
       '>' => t('Is greater than'),
+      'between' => t('Is between'),
+    );
+  }
+
+  /**
+   * Display the filter on the administrative summary.
+   */
+  public function admin_summary() {
+    if ($this->operator != 'between') {
+      return check_plain((string) $this->operator) . ' ' . check_plain((string) $this->value['value']);
+    }
+    return check_plain((string) $this->operator) . ' ' . check_plain((string) $this->value['min']) . ' / ' . check_plain((string) $this->value['max']);
+  }
+
+  /**
+   * Redefine the value option to be able to deal with multiple values.
+   */
+  public function option_definition() {
+    $options = parent::option_definition();
+    $options['value'] = array(
+      'contains' => array(
+        'min' => array('default' => ''),
+        'max' => array('default' => ''),
+        'value' => array('default' => ''),
+      ),
     );
+    return $options;
   }
 
   /**
    * Provide a form for setting the filter value.
+   *
+   * Heavily borrowed from views_handler_filter_numeric
+   *
+   * @see views_handler_filter_numeric::value_form()
    */
   public function value_form(&$form, &$form_state) {
-    while (is_array($this->value)) {
-      $this->value = $this->value ? array_shift($this->value) : NULL;
+    $form['value']['#tree'] = TRUE;
+
+    $single_field_operators = $this->operator_options();
+    unset($single_field_operators['between']);
+
+    // We have to make some choices when creating this as an exposed
+    // filter form. For example, if the operator is locked and thus
+    // not rendered, we can't render dependencies; instead we only
+    // render the form items we need.
+    $which = 'all';
+    if (!empty($form['operator'])) {
+      $source = ($form['operator']['#type'] == 'radios') ? 'radio:options[operator]' : 'edit-options-operator';
+    }
+
+    if (!empty($form_state['exposed'])) {
+      $identifier = $this->options['expose']['identifier'];
+
+      if (empty($this->options['expose']['use_operator']) || empty($this->options['expose']['operator_id'])) {
+        // Exposed and locked.
+        $which = ($this->operator == 'between') ? 'minmax' : 'value';
+      }
+      else {
+        $source = 'edit-' . drupal_html_id($this->options['expose']['operator_id']);
+      }
+    }
+
+    if ($which == 'all') {
+      $form['value']['value'] = array(
+        '#type' => 'textfield',
+        '#title' => empty($form_state['exposed']) ? t('Value') : '',
+        '#size' => 30,
+        '#default_value' => $this->value['value'],
+        '#dependency' => array($source => array_keys($single_field_operators)),
+      );
+      if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['value'])) {
+        $form_state['input'][$identifier]['value'] = $this->value['value'];
+      }
+    }
+    elseif ($which == 'value') {
+      // When exposed we drop the value-value and just do value if
+      // the operator is locked.
+      $form['value'] = array(
+        '#type' => 'textfield',
+        '#title' => empty($form_state['exposed']) ? t('Value') : '',
+        '#size' => 30,
+        '#default_value' => $this->value['value'],
+      );
+      if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier])) {
+        $form_state['input'][$identifier] = $this->value['value'];
+      }
+    }
+
+    if ($which == 'all' || $which == 'minmax') {
+      $form['value']['min'] = array(
+        '#type' => 'textfield',
+        '#title' => empty($form_state['exposed']) ? t('Min') : '',
+        '#size' => 30,
+        '#default_value' => $this->value['min'],
+      );
+      $form['value']['max'] = array(
+        '#type' => 'textfield',
+        '#title' => empty($form_state['exposed']) ? t('And max') : t('And'),
+        '#size' => 30,
+        '#default_value' => $this->value['max'],
+      );
+      if ($which == 'all') {
+        $dependency = array(
+          '#dependency' => array($source => array('between')),
+        );
+        $form['value']['min'] += $dependency;
+        $form['value']['max'] += $dependency;
+      }
+      if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['min'])) {
+        $form_state['input'][$identifier]['min'] = $this->value['min'];
+      }
+      if (!empty($form_state['exposed']) && !isset($form_state['input'][$identifier]['max'])) {
+        $form_state['input'][$identifier]['max'] = $this->value['max'];
+      }
+
+      if (!isset($form['value'])) {
+        // Ensure there is something in the 'value'.
+        $form['value'] = array(
+          '#type' => 'value',
+          '#value' => NULL,
+        );
+      }
     }
-    $form['value'] = array(
-      '#type' => 'textfield',
-      '#title' => empty($form_state['exposed']) ? t('Value') : '',
-      '#size' => 30,
-      '#default_value' => isset($this->value) ? $this->value : '',
-    );
   }
 
   /**
    * Add this filter to the query.
    */
   public function query() {
-    while (is_array($this->value)) {
+    while (is_array($this->value) && !isset($this->value['value']) && !isset($this->value['min'])) {
       $this->value = $this->value ? reset($this->value) : NULL;
     }
-    if ($this->value) {
-      $this->query->condition($this->real_field, $this->value, $this->operator, $this->options['group']);
+
+    $value = $this->value['value'];
+    if ($this->operator == 'between') {
+      $value = array(
+        $this->value['value']['min'],
+        $this->value['value']['max'],
+      );
     }
+    $this->query->condition($this->real_field, $value, $this->operator, $this->options['group']);
   }
-
 }
diff --git a/contrib/search_api_views/includes/handler_filter_boolean.inc b/contrib/search_api_views/includes/handler_filter_boolean.inc
index b3b2172..4c7a694 100644
--- a/contrib/search_api_views/includes/handler_filter_boolean.inc
+++ b/contrib/search_api_views/includes/handler_filter_boolean.inc
@@ -16,14 +16,14 @@ class SearchApiViewsHandlerFilterBoolean extends SearchApiViewsHandlerFilter {
    * Provide a form for setting the filter value.
    */
   public function value_form(&$form, &$form_state) {
-    while (is_array($this->value)) {
-      $this->value = $this->value ? array_shift($this->value) : NULL;
+    while (is_array($this->value['value'])) {
+      $this->value['value'] = $this->value['value'] ? array_shift($this->value['value']) : NULL;
     }
-    $form['value'] = array(
+    $form['value']['value'] = array(
       '#type' => 'select',
       '#title' => empty($form_state['exposed']) ? t('Value') : '',
       '#options' => array(1 => t('True'), 0 => t('False')),
-      '#default_value' => isset($this->value) ? $this->value : '',
+      '#default_value' => isset($this->value['value']) ? $this->value['value'] : '',
     );
   }
 
diff --git a/contrib/search_api_views/includes/handler_filter_date.inc b/contrib/search_api_views/includes/handler_filter_date.inc
index 9263bf9..e514a5c 100644
--- a/contrib/search_api_views/includes/handler_filter_date.inc
+++ b/contrib/search_api_views/includes/handler_filter_date.inc
@@ -1,7 +1,12 @@
 <?php
 
 /**
- * Views filter handler base class for handling all "normal" cases.
+ * @file
+ * Views filter handler class for handling dates.
+ */
+
+/**
+ * Views filter handler class for handling dates.
  */
 class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter {
 
@@ -9,29 +14,42 @@ class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter {
    * Provide a form for setting the filter value.
    */
   public function value_form(&$form, &$form_state) {
-    while (is_array($this->value)) {
-      $this->value = $this->value ? array_shift($this->value) : NULL;
-    }
-    $form['value'] = array(
-      '#type' => 'textfield',
-      '#title' => empty($form_state['exposed']) ? t('Value') : '',
-      '#description' => t('A date in any format understood by <a href="@doc-link">PHP</a>. For example, "@date1" or "@date2".',
-          array('@doc-link' => 'http://php.net/manual/en/function.strtotime.php', '@date1' => format_date(REQUEST_TIME, 'custom', 'Y-m-d H:i:s'), '@date2' => 'now + 1 day')),
-      '#size' => 30,
-      '#default_value' => isset($this->value) ? $this->value : '',
+    parent::value_form($form, $form_state);
+
+    // Add some more description to clarify which values are valid.
+    $description = t(
+      'A date in any format understood by <a href="@doc-link">PHP</a>. For example, "@date1" or "@date2".',
+      array(
+        '@doc-link' => 'http://php.net/manual/en/function.strtotime.php',
+        '@date1' => format_date(REQUEST_TIME, 'custom', 'Y-m-d H:i:s'),
+        '@date2' => 'now + 1 day',
+      )
     );
+    $form['value']['value']['#description'] = $description;
+    $form['value']['min']['#description'] = $description;
+    $form['value']['max']['#description'] = $description;
   }
 
   /**
    * Add this filter to the query.
    */
   public function query() {
-    while (is_array($this->value)) {
+    while (is_array($this->value['value']) && !isset($this->value['value']) && !isset($this->value['min'])) {
       $this->value = $this->value ? reset($this->value) : NULL;
     }
-    $v = is_numeric($this->value) ? $this->value : strtotime($this->value, REQUEST_TIME);
-    if ($v !== FALSE) {
-      $this->query->condition($this->real_field, $v, $this->operator, $this->options['group']);
+
+    // Convert php relative dates to valid timestamps for searching.
+    if ($this->operator != 'between') {
+      $value = is_numeric($this->value['value']) ? $this->value['value'] : strtotime($this->value['value'], REQUEST_TIME);
+    }
+    else {
+      $value = array(
+        (is_numeric($this->value['min']) ? $this->value['min'] : strtotime($this->value['min'], REQUEST_TIME)),
+        (is_numeric($this->value['max']) ? $this->value['max'] : strtotime($this->value['max'], REQUEST_TIME)),
+      );
+    }
+    if ($value !== FALSE) {
+      $this->query->condition($this->real_field, $value, $this->operator, $this->options['group']);
     }
   }
 
diff --git a/contrib/search_api_views/includes/handler_filter_fulltext.inc b/contrib/search_api_views/includes/handler_filter_fulltext.inc
index b7f2e1e..874b937 100644
--- a/contrib/search_api_views/includes/handler_filter_fulltext.inc
+++ b/contrib/search_api_views/includes/handler_filter_fulltext.inc
@@ -60,11 +60,11 @@ class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterTex
    * Add this filter to the query.
    */
   public function query() {
-    while (is_array($this->value)) {
-      $this->value = $this->value ? reset($this->value) : '';
+    while (is_array($this->value['value'])) {
+      $this->value['value'] = $this->value['value'] ? reset($this->value['value']) : '';
     }
     // Catch empty strings entered by the user, but not "0".
-    if ($this->value === '') {
+    if ($this->value['value'] === '') {
       return;
     }
     $fields = $this->options['fields'];
diff --git a/contrib/search_api_views/includes/handler_filter_language.inc b/contrib/search_api_views/includes/handler_filter_language.inc
index f95ddaf..1ad3216 100644
--- a/contrib/search_api_views/includes/handler_filter_language.inc
+++ b/contrib/search_api_views/includes/handler_filter_language.inc
@@ -18,10 +18,10 @@ class SearchApiViewsHandlerFilterLanguage extends SearchApiViewsHandlerFilterOpt
    */
   public function value_form(&$form, &$form_state) {
     parent::value_form($form, $form_state);
-    $form['value']['#options'] = array(
+    $form['value']['value']['#options'] = array(
       'current' => t("Current user's language"),
       'default' => t('Default site language'),
-    ) + $form['value']['#options'];
+    ) + $form['value']['value']['#options'];
   }
 
   /**
@@ -41,12 +41,12 @@ class SearchApiViewsHandlerFilterLanguage extends SearchApiViewsHandlerFilterOpt
    */
   public function query() {
     global $language_content;
-    foreach ($this->value as $i => $v) {
+    foreach ($this->value['value'] as $i => $v) {
       if ($v == 'current') {
-        $this->value[$i] = $language_content->language;
+        $this->value['value'][$i] = $language_content->language;
       }
       elseif ($v == 'default') {
-        $this->value[$i] = language_default('language');
+        $this->value['value'][$i] = language_default('language');
       }
     }
     parent::query();
diff --git a/contrib/search_api_views/includes/handler_filter_options.inc b/contrib/search_api_views/includes/handler_filter_options.inc
index e8606f4..5606aab 100644
--- a/contrib/search_api_views/includes/handler_filter_options.inc
+++ b/contrib/search_api_views/includes/handler_filter_options.inc
@@ -77,7 +77,7 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
     // *only* a list of checkboxes that were set, and we can use that
     // instead.
 
-    $form_state['values']['options']['value'] = $form['value']['#value'];
+    $form_state['values']['options']['value']['value'] = $form['value']['value']['#value'];
   }
 
   /**
@@ -91,13 +91,13 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
     else {
       $options += $this->definition['options'];
     }
-    $form['value'] = array(
+    $form['value']['value'] = array(
       '#type' => $this->value_form_type,
       '#title' => empty($form_state['exposed']) ? t('Value') : '',
       '#options' => $options,
       '#multiple' => TRUE,
       '#size' => min(4, count($this->definition['options'])),
-      '#default_value' => isset($this->value) ? $this->value : array(),
+      '#default_value' => isset($this->value['value']) ? $this->value['value'] : array(),
     );
   }
 
@@ -109,7 +109,7 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
       return t('exposed');
     }
 
-    if (!is_array($this->value)) {
+    if (!is_array($this->value['value'])) {
       return;
     }
 
@@ -118,22 +118,22 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
     $values = '';
 
     // Remove every element which is not known.
-    foreach ($this->value as $i => $value) {
+    foreach ($this->value['value'] as $i => $value) {
       if (!isset($this->definition['options'][$value])) {
-        unset($this->value[$i]);
+        unset($this->value['value'][$i]);
       }
     }
     // Choose different kind of ouput for 0, a single and multiple values.
-    if (count($this->value) == 0) {
+    if (count($this->value['value']) == 0) {
       return $this->operator == '=' ? t('none') : t('any');
     }
-    elseif (count($this->value) == 1) {
+    elseif (count($this->value['value']) == 1) {
       // If there is only a single value, use just the plain operator, = or <>.
       $operator = check_plain($this->operator);
-      $values = check_plain($this->definition['options'][reset($this->value)]);
+      $values = check_plain($this->definition['options'][reset($this->value['value'])]);
     }
     else {
-      foreach ($this->value as $value) {
+      foreach ($this->value['value'] as $value) {
         if ($values !== '') {
           $values .= ', ';
         }
@@ -152,25 +152,25 @@ class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
    * Add this filter to the query.
    */
   public function query() {
-    while (is_array($this->value) && count($this->value) == 1) {
-      $this->value = reset($this->value);
+    while (is_array($this->value['value']) && count($this->value['value']) == 1) {
+      $this->value['value'] = reset($this->value['value']);
     }
-    if (is_scalar($this->value) && $this->value !== '') {
-      $this->query->condition($this->real_field, $this->value, $this->operator, $this->options['group']);
+    if (is_scalar($this->value['value']) && $this->value['value'] !== '') {
+      $this->query->condition($this->real_field, $this->value['value'], $this->operator, $this->options['group']);
     }
-    elseif ($this->value) {
+    elseif ($this->value['value']) {
       if ($this->operator == '=') {
         $filter = $this->query->createFilter('OR');
         // $filter will be NULL if there were errors in the query.
         if ($filter) {
-          foreach ($this->value as $v) {
+          foreach ($this->value['value'] as $v) {
             $filter->condition($this->real_field, $v, '=');
           }
           $this->query->filter($filter, $this->options['group']);
         }
       }
       else {
-        foreach ($this->value as $v) {
+        foreach ($this->value['value'] as $v) {
           $this->query->condition($this->real_field, $v, $this->operator, $this->options['group']);
         }
       }
diff --git a/includes/query.inc b/includes/query.inc
index bba27fc..98e6142 100644
--- a/includes/query.inc
+++ b/includes/query.inc
@@ -939,11 +939,13 @@ interface SearchApiQueryFilterInterface {
    *   The value the field should have (or be related to by the operator).
    * @param $operator
    *   The operator to use for checking the constraint. The following operators
-   *   are supported for primitive types: "=", "<>", "<", "<=", ">=", ">". They
-   *   have the same semantics as the corresponding SQL operators.
-   *   If $field is a fulltext field, $operator can only be "=" or "<>", which
-   *   are in this case interpreted as "contains" or "doesn't contain",
-   *   respectively.
+   *   are supported for primitive types: "=", "<>", "<", "<=", ">=", ">",
+   *   "between". They have the same semantics as the corresponding SQL
+   *   operators. If $field is a fulltext field, $operator can only be "=" or
+   *   "<>", which are in this case interpreted as "contains" or
+   *   "doesn't contain", respectively.
+   *   The "between" operator expects the value to be an array with a min and
+   *   max value.
    *   If $value is NULL, $operator also can only be "=" or "<>", meaning the
    *   field must have no or some value, respectively.
    *
@@ -1030,11 +1032,13 @@ class SearchApiQueryFilter implements SearchApiQueryFilterInterface {
    *   The value the field should have (or be related to by the operator).
    * @param $operator
    *   The operator to use for checking the constraint. The following operators
-   *   are supported for primitive types: "=", "<>", "<", "<=", ">=", ">". They
-   *   have the same semantics as the corresponding SQL operators.
-   *   If $field is a fulltext field, $operator can only be "=" or "<>", which
-   *   are in this case interpreted as "contains" or "doesn't contain",
-   *   respectively.
+   *   are supported for primitive types: "=", "<>", "<", "<=", ">=", ">",
+   *   "between". They have the same semantics as the corresponding SQL
+   *   operators. If $field is a fulltext field, $operator can only be "=" or
+   *   "<>", which are in this case interpreted as "contains" or
+   *   "doesn't contain", respectively.
+   *   The "between" operator expects the value to be an array with a min and
+   *   max value.
    *   If $value is NULL, $operator also can only be "=" or "<>", meaning the
    *   field must have no or some value, respectively.
    *
-- 
1.7.10.msysgit.1

