Commit c0b214bc authored by mkanat%bugzilla.org's avatar mkanat%bugzilla.org

Bug 518024: Make quicksearch accept any field name or any unique starting substring of a fieldname

Patch by Max Kanat-Alexander <mkanat@bugzilla.org> r=LpSolit, a=LpSolit
parent eb6a2a89
...@@ -33,65 +33,80 @@ use Bugzilla::Util; ...@@ -33,65 +33,80 @@ use Bugzilla::Util;
use base qw(Exporter); use base qw(Exporter);
@Bugzilla::Search::Quicksearch::EXPORT = qw(quicksearch); @Bugzilla::Search::Quicksearch::EXPORT = qw(quicksearch);
# Word renamings # Custom mappings for some fields.
use constant MAPPINGS => { use constant MAPPINGS => {
# Status, Resolution, Platform, OS, Priority, Severity # Status, Resolution, Platform, OS, Priority, Severity
"status" => "bug_status", "status" => "bug_status",
"resolution" => "resolution", # no change "platform" => "rep_platform",
"platform" => "rep_platform", "os" => "op_sys",
"os" => "op_sys", "severity" => "bug_severity",
"opsys" => "op_sys",
"priority" => "priority", # no change # People: AssignedTo, Reporter, QA Contact, CC, etc.
"pri" => "priority", "assignee" => "assigned_to",
"severity" => "bug_severity",
"sev" => "bug_severity", # Product, Version, Component, Target Milestone
# People: AssignedTo, Reporter, QA Contact, CC, Added comment (?) "milestone" => "target_milestone",
"owner" => "assigned_to", # deprecated since bug 76507
"assignee" => "assigned_to", # Summary, Description, URL, Status whiteboard, Keywords
"assignedto" => "assigned_to", "summary" => "short_desc",
"reporter" => "reporter", # no change "description" => "longdesc",
"rep" => "reporter", "comment" => "longdesc",
"qa" => "qa_contact", "url" => "bug_file_loc",
"qacontact" => "qa_contact", "whiteboard" => "status_whiteboard",
"cc" => "cc", # no change "sw" => "status_whiteboard",
# Product, Version, Component, Target Milestone "kw" => "keywords",
"product" => "product", # no change "group" => "bug_group",
"prod" => "product",
"version" => "version", # no change # Flags
"ver" => "version", "flag" => "flagtypes.name",
"component" => "component", # no change "requestee" => "requestees.login_name",
"comp" => "component", "setter" => "setters.login_name",
"milestone" => "target_milestone",
"target" => "target_milestone", # Attachments
"targetmilestone" => "target_milestone", "attachment" => "attachments.description",
# Summary, Description, URL, Status whiteboard, Keywords "attachmentdesc" => "attachments.description",
"summary" => "short_desc", "attachdesc" => "attachments.description",
"shortdesc" => "short_desc", "attachmentdata" => "attach_data.thedata",
"desc" => "longdesc", "attachdata" => "attach_data.thedata",
"description" => "longdesc", "attachmentmimetype" => "attachments.mimetype",
#"comment" => "longdesc", # ??? "attachmimetype" => "attachments.mimetype"
# reserve "comment" for "added comment" email search? };
"longdesc" => "longdesc",
"url" => "bug_file_loc", sub FIELD_MAP {
"whiteboard" => "status_whiteboard", my $cache = Bugzilla->request_cache;
"statuswhiteboard" => "status_whiteboard", return $cache->{quicksearch_fields} if $cache->{quicksearch_fields};
"sw" => "status_whiteboard",
"keywords" => "keywords", # no change # Get all the fields whose names don't contain periods. (Fields that
"kw" => "keywords", # contain periods are always handled in MAPPINGS.)
"group" => "bug_group", my @db_fields = grep { $_->name !~ /\./ }
"flag" => "flagtypes.name", Bugzilla->get_fields({ obsolete => 0 });
"requestee" => "requestees.login_name", my %full_map = (%{ MAPPINGS() }, map { $_->name => $_->name } @db_fields);
"req" => "requestees.login_name",
"setter" => "setters.login_name", # Eliminate the fields that start with bug_ or rep_, because those are
"set" => "setters.login_name", # handled by the MAPPINGS instead, and we don't want too many names
# Attachments # for them. (Also, otherwise "rep" doesn't match "reporter".)
"attachment" => "attachments.description", #
"attachmentdesc" => "attachments.description", # Remove "status_whiteboard" because we have "whiteboard" for it in
"attachdesc" => "attachments.description", # the mappings, and otherwise "stat" can't match "status".
"attachmentdata" => "attach_data.thedata", #
"attachdata" => "attach_data.thedata", # Also, don't allow searching the _accessible stuff via quicksearch
"attachmentmimetype" => "attachments.mimetype", # (both because it's unnecessary and because otherwise
"attachmimetype" => "attachments.mimetype" # "reporter_accessible" and "reporter" both match "rep".
delete @full_map{qw(rep_platform bug_status bug_file_loc bug_group
bug_severity bug_status
status_whiteboard
cclist_accessible reporter_accessible)};
$cache->{quicksearch_fields} = \%full_map;
return $cache->{quicksearch_fields};
}
# Certain fields, when specified like "field:value" get an operator other
# than "substring"
use constant FIELD_OPERATOR => {
content => 'matches',
owner_idle_time => 'greaterthan',
}; };
# We might want to put this into localconfig or somewhere # We might want to put this into localconfig or somewhere
...@@ -137,7 +152,7 @@ sub quicksearch { ...@@ -137,7 +152,7 @@ sub quicksearch {
my @words = splitString($searchstring); my @words = splitString($searchstring);
_handle_status_and_resolution(\@words); _handle_status_and_resolution(\@words);
my @unknownFields; my (@unknownFields, %ambiguous_fields);
# Loop over all main-level QuickSearch words. # Loop over all main-level QuickSearch words.
foreach my $qsword (@words) { foreach my $qsword (@words) {
...@@ -151,7 +166,8 @@ sub quicksearch { ...@@ -151,7 +166,8 @@ sub quicksearch {
# Split by '|' to get all operands for a boolean OR. # Split by '|' to get all operands for a boolean OR.
foreach my $or_operand (split(/\|/, $qsword)) { foreach my $or_operand (split(/\|/, $qsword)) {
if (!_handle_field_names($or_operand, $negate, if (!_handle_field_names($or_operand, $negate,
\@unknownFields)) \@unknownFields,
\%ambiguous_fields))
{ {
# Having ruled out the special cases, we may now split # Having ruled out the special cases, we may now split
# by comma, which is another legal boolean OR indicator. # by comma, which is another legal boolean OR indicator.
...@@ -170,9 +186,10 @@ sub quicksearch { ...@@ -170,9 +186,10 @@ sub quicksearch {
} # foreach (@words) } # foreach (@words)
# Inform user about any unknown fields # Inform user about any unknown fields
if (scalar(@unknownFields)) { if (scalar(@unknownFields) || scalar(keys %ambiguous_fields)) {
ThrowUserError("quicksearch_unknown_field", ThrowUserError("quicksearch_unknown_field",
{ fields => \@unknownFields }); { unknown => \@unknownFields,
ambiguous => \%ambiguous_fields });
} }
# Make sure we have some query terms left # Make sure we have some query terms left
...@@ -342,7 +359,7 @@ sub _handle_special_first_chars { ...@@ -342,7 +359,7 @@ sub _handle_special_first_chars {
} }
sub _handle_field_names { sub _handle_field_names {
my ($or_operand, $negate, $unknownFields) = @_; my ($or_operand, $negate, $unknownFields, $ambiguous_fields) = @_;
# votes:xx ("at least xx votes") # votes:xx ("at least xx votes")
if ($or_operand =~ /^votes:([0-9]+)$/) { if ($or_operand =~ /^votes:([0-9]+)$/) {
...@@ -363,14 +380,21 @@ sub _handle_field_names { ...@@ -363,14 +380,21 @@ sub _handle_field_names {
my @fields = split(/,/, $1); my @fields = split(/,/, $1);
my @values = split(/,/, $2); my @values = split(/,/, $2);
foreach my $field (@fields) { foreach my $field (@fields) {
my $translated = _translate_field_name($field);
# Skip and record any unknown fields # Skip and record any unknown fields
if (!defined(MAPPINGS->{$field})) { if (!defined $translated) {
push(@$unknownFields, $field); push(@$unknownFields, $field);
next; next;
} }
$field = MAPPINGS->{$field}; # If we got back an array, that means the substring is
foreach (@values) { # ambiguous and could match more than field name
addChart($field, 'substring', $_, $negate); elsif (ref $translated) {
$ambiguous_fields->{$field} = $translated;
next;
}
foreach my $value (@values) {
my $operator = FIELD_OPERATOR->{$translated} || 'substring';
addChart($translated, $operator, $value, $negate);
} }
} }
return 1; return 1;
...@@ -379,6 +403,59 @@ sub _handle_field_names { ...@@ -379,6 +403,59 @@ sub _handle_field_names {
return 0; return 0;
} }
sub _translate_field_name {
my $field = shift;
$field = lc($field);
my $field_map = FIELD_MAP;
# If the field exactly matches a mapping, just return right now.
return $field_map->{$field} if exists $field_map->{$field};
# Check if we match, as a starting substring, exactly one field.
my @field_names = keys %$field_map;
my @matches = grep { $_ =~ /^\Q$field\E/ } @field_names;
# Eliminate duplicates that are actually the same field
# (otherwise "assi" matches both "assignee" and "assigned_to", and
# the lines below fail when they shouldn't.)
my %match_unique = map { $field_map->{$_} => $_ } @matches;
@matches = values %match_unique;
if (scalar(@matches) == 1) {
return $field_map->{$matches[0]};
}
elsif (scalar(@matches) > 1) {
return \@matches;
}
# Check if we match exactly one custom field, ignoring the cf_ on the
# custom fields (to allow people to type things like "build" for
# "cf_build").
my %cfless;
foreach my $name (@field_names) {
my $no_cf = $name;
if ($no_cf =~ s/^cf_//) {
if ($field eq $no_cf) {
return $field_map->{$name};
}
$cfless{$no_cf} = $name;
}
}
# See if we match exactly one substring of any of the cf_-less fields.
my @cfless_matches = grep { $_ =~ /^\Q$field\E/ } (keys %cfless);
if (scalar(@cfless_matches) == 1) {
my $match = $cfless_matches[0];
my $actual_field = $cfless{$match};
return $field_map->{$actual_field};
}
elsif (scalar(@matches) > 1) {
return \@matches;
}
return undef;
}
sub _special_field_syntax { sub _special_field_syntax {
my ($word, $negate) = @_; my ($word, $negate) = @_;
# Platform and operating system # Platform and operating system
......
...@@ -1400,18 +1400,20 @@ ...@@ -1400,18 +1400,20 @@
characters long. characters long.
[% ELSIF error == "quicksearch_unknown_field" %] [% ELSIF error == "quicksearch_unknown_field" %]
[% title = "Unknown QuickSearch Field" %] [% title = "QuickSearch Error" %]
[% IF fields.unique.size == 1 %] There is a problem with your search:
Field <code>[% fields.first FILTER html %]</code> is not a known field. [% FOREACH field = unknown %]
[% ELSE %] <p><code>[% field FILTER html %]</code> is not a valid field name.</p>
Fields [% END %]
[% FOREACH field = fields.unique.sort %] [% FOREACH field = ambiguous.keys %]
<code>[% field FILTER html %]</code> <p><code>[% field FILTER html %]</code> matches more than one field:
[% ', ' UNLESS loop.last() %] [%+ ambiguous.${field}.join(', ') FILTER html %]</p>
[% END %] [% END %]
are not known fields.
[% IF unknown.size %]
<p>The legal field names are
<a href="page.cgi?id=quicksearchhack.html">listed here</a>.</p>
[% END %] [% END %]
The legal field names are <a href="page.cgi?id=quicksearchhack.html">listed here</a>.
[% ELSIF error == "reassign_to_empty" %] [% ELSIF error == "reassign_to_empty" %]
[% title = "Illegal Reassignment" %] [% title = "Illegal Reassignment" %]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment