diff options
author | Byron Jones <bjones@mozilla.com> | 2012-11-22 15:25:07 +0100 |
---|---|---|
committer | Byron Jones <bjones@mozilla.com> | 2012-11-22 15:25:07 +0100 |
commit | ed17a711ebddc980f89e8290632f566c37bf762f (patch) | |
tree | 19491f770ce31dd1416e481f4c4c30a7ffd39841 | |
parent | 468c576588450202d96998d2378949e08406361e (diff) | |
download | bugzilla-ed17a711ebddc980f89e8290632f566c37bf762f.tar.gz bugzilla-ed17a711ebddc980f89e8290632f566c37bf762f.tar.xz |
Bug 780820: Allows for multiple custom search criteria to match one field
-rw-r--r-- | Bugzilla/DB.pm | 6 | ||||
-rw-r--r-- | Bugzilla/DB/Oracle.pm | 6 | ||||
-rw-r--r-- | Bugzilla/Search.pm | 70 | ||||
-rw-r--r-- | Bugzilla/Search/Clause.pm | 7 | ||||
-rw-r--r-- | extensions/Example/Extension.pm | 6 | ||||
-rw-r--r-- | js/custom-search.js | 174 | ||||
-rw-r--r-- | t/Support/Files.pm | 8 | ||||
-rw-r--r-- | template/en/default/global/user-error.html.tmpl | 11 | ||||
-rw-r--r-- | template/en/default/search/boolean-charts.html.tmpl | 13 |
9 files changed, 247 insertions, 54 deletions
diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm index 2f708d065..5eb44c403 100644 --- a/Bugzilla/DB.pm +++ b/Bugzilla/DB.pm @@ -405,8 +405,10 @@ sub sql_string_until { } sub sql_in { - my ($self, $column_name, $in_list_ref) = @_; - return " $column_name IN (" . join(',', @$in_list_ref) . ") "; + my ($self, $column_name, $in_list_ref, $negate) = @_; + return " $column_name " + . ($negate ? "NOT " : "") + . "IN (" . join(',', @$in_list_ref) . ") "; } sub sql_fulltext_search { diff --git a/Bugzilla/DB/Oracle.pm b/Bugzilla/DB/Oracle.pm index ebf59533f..7df2c09bb 100644 --- a/Bugzilla/DB/Oracle.pm +++ b/Bugzilla/DB/Oracle.pm @@ -216,16 +216,16 @@ sub sql_position { } sub sql_in { - my ($self, $column_name, $in_list_ref) = @_; + my ($self, $column_name, $in_list_ref, $negate) = @_; my @in_list = @$in_list_ref; - return $self->SUPER::sql_in($column_name, $in_list_ref) if $#in_list < 1000; + return $self->SUPER::sql_in($column_name, $in_list_ref, $negate) if $#in_list < 1000; my @in_str; while (@in_list) { my $length = $#in_list + 1; my $splice = $length > 1000 ? 1000 : $length; my @sub_in_list = splice(@in_list, 0, $splice); push(@in_str, - $self->SUPER::sql_in($column_name, \@sub_in_list)); + $self->SUPER::sql_in($column_name, \@sub_in_list, $negate)); } return "( " . join(" OR ", @in_str) . " )"; } diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm index 542b01045..8ba9cdc6b 100644 --- a/Bugzilla/Search.pm +++ b/Bugzilla/Search.pm @@ -48,6 +48,7 @@ use Bugzilla::Group; use Bugzilla::User; use Bugzilla::Field; use Bugzilla::Search::Clause; +use Bugzilla::Search::ClauseGroup; use Bugzilla::Search::Condition qw(condition); use Bugzilla::Status; use Bugzilla::Keyword; @@ -1116,8 +1117,8 @@ sub _translate_join { die "join with no table: " . Dumper($join_info) if !$join_info->{table}; die "join with no 'as': " . Dumper($join_info) if !$join_info->{as}; - - my $from_table = "bugs"; + + my $from_table = $join_info->{bugs_table} || "bugs"; my $from = $join_info->{from} || "bug_id"; if ($from =~ /^(\w+)\.(\w+)$/) { ($from_table, $from) = ($1, $2); @@ -1522,7 +1523,7 @@ sub _charts_to_conditions { my $clause = $self->_charts; my @joins; $clause->walk_conditions(sub { - my ($condition) = @_; + my ($clause, $condition) = @_; return if !$condition->translated; push(@joins, @{ $condition->translated->{joins} }); }); @@ -1542,7 +1543,7 @@ sub _params_to_data_structure { my ($self) = @_; # First we get the "special" charts, representing all the normal - # field son the search page. This may modify _params, so it needs to + # fields on the search page. This may modify _params, so it needs to # happen first. my $clause = $self->_special_charts; @@ -1551,7 +1552,7 @@ sub _params_to_data_structure { # And then process the modern "custom search" format. $clause->add( $self->_custom_search ); - + return $clause; } @@ -1582,7 +1583,7 @@ sub _boolean_charts { my $identifier = "$chart_id-$and_id-$or_id"; my $field = $params->{"field$identifier"}; my $operator = $params->{"type$identifier"}; - my $value = $params->{"value$identifier"}; + my $value = $params->{"value$identifier"}; $or_clause->add($field, $operator, $value); } $and_clause->add($or_clause); @@ -1598,13 +1599,18 @@ sub _custom_search { my ($self) = @_; my $params = $self->_params; - my $current_clause = new Bugzilla::Search::Clause($params->{j_top}); + my $joiner = $params->{j_top} || ''; + my $current_clause = $joiner eq 'AND_G' + ? new Bugzilla::Search::ClauseGroup() + : new Bugzilla::Search::Clause($joiner); my @clause_stack; foreach my $id ($self->_field_ids) { my $field = $params->{"f$id"}; if ($field eq 'OP') { - my $joiner = $params->{"j$id"}; - my $new_clause = new Bugzilla::Search::Clause($joiner); + my $joiner = $params->{"j$id"} || ''; + my $new_clause = $joiner eq 'AND_G' + ? new Bugzilla::Search::ClauseGroup() + : new Bugzilla::Search::Clause($joiner); $new_clause->negate($params->{"n$id"}); $current_clause->add($new_clause); push(@clause_stack, $current_clause); @@ -1643,14 +1649,12 @@ sub _field_ids { } sub _handle_chart { - my ($self, $chart_id, $condition) = @_; + my ($self, $chart_id, $clause, $condition) = @_; my $dbh = Bugzilla->dbh; my $params = $self->_params; my ($field, $operator, $value) = $condition->fov; - - $field = FIELD_MAP->{$field} || $field; - return if (!defined $field or !defined $operator or !defined $value); + $field = FIELD_MAP->{$field} || $field; my $string_value; if (ref $value eq 'ARRAY') { @@ -1681,15 +1685,19 @@ sub _handle_chart { # on multiple values, like anyexact. my %search_args = ( - chart_id => $chart_id, - sequence => $chart_id, - field => $field, - full_field => $full_field, - operator => $operator, - value => $string_value, - all_values => $value, - joins => [], + chart_id => $chart_id, + sequence => $chart_id, + field => $field, + full_field => $full_field, + operator => $operator, + value => $string_value, + all_values => $value, + joins => [], + bugs_table => 'bugs', + table_suffix => '', ); + $clause->update_search_args(\%search_args); + $search_args{quoted} = $self->_quote_unless_numeric(\%search_args); # This should add a "term" selement to %search_args. $self->do_search_function(\%search_args); @@ -1705,7 +1713,12 @@ sub _handle_chart { field => $field, type => $operator, value => $string_value, term => $search_args{term}, }); - + + foreach my $join (@{ $search_args{joins} }) { + $join->{bugs_table} = $search_args{bugs_table}; + $join->{table_suffix} = $search_args{table_suffix}; + } + $condition->translated(\%search_args); } @@ -1861,8 +1874,14 @@ sub _quote_unless_numeric { } sub build_subselect { - my ($outer, $inner, $table, $cond) = @_; - return "$outer IN (SELECT $inner FROM $table WHERE $cond)"; + my ($outer, $inner, $table, $cond, $negate) = @_; + # Execute subselects immediately to avoid dependent subqueries, which are + # large performance hits on MySql + my $q = "SELECT DISTINCT $inner FROM $table WHERE $cond"; + my $dbh = Bugzilla->dbh; + my $list = $dbh->selectcol_arrayref($q); + return $negate ? "1=1" : "1=2" unless @$list; + return $dbh->sql_in($outer, $list, $negate); } # Used by anyexact to get the list of input values. This allows us to @@ -2659,8 +2678,7 @@ sub _multiselect_term { my $term = $args->{term}; $term .= $args->{_extra_where} || ''; my $select = $args->{_select_field} || 'bug_id'; - my $not_sql = $not ? "NOT " : ''; - return "bugs.bug_id ${not_sql}IN (SELECT $select FROM $table WHERE $term)"; + return build_subselect("$args->{bugs_table}.bug_id", $select, $table, $term, $not); } ############################### diff --git a/Bugzilla/Search/Clause.pm b/Bugzilla/Search/Clause.pm index a068ce5ed..38f6f30be 100644 --- a/Bugzilla/Search/Clause.pm +++ b/Bugzilla/Search/Clause.pm @@ -42,6 +42,11 @@ sub children { return $self->{children}; } +sub update_search_args { + my ($self, $search_args) = @_; + # abstract +} + sub joiner { return $_[0]->{joiner} } sub has_translated_conditions { @@ -83,7 +88,7 @@ sub walk_conditions { my ($self, $callback) = @_; foreach my $child (@{ $self->children }) { if ($child->isa('Bugzilla::Search::Condition')) { - $callback->($child); + $callback->($self, $child); } else { $child->walk_conditions($callback); diff --git a/extensions/Example/Extension.pm b/extensions/Example/Extension.pm index 8eef19a6e..d7b892cfa 100644 --- a/extensions/Example/Extension.pm +++ b/extensions/Example/Extension.pm @@ -724,10 +724,12 @@ sub _check_short_desc { my $invocant = shift; my $value = $invocant->$original(@_); if ($value !~ /example/i) { - # Uncomment this line to make Bugzilla throw an error every time + # Use this line to make Bugzilla throw an error every time # you try to file a bug or update a bug without the word "example" # in the summary. - #ThrowUserError('example_short_desc_invalid'); + if (0) { + ThrowUserError('example_short_desc_invalid'); + } } return $value; } diff --git a/js/custom-search.js b/js/custom-search.js index 73897035d..e5c172d3b 100644 --- a/js/custom-search.js +++ b/js/custom-search.js @@ -40,7 +40,7 @@ function custom_search_new_row() { var row = document.getElementById('custom_search_last_row'); var clone = row.cloneNode(true); - _cs_fix_ids(clone); + _cs_fix_row_ids(clone); // We only want one copy of the buttons, in the new row. So the old // ones get deleted. @@ -55,13 +55,28 @@ function custom_search_new_row() { // Always make sure there's only one row with this id. row.id = null; row.parentNode.appendChild(clone); + cs_reconfigure(row); fix_query_string(row); return clone; } +var _cs_source_any_all; function custom_search_open_paren() { var row = document.getElementById('custom_search_last_row'); + // create a copy of j_top and use that as the source, so we can modify + // j_top if required + if (!_cs_source_any_all) { + var j_top = document.getElementById('j_top'); + _cs_source_any_all = j_top.cloneNode(true); + } + + // find the parent any/all select, and remove the grouped option + var structure = _cs_build_structure(row); + var old_id = _cs_get_row_id(row); + var parent_j = document.getElementById(_cs_get_join(structure, 'f' + old_id)); + _cs_remove_and_g(parent_j); + // If there's an "Any/All" select in this row, it needs to stay as // part of the parent paren set. var old_any_all = _remove_any_all(row); @@ -78,21 +93,20 @@ function custom_search_open_paren() { var not_for_paren = new_not[0].cloneNode(true); // Preserve the values when modifying the row. - var id = _cs_fix_ids(row, true); + var id = _cs_fix_row_ids(row, true); var prev_id = id - 1; var paren_row = row.cloneNode(false); paren_row.id = null; paren_row.innerHTML = '(<input type="hidden" name="f' + prev_id - + '" value="OP">'; + + '" id="f' + prev_id + '" value="OP">'; paren_row.insertBefore(not_for_paren, paren_row.firstChild); row.parentNode.insertBefore(paren_row, row); - + // New paren set needs a new "Any/All" select. var any_all_container = document.createElement('div'); YAHOO.util.Dom.addClass(any_all_container, ANY_ALL_SELECT_CLASS); - var j_top = document.getElementById('j_top'); - var any_all = j_top.cloneNode(true); + var any_all = _cs_source_any_all.cloneNode(true); any_all.name = 'j' + prev_id; any_all.id = any_all.name; any_all_container.appendChild(any_all); @@ -104,6 +118,7 @@ function custom_search_open_paren() { YAHOO.util.Dom.setStyle(row, 'margin-left', new_margin + 'em'); YAHOO.util.Dom.removeClass('cp_container', 'bz_default_hidden'); + cs_reconfigure(any_all_container); fix_query_string(any_all_container); } @@ -112,7 +127,7 @@ function custom_search_close_paren() { // We need to up the new row's id by one more, because we're going // to insert a "CP" before it. - var id = _cs_fix_ids(new_row); + var id = _cs_fix_row_ids(new_row); var margin = YAHOO.util.Dom.getStyle(new_row, 'margin-left'); var int_match = margin.match(/\d+/); @@ -122,7 +137,7 @@ function custom_search_close_paren() { var paren_row = new_row.cloneNode(false); paren_row.id = null; paren_row.innerHTML = ')<input type="hidden" name="f' + (id - 1) - + '" value="CP">'; + + '" id="f' + (id - 1) + '" value="CP">'; new_row.parentNode.insertBefore(paren_row, new_row); @@ -130,6 +145,7 @@ function custom_search_close_paren() { YAHOO.util.Dom.addClass('cp_container', 'bz_default_hidden'); } + cs_reconfigure(new_row); fix_query_string(new_row); } @@ -172,19 +188,17 @@ function redirect_html4_browsers() { document.location = url; } -function _cs_fix_ids(parent, preserve_values) { +function _cs_fix_row_ids(row, preserve_values) { // Update the label of the checkbox. - var label = YAHOO.util.Dom.getElementBy(function() { return true }, - 'label', parent); + var label = YAHOO.util.Dom.getElementBy(function() { return true }, 'label', row); var id_match = label.htmlFor.match(/\d+$/); var id = parseInt(id_match[0]) + 1; label.htmlFor = label.htmlFor.replace(/\d+$/, id); - // Sets all the inputs in the parent back to their default + // Sets all the inputs in the row back to their default // and fixes their id. var fields = - YAHOO.util.Dom.getElementsByClassName('custom_search_form_field', null, - parent); + YAHOO.util.Dom.getElementsByClassName('custom_search_form_field', null, row); for (var i = 0; i < fields.length; i++) { var field = fields[i]; @@ -196,15 +210,141 @@ function _cs_fix_ids(parent, preserve_values) { field.value = ''; } } - - // Update the numeric id for the new row. + + // Update the numeric id for the row. field.name = field.name.replace(/\d+$/, id); field.id = field.name; } - + return id; } +function _cs_build_structure(form_member) { + // build a map of the structure of the custom fields + var form = YAHOO.util.Dom.getAncestorByTagName(form_member, 'form'); + var last_id = _get_last_cs_row_id(form); + var structure = [ 'j_top' ]; + var nested = [ structure ]; + for (var id = 1; id <= last_id; id++) { + var f = form['f' + id]; + if (!f || !f.parentNode.parentNode) continue; + + if (f.value == 'OP') { + var j = [ 'j' + id ]; + nested[nested.length - 1].push(j); + nested.push(j); + continue; + } else if (f.value == 'CP') { + nested.pop(); + continue; + } else { + nested[nested.length - 1].push('f' + id); + } + } + return structure; +} + +function cs_reconfigure(form_member) { + var structure = _cs_build_structure(form_member); + _cs_add_listeners(structure); + _cs_trigger_j_listeners(structure); + fix_query_string(form_member); + + var j = _cs_get_join(structure, 'f' + _get_last_cs_row_id()); + document.getElementById('op_button').disabled = document.getElementById(j).value == 'AND_G'; +} + +function _cs_add_listeners(parents) { + for (var i = 0, l = parents.length; i < l; i++) { + if (typeof(parents[i]) == 'object') { + // nested + _cs_add_listeners(parents[i]); + } else if (i == 0) { + // joiner + YAHOO.util.Event.removeListener(parents[i], 'change', _cs_j_change); + YAHOO.util.Event.addListener(parents[i], 'change', _cs_j_change, parents); + } else { + // field + YAHOO.util.Event.removeListener(parents[i], 'change', _cs_f_change); + YAHOO.util.Event.addListener(parents[i], 'change', _cs_f_change, parents); + } + } +} + +function _cs_trigger_j_listeners(fields) { + var has_children = false; + for (var i = 0, l = fields.length; i < l; i++) { + if (typeof(fields[i]) == 'undefined') { + continue; + } else if (typeof(fields[i]) == 'object') { + // nested + _cs_trigger_j_listeners(fields[i]); + has_children = true; + } else if (i == 0) { + _cs_j_change(undefined, fields); + } + } + if (has_children) { + _cs_remove_and_g(document.getElementById(fields[0])); + } +} + +function _cs_get_join(parents, field) { + for (var i = 0, l = parents.length; i < l; i++) { + if (typeof(parents[i]) == 'object') { + // nested + var result = _cs_get_join(parents[i], field); + if (result) return result; + } else if (parents[i] == field) { + return parents[0]; + } + } + return false; +} + +function _cs_remove_and_g(join_field) { + var index = bz_optionIndex(join_field, 'AND_G'); + join_field.options[index] = null; + join_field.options[bz_optionIndex(join_field, 'AND')].innerHTML = cs_and_label; + join_field.options[bz_optionIndex(join_field, 'OR')].innerHTML = cs_or_label; +} + +function _cs_j_change(evt, fields, field) { + var j = document.getElementById(fields[0]); + if (j && j.value == 'AND_G') { + for (var i = 1, l = fields.length; i < l; i++) { + if (typeof(fields[i]) == 'object') continue; + if (!field) { + field = document.getElementById(fields[i]).value; + } else { + document.getElementById(fields[i]).value = field; + } + } + if (evt) { + fix_query_string(j); + } + if ('f' + _get_last_cs_row_id() == fields[fields.length - 1]) { + document.getElementById('op_button').style.display = 'none'; + } + } else { + document.getElementById('op_button').style.display = ''; + } +} + +function _cs_f_change(evt, args) { + var field = YAHOO.util.Event.getTarget(evt); + _cs_j_change(evt, args, field.value); +} + +function _get_last_cs_row_id() { + return _cs_get_row_id('custom_search_last_row'); +} + +function _cs_get_row_id(row) { + var label = YAHOO.util.Dom.getElementBy(function() { return true }, 'label', row); + return parseInt(label.htmlFor.match(/\d+$/)[0]); +} + function _remove_any_all(parent) { var any_all = YAHOO.util.Dom.getElementsByClassName( ANY_ALL_SELECT_CLASS, null, parent); diff --git a/t/Support/Files.pm b/t/Support/Files.pm index 31a0058db..2898fdd3f 100644 --- a/t/Support/Files.pm +++ b/t/Support/Files.pm @@ -34,6 +34,14 @@ my @extension_paths = map { $_->package_dir } @{ Bugzilla->extensions }; find(sub { push(@files, $File::Find::name) if $_ =~ /\.pm$/;}, 'Bugzilla', @extension_paths); push(@files, 'extensions/create.pl'); +my @extensions = glob('extensions/*'); +foreach my $extension (@extensions) { + # Skip disabled extensions + next if -e "$extension/disabled"; + + find(sub { push(@files, $File::Find::name) if $_ =~ /\.pm$/;}, $extension); +} + sub isTestingFile { my ($file) = @_; my $exclude; diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl index c2b2ceb28..fca219401 100644 --- a/template/en/default/global/user-error.html.tmpl +++ b/template/en/default/global/user-error.html.tmpl @@ -1575,6 +1575,17 @@ and the "matches" search can only be used with the "content" field. + [% ELSIF error == "search_grouped_field_invalid" %] + [% terms.Bugzilla %] does not support using the + "[%+ field_descs.$field FILTER html %]" ([% field FILTER html %]) + field with grouped search conditions. + + [% ELSIF error == "search_grouped_invalid_nesting" %] + You cannot nest clauses within grouped search conditions. + + [% ELSIF error == "search_grouped_field_mismatch" %] + All conditions under a groups search must use the same field. + [% ELSIF error == "search_field_operator_invalid" %] [% terms.Bugzilla %] does not support using the "[%+ field_descs.$field FILTER html %]" ([% field FILTER html %]) diff --git a/template/en/default/search/boolean-charts.html.tmpl b/template/en/default/search/boolean-charts.html.tmpl index 878589cea..d87db1c6a 100644 --- a/template/en/default/search/boolean-charts.html.tmpl +++ b/template/en/default/search/boolean-charts.html.tmpl @@ -78,6 +78,10 @@ <script type="text/javascript" src="[% 'js/history.js/native.history.js' FILTER mtime %]"></script> <script type="text/javascript"> redirect_html4_browsers(); + [%# These are alternative labels for the AND and OR options in and_all_select %] + var cs_and_label = 'Match ALL of the following:'; + var cs_or_label = 'Match ANY of the following:'; + cs_reconfigure('custom_search_last_row'); </script> </div> @@ -134,7 +138,8 @@ ( [% indent_level = indent_level + 1 %] [% ELSIF condition.f == "CP" %] - <input type="hidden" name="f[% cond_num FILTER html %]" value="CP"> + <input type="hidden" name="f[% cond_num FILTER html %]" + id="f[% cond_num FILTER html %]" value="CP"> ) [% ELSE %] <select name="f[% cond_num FILTER html %]" title="Field" @@ -178,9 +183,11 @@ <div class="any_all_select"> <select name="[% name FILTER html %]" id="[% name FILTER html %]" onchange="fix_query_string(this)"> - <option value="AND">Match ALL of the following:</option> + <option value="AND">Match ALL of the following separately:</option> <option value="OR" [% ' selected="selected"' IF selected == "OR" %]> - Match ANY of the following:</option> + Match ANY of the following separately:</option> + <option value="AND_G" [% ' selected' IF selected == "AND_G" %]> + Match ALL of the following against the same field:</option> </select> [% IF with_advanced_link %] <a id="custom_search_advanced_controller" |