Commit 7307c8c7 authored by Max Kanat-Alexander's avatar Max Kanat-Alexander

Bug 26074 - Ability to limit search by number of Comments

r=mkanat, a=mkanat (module owner)
parent daa533e7
...@@ -56,7 +56,10 @@ use constant UPDATE_COLUMNS => qw( ...@@ -56,7 +56,10 @@ use constant UPDATE_COLUMNS => qw(
use constant DB_TABLE => 'longdescs'; use constant DB_TABLE => 'longdescs';
use constant ID_FIELD => 'comment_id'; use constant ID_FIELD => 'comment_id';
use constant LIST_ORDER => 'bug_when'; # In some rare cases, two comments can have identical timestamps. If
# this happens, we want to be sure that the comment added later shows up
# later in the sequence.
use constant LIST_ORDER => 'bug_when, comment_id';
use constant VALIDATORS => { use constant VALIDATORS => {
extra_data => \&_check_extra_data, extra_data => \&_check_extra_data,
......
...@@ -218,6 +218,8 @@ use constant DEFAULT_FIELDS => ( ...@@ -218,6 +218,8 @@ use constant DEFAULT_FIELDS => (
buglist => 1}, buglist => 1},
{name => 'longdesc', desc => 'Comment'}, {name => 'longdesc', desc => 'Comment'},
{name => 'longdescs.isprivate', desc => 'Comment is private'}, {name => 'longdescs.isprivate', desc => 'Comment is private'},
{name => 'longdescs.count', desc => 'Number of Comments',
buglist => 1},
{name => 'alias', desc => 'Alias', buglist => 1}, {name => 'alias', desc => 'Alias', buglist => 1},
{name => 'everconfirmed', desc => 'Ever Confirmed'}, {name => 'everconfirmed', desc => 'Ever Confirmed'},
{name => 'reporter_accessible', desc => 'Reporter Accessible'}, {name => 'reporter_accessible', desc => 'Reporter Accessible'},
......
...@@ -272,6 +272,14 @@ use constant OPERATOR_FIELD_OVERRIDE => { ...@@ -272,6 +272,14 @@ use constant OPERATOR_FIELD_OVERRIDE => {
changedafter => \&_long_desc_changedbefore_after, changedafter => \&_long_desc_changedbefore_after,
_default => \&_long_desc, _default => \&_long_desc,
}, },
'longdescs.count' => {
changedby => \&_long_desc_changedby,
changedbefore => \&_long_desc_changedbefore_after,
changedafter => \&_long_desc_changedbefore_after,
changedfrom => \&_invalid_combination,
changedto => \&_invalid_combination,
_default => \&_long_descs_count,
},
'longdescs.isprivate' => { 'longdescs.isprivate' => {
_default => \&_longdescs_isprivate, _default => \&_longdescs_isprivate,
}, },
...@@ -466,6 +474,10 @@ use constant COLUMN_JOINS => { ...@@ -466,6 +474,10 @@ use constant COLUMN_JOINS => {
to => 'id', to => 'id',
}, },
}, },
'longdescs.count' => {
table => 'longdescs',
join => 'INNER',
},
}; };
# This constant defines the columns that can be selected in a query # This constant defines the columns that can be selected in a query
...@@ -524,6 +536,8 @@ sub COLUMNS { ...@@ -524,6 +536,8 @@ sub COLUMNS {
. $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status')), . $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status')),
'keywords' => $dbh->sql_group_concat('DISTINCT map_keyworddefs.name'), 'keywords' => $dbh->sql_group_concat('DISTINCT map_keyworddefs.name'),
'longdescs.count' => 'COUNT(DISTINCT map_longdescs_count.comment_id)',
); );
# Backward-compatibility for old field names. Goes new_name => old_name. # Backward-compatibility for old field names. Goes new_name => old_name.
...@@ -620,6 +634,7 @@ use constant GROUP_BY_SKIP => EMPTY_COLUMN, qw( ...@@ -620,6 +634,7 @@ use constant GROUP_BY_SKIP => EMPTY_COLUMN, qw(
bug_id bug_id
flagtypes.name flagtypes.name
keywords keywords
longdescs.count
percentage_complete percentage_complete
); );
...@@ -2171,7 +2186,7 @@ sub _content_matches { ...@@ -2171,7 +2186,7 @@ sub _content_matches {
COLUMNS->{'relevance'}->{name} = $select_term; COLUMNS->{'relevance'}->{name} = $select_term;
} }
sub _long_desc { sub _join_longdescs {
my ($self, $args) = @_; my ($self, $args) = @_;
my ($chart_id, $joins) = @$args{qw(chart_id joins)}; my ($chart_id, $joins) = @$args{qw(chart_id joins)};
...@@ -2182,22 +2197,37 @@ sub _long_desc { ...@@ -2182,22 +2197,37 @@ sub _long_desc {
as => $table, as => $table,
extra => $extra, extra => $extra,
}; };
# We only want to do an INNER JOIN if we're not checking isprivate.
# Otherwise we'd exclude all bugs with only private comments from
# the search entirely.
$join->{join} = 'INNER' if $self->_user->is_insider;
push(@$joins, $join); push(@$joins, $join);
return $table;
}
sub _long_desc {
my ($self, $args) = @_;
my $table = $self->_join_longdescs($args);
$args->{full_field} = "$table.thetext"; $args->{full_field} = "$table.thetext";
} }
sub _longdescs_isprivate { sub _long_descs_count {
my ($self, $args) = @_; my ($self, $args) = @_;
my ($chart_id, $joins) = @$args{qw(chart_id joins)}; my ($chart_id, $joins) = @$args{qw(chart_id joins)};
my $table = "longdescs_count_$chart_id";
my $table = "longdescs_$chart_id"; my $extra = $self->_user->is_insider ? "" : "WHERE isprivate = 0";
my $extra = $self->_user->is_insider ? [] : ["$table.isprivate = 0"];
my $join = { my $join = {
table => 'longdescs', table => "(SELECT bug_id, COUNT(*) AS num"
. " FROM longdescs $extra GROUP BY bug_id)",
as => $table, as => $table,
extra => $extra,
}; };
push(@$joins, $join); push(@$joins, $join);
$args->{full_field} = "${table}.num";
}
sub _longdescs_isprivate {
my ($self, $args) = @_;
my $table = $self->_join_longdescs($args);
$args->{full_field} = "$table.isprivate"; $args->{full_field} = "$table.isprivate";
} }
......
...@@ -133,6 +133,7 @@ ...@@ -133,6 +133,7 @@
"flagtypes.name" => "Flags", "flagtypes.name" => "Flags",
"keywords" => "Keywords", "keywords" => "Keywords",
"longdesc" => "Comment", "longdesc" => "Comment",
"longdescs.count" => "Number of Comments",
"longdescs.isprivate" => "Comment is private", "longdescs.isprivate" => "Comment is private",
"newcc" => "CC", "newcc" => "CC",
"op_sys" => "OS", "op_sys" => "OS",
......
...@@ -66,6 +66,7 @@ ...@@ -66,6 +66,7 @@
"op_sys" => { maxlength => 4 } , "op_sys" => { maxlength => 4 } ,
"bug_file_loc" => { maxlength => 30 } , "bug_file_loc" => { maxlength => 30 } ,
"target_milestone" => { title => "TargetM" } , "target_milestone" => { title => "TargetM" } ,
"longdescs.count" => { title => "# Comments" },
"percentage_complete" => { format_value => "%d %%" } , "percentage_complete" => { format_value => "%d %%" } ,
} }
%] %]
......
...@@ -667,6 +667,13 @@ sub _create_one_bug { ...@@ -667,6 +667,13 @@ sub _create_one_bug {
undef, $bug->id); undef, $bug->id);
} }
# Bug 1 gets three comments, so that longdescs.count matches it
# uniquely. The third comment is added in the middle, so that the
# last comment contains all of the important data, like work_time.
if ($number == 1) {
$bug->add_comment("1-comment-" . random(100));
}
my %update_params = %{ $self->_bug_update_values->{$number} }; my %update_params = %{ $self->_bug_update_values->{$number} };
my %reverse_map = reverse %{ Bugzilla::Bug->FIELD_MAP }; my %reverse_map = reverse %{ Bugzilla::Bug->FIELD_MAP };
foreach my $db_name (keys %reverse_map) { foreach my $db_name (keys %reverse_map) {
......
...@@ -309,6 +309,7 @@ use constant CHANGED_VALUE_BROKEN => ( ...@@ -309,6 +309,7 @@ use constant CHANGED_VALUE_BROKEN => (
estimated_time => { contains => [1] }, estimated_time => { contains => [1] },
'flagtypes.name' => { contains => [1] }, 'flagtypes.name' => { contains => [1] },
keywords => { contains => [1] }, keywords => { contains => [1] },
'longdescs.count' => { search => 1 },
work_time => { contains => [1] }, work_time => { contains => [1] },
FIELD_TYPE_MULTI_SELECT, { contains => [1] }, FIELD_TYPE_MULTI_SELECT, { contains => [1] },
); );
...@@ -511,6 +512,7 @@ use constant CHANGED_BROKEN_NOT => ( ...@@ -511,6 +512,7 @@ use constant CHANGED_BROKEN_NOT => (
# For changedfrom and changedto. # For changedfrom and changedto.
use constant CHANGED_FROM_TO_BROKEN_NOT => ( use constant CHANGED_FROM_TO_BROKEN_NOT => (
'longdescs.count' => { search => 1 },
"bug_group" => { contains => [1] }, "bug_group" => { contains => [1] },
"cc" => { contains => [1] }, "cc" => { contains => [1] },
"cf_multi_select" => { contains => [1] }, "cf_multi_select" => { contains => [1] },
...@@ -737,6 +739,7 @@ use constant REGEX_OVERRIDE => { ...@@ -737,6 +739,7 @@ use constant REGEX_OVERRIDE => {
cclist_accessible => { value => '^1' }, cclist_accessible => { value => '^1' },
reporter_accessible => { value => '^1' }, reporter_accessible => { value => '^1' },
everconfirmed => { value => '^1' }, everconfirmed => { value => '^1' },
'longdescs.count' => { value => '^3' },
'longdescs.isprivate' => { value => '^1' }, 'longdescs.isprivate' => { value => '^1' },
creation_ts => { value => '^2037-01-01' }, creation_ts => { value => '^2037-01-01' },
delta_ts => { value => '^2037-01-01' }, delta_ts => { value => '^2037-01-01' },
...@@ -808,6 +811,7 @@ use constant NEGATIVE_MULTI_BOOLEAN_OVERRIDE => ( ...@@ -808,6 +811,7 @@ use constant NEGATIVE_MULTI_BOOLEAN_OVERRIDE => (
# For anyexact and anywordssubstr # For anyexact and anywordssubstr
use constant ANY_OVERRIDE => ( use constant ANY_OVERRIDE => (
'longdescs.count' => { contains => [1,2,3,4] },
'work_time' => { value => '1.0,2.0' }, 'work_time' => { value => '1.0,2.0' },
dependson => { value => '<1>,<3>', contains => [1,3] }, dependson => { value => '<1>,<3>', contains => [1,3] },
MULTI_BOOLEAN_OVERRIDE, MULTI_BOOLEAN_OVERRIDE,
...@@ -944,6 +948,7 @@ use constant TESTS => { ...@@ -944,6 +948,7 @@ use constant TESTS => {
'attachments.ispatch' => { value => 1, contains => [2,3,4] }, 'attachments.ispatch' => { value => 1, contains => [2,3,4] },
cclist_accessible => { value => 1, contains => [2,3,4,5] }, cclist_accessible => { value => 1, contains => [2,3,4,5] },
reporter_accessible => { value => 1, contains => [2,3,4,5] }, reporter_accessible => { value => 1, contains => [2,3,4,5] },
'longdescs.count' => { value => 3, contains => [2,3,4,5] },
'longdescs.isprivate' => { value => 1, contains => [2,3,4,5] }, 'longdescs.isprivate' => { value => 1, contains => [2,3,4,5] },
everconfirmed => { value => 1, contains => [2,3,4,5] }, everconfirmed => { value => 1, contains => [2,3,4,5] },
creation_ts => { value => '2037-01-02', contains => [1,5] }, creation_ts => { value => '2037-01-02', contains => [1,5] },
...@@ -967,6 +972,7 @@ use constant TESTS => { ...@@ -967,6 +972,7 @@ use constant TESTS => {
'attachments.isprivate' => { value => 0, contains => [2,3,4] }, 'attachments.isprivate' => { value => 0, contains => [2,3,4] },
cclist_accessible => { value => 0, contains => [2,3,4,5] }, cclist_accessible => { value => 0, contains => [2,3,4,5] },
reporter_accessible => { value => 0, contains => [2,3,4,5] }, reporter_accessible => { value => 0, contains => [2,3,4,5] },
'longdescs.count' => { value => 2, contains => [2,3,4,5] },
'longdescs.isprivate' => { value => 0, contains => [2,3,4,5] }, 'longdescs.isprivate' => { value => 0, contains => [2,3,4,5] },
everconfirmed => { value => 0, contains => [2,3,4,5] }, everconfirmed => { value => 0, contains => [2,3,4,5] },
blocked => { contains => [1,2] }, blocked => { contains => [1,2] },
...@@ -991,6 +997,7 @@ use constant TESTS => { ...@@ -991,6 +997,7 @@ use constant TESTS => {
'attachments.isprivate' => { value => 0, contains => [1] }, 'attachments.isprivate' => { value => 0, contains => [1] },
cclist_accessible => { value => 0, contains => [1] }, cclist_accessible => { value => 0, contains => [1] },
reporter_accessible => { value => 0, contains => [1] }, reporter_accessible => { value => 0, contains => [1] },
'longdescs.count' => { value => 2, contains => [1] },
'longdescs.isprivate' => { value => 0, contains => [1] }, 'longdescs.isprivate' => { value => 0, contains => [1] },
everconfirmed => { value => 0, contains => [1] }, everconfirmed => { value => 0, contains => [1] },
'flagtypes.name' => { value => 2, contains => [2,3,4] }, 'flagtypes.name' => { value => 2, contains => [2,3,4] },
...@@ -1006,6 +1013,7 @@ use constant TESTS => { ...@@ -1006,6 +1013,7 @@ use constant TESTS => {
'attachments.isprivate' => { value => 1, contains => [1] }, 'attachments.isprivate' => { value => 1, contains => [1] },
cclist_accessible => { value => 1, contains => [1] }, cclist_accessible => { value => 1, contains => [1] },
reporter_accessible => { value => 1, contains => [1] }, reporter_accessible => { value => 1, contains => [1] },
'longdescs.count' => { value => 3, contains => [1] },
'longdescs.isprivate' => { value => 1, contains => [1] }, 'longdescs.isprivate' => { value => 1, contains => [1] },
everconfirmed => { value => 1, contains => [1] }, everconfirmed => { value => 1, contains => [1] },
dependson => { value => '<3>', contains => [1,3] }, dependson => { value => '<3>', contains => [1,3] },
...@@ -1074,6 +1082,7 @@ use constant TESTS => { ...@@ -1074,6 +1082,7 @@ use constant TESTS => {
override => { override => {
MULTI_BOOLEAN_OVERRIDE, MULTI_BOOLEAN_OVERRIDE,
dependson => { value => '<1> <3>', contains => [1,3] }, dependson => { value => '<1> <3>', contains => [1,3] },
'longdescs.count' => { contains => [1,2,3,4] },
work_time => { value => '1.0 2.0' }, work_time => { value => '1.0 2.0' },
}, },
}, },
...@@ -1109,6 +1118,7 @@ use constant TESTS => { ...@@ -1109,6 +1118,7 @@ use constant TESTS => {
blocked => { contains => [1,2] }, blocked => { contains => [1,2] },
dependson => { contains => [1,3] }, dependson => { contains => [1,3] },
longdesc => { contains => [1,5] }, longdesc => { contains => [1,5] },
'longdescs.count' => { contains => [1,5] },
} }
}, },
], ],
...@@ -1195,6 +1205,15 @@ use constant INJECTION_BROKEN_FIELD => { ...@@ -1195,6 +1205,15 @@ use constant INJECTION_BROKEN_FIELD => {
FIELD_TYPE_BUG_ID, { db_skip => ['Pg'] }, FIELD_TYPE_BUG_ID, { db_skip => ['Pg'] },
FIELD_TYPE_DATETIME, { db_skip => ['Pg'] }, FIELD_TYPE_DATETIME, { db_skip => ['Pg'] },
owner_idle_time => { search => 1 }, owner_idle_time => { search => 1 },
'longdescs.count' => {
search => 1,
db_skip => ['Pg'],
operator_ok => [qw(allwordssubstr anywordssubstr casesubstring
changedbefore changedafter greaterthan greaterthaneq
lessthan lessthaneq notregexp notsubstring
nowordssubstr regexp substring anywords
notequals nowords)],
},
keywords => { keywords => {
search => 1, search => 1,
operator_ok => [qw(allwordssubstr anywordssubstr casesubstring operator_ok => [qw(allwordssubstr anywordssubstr casesubstring
......
...@@ -332,6 +332,9 @@ sub _field_values_for_bug { ...@@ -332,6 +332,9 @@ sub _field_values_for_bug {
# searches use the last comment. # searches use the last comment.
@values = reverse @values; @values = reverse @values;
} }
elsif ($field eq 'longdescs.count') {
@values = scalar(@{ $self->bug($number)->comments });
}
elsif ($field eq 'bug_group') { elsif ($field eq 'bug_group') {
@values = $self->_values_for($number, 'groups_in', 'name'); @values = $self->_values_for($number, 'groups_in', 'name');
} }
......
...@@ -54,7 +54,11 @@ sub sql_error_ok { return $_[0]->_known_broken->{sql_error} } ...@@ -54,7 +54,11 @@ sub sql_error_ok { return $_[0]->_known_broken->{sql_error} }
# Injection tests only skip fields on certain dbs. # Injection tests only skip fields on certain dbs.
sub field_not_yet_implemented { sub field_not_yet_implemented {
my ($self) = @_; my ($self) = @_;
my $skip_for_dbs = $self->_known_broken->{db_skip}; # We use the constant directly because we don't want operator_ok
# or field_ok to stop us.
my $broken = INJECTION_BROKEN_FIELD->{$self->field}
|| INJECTION_BROKEN_FIELD->{$self->field_object->type};
my $skip_for_dbs = $broken->{db_skip};
return undef if !$skip_for_dbs; return undef if !$skip_for_dbs;
my $dbh = Bugzilla->dbh; my $dbh = Bugzilla->dbh;
if (my ($skip) = grep { $dbh->isa("Bugzilla::DB::$_") } @$skip_for_dbs) { if (my ($skip) = grep { $dbh->isa("Bugzilla::DB::$_") } @$skip_for_dbs) {
......
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