And more precisely, one using a custom field. I'm trying to achieve an interval based filter with a select list for the interface.

As of now, I managed to make the filter appear by implementing hook_views_data :

function interval_select_views_data() {
  $data['node']['field_pv_value'] = array(    
    'group' => t('Annonce'),
    'title' => t('Prix de vente du bien'),
    'help' => t('Filtrer sur intervalles de prix.'), // The help that appears on the UI,  
    'real field' => 'field_pv_value',
    'filter' => array(
      'handler' => 'interval_select_handler_filter_interval',
    ),    
  );

I wrote a custom handler derived from views_handler_filter_numeric ; the filter is taken into account and shown when I expose it, and the query is affected so I guess I did fine so far.

But whenever the query gets executed, I got an "Exception: SQLSTATE[42S22]: Column not found" error, because the query searches for the field on the node table where it isn't, because the data is in the field table (field_data_field_pv in this case).

I tried several join parameters in the views_data hook (Views 2 style), joining the node table with the other via entity_id, but couldn't manage to make the thing work.

I think I'm close, but would welcome some help because I'm stuck and can't find any info on this particular case.

CommentFileSizeAuthor
#14 interval_select.zip8.55 KBCountzero

Comments

merlinofchaos’s picture

You need to paste your handler code.

Countzero’s picture

It's still very messy and far from production state, but here it is :


/**
 * Simple filter to handle greater than/less than filters
 */
class interval_select_handler_filter_interval extends views_handler_filter_numeric {
  var $always_multiple = TRUE;
  function option_definition() {
    $options = parent::option_definition();

    $options['value'] = array(
      'contains' => array(
        'min' => array('default' => ''),
        'max' => array('default' => ''),
        'value' => array('default' => ''),
      ),
    );

    return $options;
  }

  function operators() {
    $operators = array(
      'between' => array(
        'title' => t('Is between'),
        'method' => 'op_between',
        'short' => t('between'),
        'values' => 2,
      ),
      'not between' => array(
        'title' => t('Is not between'),
        'method' => 'op_between',
        'short' => t('not between'),
        'values' => 2,
      ),
    );

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

    return $operators;
  }

  /**
   * Provide a list of all the numeric operators
   */
  function operator_options($which = 'title') {
    $options = array();
    foreach ($this->operators() as $id => $info) {
      $options[$id] = $info[$which];
    }

    return $options;
  }

  function operator_values($values = 1) {
    $options = array();
    foreach ($this->operators() as $id => $info) {
      if ($info['values'] == $values) {
        $options[] = $id;
      }
    }

    return $options;
  }
  /**
   * Provide a simple textfield for equality
   */
  function value_form(&$form, &$form_state) {
    $form['value']['#tree'] = TRUE;

    // 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 = in_array($this->operator, $this->operator_values(2)) ? '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 => $this->operator_values(1)),
      );
      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') {
        $tranches_prix = array(
          10000000 => 'Indifférent',
          100000 => '< 100 000 €',
          200000 => '100 000 € - 200 000 €',
          300000 => '200 000 € - 300 000 €',
          400000 => '300 000 € - 400 000 €',
          500000 => '> 400 000 €',
        );
      
      $form['value']['interval'] = array(
        '#type' => 'select',
        '#title' => 'Entre :',
//        '#size' => 30,
        '#options' => $tranches_prix,
//        '#default_value' => $this->value['min'],
      );
      $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 => $this->operator_values(2)),
        );
        $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
        );
      }
    }
  }

  function query() {
    $this->ensure_my_table();
    $field = "$this->table_alias.$this->real_field";

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

  function op_between($field) {
    // A changer pour intervalle
//          10000000 => 'Indifférent',
//          100000 => '< 100 000 €',
//          200000 => '100 000 € - 200 000 €',
//          300000 => '200 000 € - 300 000 €',
//          400000 => '300 000 € - 400 000 €',
//          500000 => '> 400 000 €',
//dsm($this->value['interval']);
    switch ($this->value['interval']) {
      case 10000000:
        $min = 0;
        $max = 10000000;
        break;
      case 100000:
        $min = 0;
        $max = 100000;
        break;
      case 200000:
        $min = 100000;
        $max = 200000;
        break;
      case 300000:
        $min = 200000;
        $max = 300000;
        break;
      case 400000:
        $min = 300000;
        $max = 400000;
        break;
      case 500000:
        $min = 500000;
        $max = 10000000;
        break;        
    }
    if ($this->operator == 'between') {
      $this->query->add_where($this->options['group'], $field, array($min, $max), 'BETWEEN');
    }
    else {
      $this->query->add_where($this->options['group'], db_or()->condition($field, $this->value['min'], '<=')->condition($field, $this->value['max'], '>='));
    }
  }

  function op_simple($field) {
    $this->query->add_where($this->options['group'], $field, $this->value['value'], $this->operator);
  }

  function op_empty($field) {
    if ($this->operator == 'empty') {
      $operator = "IS NULL";
    }
    else {
      $operator = "IS NOT NULL";
    }

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

  function admin_summary() {
    if (!empty($this->options['exposed'])) {
      return t('exposed');
    }

    $options = $this->operator_options('short');
    $output = check_plain($options[$this->operator]);
    if (in_array($this->operator, $this->operator_values(2))) {
      $output .= ' ' . t('@min and @max', array('@min' => $this->value['min'], '@max' => $this->value['max']));
    }
    elseif (in_array($this->operator, $this->operator_values(1))) {
      $output .= ' ' . check_plain($this->value['value']);
    }
    return $output;
  }

  /**
   * Do some minor translation of the exposed input
   */
  function accept_exposed_input($input) {
    if (empty($this->options['exposed'])) {
      return TRUE;
    }

    // rewrite the input value so that it's in the correct format so that
    // the parent gets the right data.
    if (!empty($this->options['expose']['identifier'])) {
      $value = &$input[$this->options['expose']['identifier']];
      if (!is_array($value)) {
        $value = array(
          'value' => $value,
        );
      }
    }

    $rc = parent::accept_exposed_input($input);

    if (empty($this->options['expose']['required'])) {
      // We have to do some of our own checking for non-required filters.
      $info = $this->operators();
      if (!empty($info[$this->operator]['values'])) {
        switch ($info[$this->operator]['values']) {
          case 1:
            if ($value['value'] === '') {
              return FALSE;
            }
            break;
//          case 2:
//            if ($value['min'] === '' && $value['max'] === '') {
//              return FALSE;
//            }
            break;
        }
      }
    }

    return $rc;
  }
}

It's basicaly a cut and paste from the original handler. The query seems correct except for the wrong field :

SELECT node.title AS node_title, node.nid AS nid, node.created AS node_created, 'node' AS field_data_field_pv_node_entity_type
FROM 
{node} node
WHERE (( (node.status = '1') AND (node.type IN  ('annonce')) AND (node.field_pv_value BETWEEN '200000' AND '300000') ))
ORDER BY node_created DESC
merlinofchaos’s picture

Status: Active » Fixed

Well, you've attached you field ot the node table:

  $data['node']['field_pv_value'] = array(    

So that is the table that it thinks it's going to add it to. You probably need to attach it to the table that field API is using, which I believe is the actual name of the table the field is on.

Countzero’s picture

Status: Fixed » Active

I tried this code :

  $data['field_data_field_pv']['field_pv_value'] = array(    
    'group' => t('Annonce'),
    'title' => t('Prix de vente du bien'),
    'help' => t('Filtrer sur intervalles de prix.'), // The help that appears on the UI,  
    'real field' => 'field_pv_value',
    'filter' => array(
      'handler' => 'interval_select_handler_filter_interval',
    ),    
  );

... simply replacing 'node' with the field's table name, and got this error :

Warning : class_exists() expects parameter 1 to be string, array given dans _views_create_handler() (ligne 22 dans /srv/sites/web59/web/sites/all/modules/views/includes/handlers.inc).

... repeated four times.

merlinofchaos’s picture

Status: Active » Fixed

Additional comments:

1) Since you're modifying existing tables, not declaring your own, you should be using hook_views_data_alter
2) If that field exists, probably the array_merge_recursive() is what's screwing you up there. Use the hook_views_data_alter() and make absolutely sure you're not colliding with already existing field. YOu can use a pseudo field and use 'real field' to make sure the field name is correct.

Countzero’s picture

It worked ! And in the meantime, I understood something new : I really thought hook_views_data was mandatory for any handler.

For the record, here's the definitive code I placed in my .views.inc:

function interval_select_views_data_alter(&$data) {
  $data['field_data_field_pv']['prix'] = array(
    'title' => t('Prix'),
    'group' => 'Annonces',
    'help' => t('Le prix de vente'),
   'real field' => 'field_pv_value',
   'filter' => array(
      'handler' => 'interval_select_handler_filter_interval',
    ),        
  );    
}

Thanks a lot for your time.

As usual, I'm amazed at the same time at how the solution was elegant and how hard it was to find the info.

neoglez’s picture

Component: Documentation » Code

1)
Since you're modifying existing tables, not declaring your own, you should be using hook_views_data_alter.

This is an enormous important piece of info that should be included in the docu, all we know so far (as per docu.) about hook_views_data_alter is that it should be in my_module.views.inc.

neoglez’s picture

Component: Code » Documentation
Category: support » task
Status: Fixed » Active
Countzero’s picture

Component: Code » Documentation

Sorry to reopen (kind of), but I have another problem : I stumble on the 'An illegal choice has been detected' issue filed here http://drupal.org/node/1177882.

I suspect my select lists are causing the trouble. They're produced that way (same as above, just repeating for convenience) in the value_form method :

    if ($which == 'all' || $which == 'minmax') {
        $tranches = array(
          10000000 => 'Any',
          100000 => '< 100 000 €',
          200000 => '100 000 € - 200 000 €',
          300000 => '200 000 € - 300 000 €',
          400000 => '300 000 € - 400 000 €',
          500000 => '> 400 000 €',
        );
      $form['value']['interval'] = array(
        '#type' => 'select',
        '#title' => ' ',
        '#options' => $tranches,
      );

And here is the op_between method overwritten by my handler :

  function op_between($field) {
        $min = 0;
        $max = 10000000;
        switch ($this->value['interval']) {
          case 10000000:
            $min = 0;
            $max = 10000000;
            break;
          case 100000:
            $min = 0;
            $max = 100000;
            break;
          case 200000:
            $min = 100000;
            $max = 200000;
            break;
          case 300000:
            $min = 200000;
            $max = 300000;
            break;
          case 400000:
            $min = 300000;
            $max = 400000;
            break;
          case 500000:
            $min = 400000;
            $max = 10000000;
            break;        
        }
      $this->query->add_where($this->options['group'], $field, array($min, $max), 'BETWEEN');
  }

I couldn't grep the message in Views code to see what triggers it.

The view works fine otherwise, except it seems to limit the results even if the default choice of 'Any' ('Indifférent' in french) is chosen in the list.

Countzero’s picture

I noticed that my selects don't get the 'Any' value, so I looked into exposed_translate in views_handler_filter.inc.

If I comment this part :

    if ($type == 'value' && empty($this->always_required) && empty($this->options['expose']['required']) && $form['#type'] == 'select' && empty($form['#multiple'])) {
      $any_label = variable_get('views_exposed_filter_any_label', 'new_any') == 'old_any' ? t('<Any>') : t('- Any -');
      $form['#options'] = array('All' => $any_label) + $form['#options'];
      $form['#default_value'] = 'All';
    }

... the view generates one more message. What's strange is that dpm'ing $form['#options'] never outputs my custon filters but does print info about regular ones (mean : normal filters based on select widget fields).

So, the message is related to my issue, but the code doesn't seem to affect the custom selects.

Not sure about anything now ; I'm just trying to help people helping me.

Countzero’s picture

Progressed a bit : One of my two very similar filters was causing the loss of records, because null values are not considered. So the issue actually splits in two, but all two problems are about filter handlers writing, so I'm sticking with this issue alone.

Here are the two issues :

1 - How to modify the query in case of a select widget filter to add an OR clause to pull rows with NULL values for the field ?

2 - How to explain, and get rid of, the error message 'An illegal choice was detected etc.' ?

Hope it's clear and interesting enough to obtain help from experimented views devs.

Countzero’s picture

Category: task » support

For point 1, I managed to condition the add_where to fit my needs, and saw some examples of adding OR clauses elsewhere in the code. Just for the record it's done like that :

      $this->query->add_where($this->options['group'], db_or()->condition($field, $this->value['min'], '<=')->condition($field, $this->value['max'], '>='));

So only the "Illegal choice" message needs to be explained, given it doesn't seem to prevent the view from functioning, except for the preview and th annoying message.

Putting this issue back to 'Support Request' until some kind of answer is given, or until a maintainer wants to discard it.

dawehner’s picture

Such errors are hard to track from just looking at it. Perhaps it would help if you could bundle this into a module
so it's reusable.

Countzero’s picture

StatusFileSize
new8.55 KB

Here is the module. It's very case specific, so please don't take it for anything pretending to be universal.

Countzero’s picture

Another information : when I set the filter values to be remembered, they are not.

Countzero’s picture

After further investigation, including dumping info from form.inc, I'm pretty sure this has something to do with my handler not providing a default value.

If I set the #value in value_form, the error disappears. Of course, it's not a solution because then the filter always behaves like the user had entered the value I set, but it's the best hint I got yet.

So I'm gonna look at the original handler code and try to figure what method I should override to provide a default value. Any clue would be welcome as I'm at the very limit of my skills here.

dawehner’s picture

    $options['value'] = array(
      'contains' => array(
        'value' => array('default' => ''),
      ),
    );

So the default value is something like $this->value['value'] but you probably want perhaps $this->value['interval']

Countzero’s picture

Sorry but I'm not sure where to put this code.

dawehner’s picture

Oh this is just a quote from your code, you expect something like value['value'] in the quoted code.

But your form looks totally different.

Countzero’s picture

Well, I just added an ['interval'] element and try to manage it as I can. As I don't really understand the processing of all this, it's hit and miss.

Setting something in option_definition doesn't seem to do any good.

I tried

    $options['value']['interval'] = array(
      'contains' => array(
        'value' => array('default' => 10000000),
      ),
    );

But it doesn't work either. I'm a bit lost.

Before your post, I was reduced to comment the form validating code in form.inc. I know it's genocidal from a kitten point of view, but the view works perfectly that way. I can't of course go production with this but it'll have to do as of now.

If you'd be glad enough to explain to me the basics of default values in filter handlers, I could try to do something, but as of now I'm clueless.

Countzero’s picture

I tried various combinations with this array and cannot change anything. Even removing it entirely dosn't affect the handler's behavior.

I looked into the code of many (MANY) other handlers and couln't figure out how these defaults are handled. Googling about filters handlers led me to examples but no really meaningful explanation.

So I'll stick with my kitten slaughtering solution as of now and see if I can wrap my mind around this problem next week.

Thanks for your help.

dawehner’s picture

    $options['value'] = array(
      'contains' => array(
        'min' => array('default' => ''),
        'max' => array('default' => ''),
        'value' => array('default' => ''),
      ),
    );

You could probably use the same kind of structure.

jphil’s picture

How did this turn out in the end?

Countzero’s picture

It's been a while now, but if I remember correctly, kittens got killed in the process, because I couldn't figure out how to implement the correct default values.

ejustice’s picture

I don't know if this will help you, but I've got a custom widget that has an optgroup for the select. For my view I wanted to allow users to select based on the values from this select. This was causing havoc with selecting all. I ended up creating my own custom views_handler_filter that extends views_handler_filter_in_operator. Then I created my own get_value_options(). In here I could set $this->value_options to my 2D array for the optgroup so that the drop down would look like the expect and then a 1D array version of the data to $this->value. The main downside I've found is that the fact that "select all" is the default isn't reflected in the views UI.

  class views_handler_filter_ac_details_id_w_other extends views_handler_filter_in_operator {
function get_value_options() {
    $aircraft_list = aircraft_details_get_aircraft_schedule_list();
    $allowed = array();
    $all = array();
    foreach ($aircraft_list as $acname => $acdata) {
      $allowed[$acdata['type']][$acdata['details_id']] = $acname;
      $all[$acname] = $acdata['details_id'];
    }
    $this->value_options = $allowed;
    //NB - This will force the view to default select all, but it won't show up that way in the UI....
    $this->value = $all;
  }
}
kars-t’s picture

Status: Active » Fixed

Dear fellow Drupal enthusiasts,

this issue is now lasting for a very long time in the issue queue and was unfortunately never solved. As Drupal is a open source project everyone is helping on voluntary basis. So that this is was not solved is nothing personal and means no harm. But perhaps no one had time to deal with this issue, maybe it is too complex or did not describe the problem comprehensibly.

But this issue is not the only one. There are thousands of issues on Drupal.org that have never been worked on or could not be processed. This means that we are building a wave that is unmanageable and just a problem for the Drupal project as a whole. Please help us keep the issue queue smaller and more manageable.

Please read again, "Making an issue report" and see if you can improve the issue. Test the problem with the current Core and modules. Maybe the problem doesn't exist anymore, is a duplicate or has even been solved within this issue but never closed.

Help can also be found for it on IRC and in the user groups.

In order to remove this issue, I have set this issue to "fixed".

If there is new information, please re-open the issue.

--
This issue was edited with the help of Issue Helper

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

tuwebo’s picture

Status: Closed (fixed) » Active

Hi all,
Sorry, but I'm opening this issue, since looks like it never got solved with a clear solution, and I have one that it is working for me, and for my case.

In my case, I wanted to filter a view by a price_field using a custom checkbox exposed filter and "between" operator (pretty similar to what we have in the issue's description, which uses a select list instead).

I just come up with this solution, that it is working for me (I didn't test it very much, but maybe you guys could help me here).

You have to take in account that:

  • My custom filter handler extends views_handler_filter_numeric.
  • filter_name_in_view_ui: Is the name of the "filter identifier" that I set in views UI, when setting the filter options (it is under the "More" link when you configure the filter)
  • This function is just a quick and dirty prototype to see how views+custom exposed filter behaves, so even if it works, you should take a look at it and perform the changes that you need, specially if you have a long list of filter values.
  • And, of course, I clear the cache before trying this code.

The "key" for me was views_handler_filter::value_form(&$form, &$form_state) function which I have overwritten inside my custom class in this way:

<?php
function value_form(&$form, &$form_state) {
  // Parent needs to do many things here
  parent::value_form($form, $form_state);
  
  // I will set a new checkbox element for this form (which I exposed in the views UI)
  $form['my_checkbox_prices'] = array(
    '#type' => 'checkboxes',
    '#options' => array(
      '0_10' => 'Prices between 0 and 10 €',
      '11_50' => 'Prices between 11 and 50 €',
      '51_100' => 'Prices between 51 and 100 €',
      '101_150' => 'Prices between 101 and 150 €',
      '151_200' => 'Prices between 151 and 200 €',
    ),
    '#title' => t('Please select a price'),
  );
  
  // @todo for best performance. Copy and sort the array, then perform shift/pop for getting min/max values instead of looping.
  if (isset($form_state['input']) && !empty($form_state['input']['my_checkbox_prices'])) {
    $checkbox_min = NULL;
    $checkbox_max = NULL;
    foreach($form_state['input']['my_checkbox_prices'] as $key => $prices) {
      $min_max = explode('_', $prices);

       $checkbox_min = ((is_numeric($min_max[0]) && ($min_max[0] < $checkbox_min)) || (is_numeric($min_max[0]) && is_null($checkbox_min))) ? $min_max[0] : $checkbox_min;

        $checkbox_max = ((is_numeric($min_max[1]) && ($min_max[1] > $checkbox_max)) || (is_numeric($min_max[1]) && is_null($checkbox_max))) ? $min_max[1] : $checkbox_max;
    }
  }
  
  // NULL min/max values will behave as if no filter had been set showing all view results.
  $form_state['input']['filter_name_in_view_ui']['min'] = isset($checkbox_min) ? $checkbox_min : NULL;
  $form_state['input']['filter_name_in_view_ui']['max'] = isset($checkbox_max) ? $checkbox_max : NULL;
}
?>

After this code, every time I check/unckeck one checkbox, the view gets updated "magically" (thanks merlin) with the correct min/max values. It means, that it will get the lowest and highest price selected from all checkboxes that have been checked.

Hope this help, and hope you guys send some feedback here since I don´t know if I am doing it right or wrong, but it is done ;)

mustanggb’s picture

Issue summary: View changes
Status: Active » Closed (outdated)