Commit 40d5f16f authored by tim.plunkett's avatar tim.plunkett

Issue #731662 by dagmar, dawehner, tim.plunkett, bojanz, SuperXren, BWPanda:...

Issue #731662 by dagmar, dawehner, tim.plunkett, bojanz, SuperXren, BWPanda: Added Hybrid Exposed Filters.
parent a369a0e3
......@@ -380,6 +380,7 @@ td.group-title {
text-transform: uppercase;
}
.grouped-description,
.exposed-description {
float: left;
padding-top: 3px;
......
......@@ -15,6 +15,21 @@
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 <a href="topic:views/exposed-form">here</a>.
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:
<table>
<thead>
<tr><th>What the user see</th><th>What views does</th></tr>
</thead>
<tbody>
<tr><td>Is lower than 10</td><td><strong>Operator:</strong> Is Lower than. <strong>Value:</strong> 10</td></tr>
<tr><td>Is between 10 and 20</td><td><strong>Operator:</strong> Is between. <strong>Value:</strong> 10 and 20</td></tr>
<tr><td>Is greater than 20</td><td><strong>Operator:</strong> Is Greater. <strong>Value:</strong> 20</td></tr>
</tbody>
</table>
<strong>Please note:</strong> 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 <a href="topic:views/overrides">here</a> for more information.
......@@ -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 .= '<div class="views-left-40">';
$output .= drupal_render($form['optional']);
$output .= drupal_render($form['remember']);
$output .= '</div>';
$output .= '<div class="views-right-60">';
$output .= drupal_render($form['widget']);
$output .= drupal_render($form['label']);
$output .= '</div>';
$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('&lt;Any&gt;') : 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('<span>' . t('Remove') . '</span>', '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
*/
......
......@@ -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'));
};
/**
......
......@@ -554,6 +554,18 @@ function is_exposed() {
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.
*/
......
......@@ -2789,8 +2789,15 @@ function is_identifier_unique($id, $identifier) {
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;
}
}
}
}
......
......@@ -138,6 +138,9 @@ function value_validate($form, &$form_state) {
}
function admin_summary() {
if ($this->is_a_group()) {
return t('grouped');
}
if (!empty($this->options['exposed'])) {
return t('exposed');
}
......
......@@ -104,6 +104,34 @@ function validate_valid_time(&$form, $operator, $value) {
}
}
/**
* 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;
......
......@@ -304,6 +304,9 @@ function value_submit($form, &$form_state) {
}
function admin_summary() {
if ($this->is_a_group()) {
return t('grouped');
}
if (!empty($this->options['exposed'])) {
return t('exposed');
}
......
......@@ -282,6 +282,9 @@ function op_regex($field) {
}
function admin_summary() {
if ($this->is_a_group()) {
return t('grouped');
}
if (!empty($this->options['exposed'])) {
return t('exposed');
}
......
......@@ -158,6 +158,9 @@ function operator_options($which = 'title') {
}
function admin_summary() {
if ($this->is_a_group()) {
return t('grouped');
}
if (!empty($this->options['exposed'])) {
return t('exposed');
}
......
......@@ -12,6 +12,8 @@
*/
class ExposedFormTest extends ViewsSqlTest {
protected $profile = 'standard';
public static function getInfo() {
return array(
'name' => 'Exposed forms',
......@@ -20,15 +22,6 @@ public static function getInfo() {
);
}
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 @@ function testExposedAdminUi() {
$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 @@ function testExposedAdminUi() {
$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'));
}
}
......@@ -59,6 +59,24 @@ function testEqual() {
$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 @@ function testNotEqual() {
);
$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;
}
}
......@@ -98,4 +98,103 @@ public function testFilterInOperatorSimple() {
'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(