diff options
-rw-r--r-- | Bugzilla/Search.pm | 249 |
1 files changed, 165 insertions, 84 deletions
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm index ab6c38a1e..ef888df65 100644 --- a/Bugzilla/Search.pm +++ b/Bugzilla/Search.pm @@ -54,7 +54,7 @@ use Bugzilla::Keyword; use Date::Format; use Date::Parse; - +use List::MoreUtils qw(uniq); use Storable qw(dclone); ############# @@ -291,6 +291,78 @@ use constant SPECIAL_ORDER_JOIN => { 'target_milestone' => 'LEFT JOIN milestones AS ms_order ON ms_order.value = bugs.target_milestone AND ms_order.product_id = bugs.product_id', }; +# Certain columns require other columns to come before them +# in _display_columns, and should be put there if they're not there. +use constant COLUMN_DEPENDS => { + classification => ['product'], + percentage_complete => ['actual_time', 'remaining_time'], +}; + +# This describes tables that must be joined when you want to display +# certain columns in the buglist. For the most part, Search.pm uses +# DB::Schema to figure out what needs to be joined, but for some +# fields it needs a little help. +use constant COLUMN_JOINS => { + assigned_to => { + from => 'assigned_to', + to => 'userid', + table => 'profiles', + join => 'INNER', + }, + reporter => { + from => 'reporter', + to => 'userid', + table => 'profiles', + join => 'INNER', + }, + qa_contact => { + from => 'qa_contact', + to => 'userid', + table => 'profiles', + }, + component => { + from => 'component_id', + to => 'id', + table => 'components', + join => 'INNER', + }, + product => { + from => 'product_id', + to => 'id', + table => 'products', + join => 'INNER', + }, + classification => { + table => 'classifications', + from => 'map_product.classification_id', + to => 'id', + join => 'INNER', + }, + actual_time => { + table => 'longdescs', + }, + 'flagtypes.name' => { + name => 'map_flags', + table => 'flags', + extra => ' AND attach_id IS NULL', + then_to => { + name => 'map_flagtypes', + table => 'flagtypes', + from => 'map_flags.type_id', + to => 'id', + }, + }, + keywords => { + table => 'keywords', + then_to => { + name => 'map_keyworddefs', + table => 'keyworddefs', + from => 'map_keywords.keywordid', + to => 'id', + }, + }, +}; + # This constant defines the columns that can be selected in a query # and/or displayed in a bug list. Column records include the following # fields: @@ -328,8 +400,8 @@ sub COLUMNS { # Next we define columns that have special SQL instead of just something # like "bugs.bug_id". - my $actual_time = '(SUM(ldtime.work_time)' - . ' * COUNT(DISTINCT ldtime.bug_when)/COUNT(bugs.bug_id))'; + my $actual_time = '(SUM(map_actual_time.work_time)' + . ' * COUNT(DISTINCT map_actual_time.bug_when)/COUNT(bugs.bug_id))'; my %special_sql = ( deadline => $dbh->sql_date_format('bugs.deadline', '%Y-%m-%d'), actual_time => $actual_time, @@ -342,9 +414,9 @@ sub COLUMNS { . " END)", 'flagtypes.name' => $dbh->sql_group_concat('DISTINCT ' - . $dbh->sql_string_concat('flagtypes.name', 'flags.status')), + . $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status')), - 'keywords' => $dbh->sql_group_concat('DISTINCT keyworddefs.name'), + 'keywords' => $dbh->sql_group_concat('DISTINCT map_keyworddefs.name'), ); # Backward-compatibility for old field names. Goes new_name => old_name. @@ -374,7 +446,7 @@ sub COLUMNS { } foreach my $col (@id_fields) { - $special_sql{$col} = "map_${col}s.name"; + $special_sql{$col} = "map_${col}.name"; } # Do the actual column-getting from fielddefs, now. @@ -387,7 +459,7 @@ sub COLUMNS { } elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { $sql = $dbh->sql_group_concat( - 'DISTINCT map_bug_' . $field->name . '.value'); + 'DISTINCT map_' . $field->name . '.value'); } else { $sql = 'bugs.' . $field->name; @@ -434,6 +506,7 @@ sub REPORT_COLUMNS { # Internal Accessors # ###################### +# Fields that are legal for boolean charts of any kind. sub _chart_fields { my ($self) = @_; @@ -453,10 +526,81 @@ sub _chart_fields { sub _multi_select_fields { my ($self) = @_; $self->{multi_select_fields} ||= Bugzilla->fields({ - type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_URLS]}); + by_name => 1, + type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_URLS]}); return $self->{multi_select_fields}; } +# These are the fields the user has chosen to display on the buglist. +sub _display_columns { + my ($self, $columns) = @_; + if ($columns) { + my @actual_columns; + foreach my $column (@$columns) { + if (my $add_first = COLUMN_DEPENDS->{$column}) { + push(@actual_columns, @$add_first); + } + push(@actual_columns, $column); + } + $self->{display_columns} = [uniq @actual_columns]; + } + return $self->{display_columns} || []; +} + +# JOIN statements for the display columns. This should not be called +# Until the moment it is needed, because _display_columns might be +# modified by the charts. +sub _display_column_joins { + my ($self) = @_; + $self->{display_column_joins} ||= $self->_build_display_column_joins(); + return @{ $self->{display_column_joins} }; +} + +sub _build_display_column_joins { + my ($self) = @_; + my @joins; + foreach my $field (@{ $self->_display_columns }) { + my @column_join = $self->_column_join($field); + push(@joins, @column_join); + } + return \@joins; +} + +sub _column_join { + my ($self, $field) = @_; + my $join_info = COLUMN_JOINS->{$field}; + if (!$join_info) { + if ($self->_multi_select_fields->{$field}) { + return $self->_translate_join($field, { table => "bug_$field" }); + } + return (); + } + return $self->_translate_join($field, $join_info); +} + +sub _translate_join { + my ($self, $field, $join_info) = @_; + my $from_table = "bugs"; + my $from = $join_info->{from} || "bug_id"; + if ($from =~ /^(\w+)\.(\w+)$/) { + ($from_table, $from) = ($1, $2); + } + my $to = $join_info->{to} || "bug_id"; + my $join = $join_info->{join} || 'LEFT'; + my $table = $join_info->{table}; + die "$field requires a table in COLUMN_JOINS" if !$table; + my $extra = $join_info->{extra} || ''; + my $name = $join_info->{name} || "map_$field"; + $name =~ s/\./_/g; + + my @join_sql = "$join JOIN $table AS $name" + . " ON $from_table.$from = $name.$to$extra"; + if (my $then_to = $join_info->{then_to}) { + push(@join_sql, $self->_translate_join($field, $then_to)); + } + return @join_sql; +} + ############### # Constructor # ############### @@ -477,7 +621,7 @@ sub new { sub init { my $self = shift; - my @fields = @{ $self->{'fields'} || [] }; + my $fields = $self->_display_columns($self->{'fields'}); my $params = $self->{'params'}; $params->convert_old_params(); $self->{'user'} ||= Bugzilla->user; @@ -510,74 +654,11 @@ sub init { # All items that are in the ORDER BY must be in the SELECT. foreach my $orderitem (@inputorder) { my $column_name = split_order_term($orderitem); - if (!grep($_ eq $column_name, @fields)) { - push(@fields, $column_name); + if (!grep($_ eq $column_name, @$fields)) { + push(@$fields, $column_name); } } - # First, deal with all the old hard-coded non-chart-based poop. - if (grep(/^assigned_to/, @fields)) { - push @supptables, "INNER JOIN profiles AS map_assigned_to " . - "ON bugs.assigned_to = map_assigned_to.userid"; - } - - if (grep(/^reporter/, @fields)) { - push @supptables, "INNER JOIN profiles AS map_reporter " . - "ON bugs.reporter = map_reporter.userid"; - } - - if (grep(/^qa_contact/, @fields)) { - push @supptables, "LEFT JOIN profiles AS map_qa_contact " . - "ON bugs.qa_contact = map_qa_contact.userid"; - } - - if (grep($_ eq 'product' || $_ eq 'classification', @fields)) - { - push @supptables, "INNER JOIN products AS map_products " . - "ON bugs.product_id = map_products.id"; - } - - if (grep($_ eq 'classification', @fields)) { - push @supptables, - "INNER JOIN classifications AS map_classifications " . - "ON map_products.classification_id = map_classifications.id"; - } - - if (grep($_ eq 'component', @fields)) { - push @supptables, "INNER JOIN components AS map_components " . - "ON bugs.component_id = map_components.id"; - } - - if (grep($_ eq 'actual_time' || $_ eq 'percentage_complete', @fields)) { - push(@supptables, "LEFT JOIN longdescs AS ldtime " . - "ON ldtime.bug_id = bugs.bug_id"); - } - foreach my $field (@{ $self->_multi_select_fields }) { - my $field_name = $field->name; - next if !grep($_ eq $field_name, @fields); - push(@supptables, "LEFT JOIN bug_$field_name AS map_bug_$field_name" - . " ON map_bug_$field_name.bug_id = bugs.bug_id"); - } - - if (grep($_ eq 'flagtypes.name', @fields)) { - push(@supptables, "LEFT JOIN flags ON flags.bug_id = bugs.bug_id AND attach_id IS NULL"); - push(@supptables, "LEFT JOIN flagtypes ON flagtypes.id = flags.type_id"); - } - - if (grep($_ eq 'keywords', @fields)) { - push(@supptables, "LEFT JOIN keywords ON keywords.bug_id = bugs.bug_id"); - push(@supptables, "LEFT JOIN keyworddefs ON keyworddefs.id = keywords.keywordid"); - } - - # Calculating percentage_complete requires remaining_time. Mostly, - # we just need remaining_time in the GROUP_BY, but it simplifies - # things to just add it in the SELECT. - if (grep($_ eq 'percentage_complete', @fields) - and !grep($_ eq 'remaining_time', @fields)) - { - push(@fields, 'remaining_time'); - } - # If the user has selected all of either status or resolution, change to # selecting none. This is functionally equivalent, but quite a lot faster. # Also, if the status is __open__ or __closed__, translate those @@ -1023,7 +1104,7 @@ sub init { where => \@wherepart, having => \@having, group_by => \@groupby, - fields => \@fields, + fields => $fields, ); # This should add a "term" selement to %search_args. $self->do_search_function(\%search_args); @@ -1078,7 +1159,7 @@ sub init { my %suppseen = ("bugs" => 1); my $suppstring = "bugs"; my @supplist = (" "); - foreach my $str (@supptables) { + foreach my $str ($self->_display_column_joins, @supptables) { if ($str =~ /^(LEFT|INNER|RIGHT)\s+JOIN/i) { $str =~ /^(.*?)\s+ON\s+(.*)$/i; @@ -1101,7 +1182,7 @@ sub init { @andlist = ("1 = 1") if !@andlist; my @sql_fields; - foreach my $field (@fields) { + foreach my $field (@$fields) { my $alias = $field; # Aliases cannot contain dots in them. We convert them to underscores. $alias =~ s/\./_/g; @@ -1137,13 +1218,13 @@ sub init { } # For some DBs, every field in the SELECT must be in the GROUP BY. - foreach my $field (@fields) { + foreach my $field (@$fields) { # These fields never go into the GROUP BY (bug_id goes in # explicitly, below). my @skip_group_by = (EMPTY_COLUMN, qw(bug_id actual_time percentage_complete flagtypes.name keywords)); - push(@skip_group_by, map { $_->name } @{ $self->_multi_select_fields }); + push(@skip_group_by, keys %{ $self->_multi_select_fields }); next if grep { $_ eq $field } @skip_group_by; my $col = COLUMNS->{$field}->{name}; @@ -1195,7 +1276,7 @@ sub do_search_function { my $override = OPERATOR_FIELD_OVERRIDE->{$actual_field}; if (!$override) { # Multi-select fields get special handling. - if (grep { $_->name eq $actual_field } @{ $self->_multi_select_fields }) { + if ($self->_multi_select_fields->{$actual_field}) { $override = OPERATOR_FIELD_OVERRIDE->{_multi_select}; } # And so do attachment fields, if they don't have a specific @@ -1843,7 +1924,7 @@ sub _percentage_complete { push(@$joins, "LEFT JOIN longdescs AS $table " . "ON $table.bug_id = bugs.bug_id"); - # We need remaining_time in @fields, otherwise we can't use + # We need remaining_time in _display_columns, otherwise we can't use # it in the expression for creating percentage_complete. if (!grep { $_ eq 'remaining_time' } @$fields) { push(@$fields, 'remaining_time'); @@ -2070,12 +2151,12 @@ sub _classification_nonchanged { my $joins = $args->{joins}; # Generate the restriction condition - push(@$joins, "INNER JOIN products AS map_products " . - "ON bugs.product_id = map_products.id"); + push(@$joins, "INNER JOIN products AS map_product " . + "ON bugs.product_id = map_product.id"); $args->{full_field} = "classifications.name"; $self->_do_operator_function($args); my $term = $args->{term}; - $args->{term} = build_subselect("map_products.classification_id", + $args->{term} = build_subselect("map_product.classification_id", "classifications.id", "classifications", $term); } |