From 40d5f16f5577d072228b9545e741a3edba6f38f7 Mon Sep 17 00:00:00 2001 From: Tim Plunkett Date: Fri, 17 Aug 2012 17:09:47 +0200 Subject: [PATCH] Issue #731662 by dagmar, dawehner, tim.plunkett, bojanz, SuperXren, BWPanda: Added Hybrid Exposed Filters. --- css/views-admin.theme.css | 1 + help/filter.html | 15 + includes/admin.inc | 119 +++- js/views-admin.js | 40 +- lib/Drupal/views/Plugin/views/Handler.php | 12 + .../views/display/DisplayPluginBase.php | 11 +- .../Plugin/views/filter/BooleanOperator.php | 3 + lib/Drupal/views/Plugin/views/filter/Date.php | 28 + .../Plugin/views/filter/FilterPluginBase.php | 667 +++++++++++++++++- .../views/Plugin/views/filter/InOperator.php | 3 + .../views/Plugin/views/filter/Numeric.php | 3 + .../views/Plugin/views/filter/String.php | 3 + lib/Drupal/views/Tests/ExposedFormTest.php | 57 +- .../Tests/Handler/FilterEqualityTest.php | 83 +++ .../Tests/Handler/FilterInOperatorTest.php | 99 +++ .../views/Tests/Handler/FilterNumericTest.php | 188 ++++- .../views/Tests/Handler/FilterStringTest.php | 395 ++++++++++- lib/Drupal/views/Tests/ViewsSqlTest.php | 20 +- lib/Drupal/views/View.php | 33 +- views.module | 22 +- views_ui.module | 6 + 21 files changed, 1751 insertions(+), 57 deletions(-) diff --git a/css/views-admin.theme.css b/css/views-admin.theme.css index b785abd77c23..ee52fd2d9426 100644 --- a/css/views-admin.theme.css +++ b/css/views-admin.theme.css @@ -380,6 +380,7 @@ td.group-title { text-transform: uppercase; } +.grouped-description, .exposed-description { float: left; padding-top: 3px; diff --git a/help/filter.html b/help/filter.html index 752617f3ab92..cb84f8a8a661 100644 --- a/help/filter.html +++ b/help/filter.html @@ -15,6 +15,21 @@ When you click the Rearrange Icon you can first rearrange your filters, easily d When you want that the user to select their own filter, you can expose the filter. A selection box will show for the user and they will be able to select one item. After that the view will reload and only the selected item will be displayed. You can also choose to expose the selection to a block, see here. +For exposed filters, you can create a grouped filter. When filters are in a group, each item of the group represents a set of operators and values. The following table illustrates how this feature works. The values of the first column of the table are displayed as options of a single select box: + + + + + + + + + + +
What the user seeWhat views does
Is lower than 10Operator: Is Lower than. Value: 10
Is between 10 and 20Operator: Is between. Value: 10 and 20
Is greater than 20Operator: Is Greater. Value: 20
+ +Please note: When using grouped filters with the option: 'Enable to allow users to select multiple items' checked, you probably may want to to place the filter in a separated group and define the operator of the group as 'OR'. This may be neccesary because in order to use multiple times the same filter, all options have to be applied using the OR operator, if not, probably you will get nothing listed since usually items in a group are mutually exclusive. + Taxonomy filters have been significantly altered in Views 7.x-3.x. D7 significantly re-organized taxonomy, there was a lot of duplicate taxonomy related fields and filters. Some of them were removed to try and reduce confusion between them. Implicit relationships to taxonomy have been removed, in favor of explicit relationships. If the filters you can find don't do what you need, try adding either the related taxonomy terms relationship, or a relationship on the specific taxonomy field. That will give you the term specific filters. You can override the complete filter section - see here for more information. diff --git a/includes/admin.inc b/includes/admin.inc index 512f9df96391..e9b9e061c381 100644 --- a/includes/admin.inc +++ b/includes/admin.inc @@ -67,7 +67,6 @@ function views_ui_get_admin_css() { } } - return $list; } @@ -152,7 +151,6 @@ function views_ui_preview($view, $display_id, $args = array()) { $view->set_exposed_input($exposed_input); - if (!$view->set_display($display_id)) { return t('Invalid display id @display', array('@display' => $display_id)); } @@ -2716,7 +2714,6 @@ function views_ui_get_form_progress($view) { return $progress; } - // -------------------------------------------------------------------------- // Various subforms for editing the pieces of a view. @@ -3506,6 +3503,7 @@ function theme_views_ui_expose_filter_form($variables) { $output = drupal_render($form['form_description']); $output .= drupal_render($form['expose_button']); + $output .= drupal_render($form['group_button']); if (isset($form['required'])) { $output .= drupal_render($form['required']); } @@ -3536,7 +3534,76 @@ function theme_views_ui_expose_filter_form($variables) { } /** - * Submit handler for rearranging form + * Theme the build group filter form. + */ +function theme_views_ui_build_group_filter_form($variables) { + $form = $variables['form']; + $more = drupal_render($form['more']); + + $output = drupal_render($form['form_description']); + $output .= drupal_render($form['expose_button']); + $output .= drupal_render($form['group_button']); + if (isset($form['required'])) { + $output .= drupal_render($form['required']); + } + + $output .= drupal_render($form['operator']); + $output .= drupal_render($form['value']); + + $output .= '
'; + $output .= drupal_render($form['optional']); + $output .= drupal_render($form['remember']); + $output .= '
'; + + $output .= '
'; + $output .= drupal_render($form['widget']); + $output .= drupal_render($form['label']); + $output .= '
'; + + $header = array( + t('Default'), + t('Weight'), + t('Label'), + t('Operator'), + t('Value'), + t('Operations'), + ); + + $form['default_group'] = form_process_radios($form['default_group']); + $form['default_group_multiple'] = form_process_checkboxes($form['default_group_multiple']); + $form['default_group']['All']['#title'] = ''; + + hide($form['default_group_multiple']['All']); + $rows[] = array( + drupal_render($form['default_group']['All']), + '', + array( + 'data' => variable_get('views_exposed_filter_any_label', 'new_any') == 'old_any' ? t('<Any>') : t('- Any -'), + 'colspan' => 4, + 'class' => array('class' => 'any-default-radios-row'), + ), + ); + + foreach (element_children($form['group_items']) as $group_id) { + $form['group_items'][$group_id]['value']['#title'] = ''; + $data = array( + 'default' => drupal_render($form['default_group'][$group_id]) . drupal_render($form['default_group_multiple'][$group_id]), + 'weight' => drupal_render($form['group_items'][$group_id]['weight']), + 'title' => drupal_render($form['group_items'][$group_id]['title']), + 'operator' => drupal_render($form['group_items'][$group_id]['operator']), + 'value' => drupal_render($form['group_items'][$group_id]['value']), + 'remove' => drupal_render($form['group_items'][$group_id]['remove']) . l('' . t('Remove') . '', 'javascript:void()', array('attributes' => array('id' => 'views-remove-link-' . $group_id, 'class' => array('views-hidden', 'views-button-remove', 'views-groups-remove-link', 'views-remove-link'), 'alt' => t('Remove this item'), 'title' => t('Remove this item')), 'html' => true)), + ); + $rows[] = array('data' => $data, 'id' => 'views-row-' . $group_id, 'class' => array('draggable')); + } + $table = theme('table', array('header' => $header, 'rows' => $rows, 'attributes' => array('class' => array('views-filter-groups'), 'id' => 'views-filter-groups'))) . drupal_render($form['add_group']); + drupal_add_tabledrag('views-filter-groups', 'order', 'sibling', 'weight'); + $render_form = drupal_render_children($form); + return $output . $render_form . $table . $more; +} + +/** + * Submit handler for rearranging form. */ function views_ui_rearrange_form_submit($form, &$form_state) { $types = View::views_object_types(); @@ -3977,7 +4044,6 @@ function views_ui_add_item_form($form, &$form_state) { $form['#title'] = t('Add @type', array('@type' => $ltitle)); $form['#section'] = $display_id . 'add-item'; - // Add the display override dropdown. views_ui_standard_display_dropdown($form, $form_state, $section); @@ -4144,6 +4210,45 @@ function views_ui_add_item_form_submit($form, &$form_state) { views_ui_cache_set($form_state['view']); } +/** + * Override handler for views_ui_edit_display_form + */ +function views_ui_config_item_form_build_group($form, &$form_state) { + $item = &$form_state['handler']->options; + // flip. If the filter was a group, set back to a standard filter. + $item['is_grouped'] = empty($item['is_grouped']); + + // If necessary, set new defaults: + if ($item['is_grouped']) { + $form_state['handler']->build_group_options(); + } + + $form_state['view']->set_item($form_state['display_id'], $form_state['type'], $form_state['id'], $item); + + views_ui_add_form_to_stack($form_state['form_key'], $form_state['view'], $form_state['display_id'], array($form_state['type'], $form_state['id']), TRUE, TRUE); + + views_ui_cache_set($form_state['view']); + $form_state['rerender'] = TRUE; + $form_state['rebuild'] = TRUE; + $form_state['force_build_group_options'] = TRUE; +} + +/** + * Add a new group to the exposed filter groups. + */ +function views_ui_config_item_form_add_group($form, &$form_state) { + $item =& $form_state['handler']->options; + + // Add a new row. + $item['group_info']['group_items'][] = array(); + + $form_state['view']->set_item($form_state['display_id'], $form_state['type'], $form_state['id'], $item); + + views_ui_cache_set($form_state['view']); + $form_state['rerender'] = TRUE; + $form_state['rebuild'] = TRUE; + $form_state['force_build_group_options'] = TRUE; +} /** * Form to config_item items in the views UI. @@ -4317,7 +4422,6 @@ function views_ui_config_item_form_submit_temporary($form, &$form_state) { $handler = views_get_handler($item['table'], $item['field'], $handler_type, $override); $handler->init($form_state['view'], $item); - // Add the incoming options to existing options because items using // the extra form may not have everything in the form here. $options = $form_state['values']['options'] + $form_state['handler']->options; @@ -4372,7 +4476,6 @@ function views_ui_config_item_form_submit($form, &$form_state) { $handler = views_get_handler($item['table'], $item['field'], $handler_type, $override); $handler->init($form_state['view'], $item); - // Add the incoming options to existing options because items using // the extra form may not have everything in the form here. $options = $form_state['values']['options'] + $form_state['handler']->options; @@ -4901,7 +5004,6 @@ function views_ui_admin_settings_advanced() { ); } - $form['actions']['#type'] = 'actions'; $form['actions']['submit'] = array( '#type' => 'submit', @@ -5164,7 +5266,6 @@ function views_fetch_fields($base, $type, $grouping = FALSE) { return array(); } - /** * Theme the form for the table style plugin */ diff --git a/js/views-admin.js b/js/views-admin.js index 78fb0fbfe174..1eb389787532 100644 --- a/js/views-admin.js +++ b/js/views-admin.js @@ -827,7 +827,7 @@ Drupal.behaviors.viewsRemoveIconClass.attach = function (context, settings) { $('.icon', $this).removeClass('icon'); $('.horizontal', $this).removeClass('horizontal'); }); -} +}; /** * Change "Expose filter" buttons into checkboxes. @@ -835,7 +835,7 @@ Drupal.behaviors.viewsRemoveIconClass.attach = function (context, settings) { Drupal.behaviors.viewsUiCheckboxify = {}; Drupal.behaviors.viewsUiCheckboxify.attach = function (context, settings) { var $ = jQuery; - var $buttons = $('#edit-options-expose-button-button').once('views-ui-checkboxify'); + var $buttons = $('#edit-options-expose-button-button, #edit-options-group-button-button').once('views-ui-checkboxify'); var length = $buttons.length; var i; for (i = 0; i < length; i++) { @@ -843,6 +843,33 @@ Drupal.behaviors.viewsUiCheckboxify.attach = function (context, settings) { } }; +/** + * Change the default widget to select the default group according to the + * selected widget for the exposed group. + */ +Drupal.behaviors.viewsUiChangeDefaultWidget = {}; +Drupal.behaviors.viewsUiChangeDefaultWidget.attach = function (context, settings) { + var $ = jQuery; + function change_default_widget(multiple) { + if (multiple) { + $('input.default-radios').hide(); + $('td.any-default-radios-row').parent().hide(); + $('input.default-checkboxes').show(); + } + else { + $('input.default-checkboxes').hide(); + $('td.any-default-radios-row').parent().show(); + $('input.default-radios').show(); + } + } + // Update on widget change. + $('input[name="options[group_info][multiple]"]').change(function() { + change_default_widget($(this).attr("checked")); + }); + // Update the first time the form is rendered. + $('input[name="options[group_info][multiple]"]').trigger('change'); +}; + /** * Attaches an expose filter button to a checkbox that triggers its click event. * @@ -852,13 +879,14 @@ Drupal.behaviors.viewsUiCheckboxify.attach = function (context, settings) { Drupal.viewsUi.Checkboxifier = function (button) { var $ = jQuery; this.$button = $(button); - this.$parent = this.$button.parent('div.views-expose'); - this.$checkbox = this.$parent.find('input:checkbox'); + this.$parent = this.$button.parent('div.views-expose, div.views-grouped'); + this.$input = this.$parent.find('input:checkbox, input:radio'); // Hide the button and its description. this.$button.hide(); - this.$parent.find('.exposed-description').hide(); + this.$parent.find('.exposed-description, .grouped-description').hide(); + + this.$input.click($.proxy(this, 'clickHandler')); - this.$checkbox.click($.proxy(this, 'clickHandler')); }; /** diff --git a/lib/Drupal/views/Plugin/views/Handler.php b/lib/Drupal/views/Plugin/views/Handler.php index b746f55b91b9..c6f8044982a8 100644 --- a/lib/Drupal/views/Plugin/views/Handler.php +++ b/lib/Drupal/views/Plugin/views/Handler.php @@ -554,6 +554,18 @@ abstract class Handler extends Plugin { return !empty($this->options['exposed']); } + /** + * Returns TRUE if the exposed filter works like a grouped filter. + */ + function is_a_group() { return FALSE; } + + /** + * Define if the exposed input has to be submitted multiple times. + * This is TRUE when exposed filters grouped are using checkboxes as + * widgets. + */ + function multiple_exposed_input() { return FALSE; } + /** * Take input from exposed handlers and assign to this handler, if necessary. */ diff --git a/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php b/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php index d0bb195ec918..5fc77acc883a 100644 --- a/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php +++ b/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.php @@ -2789,8 +2789,15 @@ abstract class DisplayPluginBase extends Plugin { foreach (View::views_object_types() as $type => $info) { foreach ($this->get_handlers($type) as $key => $handler) { if ($handler->can_expose() && $handler->is_exposed()) { - if ($id != $key && $identifier == $handler->options['expose']['identifier']) { - return FALSE; + if ($handler->is_a_group()) { + if ($id != $key && $identifier == $handler->options['group_info']['identifier']) { + return FALSE; + } + } + else { + if ($id != $key && $identifier == $handler->options['expose']['identifier']) { + return FALSE; + } } } } diff --git a/lib/Drupal/views/Plugin/views/filter/BooleanOperator.php b/lib/Drupal/views/Plugin/views/filter/BooleanOperator.php index ee1ba55896bc..9fd6526d9755 100644 --- a/lib/Drupal/views/Plugin/views/filter/BooleanOperator.php +++ b/lib/Drupal/views/Plugin/views/filter/BooleanOperator.php @@ -138,6 +138,9 @@ class BooleanOperator extends FilterPluginBase { } function admin_summary() { + if ($this->is_a_group()) { + return t('grouped'); + } if (!empty($this->options['exposed'])) { return t('exposed'); } diff --git a/lib/Drupal/views/Plugin/views/filter/Date.php b/lib/Drupal/views/Plugin/views/filter/Date.php index 00e616837640..c3cd59399c17 100644 --- a/lib/Drupal/views/Plugin/views/filter/Date.php +++ b/lib/Drupal/views/Plugin/views/filter/Date.php @@ -104,6 +104,34 @@ class Date extends Numeric { } } + /** + * Validate the build group options form. + */ + function build_group_validate($form, &$form_state) { + // Special case to validate grouped date filters, this is because the + // $group['value'] array contains the type of filter (date or offset) + // and therefore the number of items the comparission has to be done + // against 'one' instead of 'zero'. + foreach ($form_state['values']['options']['group_info']['group_items'] as $id => $group) { + if (empty($group['remove'])) { + // Check if the title is defined but value wasn't defined. + if (!empty($group['title'])) { + if ((!is_array($group['value']) && empty($group['value'])) || (is_array($group['value']) && count(array_filter($group['value'])) == 1)) { + form_error($form['group_info']['group_items'][$id]['value'], t('The value is required if title for this item is defined.')); + } + } + + // Check if the value is defined but title wasn't defined. + if ((!is_array($group['value']) && !empty($group['value'])) || (is_array($group['value']) && count(array_filter($group['value'])) > 1)) { + if (empty($group['title'])) { + form_error($form['group_info']['group_items'][$id]['title'], t('The title is required if value for this item is defined.')); + } + } + } + } + } + + function accept_exposed_input($input) { if (empty($this->options['exposed'])) { return TRUE; diff --git a/lib/Drupal/views/Plugin/views/filter/FilterPluginBase.php b/lib/Drupal/views/Plugin/views/filter/FilterPluginBase.php index 923ed0caddac..ca1759e18b61 100644 --- a/lib/Drupal/views/Plugin/views/filter/FilterPluginBase.php +++ b/lib/Drupal/views/Plugin/views/filter/FilterPluginBase.php @@ -49,6 +49,11 @@ abstract class FilterPluginBase extends Handler { */ var $operator = '='; + /** + * Contains the information of the selected item in a gruped filter. + */ + var $group_info = NULL; + /** * @var bool * Disable the possibility to force a single value. @@ -78,6 +83,7 @@ abstract class FilterPluginBase extends Handler { $this->operator = $this->options['operator']; $this->value = $this->options['value']; + $this->group_info = $this->options['group_info']['default_group']; // Compatibility: The new UI changed several settings. if (!empty($options['exposed']) && !empty($options['expose']['optional']) && !isset($options['expose']['required'])) { @@ -90,6 +96,11 @@ abstract class FilterPluginBase extends Handler { $this->options['expose']['operator_id'] = $options['expose']['operator_id'] = $options['expose']['operator']; } + if ($this->multiple_exposed_input()) { + $this->group_info = array_filter($options['group_info']['default_group_multiple']); + $this->options['expose']['multiple'] = TRUE; + } + // If there are relationships in the view, allow empty should be true // so that we can do IS NULL checks on items. Not all filters respect // allow empty, but string and numeric do and that covers enough. @@ -121,6 +132,29 @@ abstract class FilterPluginBase extends Handler { ), ); + // A group is a combination of a filter, an operator and a value + // operating like a single filter. + // Users can choose from a select box which group they want to apply. + // Views will filter the view according to the defined values. + // Because it acts as a standard filter, we have to define + // an identifier and other settings like the widget and the label. + // This settings are saved in another array to allow users to switch + // between a normal filter and a group of filters with a single click. + $options['is_grouped'] = array('default' => FALSE, 'bool' => TRUE); + $options['group_info'] = array( + 'contains' => array( + 'label' => array('default' => '', 'translatable' => TRUE), + 'identifier' => array('default' => ''), + 'optional' => array('default' => TRUE, 'bool' => TRUE), + 'widget' => array('default' => 'select'), + 'multiple' => array('default' => FALSE, 'bool' => TRUE), + 'remember' => array('default' => 0), + 'default_group' => array('default' => 'All'), + 'default_group_multiple' => array('default' => array()), + 'group_items' => array('default' => array()), + ), + ); + return $options; } @@ -136,6 +170,21 @@ abstract class FilterPluginBase extends Handler { */ function can_expose() { return TRUE; } + /** + * Determine if a filter can be converted into a group. + * Only exposed filters with operators available can be converted into groups. + */ + function can_build_group() { + return $this->is_exposed() && (count($this->operator_options()) > 0); + } + + /** + * Returns TRUE if the exposed filter works like a grouped filter. + */ + function is_a_group() { + return !empty($this->options['is_grouped']); + } + /** * Provide the basic form which calls through to subforms. * If overridden, it is best to call through to the parent, @@ -147,19 +196,36 @@ abstract class FilterPluginBase extends Handler { if ($this->can_expose()) { $this->show_expose_button($form, $form_state); } + if ($this->can_build_group()) { + $this->show_build_group_button($form, $form_state); + } $form['clear_markup_start'] = array( '#markup' => '
', ); - // Add the subform from operator_form(). - $this->show_operator_form($form, $form_state); - // Add the subform from value_form(). - $this->show_value_form($form, $form_state); - $form['clear_markup_end'] = array( - '#markup' => '
', - ); - if ($this->can_expose()) { - // Add the subform from expose_form(). - $this->show_expose_form($form, $form_state); + if ($this->is_a_group()) { + if ($this->can_build_group()) { + $form['clear_markup_start'] = array( + '#markup' => '
', + ); + // Render the build group form. + $this->show_build_group_form($form, $form_state); + $form['clear_markup_end'] = array( + '#markup' => '
', + ); + } + } + else { + // Add the subform from operator_form(). + $this->show_operator_form($form, $form_state); + // Add the subform from value_form(). + $this->show_value_form($form, $form_state); + $form['clear_markup_end'] = array( + '#markup' => '', + ); + if ($this->can_expose()) { + // Add the subform from expose_form(). + $this->show_expose_form($form, $form_state); + } } } @@ -169,9 +235,12 @@ abstract class FilterPluginBase extends Handler { function options_validate(&$form, &$form_state) { $this->operator_validate($form, $form_state); $this->value_validate($form, $form_state); - if (!empty($this->options['exposed'])) { + if (!empty($this->options['exposed']) && !$this->is_a_group()) { $this->expose_validate($form, $form_state); } + if ($this->is_a_group()) { + $this->build_group_validate($form, $form_state); + } } /** @@ -179,11 +248,17 @@ abstract class FilterPluginBase extends Handler { */ function options_submit(&$form, &$form_state) { unset($form_state['values']['expose_button']); // don't store this. - $this->operator_submit($form, $form_state); - $this->value_submit($form, $form_state); + unset($form_state['values']['group_button']); // don't store this. + if (!$this->is_a_group()) { + $this->operator_submit($form, $form_state); + $this->value_submit($form, $form_state); + } if (!empty($this->options['exposed'])) { $this->expose_submit($form, $form_state); } + if ($this->is_a_group()) { + $this->build_group_submit($form, $form_state); + } } /** @@ -264,6 +339,78 @@ abstract class FilterPluginBase extends Handler { */ function value_submit($form, &$form_state) { } + /** + * Shortcut to display the exposed options form. + */ + function show_build_group_form(&$form, &$form_state) { + if (empty($this->options['is_grouped'])) { + return; + } + + $this->build_group_form($form, $form_state); + + // When we click the expose button, we add new gadgets to the form but they + // have no data in $_POST so their defaults get wiped out. This prevents + // these defaults from getting wiped out. This setting will only be TRUE + // during a 2nd pass rerender. + if (!empty($form_state['force_build_group_options'])) { + foreach (element_children($form['group_info']) as $id) { + if (isset($form['group_info'][$id]['#default_value']) && !isset($form['group_info'][$id]['#value'])) { + $form['group_info'][$id]['#value'] = $form['group_info'][$id]['#default_value']; + } + } + } + } + + /** + * Shortcut to display the build_group/hide button. + */ + function show_build_group_button(&$form, &$form_state) { + + $form['group_button'] = array( + '#prefix' => '
', + '#suffix' => '
', + // Should always come after the description and the relationship. + '#weight' => -190, + ); + + $grouped_description = t('Grouped filters allow a choice between predefined operator|value pairs.'); + $form['group_button']['radios'] = array( + '#theme_wrappers' => array('container'), + '#attributes' => array('class' => array('js-only')), + ); + $form['group_button']['radios']['radios'] = array( + '#title' => t('Filter type to expose'), + '#description' => $grouped_description, + '#type' => 'radios', + '#options' => array( + t('Single filter'), + t('Grouped filters'), + ), + ); + + if (empty($this->options['is_grouped'])) { + $form['group_button']['markup'] = array( + '#markup' => '
' . $grouped_description . '
', + ); + $form['group_button']['button'] = array( + '#limit_validation_errors' => array(), + '#type' => 'submit', + '#value' => t('Grouped filters'), + '#submit' => array('views_ui_config_item_form_build_group'), + ); + $form['group_button']['radios']['radios']['#default_value'] = 0; + } + else { + $form['group_button']['button'] = array( + '#limit_validation_errors' => array(), + '#type' => 'submit', + '#value' => t('Single filter'), + '#submit' => array('views_ui_config_item_form_build_group'), + ); + $form['group_button']['radios']['radios']['#default_value'] = 1; + } + } /** * Shortcut to display the expose/hide button. */ @@ -437,6 +584,81 @@ abstract class FilterPluginBase extends Handler { } } + /** + * Validate the build group options form. + */ + function build_group_validate($form, &$form_state) { + if (empty($form_state['values']['options']['group_info']['identifier'])) { + form_error($form['group_info']['identifier'], t('The identifier is required if the filter is exposed.')); + } + + if (!empty($form_state['values']['options']['group_info']['identifier']) && $form_state['values']['options']['group_info']['identifier'] == 'value') { + form_error($form['group_info']['identifier'], t('This identifier is not allowed.')); + } + + if (!$this->view->display_handler->is_identifier_unique($form_state['id'], $form_state['values']['options']['group_info']['identifier'])) { + form_error($form['group_info']['identifier'], t('This identifier is used by another handler.')); + } + + if (!empty($form_state['values']['options']['group_info']['group_items'])) { + foreach ($form_state['values']['options']['group_info']['group_items'] as $id => $group) { + if (empty($group['remove'])) { + + // Check if the title is defined but value wasn't defined. + if (!empty($group['title'])) { + if ((!is_array($group['value']) && trim($group['value']) == "") || + (is_array($group['value']) && count(array_filter($group['value'], '_views_array_filter_zero')) == 0)) { + form_error($form['group_info']['group_items'][$id]['value'], + t('The value is required if title for this item is defined.')); + } + } + + // Check if the value is defined but title wasn't defined. + if ((!is_array($group['value']) && trim($group['value']) != "") || + (is_array($group['value']) && count(array_filter($group['value'], '_views_array_filter_zero')) > 0)) { + if (empty($group['title'])) { + form_error($form['group_info']['group_items'][$id]['title'], + t('The title is required if value for this item is defined.')); + } + } + } + } + } + } + + /** + * Save new group items, re-enumerates and remove groups marked to delete. + */ + function build_group_submit($form, &$form_state) { + $groups = array(); + uasort($form_state['values']['options']['group_info']['group_items'], 'drupal_sort_weight'); + // Filter out removed items. + + // Start from 1 to avoid problems with #default_value in the widget. + $new_id = 1; + $new_default = 'All'; + foreach ($form_state['values']['options']['group_info']['group_items'] as $id => $group) { + if (empty($group['remove'])) { + // Don't store this. + unset($group['remove']); + unset($group['weight']); + $groups[$new_id] = $group; + + if ($form_state['values']['options']['group_info']['default_group'] === $id) { + $new_default = $new_id; + } + } + $new_id++; + } + if ($new_default != 'All') { + $form_state['values']['options']['group_info']['default_group'] = $new_default; + } + $filter_default_multiple = array_filter($form_state['values']['options']['group_info']['default_group_multiple']); + $form_state['values']['options']['group_info']['default_group_multiple'] = $filter_default_multiple; + + $form_state['values']['options']['group_info']['group_items'] = $groups; + } + /** * Provide default options for exposed filters. */ @@ -452,6 +674,68 @@ abstract class FilterPluginBase extends Handler { ); } + /** + * Provide default options for exposed filters. + */ + function build_group_options() { + $this->options['group_info'] = array( + 'label' => $this->definition['title'], + 'identifier' => $this->options['id'], + 'optional' => TRUE, + 'widget' => 'select', + 'multiple' => FALSE, + 'remember' => FALSE, + 'default_group' => 'All', + 'default_group_multiple' => array(), + 'group_items' => array(), + ); + } + + /** + * Build a form containing a group of operator | values to apply as a + * single filter. + */ + function group_form(&$form, &$form_state) { + if (!empty($this->options['group_info']['optional']) && !$this->multiple_exposed_input()) { + + $old_any = $this->options['group_info']['widget'] == 'select' ? '' : '<Any>'; + $any_label = variable_get('views_exposed_filter_any_label', 'new_any') == 'old_any' ? $old_any : t('- Any -'); + $groups = array('All' => $any_label); + } + foreach ($this->options['group_info']['group_items'] as $id => $group) { + if (!empty($group['title'])) { + $groups[$id] = $id != 'All' ? t($group['title']) : $group['title']; + } + } + + if (count($groups)) { + $value = $this->options['group_info']['identifier']; + + $form[$value] = array( + '#type' => $this->options['group_info']['widget'], + '#default_value' => $this->group_info, + '#options' => $groups, + ); + if (!empty($this->options['group_info']['multiple'])) { + if (count($groups) < 5) { + $form[$value]['#type'] = 'checkboxes'; + } + else { + $form[$value]['#type'] = 'select'; + $form[$value]['#size'] = 5; + $form[$value]['#multiple'] = TRUE; + } + unset($form[$value]['#default_value']); + if (empty($form_state['input'])) { + $form_state['input'][$value] = $this->group_info; + } + } + + $this->options['expose']['label'] = ''; + } + } + + /** * Render our chunk of the exposed filter form when selecting * @@ -503,6 +787,247 @@ abstract class FilterPluginBase extends Handler { } } + /** + * Build the form to let users create the group of exposed filters. + * This form is displayed when users click on button 'Build group' + */ + function build_group_form(&$form, &$form_state) { + if (empty($this->options['exposed']) || empty($this->options['is_grouped'])) { + return; + } + $form['#theme'] = 'views_ui_build_group_filter_form'; + + // #flatten will move everything from $form['group_info'][$key] to $form[$key] + // prior to rendering. That's why the pre_render for it needs to run first, + // so that when the next pre_render (the one for fieldsets) runs, it gets + // the flattened data. + array_unshift($form['#pre_render'], 'views_ui_pre_render_flatten_data'); + $form['group_info']['#flatten'] = TRUE; + + if (!empty($this->options['group_info']['identifier'])) { + $identifier = $this->options['group_info']['identifier']; + } + else { + $identifier = 'group_' . $this->options['expose']['identifier']; + } + $form['group_info']['identifier'] = array( + '#type' => 'textfield', + '#default_value' => $identifier, + '#title' => t('Filter identifier'), + '#size' => 40, + '#description' => t('This will appear in the URL after the ? to identify this filter. Cannot be blank.'), + '#fieldset' => 'more', + ); + $form['group_info']['label'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['group_info']['label'], + '#title' => t('Label'), + '#size' => 40, + ); + $form['group_info']['optional'] = array( + '#type' => 'checkbox', + '#title' => t('Optional'), + '#description' => t('This exposed filter is optional and will have added options to allow it not to be set.'), + '#default_value' => $this->options['group_info']['optional'], + ); + $form['group_info']['multiple'] = array( + '#type' => 'checkbox', + '#title' => t('Allow multiple selections'), + '#description' => t('Enable to allow users to select multiple items.'), + '#default_value' => $this->options['group_info']['multiple'], + ); + $form['group_info']['widget'] = array( + '#type' => 'radios', + '#default_value' => $this->options['group_info']['widget'], + '#title' => t('Widget type'), + '#options' => array( + 'radios' => t('Radios'), + 'select' => t('Select'), + ), + '#description' => t('Select which kind of widget will be used to render the group of filters'), + ); + $form['group_info']['remember'] = array( + '#type' => 'checkbox', + '#title' => t('Remember'), + '#description' => t('Remember the last setting the user gave this filter.'), + '#default_value' => $this->options['group_info']['remember'], + ); + + if (!empty($this->options['group_info']['identifier'])) { + $identifier = $this->options['group_info']['identifier']; + } + else { + $identifier = 'group_' . $this->options['expose']['identifier']; + } + $form['group_info']['identifier'] = array( + '#type' => 'textfield', + '#default_value' => $identifier, + '#title' => t('Filter identifier'), + '#size' => 40, + '#description' => t('This will appear in the URL after the ? to identify this filter. Cannot be blank.'), + '#fieldset' => 'more', + ); + $form['group_info']['label'] = array( + '#type' => 'textfield', + '#default_value' => $this->options['group_info']['label'], + '#title' => t('Label'), + '#size' => 40, + ); + $form['group_info']['optional'] = array( + '#type' => 'checkbox', + '#title' => t('Optional'), + '#description' => t('This exposed filter is optional and will have added options to allow it not to be set.'), + '#default_value' => $this->options['group_info']['optional'], + ); + $form['group_info']['widget'] = array( + '#type' => 'radios', + '#default_value' => $this->options['group_info']['widget'], + '#title' => t('Widget type'), + '#options' => array( + 'radios' => t('Radios'), + 'select' => t('Select'), + ), + '#description' => t('Select which kind of widget will be used to render the group of filters'), + ); + $form['group_info']['remember'] = array( + '#type' => 'checkbox', + '#title' => t('Remember'), + '#description' => t('Remember the last setting the user gave this filter.'), + '#default_value' => $this->options['group_info']['remember'], + ); + + $groups = array('All' => '- Any -'); // The string '- Any -' will not be rendered see @theme_views_ui_build_group_filter_form + + // Provide 3 options to start when we are in a new group. + if (count($this->options['group_info']['group_items']) == 0) { + $this->options['group_info']['group_items'] = array_fill(1, 3, array()); + } + + // After the general settings, comes a table with all the existent groups. + $default_weight = 0; + foreach ($this->options['group_info']['group_items'] as $item_id => $item) { + if (!empty($form_state['values']['options']['group_info']['group_items'][$item_id]['remove'])) { + continue; + } + // Each rows contains three widgets: + // a) The title, where users define how they identify a pair of operator | value + // b) The operator + // c) The value (or values) to use in the filter with the selected operator + + // In each row, we have to display the operator form and the value from + // $row acts as a fake form to render each widget in a row. + $row = array(); + $groups[$item_id] = ''; + $this->operator_form($row, $form_state); + // Force the operator form to be a select box. Some handlers uses + // radios and they occupy a lot of space in a table row. + $row['operator']['#type'] = 'select'; + $row['operator']['#title'] = ''; + $this->value_form($row, $form_state); + + // Fix the dependencies to update value forms when operators + // changes. This is needed because forms are inside a new form and + // their ids changes. Dependencies are used when operator changes + // from to 'Between', 'Not Between', etc, and two or more widgets + // are displayed. + $without_children = TRUE; + foreach (element_children($row['value']) as $children) { + if (isset($row['value'][$children]['#dependency']['edit-options-operator'])) { + $row['value'][$children]['#dependency']["edit-options-group-info-group-items-$item_id-operator"] = $row['value'][$children]['#dependency']['edit-options-operator']; + unset($row['value'][$children]['#dependency']['edit-options-operator']); + $row['value'][$children]['#title'] = ''; + + if (!empty($this->options['group_info']['group_items'][$item_id]['value'][$children])) { + $row['value'][$children]['#default_value'] = $this->options['group_info']['group_items'][$item_id]['value'][$children]; + } + } + $without_children = FALSE; + } + + if ($without_children) { + if (!empty($this->options['group_info']['group_items'][$item_id]['value'])) { + $row['value']['#default_value'] = $this->options['group_info']['group_items'][$item_id]['value']; + } + } + + if (!empty($this->options['group_info']['group_items'][$item_id]['operator'])) { + $row['operator']['#default_value'] = $this->options['group_info']['group_items'][$item_id]['operator']; + } + + $default_title = ''; + if (!empty($this->options['group_info']['group_items'][$item_id]['title'])) { + $default_title = $this->options['group_info']['group_items'][$item_id]['title']; + } + + // Per item group, we have a title that identifies it. + $form['group_info']['group_items'][$item_id] = array( + 'title' => array( + '#type' => 'textfield', + '#size' => 20, + '#default_value' => $default_title, + ), + 'operator' => $row['operator'], + 'value' => $row['value'], + 'remove' => array( + '#type' => 'checkbox', + '#id' => 'views-removed-' . $item_id, + '#attributes' => array('class' => array('views-remove-checkbox')), + '#default_value' => 0, + ), + 'weight' => array( + '#type' => 'weight', + '#delta' => 10, + '#default_value' => $default_weight++, + '#attributes' => array('class' => array('weight')), + ), + ); + } + // From all groups, let chose which is the default. + $form['group_info']['default_group'] = array( + '#type' => 'radios', + '#options' => $groups, + '#default_value' => $this->options['group_info']['default_group'], + '#required' => TRUE, + '#attributes' => array( + 'class' => array('default-radios'), + ) + ); + // From all groups, let chose which is the default. + $form['group_info']['default_group_multiple'] = array( + '#type' => 'checkboxes', + '#options' => $groups, + '#default_value' => $this->options['group_info']['default_group_multiple'], + '#attributes' => array( + 'class' => array('default-checkboxes'), + ) + ); + + $form['group_info']['add_group'] = array( + '#prefix' => '
', + '#suffix' => '
', + '#type' => 'submit', + '#value' => t('Add another item'), + '#submit' => array('views_ui_config_item_form_add_group'), + ); + + $js = array(); + $js['tableDrag']['views-filter-groups']['weight'][0] = array( + 'target' => 'weight', + 'source' => NULL, + 'relationship' => 'sibling', + 'action' => 'order', + 'hidden' => TRUE, + 'limit' => 0, + ); + if (!empty($form_state['js settings']) && is_array($js)) { + $form_state['js settings'] = array_merge($form_state['js settings'], $js); + } + else { + $form_state['js settings'] = $js; + } + } + + /** * Make some translations to a form item to make it more suitable to * exposing. @@ -574,16 +1099,26 @@ abstract class FilterPluginBase extends Handler { * overridden for particularly complex forms. And maybe not even then. * * @return array|null - * An array with the following keys: + * For standard exposed filters. An array with the following keys: * - operator: The $form key of the operator. Set to NULL if no operator. * - value: The $form key of the value. Set to NULL if no value. * - label: The label to use for this piece. + * For grouped exposed filters. An array with the following keys: + * - value: The $form key of the value. Set to NULL if no value. + * - label: The label to use for this piece. */ function exposed_info() { if (empty($this->options['exposed'])) { return; } + if ($this->is_a_group()) { + return array( + 'value' => $this->options['group_info']['identifier'], + 'label' => $this->options['group_info']['label'], + ); + } + return array( 'operator' => $this->options['expose']['operator_id'], 'value' => $this->options['expose']['identifier'], @@ -591,6 +1126,110 @@ abstract class FilterPluginBase extends Handler { ); } + /* + * Transform the input from a grouped filter into a standard filter. + * + * When a filter is a group, find the set of operator and values + * that the choosed item represents, and inform views that a normal + * filter was submitted by telling the operator and the value selected. + * + * The param $selected_group_id is only passed when the filter uses the + * checkboxes widget, and this function will be called for each item + * choosed in the checkboxes. + */ + function convert_exposed_input(&$input, $selected_group_id = NULL) { + if ($this->is_a_group()) { + // If it is already defined the selected group, use it. Only valid + // when the filter uses checkboxes for widget. + if (!empty($selected_group_id)) { + $selected_group = $selected_group_id; + } + else { + $selected_group = $input[$this->options['group_info']['identifier']]; + } + if ($selected_group == 'All' && !empty($this->options['group_info']['optional'])) { + return NULL; + } + if ($selected_group != 'All' && empty($this->options['group_info']['group_items'][$selected_group])) { + return FALSE; + } + if (isset($selected_group) && isset($this->options['group_info']['group_items'][$selected_group])) { + $input[$this->options['expose']['operator']] = $this->options['group_info']['group_items'][$selected_group]['operator']; + + // Value can be optional, For example for 'empty' and 'not empty' filters. + if (!empty($this->options['group_info']['group_items'][$selected_group]['value'])) { + $input[$this->options['expose']['identifier']] = $this->options['group_info']['group_items'][$selected_group]['value']; + } + $this->options['expose']['use_operator'] = TRUE; + + $this->group_info = $input[$this->options['group_info']['identifier']]; + return TRUE; + } + else { + return FALSE; + } + } + } + + /** + * Returns the options available for a grouped filter that users checkboxes + * as widget, and therefore has to be applied several times, one per + * item selected. + */ + function group_multiple_exposed_input(&$input) { + if (!empty($input[$this->options['group_info']['identifier']])) { + return array_filter($input[$this->options['group_info']['identifier']]); + } + return array(); + } + + /** + * Returns TRUE if users can select multiple groups items of a + * grouped exposed filter. + */ + function multiple_exposed_input() { + return $this->is_a_group() && !empty($this->options['group_info']['multiple']); + } + + /** + * If set to remember exposed input in the session, store it there. + * This function is similar to store_exposed_input but modified to + * work properly when the filter is a group. + */ + function store_group_input($input, $status) { + if (!$this->is_a_group() || empty($this->options['group_info']['identifier'])) { + return TRUE; + } + + if (empty($this->options['group_info']['remember'])) { + return; + } + + // Figure out which display id is responsible for the filters, so we + // know where to look for session stored values. + $display_id = ($this->view->display_handler->is_defaulted('filters')) ? 'default' : $this->view->current_display; + + // false means that we got a setting that means to recuse ourselves, + // so we should erase whatever happened to be there. + if ($status === FALSE && isset($_SESSION['views'][$this->view->name][$display_id])) { + $session = &$_SESSION['views'][$this->view->name][$display_id]; + + if (isset($session[$this->options['group_info']['identifier']])) { + unset($session[$this->options['group_info']['identifier']]); + } + } + + if ($status !== FALSE) { + if (!isset($_SESSION['views'][$this->view->name][$display_id])) { + $_SESSION['views'][$this->view->name][$display_id] = array(); + } + + $session = &$_SESSION['views'][$this->view->name][$display_id]; + + $session[$this->options['group_info']['identifier']] = $input[$this->options['group_info']['identifier']]; + } + } + /** * Check to see if input from the exposed filters should change * the behavior of this filter. diff --git a/lib/Drupal/views/Plugin/views/filter/InOperator.php b/lib/Drupal/views/Plugin/views/filter/InOperator.php index c659cc386b16..26448c7538ba 100644 --- a/lib/Drupal/views/Plugin/views/filter/InOperator.php +++ b/lib/Drupal/views/Plugin/views/filter/InOperator.php @@ -304,6 +304,9 @@ class InOperator extends FilterPluginBase { } function admin_summary() { + if ($this->is_a_group()) { + return t('grouped'); + } if (!empty($this->options['exposed'])) { return t('exposed'); } diff --git a/lib/Drupal/views/Plugin/views/filter/Numeric.php b/lib/Drupal/views/Plugin/views/filter/Numeric.php index bee26250edf7..417c2dd63c7d 100644 --- a/lib/Drupal/views/Plugin/views/filter/Numeric.php +++ b/lib/Drupal/views/Plugin/views/filter/Numeric.php @@ -282,6 +282,9 @@ class Numeric extends FilterPluginBase { } function admin_summary() { + if ($this->is_a_group()) { + return t('grouped'); + } if (!empty($this->options['exposed'])) { return t('exposed'); } diff --git a/lib/Drupal/views/Plugin/views/filter/String.php b/lib/Drupal/views/Plugin/views/filter/String.php index 7fce6bf39628..70ff23f0fe17 100644 --- a/lib/Drupal/views/Plugin/views/filter/String.php +++ b/lib/Drupal/views/Plugin/views/filter/String.php @@ -158,6 +158,9 @@ class String extends FilterPluginBase { } function admin_summary() { + if ($this->is_a_group()) { + return t('grouped'); + } if (!empty($this->options['exposed'])) { return t('exposed'); } diff --git a/lib/Drupal/views/Tests/ExposedFormTest.php b/lib/Drupal/views/Tests/ExposedFormTest.php index 976af0b678e3..4f5082e37a16 100644 --- a/lib/Drupal/views/Tests/ExposedFormTest.php +++ b/lib/Drupal/views/Tests/ExposedFormTest.php @@ -12,6 +12,8 @@ namespace Drupal\views\Tests; */ class ExposedFormTest extends ViewsSqlTest { + protected $profile = 'standard'; + public static function getInfo() { return array( 'name' => 'Exposed forms', @@ -20,15 +22,6 @@ class ExposedFormTest extends ViewsSqlTest { ); } - public function setUp() { - parent::setUp(); - - // @TODO Figure out why it's required to clear the cache here. - views_module_include('views_default', TRUE); - views_get_all_views(TRUE); - menu_router_rebuild(); - } - /** * Tests, whether and how the reset button can be renamed. */ @@ -69,13 +62,15 @@ class ExposedFormTest extends ViewsSqlTest { $edit = array(); $this->drupalGet('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type'); - // Be sure that the button is called exposed + // Be sure that the button is called exposed. $this->helperButtonHasLabel('edit-options-expose-button-button', t('Expose filter')); // Click the Expose filter button. $this->drupalPost('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type', $edit, t('Expose filter')); - // Check the label of the expose button + // Check the label of the expose button. $this->helperButtonHasLabel('edit-options-expose-button-button', t('Hide filter')); + // Check the label of the grouped exposed button + $this->helperButtonHasLabel('edit-options-group-button-button', t('Grouped filters')); // Check the validations of the filter handler. $edit = array(); @@ -93,11 +88,49 @@ class ExposedFormTest extends ViewsSqlTest { $this->helperButtonHasLabel('edit-options-expose-button-button', t('Expose sort')); $this->assertNoFieldById('edit-options-expose-label', '', t('Make sure no label field is shown')); + // Click the Grouped Filters button. + $this->drupalGet('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type'); + $this->drupalPost(NULL, array(), t('Grouped filters')); + // Check that after click on 'Grouped Filters', a new button is shown to + // add more items to the list. + $this->helperButtonHasLabel('edit-options-group-info-add-group', t('Add another item')); + + // Create a grouped filter + $this->drupalGet('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type'); + $edit = array(); + $edit["options[group_info][group_items][1][title]"] = 'Is Article'; + $edit["options[group_info][group_items][1][value][article]"] = 'article'; + + $edit["options[group_info][group_items][2][title]"] = 'Is Page'; + $edit["options[group_info][group_items][2][value][page]"] = TRUE; + + $edit["options[group_info][group_items][3][title]"] = 'Is Page and Article'; + $edit["options[group_info][group_items][3][value][article]"] = TRUE; + $edit["options[group_info][group_items][3][value][page]"] = TRUE; + $this->drupalPost(NULL, $edit, t('Apply')); + + // Validate that all the titles are defined for each group + $this->drupalGet('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/filter/type'); + $edit = array(); + $edit["options[group_info][group_items][1][title]"] = 'Is Article'; + $edit["options[group_info][group_items][1][value][article]"] = TRUE; + + // This should trigger an error + $edit["options[group_info][group_items][2][title]"] = ''; + $edit["options[group_info][group_items][2][value][page]"] = TRUE; + + $edit["options[group_info][group_items][3][title]"] = 'Is Page and Article'; + $edit["options[group_info][group_items][3][value][article]"] = TRUE; + $edit["options[group_info][group_items][3][value][page]"] = TRUE; + $this->drupalPost(NULL, $edit, t('Apply')); + $this->assertRaw(t('The title is required if value for this item is defined.'), t('Group items should have a title')); + // Click the Expose sort button. $edit = array(); $this->drupalPost('admin/structure/views/nojs/config-item/test_exposed_admin_ui/default/sort/created', $edit, t('Expose sort')); - // Check the label of the expose button + // Check the label of the expose button. $this->helperButtonHasLabel('edit-options-expose-button-button', t('Hide sort')); $this->assertFieldById('edit-options-expose-label', '', t('Make sure a label field is shown')); } + } diff --git a/lib/Drupal/views/Tests/Handler/FilterEqualityTest.php b/lib/Drupal/views/Tests/Handler/FilterEqualityTest.php index 50d9ef5aa9cd..a19e152a9efe 100644 --- a/lib/Drupal/views/Tests/Handler/FilterEqualityTest.php +++ b/lib/Drupal/views/Tests/Handler/FilterEqualityTest.php @@ -59,6 +59,24 @@ class FilterEqualityTest extends ViewsSqlTest { $this->assertIdenticalResultset($view, $resultset, $this->column_map); } + public function testEqualGroupedExposed() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: =, Value: Ringo + $filters['name']['group_info']['default_group'] = 1; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + function testNotEqual() { $view = $this->getBasicView(); @@ -91,4 +109,69 @@ class FilterEqualityTest extends ViewsSqlTest { ); $this->assertIdenticalResultset($view, $resultset, $this->column_map); } + + public function testEqualGroupedNotExposed() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: !=, Value: Ringo + $filters['name']['group_info']['default_group'] = 2; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'George', + ), + array( + 'name' => 'Paul', + ), + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + + protected function getGroupedExposedFilters() { + $filters = array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + 'exposed' => TRUE, + 'expose' => array( + 'operator' => 'name_op', + 'label' => 'name', + 'identifier' => 'name', + ), + 'is_grouped' => TRUE, + 'group_info' => array( + 'label' => 'name', + 'identifier' => 'name', + 'default_group' => 'All', + 'group_items' => array( + 1 => array( + 'title' => 'Name is equal to Ringo', + 'operator' => '=', + 'value' => array('value' => 'Ringo'), + ), + 2 => array( + 'title' => 'Name is not equal to Ringo', + 'operator' => '!=', + 'value' => array('value' => 'Ringo'), + ), + ), + ), + ), + ); + return $filters; + } + } diff --git a/lib/Drupal/views/Tests/Handler/FilterInOperatorTest.php b/lib/Drupal/views/Tests/Handler/FilterInOperatorTest.php index aff37348fffe..7b5bde51b903 100644 --- a/lib/Drupal/views/Tests/Handler/FilterInOperatorTest.php +++ b/lib/Drupal/views/Tests/Handler/FilterInOperatorTest.php @@ -98,4 +98,103 @@ class FilterInOperatorTest extends ViewsSqlTest { 'views_test_age' => 'age', )); } + + public function testFilterInOperatorGroupedExposedSimple() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: in, Value: 26, 30 + $filters['age']['group_info']['default_group'] = 1; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $expected_result = array( + array( + 'name' => 'Paul', + 'age' => 26, + ), + array( + 'name' => 'Meredith', + 'age' => 30, + ), + ); + + $this->assertEqual(2, count($view->result)); + $this->assertIdenticalResultset($view, $expected_result, array( + 'views_test_name' => 'name', + 'views_test_age' => 'age', + )); + } + + public function testFilterNotInOperatorGroupedExposedSimple() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: in, Value: 26, 30 + $filters['age']['group_info']['default_group'] = 2; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $expected_result = array( + array( + 'name' => 'John', + 'age' => 25, + ), + array( + 'name' => 'George', + 'age' => 27, + ), + array( + 'name' => 'Ringo', + 'age' => 28, + ), + ); + + $this->assertEqual(3, count($view->result)); + $this->assertIdenticalResultset($view, $expected_result, array( + 'views_test_name' => 'name', + 'views_test_age' => 'age', + )); + } + + protected function getGroupedExposedFilters() { + $filters = array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + 'exposed' => TRUE, + 'expose' => array( + 'operator' => 'age_op', + 'label' => 'age', + 'identifier' => 'age', + ), + 'is_grouped' => TRUE, + 'group_info' => array( + 'label' => 'age', + 'identifier' => 'age', + 'default_group' => 'All', + 'group_items' => array( + 1 => array( + 'title' => 'Age is one of 26, 30', + 'operator' => 'in', + 'value' => array(26, 30), + ), + 2 => array( + 'title' => 'Age is not one of 26, 30', + 'operator' => 'not in', + 'value' => array(26, 30), + ), + ), + ), + ), + ); + return $filters; + } + } diff --git a/lib/Drupal/views/Tests/Handler/FilterNumericTest.php b/lib/Drupal/views/Tests/Handler/FilterNumericTest.php index f24f4334e6e2..d190ffb38bea 100644 --- a/lib/Drupal/views/Tests/Handler/FilterNumericTest.php +++ b/lib/Drupal/views/Tests/Handler/FilterNumericTest.php @@ -17,7 +17,7 @@ class FilterNumericTest extends ViewsSqlTest { public static function getInfo() { return array( - 'name' => 'Handlers: filter_numeric', + 'name' => 'Filter: Numeric', 'description' => 'Tests the numeric filter handler', 'group' => 'Views Handlers', ); @@ -64,6 +64,25 @@ class FilterNumericTest extends ViewsSqlTest { $this->assertIdenticalResultset($view, $resultset, $this->column_map); } + public function testFilterNumericExposedGroupedSimple() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: =, Value: 28 + $filters['age']['group_info']['default_group'] = 1; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Ringo', + 'age' => 28, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + public function testFilterNumericBetween() { $view = $this->getBasicView(); @@ -136,6 +155,63 @@ class FilterNumericTest extends ViewsSqlTest { $this->assertIdenticalResultset($view, $resultset, $this->column_map); } + public function testFilterNumericExposedGroupedBetween() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: between, Value: 26 and 29 + $filters['age']['group_info']['default_group'] = 2; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'George', + 'age' => 27, + ), + array( + 'name' => 'Ringo', + 'age' => 28, + ), + array( + 'name' => 'Paul', + 'age' => 26, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testFilterNumericExposedGroupedNotBetween() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: between, Value: 26 and 29 + $filters['age']['group_info']['default_group'] = 3; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + 'age' => 25, + ), + array( + 'name' => 'Paul', + 'age' => 26, + ), + array( + 'name' => 'Meredith', + 'age' => 30, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testFilterNumericEmpty() { $view = $this->getBasicView(); @@ -195,6 +271,60 @@ class FilterNumericTest extends ViewsSqlTest { $this->assertIdenticalResultset($view, $resultset, $this->column_map); } + + public function testFilterNumericExposedGroupedEmpty() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: empty, Value: + $filters['age']['group_info']['default_group'] = 4; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + + $this->executeView($view); + $resultset = array( + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testFilterNumericExposedGroupedNotEmpty() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Age, Operator: empty, Value: + $filters['age']['group_info']['default_group'] = 5; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + 'age' => 25, + ), + array( + 'name' => 'George', + 'age' => 27, + ), + array( + 'name' => 'Ringo', + 'age' => 28, + ), + array( + 'name' => 'Paul', + 'age' => 26, + ), + array( + 'name' => 'Meredith', + 'age' => 30, + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + public function testAllowEmpty() { $view = $this->getBasicView(); @@ -224,4 +354,60 @@ class FilterNumericTest extends ViewsSqlTest { $this->assertTrue(isset($age_operators['empty'])); $this->assertTrue(isset($age_operators['not empty'])); } + + protected function getGroupedExposedFilters() { + $filters = array( + 'age' => array( + 'id' => 'age', + 'table' => 'views_test', + 'field' => 'age', + 'relationship' => 'none', + 'exposed' => TRUE, + 'expose' => array( + 'operator' => 'age_op', + 'label' => 'age', + 'identifier' => 'age', + ), + 'is_grouped' => TRUE, + 'group_info' => array( + 'label' => 'age', + 'identifier' => 'age', + 'default_group' => 'All', + 'group_items' => array( + 1 => array( + 'title' => 'Age is 28', + 'operator' => '=', + 'value' => array('value' => 28), + ), + 2 => array( + 'title' => 'Age is between 26 and 29', + 'operator' => 'between', + 'value' => array( + 'min' => 26, + 'max' => 29, + ), + ), + 3 => array( + 'title' => 'Age is not between 26 and 29', + 'operator' => 'not between', + 'value' => array( + 'min' => 26, + 'max' => 29, + ), + ), + 4 => array( + 'title' => 'Age is empty', + 'operator' => 'empty', + ), + 5 => array( + 'title' => 'Age is not empty', + 'operator' => 'not empty', + ), + ), + ), + ), + ); + return $filters; + } + } diff --git a/lib/Drupal/views/Tests/Handler/FilterStringTest.php b/lib/Drupal/views/Tests/Handler/FilterStringTest.php index ce6f8260a653..2f8a4940fcee 100644 --- a/lib/Drupal/views/Tests/Handler/FilterStringTest.php +++ b/lib/Drupal/views/Tests/Handler/FilterStringTest.php @@ -98,9 +98,29 @@ class FilterStringTest extends ViewsSqlTest { ), ); $this->assertIdenticalResultset($view, $resultset, $this->column_map); - $view->destroy(); + } + function testFilterStringGroupedExposedEqual() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + // Filter: Name, Operator: =, Value: Ringo + $filters['name']['group_info']['default_group'] = 1; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringNotEqual() { $view = $this->getBasicView(); // Change the filtering @@ -133,6 +153,36 @@ class FilterStringTest extends ViewsSqlTest { $this->assertIdenticalResultset($view, $resultset, $this->column_map); } + function testFilterStringGroupedExposedNotEqual() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: !=, Value: Ringo + $filters['name']['group_info']['default_group'] = '2'; + + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'George', + ), + array( + 'name' => 'Paul', + ), + array( + 'name' => 'Meredith', + ), + ); + + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + function testFilterStringContains() { $view = $this->getBasicView(); @@ -157,6 +207,28 @@ class FilterStringTest extends ViewsSqlTest { $this->assertIdenticalResultset($view, $resultset, $this->column_map); } + + function testFilterStringGroupedExposedContains() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: contains, Value: ing + $filters['name']['group_info']['default_group'] = '3'; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringWord() { $view = $this->getBasicView(); @@ -207,6 +279,47 @@ class FilterStringTest extends ViewsSqlTest { $this->assertIdenticalResultset($view, $resultset, $this->column_map); } + + function testFilterStringGroupedExposedWord() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: contains, Value: ing + $filters['name']['group_info']['default_group'] = '3'; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'Ringo', + ), + ); + + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + $view->destroy(); + + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Description, Operator: contains, Value: actor + $filters['description']['group_info']['default_group'] = '1'; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'George', + ), + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + function testFilterStringStarts() { $view = $this->getBasicView(); @@ -231,6 +344,25 @@ class FilterStringTest extends ViewsSqlTest { $this->assertIdenticalResultset($view, $resultset, $this->column_map); } + function testFilterStringGroupedExposedStarts() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: starts, Value: George + $filters['description']['group_info']['default_group'] = 2; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'George', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + function testFilterStringNotStarts() { $view = $this->getBasicView(); @@ -262,6 +394,32 @@ class FilterStringTest extends ViewsSqlTest { $this->assertIdenticalResultset($view, $resultset, $this->column_map); } + function testFilterStringGroupedExposedNotStarts() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: not_starts, Value: George + $filters['description']['group_info']['default_group'] = 3; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Ringo', + ), + array( + 'name' => 'Paul', + ), + // There is no Meredith returned because his description is empty + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + function testFilterStringEnds() { $view = $this->getBasicView(); @@ -289,6 +447,28 @@ class FilterStringTest extends ViewsSqlTest { $this->assertIdenticalResultset($view, $resultset, $this->column_map); } + function testFilterStringGroupedExposedEnds() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Descriptino, Operator: ends, Value: Beatles + $filters['description']['group_info']['default_group'] = 4; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'George', + ), + array( + 'name' => 'Ringo', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + function testFilterStringNotEnds() { $view = $this->getBasicView(); @@ -317,6 +497,29 @@ class FilterStringTest extends ViewsSqlTest { $this->assertIdenticalResultset($view, $resultset, $this->column_map); } + function testFilterStringGroupedExposedNotEnds() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Description, Operator: not_ends, Value: Beatles + $filters['description']['group_info']['default_group'] = 5; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Paul', + ), + // There is no Meredith returned because his description is empty + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + function testFilterStringNot() { $view = $this->getBasicView(); @@ -345,6 +548,31 @@ class FilterStringTest extends ViewsSqlTest { $this->assertIdenticalResultset($view, $resultset, $this->column_map); } + + function testFilterStringGroupedExposedNot() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Description, Operator: not (does not contains), Value: Beatles + $filters['description']['group_info']['default_group'] = 6; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Paul', + ), + // There is no Meredith returned because his description is empty + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + + } + function testFilterStringShorter() { $view = $this->getBasicView(); @@ -372,6 +600,27 @@ class FilterStringTest extends ViewsSqlTest { $this->assertIdenticalResultset($view, $resultset, $this->column_map); } + function testFilterStringGroupedExposedShorter() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: shorterthan, Value: 5 + $filters['name']['group_info']['default_group'] = 4; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'John', + ), + array( + 'name' => 'Paul', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + function testFilterStringLonger() { $view = $this->getBasicView(); @@ -396,6 +645,25 @@ class FilterStringTest extends ViewsSqlTest { $this->assertIdenticalResultset($view, $resultset, $this->column_map); } + function testFilterStringGroupedExposedLonger() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Name, Operator: longerthan, Value: 4 + $filters['name']['group_info']['default_group'] = 5; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + function testFilterStringEmpty() { $view = $this->getBasicView(); @@ -418,4 +686,129 @@ class FilterStringTest extends ViewsSqlTest { ); $this->assertIdenticalResultset($view, $resultset, $this->column_map); } + + function testFilterStringGroupedExposedEmpty() { + $filters = $this->getGroupedExposedFilters(); + $view = $this->getBasicPageView(); + + // Filter: Description, Operator: empty, Value: + $filters['description']['group_info']['default_group'] = 7; + $view->set_display('page_1'); + $view->display['page_1']->handler->override_option('filters', $filters); + + $this->executeView($view); + $resultset = array( + array( + 'name' => 'Meredith', + ), + ); + $this->assertIdenticalResultset($view, $resultset, $this->column_map); + } + + protected function getGroupedExposedFilters() { + $filters = array( + 'name' => array( + 'id' => 'name', + 'table' => 'views_test', + 'field' => 'name', + 'relationship' => 'none', + 'exposed' => TRUE, + 'expose' => array( + 'operator' => 'name_op', + 'label' => 'name', + 'identifier' => 'name', + ), + 'is_grouped' => TRUE, + 'group_info' => array( + 'label' => 'name', + 'identifier' => 'name', + 'default_group' => 'All', + 'group_items' => array( + 1 => array( + 'title' => 'Is Ringo', + 'operator' => '=', + 'value' => 'Ringo', + ), + 2 => array( + 'title' => 'Is not Ringo', + 'operator' => '!=', + 'value' => array('value' => 'Ringo'), + ), + 3 => array( + 'title' => 'Contains ing', + 'operator' => 'contains', + 'value' => 'ing', + ), + 4 => array( + 'title' => 'Shorter than 5 letters', + 'operator' => 'shorterthan', + 'value' => 5, + ), + 5 => array( + 'title' => 'Longer than 7 letters', + 'operator' => 'longerthan', + 'value' => 7, + ), + ), + ), + ), + 'description' => array( + 'id' => 'description', + 'table' => 'views_test', + 'field' => 'description', + 'relationship' => 'none', + 'exposed' => TRUE, + 'expose' => array( + 'operator' => 'description_op', + 'label' => 'description', + 'identifier' => 'description', + ), + 'is_grouped' => TRUE, + 'group_info' => array( + 'label' => 'description', + 'identifier' => 'description', + 'default_group' => 'All', + 'group_items' => array( + 1 => array( + 'title' => 'Contains the word: Actor', + 'operator' => 'word', + 'value' => 'actor', + ), + 2 => array( + 'title' => 'Starts with George', + 'operator' => 'starts', + 'value' => 'George', + ), + 3 => array( + 'title' => 'Not Starts with: George', + 'operator' => 'not_starts', + 'value' => 'George', + ), + 4 => array( + 'title' => 'Ends with: Beatles', + 'operator' => 'ends', + 'value' => 'Beatles.', + ), + 5 => array( + 'title' => 'Not Ends with: Beatles', + 'operator' => 'not_ends', + 'value' => 'Beatles.', + ), + 6 => array( + 'title' => 'Does not contain: Beatles', + 'operator' => 'not', + 'value' => 'Beatles.', + ), + 7 => array( + 'title' => 'Empty description', + 'operator' => 'empty', + 'value' => '', + ), + ), + ), + ), + ); + return $filters; + } + } diff --git a/lib/Drupal/views/Tests/ViewsSqlTest.php b/lib/Drupal/views/Tests/ViewsSqlTest.php index 315c6852262b..ed86e2328aec 100644 --- a/lib/Drupal/views/Tests/ViewsSqlTest.php +++ b/lib/Drupal/views/Tests/ViewsSqlTest.php @@ -9,8 +9,8 @@ use Drupal\simpletest\WebTestBase; use Drupal\views\View; /** - * Abstract class for views testing. - */ + * Abstract class for views testing. + */ abstract class ViewsSqlTest extends WebTestBase { /** @@ -160,6 +160,22 @@ abstract class ViewsSqlTest extends WebTestBase { $this->verbose('
Executed view: ' . ((string) $view->build_info['query']) . '
'); } + /** + * Build and return a Page view of the views_test table. + * + * @return view + */ + protected function getBasicPageView() { + $view = $this->getBasicView(); + + // In order to test exposed filters, we have to disable + // the exposed forms cache. + drupal_static_reset('views_exposed_form_cache'); + + $display = $view->new_display('page', 'Page', 'page_1'); + return $view; + } + /** * The schema definition. */ diff --git a/lib/Drupal/views/View.php b/lib/Drupal/views/View.php index 5f5818201fc0..6cc8baa398e7 100644 --- a/lib/Drupal/views/View.php +++ b/lib/Drupal/views/View.php @@ -1087,17 +1087,32 @@ class View extends ViewsDbObject { function _build($key) { $handlers = &$this->$key; foreach ($handlers as $id => $data) { + if (!empty($handlers[$id]) && is_object($handlers[$id])) { - // Give this handler access to the exposed filter input. - if (!empty($this->exposed_data)) { - $rc = $handlers[$id]->accept_exposed_input($this->exposed_data); - $handlers[$id]->store_exposed_input($this->exposed_data, $rc); - if (!$rc) { - continue; - } + $multiple_exposed_input = array(0 => NULL); + if ($handlers[$id]->multiple_exposed_input()) { + $multiple_exposed_input = $handlers[$id]->group_multiple_exposed_input($this->exposed_data); + } + foreach ($multiple_exposed_input as $group_id) { + // Give this handler access to the exposed filter input. + if (!empty($this->exposed_data)) { + $converted = FALSE; + if ($handlers[$id]->is_a_group()) { + $converted = $handlers[$id]->convert_exposed_input($this->exposed_data, $group_id); + $handlers[$id]->store_group_input($this->exposed_data, $converted); + if (!$converted) { + continue; + } + } + $rc = $handlers[$id]->accept_exposed_input($this->exposed_data); + $handlers[$id]->store_exposed_input($this->exposed_data, $rc); + if (!$rc) { + continue; + } + } + $handlers[$id]->set_relationship(); + $handlers[$id]->query($this->display_handler->use_group_by()); } - $handlers[$id]->set_relationship(); - $handlers[$id]->query($this->display_handler->use_group_by()); } } } diff --git a/views.module b/views.module index edff316987d3..e4ef31ce7ab5 100644 --- a/views.module +++ b/views.module @@ -2049,7 +2049,18 @@ function views_exposed_form($form, &$form_state) { foreach ($view->display_handler->handlers as $type => $value) { foreach ($view->$type as $id => $handler) { if ($handler->can_expose() && $handler->is_exposed()) { - $handler->exposed_form($form, $form_state); + // Grouped exposed filters have their own forms. + // Instead of render the standard exposed form, a new Select or + // Radio form field is rendered with the available groups. + // When an user choose an option the selected value is split + // into the operator and value that the item represents. + if ($handler->is_a_group()) { + $handler->group_form($form, $form_state); + $id = $handler->options['group_info']['identifier']; + } + else { + $handler->exposed_form($form, $form_state); + } if ($info = $handler->exposed_info()) { $form['#info']["$type-$id"] = $info; } @@ -2539,6 +2550,15 @@ function views_trim_text($alter, $value) { return $value; } +/** + * Filter by no empty values, though allow to use "0". + * @param $var + * @return bool + */ +function _views_array_filter_zero($var) { + return trim($var) != ""; +} + /** * Adds one to each key of the array. * diff --git a/views_ui.module b/views_ui.module index b0c7d8675615..ac8ef523e3a0 100644 --- a/views_ui.module +++ b/views_ui.module @@ -212,6 +212,12 @@ function views_ui_theme() { 'file' => "includes/admin.inc", ), + // Group of filters. + 'views_ui_build_group_filter_form' => array( + 'render element' => 'form', + 'file' => 'includes/admin.inc', + ), + // tab themes 'views_tabset' => array( 'variables' => array('tabs' => NULL),