I've got a View with an exposed Date filter using operator "Is equal to", granularity set to Month, and the Optional option enabled.

I'm not sure if there's supposed to be an "All"/"Any" option added to the year and month select lists when Optional is enabled, or if the "-Month" option is supposed to handle that (perhaps not very intuitive if so?). If the "-Month" option is selected no nodes show (it does filter properly if a month is selected).

#681450: Views optional exposed date filter does not work without value indicates that in 6.x-2.x-dev with exposed From and To dates it works (using -Year or -Month, etc as the "any"/"all" option). My view only exposes the From field.

You can see an example at http://harmonycentrefoundation.org/calendar

Comments

OliverColeman’s picture

Additionally, I've just started creating a new view that needs to list all dates with the selected month regardless of year (eg birthdays in the selected month), and so once again I need the Optional option to work, but this time for the Year field.

I came across an issue (#313498: Add granularity to Date Field filter & sort criteria in Views for anniversary type events) specifically about anniversary type listings of dates which would be handled if the Optional option worked (for exposed filters at least), however I think the main thing people wanted in that issue was a block listing todays birthdays (and similar), so this would only work if it was possible to select relative values separately for year, month and day in the filter settings form (and for example leaving year and month blank and selecting "now" for the day). Anyway, that's probably another can of worms/issue, I just need the Optional option to work for exposed filters! :)

OliverColeman’s picture

I've come up with a partial solution. I'm not sure if this is the way things should be done so haven't bothered rolling it as a patch yet.

The fix works by stopping the Date API from forcing empty values for date parts (year, month, day, etc) entered via select fields to default values (eg forcing an empty or zero value for a month or day to be 1) in certain cases. If the granularity is year,month,day and a value is entered for year and month but not day then the granularity of the field is effectively reduced to year,month. However if a value is entered for the day but not the month then the month value will still be forced to 1; so this isn't a solution to the "birthday" filtering problem. Which is one reason why this is probably more of a hack than a proper solution.

I don't think it will affect anything except non-required Date select widgets; although I've altered a couple of functions that aren't unique to select widgets I think I've altered them in a way that shouldn't affect anything else. However, I haven't exhaustively tested it and don't have an in-depth understanding of the Date module so use this at your own risk!

Now, onto the changes:

In file date_api.module change function date_fuzzy_datetime to:

/**
 * Create valid datetime value from incomplete ISO dates or arrays.
 */
//function date_fuzzy_datetime($date) {
function date_fuzzy_datetime($date, $granularity = array('year', 'month', 'day', 'hour', 'minute')) {
  // A text ISO date, like MMMM-YY-DD HH:MM:SS
  if (!is_array($date)) {
    $date = date_iso_array($date);
  }
  // An date/time value in the format:
  //  array('date' => MMMM-YY-DD, 'time' => HH:MM:SS).
  elseif (array_key_exists('date', $date) || array_key_exists('time', $date)) {
    $date_part = array_key_exists('date', $date) ? $date['date'] : '';
    $time_part = array_key_exists('time', $date) ? $date['time'] : '';
    $date = date_iso_array(trim($date_part .' '. $time_part));
  }
  // Otherwise date must in in format:
  //  array('year' => YYYY, 'month' => MM, 'day' => DD).
  if (empty($date['year'])) {
    $date['year'] = date('Y');
  }
  if (empty($date['month']) && in_array('month', $granularity)) {
    $date['month'] = 1;
  }
  if (empty($date['day']) && in_array('day', $granularity)) {
    $date['day'] = 1;
  }
  $value = date_pad($date['year'], 4) .'-'. date_pad($date['month']) .'-'. 
    date_pad($date['day']) .' '. date_pad($date['hour']) .':'. 
    date_pad($date['minute']) .':'. date_pad($date['second']);
  return $value;
}

I've added the argument granularity. It has a default value such that the changes in the function won't take affect the result when it is called without that argument. The changes allow preventing the function from forcing day and month values from zero or empty to 1.

In file date_api_elements.inc change function date_select_input_value to:

/**
 * Helper function for extracting a date value out of user input.
 */
function date_select_input_value($element) {
  $granularity = date_format_order($element['#date_format']);
  if (date_is_valid($element['#value'], DATE_ARRAY, $granularity)) {
    // Use fuzzy_datetime here to be sure year-only dates
    // aren't inadvertantly shifted to the wrong year by trying
    // to save '2009-00-00 00:00:00'.
    
    //decrease granularity to that entered by user (for non-required date select fields)
    $remove = FALSE;
    for ($i = 1; $i < sizeof($granularity); $i++) { //skip year
      if (!$remove && !is_numeric($element['#value'][$granularity[$i]]))
        $remove = TRUE;
      if ($remove)
        unset($granularity[$i]);
    }
    
    return date_fuzzy_datetime(date_convert($element['#value'], DATE_ARRAY, DATE_DATETIME), $granularity);
  }
  return NULL;
}

Here we're decreasing granularity to that entered by the user (for non-required date select fields). The decreased granularity is then passed to date_fuzzy_datetime which we modified above to allow specifying the granularity.

Finally, in file includes/date_api_filter_handler.inc change the function date_filter to:

  function date_filter($prefix, $query_field, $operator) {
    $field = $query_field['field'];
    // Handle the simple operators first.
    if ($operator == 'empty') {
      $this->add_date_field($field);
      return $field['fullname'] .' IS NULL';
    }
    elseif ($operator == 'not empty') {
      $this->add_date_field($field);
      return $field['fullname'] .' IS NOT NULL';
    }

    // Views treats the default values as though they are submitted
    // so we when it is really not submitted we have to adjust the
    // query to match what should have been the default.
    $value_parts = !is_array($this->value[$prefix]) ? array($this->value[$prefix]) : $this->value[$prefix];
    foreach ($value_parts as $part) {
      $default = $this->default_value($prefix);
      if (!empty($this->force_value) && !empty($default)) {
        $this->value[$prefix] = $default;
      }
      else {
        if (empty($part)) {
          return '';
        }
      }
    }
    
    $this->add_date_field($field);
    $granularity = $this->options['granularity'];
    
    //decrease granularity to that entered by user to allow optional/any function
    $date_array = date_iso_array($this->value[$prefix]);
    $granularity_array = array('year');
    foreach (array('month', 'day', 'hour', 'minute', 'second') as $p) {
      //if value not entered for this element
      if ($date_array[$p] == 0)
        break;
        
      $granularity_array[] = $p;
      
      //if minimum granularity reached for this date
      if ($p == $granularity)
        break;
    }
    $granularity = $granularity_array[sizeof($granularity_array)-1];
    
    $date_handler = $query_field['date_handler'];
    $this->format = $date_handler->views_formats($granularity, 'sql');
    $complete_date = date_fuzzy_datetime($this->value[$prefix], $granularity_array);
    //$complete_date = date_fuzzy_datetime($this->value[$prefix]);
    $date = date_make_date($complete_date, NULL, DATE_DATETIME, $granularity_array);
    //$date = date_make_date($complete_date);
    $value = date_format($date, $this->format);
    
    $range = $this->date_handler->arg_range($value);
    $year_range = date_range_years($this->options['year_range']);
    if ($this->operator != 'not between') {
      switch ($operator) {
        case '>':
        case '>=':
          $range[1] = date_make_date(date_pad($year_range[1], 4) .'-12-31 23:59:59');  
          if ($operator == '>') {
            date_modify($range[0], '+1 second');
          }
          break;
        case '<':
        case '<=':
          $range[0] = date_make_date(date_pad($year_range[0], 4) .'-01-01 00:00:00');
          if ($operator == '<') {
            date_modify($range[1], '-1 second');
          }
          break;
      }      
    }
    
    $min_date = $range[0];
    $max_date = $range[1];
        
    $this->min_date = $min_date;
    $this->max_date = $max_date;
    $this->year = date_format($date, 'Y');
    $this->month = date_format($date, 'n');
    $this->day = date_format($date, 'j');
    $this->week = date_week(date_format($date, DATE_FORMAT_DATE));
    $this->date_handler = $date_handler;
        
    if ($this->date_handler->granularity == 'week') {
      $this->format = DATE_FORMAT_DATETIME;
    }
    switch ($prefix) {
      case 'min':
        $value = date_format($min_date, $this->format);
        break;
      case 'max':
        $value = date_format($max_date, $this->format);
        break;
      default:
        $value = date_format($date, $this->format);
        break;
    }
    if ($this->date_handler->granularity != 'week') {
      $sql = $date_handler->sql_where_format($this->format, $field['fullname'], $operator, $value);
    }
    else {
      $sql = $date_handler->sql_where_date('DATE', $field['fullname'], $operator, $value);
    }
    return $sql;
  }

Again here we're decreasing the granularity to that entered by the user. The decreased granularity is then passed to date_fuzzy_datetime and date_make_date. The resulting date formatted for the SQL query then only contains the elements according to the reduced granularity.

OliverColeman’s picture

And if you want to replace "-Month", "-Day", etc options in optional date select fields with "", in function date_api_elements around line 323 replace the lines

      $sub_element[$field]['#type'] = 'select';
      $sub_element[$field]['#theme'] = 'date_select_element';
      if ($element['#date_label_position'] == 'within') {
        $sub_element[$field]['#options'] = array('' => '-'. $label) + $sub_element[$field]['#options'];
      }
      elseif ($element['#date_label_position'] != 'none') {
        $sub_element[$field]['#title'] = $label;
      }

with

      $sub_element[$field]['#type'] = 'select';
      $sub_element[$field]['#theme'] = 'date_select_element';
      if ($element['#date_label_position'] == 'within') {
        if ($sub_element[$field]['#required'] || $field == 'year')
          $sub_element[$field]['#options'] = array('' => '-'. $label) + $sub_element[$field]['#options'];
        else
          $sub_element[$field]['#options'] = array('' => '<Any>') + $sub_element[$field]['#options'];
      }
      elseif ($element['#date_label_position'] != 'none') {
        $sub_element[$field]['#title'] = $label;
      }
chellman’s picture

Subscribing for now. I'm having the same issue and want to investigate this further.

YK85’s picture

subscribing

patcon’s picture

subscribe

zachwood’s picture

subscribe

Bevan’s picture

Status: Active » Closed (duplicate)

AFAICT this is the same issue as #681450: Views optional exposed date filter does not work without value, which has a cleaner fix.