diff options
16 files changed, 1884 insertions, 0 deletions
diff --git a/extensions/ProductDashboard/Config.pm b/extensions/ProductDashboard/Config.pm new file mode 100644 index 000000000..3a4654974 --- /dev/null +++ b/extensions/ProductDashboard/Config.pm @@ -0,0 +1,14 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::ProductDashboard; + +use strict; + +use constant NAME => 'ProductDashboard'; + +__PACKAGE__->NAME; diff --git a/extensions/ProductDashboard/Extension.pm b/extensions/ProductDashboard/Extension.pm new file mode 100644 index 000000000..08755da2d --- /dev/null +++ b/extensions/ProductDashboard/Extension.pm @@ -0,0 +1,189 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. + +package Bugzilla::Extension::ProductDashboard; + +use strict; + +use base qw(Bugzilla::Extension); + +use Bugzilla; +use Bugzilla::Constants; +use Bugzilla::Util; +use Bugzilla::Error; +use Bugzilla::Product; +use Bugzilla::Field; + +use Bugzilla::Extension::ProductDashboard::Queries; +use Bugzilla::Extension::ProductDashboard::Util; + +our $VERSION = BUGZILLA_VERSION; + +sub page_before_template { + my ($self, $args) = @_; + + my $page = $args->{page_id}; + my $vars = $args->{vars}; + + if ($page =~ m{^productdashboard\.}) { + _page_dashboard($vars); + } +} + +sub _page_dashboard { + my $vars = shift; + + my $cgi = Bugzilla->cgi; + my $input = Bugzilla->input_params; + my $user = Bugzilla->user; + + # Switch to shadow db since we are just reading information + Bugzilla->switch_to_shadow_db(); + + # All pages point to the same part of the documentation. + $vars->{'doc_section'} = 'bugreports.html'; + + # Forget any previously selected product + $cgi->send_cookie(-name => 'PRODUCT_DASHBOARD', + -value => 'X', + -expires => "Fri, 01-Jan-1970 00:00:00 GMT"); + + # If the user cannot enter bugs in any product, stop here. + my @enterable_products = @{$user->get_enterable_products}; + ThrowUserError('no_products') unless scalar(@enterable_products); + + my $classification = Bugzilla->params->{'useclassification'} ? + $input->{'classification'} : '__all'; + + # Create data structures representing each classification + my @classifications = (); + foreach my $c (@{$user->get_selectable_classifications}) { + # Create hash to hold attributes for each classification. + my %classification = ( + 'name' => $c->name, + 'products' => [ @{$user->get_selectable_products($c->id)} ] + ); + # Assign hash back to classification array. + push @classifications, \%classification; + } + $vars->{'classifications'} = \@classifications; + + my $product_name = trim($input->{'product'} || ''); + + if (!$product_name && $cgi->cookie('PRODUCT_DASHBOARD')) { + $product_name = $cgi->cookie('PRODUCT_DASHBOARD'); + } + + return if !$product_name; + + # Do not use Bugzilla::Product::check_product() here, else the user + # could know whether the product doesn't exist or is not accessible. + my $product = new Bugzilla::Product({'name' => $product_name}); + + # We need to check and make sure that the user has permission + # to enter a bug against this product. + if (!$user->can_enter_product($product ? $product->name : $product_name)) { + return; + } + + # Remember selected product + $cgi->send_cookie(-name => 'PRODUCT_DASHBOARD', + -value => $product->name, + -expires => "Fri, 01-Jan-2038 00:00:00 GMT"); + + my $current_tab_name = $input->{'tab'} || "summary"; + trick_taint($current_tab_name); + $vars->{'current_tab_name'} = $current_tab_name; + + my $bug_status = trim($input->{'bug_status'} || 'open'); + + $vars->{'bug_status'} = $bug_status; + $vars->{'product'} = $product; + $vars->{'bug_link_all'} = bug_link_all($product); + $vars->{'bug_link_open'} = bug_link_open($product); + $vars->{'bug_link_closed'} = bug_link_closed($product); + $vars->{'total_bugs'} = total_bugs($product); + $vars->{'total_open_bugs'} = total_open_bugs($product); + $vars->{'total_closed_bugs'} = total_closed_bugs($product); + $vars->{'severities'} = get_legal_field_values('bug_severity'); + + if ($current_tab_name eq 'summary') { + $vars->{'by_priority'} = by_priority($product, $bug_status); + $vars->{'by_severity'} = by_severity($product, $bug_status); + $vars->{'by_assignee'} = by_assignee($product, $bug_status); + $vars->{'by_status'} = by_status($product, $bug_status); + } + + if ($current_tab_name eq 'recents') { + my $recent_days = $input->{'recent_days'} || 7; + (detaint_natural($recent_days) && $recent_days > 0 && $recent_days < 101) + || ThrowUserError('product_dashboard_invalid_recent_days'); + + my $params = { + product => $product, + days => $recent_days, + date_from => $input->{'date_from'} || '', + date_to => $input->{'date_to'} || '', + }; + + $vars->{'recently_opened'} = recently_opened($params); + $vars->{'recently_closed'} = recently_closed($params); + $vars->{'recent_days'} = $recent_days; + $vars->{'date_from'} = $input->{'date_from'}; + $vars->{'date_to'} = $input->{'date_to'}; + } + + if ($current_tab_name eq 'components') { + if ($input->{'component'}) { + $vars->{'summary'} = by_value_summary($product, 'component', $input->{'component'}, $bug_status); + $vars->{'summary'}{'type'} = 'component'; + $vars->{'summary'}{'value'} = $input->{'component'}; + } + elsif ($input->{'version'}) { + $vars->{'summary'} = by_value_summary($product, 'version', $input->{'version'}, $bug_status); + $vars->{'summary'}{'type'} = 'version'; + $vars->{'summary'}{'value'} = $input->{'version'}; + } + elsif ($input->{'target_milestone'} && Bugzilla->params->{'usetargetmilestone'}) { + $vars->{'summary'} = by_value_summary($product, 'target_milestone', $input->{'target_milestone'}, $bug_status); + $vars->{'summary'}{'type'} = 'target_milestone'; + $vars->{'summary'}{'value'} = $input->{'target_milestone'}; + } + else { + $vars->{'by_component'} = by_component($product, $bug_status); + $vars->{'by_version'} = by_version($product, $bug_status); + if (Bugzilla->params->{'usetargetmilestone'}) { + $vars->{'by_milestone'} = by_milestone($product, $bug_status); + } + } + } + + if ($current_tab_name eq 'duplicates') { + $vars->{'by_duplicate'} = by_duplicate($product, $bug_status); + } + + if ($current_tab_name eq 'popularity') { + $vars->{'by_popularity'} = by_popularity($product, $bug_status); + } + + if ($current_tab_name eq 'roadmap') { + foreach my $milestone (@{$product->milestones}){ + my %milestone_stats; + $milestone_stats{'name'} = $milestone->name; + $milestone_stats{'total_bugs'} = total_bug_milestone($product, $milestone); + $milestone_stats{'open_bugs'} = bug_milestone_by_status($product, $milestone, 'open'); + $milestone_stats{'closed_bugs'} = bug_milestone_by_status($product, $milestone, 'closed'); + $milestone_stats{'link_total'} = bug_milestone_link_total($product, $milestone); + $milestone_stats{'link_open'} = bug_milestone_link_open($product, $milestone); + $milestone_stats{'link_closed'} = bug_milestone_link_closed($product, $milestone); + push (@{$vars->{by_roadmap}}, \%milestone_stats); + } + } +} + +__PACKAGE__->NAME; + diff --git a/extensions/ProductDashboard/lib/Queries.pm b/extensions/ProductDashboard/lib/Queries.pm new file mode 100644 index 000000000..43d799111 --- /dev/null +++ b/extensions/ProductDashboard/lib/Queries.pm @@ -0,0 +1,463 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. +package Bugzilla::Extension::ProductDashboard::Queries; + +use strict; + +use base qw(Exporter); +@Bugzilla::Extension::ProductDashboard::Queries::EXPORT = qw( + total_bugs + total_open_bugs + total_closed_bugs + by_version + by_value_summary + by_milestone + by_priority + by_severity + by_component + by_assignee + by_status + by_duplicate + by_popularity + recently_opened + recently_closed + total_bug_milestone + bug_milestone_by_status +); + +use Bugzilla::CGI; +use Bugzilla::User; +use Bugzilla::Search; +use Bugzilla::Util; +use Bugzilla::Component; +use Bugzilla::Version; +use Bugzilla::Milestone; + +use Bugzilla::Extension::ProductDashboard::Util qw(open_states closed_states); + +sub total_bugs { + my $product = shift; + my $dbh = Bugzilla->dbh; + + return $dbh->selectrow_array("SELECT COUNT(bug_id) + FROM bugs + WHERE product_id = ?", undef, $product->id); +} + +sub total_open_bugs { + my $product = shift; + my $bug_status = shift; + my $dbh = Bugzilla->dbh; + + return $dbh->selectrow_array("SELECT COUNT(bug_id) + FROM bugs + WHERE bug_status IN (" . open_states() . ") + AND product_id = ?", undef, $product->id); +} + +sub total_closed_bugs { + my $product = shift; + my $dbh = Bugzilla->dbh; + + return $dbh->selectrow_array("SELECT COUNT(bug_id) + FROM bugs + WHERE bug_status IN ('CLOSED') + AND product_id = ?", undef, $product->id); +} + +sub bug_link_all { + my $product = shift; + + return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name); +} + +sub bug_link_open { + my $product = shift; + + return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) . "&bug_status=__open__"; +} + +sub bug_link_closed { + my $product = shift; + + return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) . "&bug_status=__closed__"; +} + +sub by_version { + my ($product, $bug_status) = @_; + my $dbh = Bugzilla->dbh; + my $extra; + + $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed'; + + return $dbh->selectall_arrayref("SELECT version, COUNT(bug_id) + FROM bugs + WHERE product_id = ? + $extra + GROUP BY version + ORDER BY COUNT(bug_id) DESC", undef, $product->id); +} + +sub by_milestone { + my ($product, $bug_status) = @_; + my $dbh = Bugzilla->dbh; + my $extra; + + $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed'; + + return $dbh->selectall_arrayref("SELECT target_milestone, COUNT(bug_id) + FROM bugs + WHERE product_id = ? + $extra + GROUP BY target_milestone + ORDER BY COUNT(bug_id) DESC", undef, $product->id); +} + +sub by_priority { + my ($product, $bug_status) = @_; + my $dbh = Bugzilla->dbh; + my $extra; + + $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed'; + + return $dbh->selectall_arrayref("SELECT priority, COUNT(bug_id) + FROM bugs + WHERE product_id = ? + $extra + GROUP BY priority + ORDER BY COUNT(bug_id) DESC", undef, $product->id); +} + +sub by_severity { + my ($product, $bug_status) = @_; + my $dbh = Bugzilla->dbh; + my $extra; + + $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed'; + + return $dbh->selectall_arrayref("SELECT bug_severity, COUNT(bug_id) + FROM bugs + WHERE product_id = ? + $extra + GROUP BY bug_severity + ORDER BY COUNT(bug_id) DESC", undef, $product->id); +} + +sub by_component { + my ($product, $bug_status) = @_; + my $dbh = Bugzilla->dbh; + my $extra; + + $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed'; + + return $dbh->selectall_arrayref("SELECT components.name, COUNT(bugs.bug_id) + FROM bugs INNER JOIN components ON bugs.component_id = components.id + WHERE bugs.product_id = ? + $extra + GROUP BY components.name + ORDER BY COUNT(bugs.bug_id) DESC", undef, $product->id); +} + +sub by_value_summary { + my ($product, $type, $value, $bug_status) = @_; + my $dbh = Bugzilla->dbh; + my $extra; + + my $query = "SELECT bugs.bug_id AS id, + bugs.bug_status AS status, + bugs.version AS version, + components.name AS component, + bugs.bug_severity AS severity, + bugs.short_desc AS summary + FROM bugs, components + WHERE bugs.product_id = ? + AND bugs.component_id = components.id "; + + if ($type eq 'component') { + Bugzilla::Component->check({ product => $product, name => $value }); + $query .= "AND components.name = ? " if $type eq 'component'; + } + elsif ($type eq 'version') { + Bugzilla::Version->check({ product => $product, name => $value }); + $query .= "AND bugs.version = ? " if $type eq 'version'; + } + elsif ($type eq 'target_milestone') { + Bugzilla::Milestone->check({ product => $product, name => $value }); + $query .= "AND bugs.target_milestone = ? " if $type eq 'target_milestone'; + } + + $query .= "AND bugs.bug_status IN (" . open_states() . ") " if $bug_status eq 'open'; + $query .= "AND bugs.bug_status IN (" . closed_states() . ") " if $bug_status eq 'closed'; + + trick_taint($value); + + my $past_due_bugs = $dbh->selectall_arrayref($query . + "AND (bugs.deadline IS NOT NULL AND bugs.deadline != '') + AND bugs.deadline < now() ORDER BY bugs.deadline LIMIT 10", + {'Slice' => {}}, $product->id, $value); + + my $updated_recently_bugs = $dbh->selectall_arrayref($query . + "AND bugs.delta_ts != bugs.creation_ts " . + "ORDER BY bugs.delta_ts DESC LIMIT 10", + {'Slice' => {}}, $product->id, $value); + + my $timestamp = $dbh->selectrow_array("SELECT " . $dbh->sql_date_format("LOCALTIMESTAMP(0)", "%Y-%m-%d")); + + return { + timestamp => $timestamp, + past_due => _filter_bugs($past_due_bugs), + updated_recently => _filter_bugs($updated_recently_bugs), + }; +} + +sub by_assignee { + my ($product, $bug_status) = @_; + my $dbh = Bugzilla->dbh; + my $extra; + + $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed'; + + my @result = map { [ Bugzilla::User->new($_->[0]), $_->[1] ] } + @{$dbh->selectall_arrayref("SELECT bugs.assigned_to AS userid, COUNT(bugs.bug_id) + FROM bugs, profiles + WHERE bugs.product_id = ? + AND bugs.assigned_to = profiles.userid + $extra + GROUP BY profiles.login_name + ORDER BY COUNT(bugs.bug_id) DESC", undef, $product->id)}; + + return \@result; +} + +sub by_status { + my ($product, $bug_status) = @_; + my $dbh = Bugzilla->dbh; + my $extra; + + $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed'; + + return $dbh->selectall_arrayref("SELECT bugs.bug_status, COUNT(bugs.bug_id) + FROM bugs + WHERE bugs.product_id = ? + $extra + GROUP BY bugs.bug_status + ORDER BY COUNT(bugs.bug_id) DESC", undef, $product->id); +} + +sub total_bug_milestone { + my ($product, $milestone) = @_; + my $dbh = Bugzilla->dbh; + + return $dbh->selectrow_array("SELECT COUNT(bug_id) + FROM bugs + WHERE target_milestone = ? + AND product_id = ?", + undef, + $milestone->name, + $product->id); + +} + +sub bug_milestone_by_status { + my ($product, $milestone, $bug_status) = @_; + my $dbh = Bugzilla->dbh; + my $extra; + + $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed'; + + return $dbh->selectrow_array("SELECT COUNT(bug_id) + FROM bugs + WHERE target_milestone = ? + AND product_id = ? $extra", + undef, + $milestone->name, + $product->id); + +} + +sub by_duplicate { + my ($product, $bug_status, $limit) = @_; + my $dbh = Bugzilla->dbh; + $limit = detaint_natural($limit) ? $dbh->sql_limit($limit) : ""; + + my $extra; + $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed'; + + my $unfiltered_bugs = $dbh->selectall_arrayref("SELECT bugs.bug_id AS id, + bugs.bug_status AS status, + bugs.version AS version, + components.name AS component, + bugs.bug_severity AS severity, + bugs.short_desc AS summary, + COUNT(duplicates.dupe) AS dupe_count + FROM bugs, duplicates, components + WHERE bugs.product_id = ? + AND bugs.component_id = components.id + AND bugs.bug_id = duplicates.dupe_of + $extra + GROUP BY bugs.bug_id, bugs.bug_status, components.name, + bugs.bug_severity, bugs.short_desc + HAVING COUNT(duplicates.dupe) > 1 + ORDER BY COUNT(duplicates.dupe) DESC $limit", + {'Slice' => {}}, $product->id); + + return _filter_bugs($unfiltered_bugs); +} + +sub by_popularity { + my ($product, $bug_status, $limit) = @_; + my $dbh = Bugzilla->dbh; + $limit = detaint_natural($limit) ? $dbh->sql_limit($limit) : ""; + + my $extra; + $extra = "AND bugs.bug_status IN (" . open_states() . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . closed_states() . ")" if $bug_status eq 'closed'; + + my $unfiltered_bugs = $dbh->selectall_arrayref("SELECT bugs.bug_id AS id, + bugs.bug_status AS status, + bugs.version AS version, + components.name AS component, + bugs.bug_severity AS severity, + bugs.short_desc AS summary, + bugs.votes AS votes + FROM bugs, components + WHERE bugs.product_id = ? + AND bugs.component_id = components.id + AND bugs.votes > 1 + $extra + ORDER BY bugs.votes DESC $limit", + {'Slice' => {}}, $product->id); + + return _filter_bugs($unfiltered_bugs); +} + +sub recently_opened { + my ($params) = @_; + my $dbh = Bugzilla->dbh; + + my $product = $params->{'product'}; + my $days = $params->{'days'}; + my $limit = $params->{'limit'}; + my $date_from = $params->{'date_from'}; + my $date_to = $params->{'date_to'}; + + $days ||= 7; + $limit = detaint_natural($limit) ? $dbh->sql_limit($limit) : ""; + + my @values = ($product->id); + + my $date_part; + if ($date_from && $date_to) { + validate_date($date_from) + || ThrowUserError('illegal_date', { date => $date_from, + format => 'YYYY-MM-DD' }); + validate_date($date_to) + || ThrowUserError('illegal_date', { date => $date_to, + format => 'YYYY-MM-DD' }); + $date_part = "AND bugs.creation_ts >= ? AND bugs.creation_ts <= ?"; + push(@values, $date_from, $date_to); + } + else { + $date_part = "AND bugs.creation_ts >= NOW() - " . $dbh->sql_to_days('?'); + push(@values, $days); + } + + my $unfiltered_bugs = $dbh->selectall_arrayref("SELECT bugs.bug_id AS id, + bugs.bug_status AS status, + bugs.version AS version, + components.name AS component, + bugs.bug_severity AS severity, + bugs.short_desc AS summary + FROM bugs, components + WHERE bugs.product_id = ? + AND bugs.component_id = components.id + AND bugs.bug_status IN (" . open_states() . ") + $date_part + ORDER BY bugs.bug_id DESC $limit", + {'Slice' => {}}, @values); + + return _filter_bugs($unfiltered_bugs); +} + +sub recently_closed { + my ($params) = @_; + my $dbh = Bugzilla->dbh; + + my $product = $params->{'product'}; + my $days = $params->{'days'}; + my $limit = $params->{'limit'}; + my $date_from = $params->{'date_from'}; + my $date_to = $params->{'date_to'}; + + $days ||= 7; + $limit = detaint_natural($limit) ? $dbh->sql_limit($limit) : ""; + + my @values = ($product->id); + + my $date_part; + if ($date_from && $date_to) { + validate_date($date_from) + || ThrowUserError('illegal_date', { date => $date_from, + format => 'YYYY-MM-DD' }); + validate_date($date_to) + || ThrowUserError('illegal_date', { date => $date_to, + format => 'YYYY-MM-DD' }); + $date_part = "AND bugs.creation_ts >= ? AND bugs.creation_ts <= ?"; + push(@values, $date_from, $date_to); + } + else { + $date_part = "AND bugs.creation_ts >= NOW() - " . $dbh->sql_to_days('?'); + push(@values, $days); + } + + my $unfiltered_bugs = $dbh->selectall_arrayref("SELECT DISTINCT bugs.bug_id AS id, + bugs.bug_status AS status, + bugs.version AS version, + components.name AS component, + bugs.bug_severity AS severity, + bugs.short_desc AS summary + FROM bugs, components, bugs_activity + WHERE bugs.product_id = ? + AND bugs.component_id = components.id + AND bugs.bug_status IN (" . closed_states() . ") + AND bugs.bug_id = bugs_activity.bug_id + AND bugs_activity.added IN (" . closed_states() . ") + $date_part + ORDER BY bugs.bug_id DESC $limit", + {'Slice' => {}}, @values); + + return _filter_bugs($unfiltered_bugs); +} + +sub _filter_bugs { + my ($unfiltered_bugs) = @_; + my $dbh = Bugzilla->dbh; + + return [] if !$unfiltered_bugs; + + my @unfiltered_bug_ids = map { $_->{'id'} } @$unfiltered_bugs; + my %filtered_bug_ids = map { $_ => 1 } @{ Bugzilla->user->visible_bugs(\@unfiltered_bug_ids) }; + + my @filtered_bugs; + foreach my $bug (@$unfiltered_bugs) { + next if !$filtered_bug_ids{$bug->{'id'}}; + push(@filtered_bugs, $bug); + } + + return \@filtered_bugs; +} + +1; diff --git a/extensions/ProductDashboard/lib/Util.pm b/extensions/ProductDashboard/lib/Util.pm new file mode 100644 index 000000000..ad46ef8f6 --- /dev/null +++ b/extensions/ProductDashboard/lib/Util.pm @@ -0,0 +1,96 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# This Source Code Form is "Incompatible With Secondary Licenses", as +# defined by the Mozilla Public License, v. 2.0. +package Bugzilla::Extension::ProductDashboard::Util; + +use strict; + +use base qw(Exporter); +@Bugzilla::Extension::ProductDashboard::Util::EXPORT = qw( + bug_link_all + bug_link_open + bug_link_closed + open_states + closed_states + filter_bugs + bug_milestone_link_total + bug_milestone_link_open + bug_milestone_link_closed +); + +use Bugzilla::Status; +use Bugzilla::Util; + +sub open_states { + my $dbh = Bugzilla->dbh; + return join(",", map { $dbh->quote($_) } BUG_STATE_OPEN); +} + +sub closed_states { + my $dbh = Bugzilla->dbh; + return join(",", map { $dbh->quote($_->name) } closed_bug_statuses()); +} + +sub bug_link_all { + my $product = shift; + + return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name); +} + +sub bug_link_open { + my $product = shift; + + return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) . + "&bug_status=__open__"; +} + +sub bug_link_closed { + my $product = shift; + + return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) . + "&bug_status=__closed__"; +} + +sub bug_milestone_link_total { + my ($product, $milestone) = @_; + + return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) . + "&target_milestone=" . url_quote($milestone->name); +} + +sub bug_milestone_link_open { + my ($product, $milestone) = @_; + + return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) . + "&target_milestone=" . url_quote($milestone->name) . "&bug_status=__open__"; +} + +sub bug_milestone_link_closed { + my ($product, $milestone) = @_; + + return correct_urlbase() . 'buglist.cgi?product=' . url_quote($product->name) . + "&target_milestone=" . url_quote($milestone->name) . "&bug_status=__closed__"; +} + +sub filter_bugs { + my ($unfiltered_bugs) = @_; + my $dbh = Bugzilla->dbh; + + # Filter out which bugs that cannot be viewed + my $params = Bugzilla::CGI->new({ bug_id => [ map { $_->{'id'} } @$unfiltered_bugs ] }); + my $search = Bugzilla::Search->new(fields => ['bug_id' ], params => $params ); + my %filtered_bug_ids = map { $_ => 1 } @{$dbh->selectcol_arrayref($search->getSQL())}; + + my @filtered_bugs; + foreach my $bug (@$unfiltered_bugs) { + next if !$filtered_bug_ids{$bug->{'id'}}; + push(@filtered_bugs, $bug); + } + + return \@filtered_bugs; +} + +1; diff --git a/extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl b/extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl new file mode 100644 index 000000000..e9be8a13d --- /dev/null +++ b/extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl @@ -0,0 +1,9 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + + <li><span class="separator"> | </span><a href="page.cgi?id=productdashboard.html">Product Dashboard</a></li> diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl new file mode 100644 index 000000000..a319060d1 --- /dev/null +++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl @@ -0,0 +1,209 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% filtered_product = product.name FILTER html %] +[% PROCESS global/header.html.tmpl + title = "Product Dashboard: $filtered_product" + style_urls = [ "skins/standard/buglist.css", + "js/yui/assets/skins/sam/paginator.css", + "extensions/ProductDashboard/web/styles/productdashboard.css" ] + yui = [ "datatable", "paginator", "calendar" ] + javascript_urls = [ "js/util.js", "js/field.js", + "extensions/ProductDashboard/web/js/productdashboard.js" ] +%] + +<script type="text/javascript"> +<!-- + [%# Set up severities list for proper sorting %] + var severities = new Array(); + [% sort_count = 0 %] + [% FOREACH s = severities %] + severities['[% s FILTER js %]'] = [% sort_count FILTER js %]; + [% sort_count = sort_count + 1 %] + [% END %] +--> +</script> + +[% url_filtered_product = product.name FILTER uri %] +[% url_filtered_status = bug_status FILTER uri %] + +[% tabs = [ + { + name => "summary", + label => "Summary", + link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=summary" + }, + { + name => "recents", + label => "Recents", + link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=recents" + }, + { + name => "components", + label => "Components/Versions", + link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=components" + }, + { + name => "duplicates", + label => "Duplicates", + link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=duplicates" + }, + { + name => "popularity", + label => "Popularity", + link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=popularity" + }, + { + name => "roadmap", + label => "Road Map", + link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=roadmap" + }, + ] +%] + +[% FOREACH tab IN tabs %] + [% IF tab.name == current_tab_name %] + [% current_tab = tab %] + [% LAST %] + [% END %] +[% END %] + +[% full_bug_count = 0 %] +[% IF bug_status == 'open' %] + [% full_bug_count = total_open_bugs %] +[% ELSIF bug_status == 'closed' %] + [% full_bug_count = total_closed_bugs %] +[% ELSE %] + [% full_bug_count = total_bugs %] +[% END %] + +[% bug_link = bug_link_all %] +[% IF bug_status == 'open' %] + [% bug_link = bug_link_open %] +[% ELSIF bug_status == 'closed' %] + [% bug_link = bug_link_closed %] +[% END %] + +<div class="yui-skin-sam"> + <a name="top"></a> + + <form action="page.cgi" method="get"> + <input type="hidden" name="id" value="productdashboard.html"> + <input type="hidden" name="tab" value="[% current_tab.name FILTER html %]"> + + [% IF summary.keys %] + <input type="hidden" name="[% summary.type FILTER html %]" value="[% summary.value FILTER html %]"> + [% END %] + + [% IF product %] + <span id="product_dashboard_links"> + <ul> + <li><a href="[% urlbase FILTER none %]enter_bug.cgi?product=[% product.name FILTER uri %]"> + Create a new [% terms.bug %] in this product</a></li> + <li><a href="[% urlbase FILTER none %]describecomponents.cgi?product=[% product.name FILTER uri %]"> + Show full component descriptions for this product</a></li> + </ul> + </span> + [% END %] + + <h3>Product: [% product.name FILTER html %]</h3> + <select name="product"> + [% FOREACH c = classifications %] + <optgroup label="[% c.name FILTER html %]"> + [% FOREACH p = c.products %] + <option value="[% p.name FILTER html %]" + [% IF p.name == product.name %]selected="selected"[% END %]> + [% p.name FILTER html %]</option> + [% END %]</optgroup> + [% END %] + </select> + <select name="bug_status" id="bug_status"> + [% statuses = [ { name = 'open', label = "Open $terms.Bugs" }, + { name = 'closed', label = "Closed $terms.Bugs" }, + { name = 'all', label = "All $terms.Bugs" } ] %] + [% FOREACH status = statuses %] + <option value="[% status.name FILTER html %]" + [% " selected" IF bug_status == "${status.name}" %]> + [% status.label FILTER html %] + </option> + [% END %] + </select> + + <input type="submit" value="[% IF product %]Change[% ELSE %]Submit[% END %]"> + + [% IF product %] + + <p><i>[% product.description FILTER none %]</i></p> + + [% WRAPPER global/tabs.html.tmpl + tabs = tabs + current_tab = current_tab + %] + + <h3>[% current_tab.label FILTER html %]</h3> + + [% IF current_tab.name == 'summary' %] + [% PROCESS pages/productdashboard/summary.html.tmpl %] + [% END %] + + [% IF current_tab.name == 'recents' %] + [% PROCESS pages/productdashboard/recents.html.tmpl %] + [% END %] + + [% IF current_tab.name == 'components' %] + [% PROCESS pages/productdashboard/components.html.tmpl %] + [% END %] + + [% IF current_tab.name == 'duplicates' %] + [% PROCESS pages/productdashboard/duplicates.html.tmpl %] + [% END %] + + [% IF current_tab.name == 'popularity' %] + [% PROCESS pages/productdashboard/popularity.html.tmpl %] + [% END %] + + [% IF current_tab.name == 'roadmap' && Param('usetargetmilestone') %] + [% PROCESS pages/productdashboard/roadmap.html.tmpl %] + [% END %] + + [% END %][%# END WRAPPER %] + [% END %] + + </form> +</div> + +[% PROCESS global/footer.html.tmpl %] + +[% BLOCK bar_graph %] + [% IF full_bug_count > 0 %][%# No divide by zero %] + [% percentage_bugs = (count / full_bug_count) * 100 FILTER format('%02.2f') %] + [% ELSE %] + [% percentage_bugs = 0 %] + [% END %] + <div class="bar_graph"> + <table cellpadding="0" cellspacing="0" width="300px"> + <tr> + <td width="[% percentage_bugs FILTER html %]%"> + <table cellpadding="0" cellspacing="0" width="100%"> + <tr> + <td bgcolor="#3c78b5"> + <a title="[% percentage_bugs FILTER html %]%"> + <img src="extensions/ProductDashboard/web/images/spacer.gif" height=10 width="100%" title="[% percentage_bugs FILTER html %]%"> + </a> + </td> + </tr> + </table> + </td> + <td width="[% 100 - percentage_bugs FILTER html %]%"> [% percentage_bugs FILTER html %]%</td> + </tr> + </table> + </div> +[% END %] + diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl new file mode 100644 index 000000000..7f5a05568 --- /dev/null +++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl @@ -0,0 +1,259 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +[% IF summary.keys %] + + <h3>Summary for [% summary.type FILTER html %]: [% summary.value FILTER html %]</h3> + + <style> + .yui-skin-sam .yui-dt table {width:100%;} + </style> + + <script type="text/javascript"> + <!-- + var column_defs = [ + { key:"id", label:"ID", sortable:true, sortOptions:{ sortFunction:sortBugIdLinks } }, + { key:"bug_status", label:"Status", sortable:true }, + { key:"version", label:"Version", sortable:true }, + { key:"component", label:"Component", sortable:true }, + { key:"bug_severity", label:"Severity", sortable:true, sortOptions:{ sortFunction:sortBugSeverity } }, + { key:"Summary", label:"Summary", sortable:false }, + ]; + var fields = [ + { key:"id" }, + { key:"bug_status" }, + { key:"version" }, + { key:"component" }, + { key:"bug_severity" }, + { key:"Summary" } + ]; + [% IF user.is_timetracker %] + addStatListener("past_due", "past_due_table", column_defs, fields, { + [% IF summary.past_due.size > 25 %] paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25 }) [% END %] + }); + [% END %] + addStatListener("updated_recently", "updated_recently_table", column_defs, fields, { + [% IF summary.updated_recently.size > 25 %] paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25 }) [% END %] + }); + --> + </script> + + [% IF user.is_timetracker %] + <p> + <a href="#past_due">Past Due</a> | + <a href="#updated_recently">Updated Recently</a> + </p> + [% END %] + + <div class="yui-skin-sam"> + + [% IF user.is_timetracker %] + <a name="past_due"></a> + <b>[% summary.past_due.size FILTER html %] Past Due [% terms.Bugs %]</b> (deadline is before today's date) + (<a href="[% bug_link FILTER html %]&[% summary.type FILTER uri %]=[% summary.value FILTER uri %]&field0-0-0=deadline&type0-0-0=lessthan&value0-0-0=[% summary.timestamp FILTER uri %]&order=deadline">full list</a>) + <div id="past_due"> + <table id="past_due_table" cellspacing="3" cellpadding="0" border="0" width="100%"> + <thead> + <tr bgcolor="#CCCCCC"> + [% FOREACH column = [ "ID", "Status", "Version", "Component", "Severity" "Summary" ] %] + <th>[% column FILTER html %]</th> + [% END %] + </tr> + </thead> + <tbody> + [% FOREACH bug = summary.past_due %] + [% count = loop.count() %] + <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]"> + <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]"> + [% bug.id FILTER html %]</a></td> + <td align="center">[% bug.status FILTER html %]</td> + <td align="center">[% bug.version FILTER html %]</td> + <td align="center">[% bug.component FILTER html %]</td> + <td align="center">[% bug.severity FILTER html %]</td> + <td>[% bug.summary FILTER html %]</td> + </tr> + [% END %] + </tbody> + </table> + </div> + <br> + [% END %] + + <a name="updated_recently"></a> + <b>[% summary.updated_recently.size FILTER html %] Most Recently Updated [% terms.Bugs %]</b> + [% IF user.is_timetracker %](<a href="#top">back to top</a>)[% END %] + (<a href="[% bug_link FILTER html %]&[% summary.type FILTER uri %]=[% summary.value FILTER uri %]&order=changeddate DESC">full list</a>) + <div id="updated_recently"> + <table id="updated_recently_table" cellspacing="3" cellpadding="0" border="0" width="100%"> + <thead> + <tr bgcolor="#CCCCCC"> + [% FOREACH column = [ "ID", "Status", "Version", "Component", "Severity" "Summary" ] %] + <th>[% column FILTER html %]</th> + [% END %] + </tr> + </thead> + <tbody> + [% FOREACH bug = summary.updated_recently %] + [% count = loop.count() %] + <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]"> + <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]"> + [% bug.id FILTER html %]</a></td> + <td align="center">[% bug.status FILTER html %]</td> + <td align="center">[% bug.version FILTER html %]</td> + <td align="center">[% bug.component FILTER html %]</td> + <td align="center">[% bug.severity FILTER html %]</td> + <td>[% bug.summary FILTER html %]</td> + </tr> + [% END %] + </tbody> + </table> + </div> + </div> + +[% ELSE %] + + [% summary_url = "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=components" %] + + <script type="text/javascript"> + <!-- + var column_defs = [ + { key:"name", label:"Name", sortable:true }, + { key:"count", label:"Count", sortable:true }, + { key:"percentage", label:"Percentage", sortable:false }, + { key:"bug_list", label:"[% terms.Bug %] List", sortable:false } + ]; + var fields = [ + { key:"name" }, + { key:"count", parser:"number" }, + { key:"percentage" }, + { key:"bug_list" } + ]; + addStatListener("component_counts", "component_counts_table", column_defs, fields, {}); + addStatListener("version_counts", "version_counts_table", column_defs, fields, {}); + addStatListener("milestone_counts", "milestone_counts_table", column_defs, fields, {}); + --> + </script> + + <p> + <a href="#component">Component</a> | + <a href="#version">Version</a> | + <a href="#milestone">Milestone</a> + </p> + + <p>Click on value to show additional information</p> + + <div class="yui-skin-sam"> + <a name="component"></a> + <b>Component</b> + <div id="component_counts"> + <table id="component_counts_table" border="0" cellspacing="3" cellpadding="0"> + <thead> + <tr> + <th>Name</th> + <th>Count</th> + <th>Percentage</th> + <th>[% terms.Bug %] List</th> + </tr> + </thead> + <tbody> + [% FOREACH col = by_component %] + <tr> + <td> + <a href="[% summary_url FILTER none %]&component=[% col.0 FILTER uri %]"> + [% col.0 FILTER html %]</a> + </td> + <td align="right"> + [% col.1 FILTER html %] + </td> + <td width="70%"> + [% INCLUDE bar_graph count = col.1 %] + </td> + <td> + <a href="[% bug_link FILTER html %]&component=[% col.0 FILTER uri %]">View</a> + </td> + </tr> + [% END %] + </tbody> + </table> + </div> + <br> + <a name="version"></a> + <b>Version</b> + (<a href="#top">back to top</a>) + <div id="version_counts"> + <table id="version_counts_table" border="0" cellspacing="3" cellpadding="0"> + <thead> + <tr> + <th>Name</th> + <th>Count</th> + <th>Percentage</th> + <th>[% terms.Bug %] List</th> + </tr> + </thead> + <tbody> + [% FOREACH col = by_version %] + <tr> + <td> + <a href="[% summary_url FILTER none %]&version=[% col.0 FILTER uri %]"> + [% col.0 FILTER html %]</a> + </td> + <td align="right"> + [% col.1 FILTER html %] + </td> + <td width="70%"> + [% INCLUDE bar_graph count = col.1 %] + </td> + <td> + <a href="[% bug_link FILTER html %]&version=[% col.0 FILTER uri %]">View</a> + </td> + </tr> + [% END %] + </tbody> + </table> + </div> + + [% IF Param('usetargetmilestone') %] + <br> + <a name="milestone"></a> + <b>Milestone</b> + (<a href="#top">back to top</a>) + <div id="milestone_counts"> + <table id="milestone_counts_table" border="0" cellspacing="3" cellpadding="0"> + <thead> + <tr> + <th>Name</th> + <th>Count</th> + <th>Percentage</th> + <th>[% terms.Bug %] List</th> + </tr> + </thead> + <tbody> + [% FOREACH col = by_milestone %] + <tr> + <td> + <a href="[% summary_url FILTER none %]&target_milestone=[% col.0 FILTER uri %]"> + [% col.0 FILTER html %]</a> + </td> + <td align="right"> + [% col.1 FILTER html %] + </td> + <td width="70%"> + [% INCLUDE bar_graph count = col.1 %] + </td> + <td> + <a href="[% bug_link FILTER html %]&target_milestone=[% col.0 FILTER uri %]">View</a> + </td> + </tr> + [% END %] + </tbody> + </table> + </div> + [% END %] + </div> + +[% END %] diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl new file mode 100644 index 000000000..36a820300 --- /dev/null +++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl @@ -0,0 +1,73 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +<style> + .yui-skin-sam .yui-dt table {width:100%;} +</style> + +<script type="text/javascript"> +<!-- +var column_defs = [ + { key:"id", label:"ID", sortable:true, sortOptions:{ sortFunction:sortBugIdLinks } }, + { key:"count", label:"Count", sortable:true }, + { key:"bug_status", label:"Status", sortable:true }, + { key:"version", label:"Version", sortable:true }, + { key:"component", label:"Component", sortable:true }, + { key:"bug_severity", label:"Severity", sortable:true, sortOptions:{ sortFunction:sortBugSeverity } }, + { key:"Summary", label:"Summary", sortable:false }, +]; +var fields = [ + { key:"id" }, + { key:"count", parser:"number" }, + { key:"bug_status" }, + { key:"version" }, + { key:"component" }, + { key:"bug_severity" }, + { key:"Summary" } +]; +addStatListener("duplicate_counts", "duplicate_counts_table", column_defs, fields, { + [% IF by_duplicate.size > 25 %] paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25 }) [% END %] +}); +--> +</script> + +[% IF by_duplicate.size %] + <b>[% by_duplicate.size FILTER html %] [% terms.Bugs %] Found</b> + <div class="yui-skin-sam"> + <div id="duplicate_counts"> + <table id="duplicate_counts_table" cellspacing="3" cellpadding="0" border="0" width="100%"> + <thead> + <tr bgcolor="#CCCCCC"> + [% FOREACH column = [ "ID", "Dupe Count", "Status", "Version" + "Component", "Severity" "Summary" ] %] + + <th>[% column FILTER html %]</th> + [% END %] + </tr> + </thead> + <tbody> + [% FOREACH bug = by_duplicate %] + [% count = loop.count() %] + <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]"> + <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]"> + [% bug.id FILTER html %]</a></td> + <td align="center">[% bug.dupe_count FILTER html %]</td> + <td align="center">[% bug.status FILTER html %]</td> + <td align="center">[% bug.version FILTER html %]</td> + <td align="center">[% bug.component FILTER html %]</td> + <td align="center">[% bug.severity FILTER html %]</td> + <td>[% bug.summary FILTER html %]</td> + </tr> + [% END %] + </tbody> + </table> + </div> + </div> +[% ELSE %] + <h3>No duplicate [% terms.bugs %] found.</h3> +[% END %] diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl new file mode 100644 index 000000000..9c7c42563 --- /dev/null +++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl @@ -0,0 +1,73 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +<style> + .yui-skin-sam .yui-dt table {width:100%;} +</style> + +<script type="text/javascript"> +<!-- +var column_defs = [ + { key:"id", label:"ID", sortable:true, sortOptions:{ sortFunction:sortBugIdLinks } }, + { key:"count", label:"Count", sortable:true }, + { key:"bug_status", label:"Status", sortable:true }, + { key:"version", label:"Version", sortable:true }, + { key:"component", label:"Component", sortable:true }, + { key:"bug_severity", label:"Severity", sortable:true, sortOptions:{ sortFunction:sortBugSeverity } }, + { key:"Summary", label:"Summary", sortable:false }, +]; +var fields = [ + { key:"id" }, + { key:"count", parser:"number" }, + { key:"bug_status" }, + { key:"version" }, + { key:"component" }, + { key:"bug_severity" }, + { key:"Summary" } +]; +addStatListener("popularity_counts", "popularity_counts_table", column_defs, fields, { + [% IF by_popularity.size > 25 %] paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25 }) [% END %] +}); +--> +</script> + +[% IF by_popularity.size %] + <b>[% by_popularity.size FILTER html %] [% terms.Bugs %] Found</b> + <div class="yui-skin-sam"> + <div id="popularity_counts"> + <table id="popularity_counts_table" cellspacing="3" cellpadding="0" border="0" width="100%"> + <thead> + <tr bgcolor="#CCCCCC"> + [% FOREACH column = [ "ID", "Count", "Status", "Version" + "Component", "Severity" "Summary" ] %] + + <th>[% column FILTER html %]</th> + [% END %] + </tr> + </thead> + <tbody> + [% FOREACH bug = by_popularity %] + [% count = loop.count() %] + <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]"> + <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]"> + [% bug.id FILTER html %]</a></td> + <td align="center">[% bug.votes FILTER html %]</td> + <td align="center">[% bug.status FILTER html %]</td> + <td align="center">[% bug.version FILTER html %]</td> + <td align="center">[% bug.component FILTER html %]</td> + <td align="center">[% bug.severity FILTER html %]</td> + <td>[% bug.summary FILTER html %]</td> + </tr> + [% END %] + </tbody> + </table> + </div> + </div> +[% ELSE %] + <h3>No [% terms.bugs %] found.</h3> +[% END %] diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl new file mode 100644 index 000000000..4350e7472 --- /dev/null +++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl @@ -0,0 +1,132 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +<style> + .yui-skin-sam .yui-dt table {width:100%;} +</style> + +<script type="text/javascript"> +<!-- +var column_defs = [ + { key:"id", label:"ID", sortable:true, sortOptions:{ sortFunction:sortBugIdLinks } }, + { key:"bug_status", label:"Status", sortable:true }, + { key:"version", label:"Version", sortable:true }, + { key:"component", label:"Component", sortable:true }, + { key:"bug_severity", label:"Severity", sortable:true, sortOptions:{ sortFunction:sortBugSeverity } }, + { key:"Summary", label:"Summary", sortable:false }, +]; +var fields = [ + { key:"id" }, + { key:"bug_status" }, + { key:"version" }, + { key:"component" }, + { key:"bug_severity" }, + { key:"Summary" } +]; +addStatListener("recently_opened", "recently_opened_table", column_defs, fields, { + [% IF recently_opened.size > 25 %] paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25 }) [% END %] +}); +addStatListener("recently_closed", "recently_closed_table", column_defs, fields, { + [% IF recently_closed.size > 25 %] paginator: new YAHOO.widget.Paginator({ rowsPerPage: 25 }) [% END %] +}); +--> +</script> + +<p> + Activity within the last <input type="text" size="4" name="recent_days" + value="[% recent_days FILTER html %]"> + days (between 1 and 100) or from + <input name="date_from" size="10" id="date_from" + value="[% date_from FILTER html %]" + onchange="updateCalendarFromField(this)"> + <button type="button" class="calendar_button" + id="button_calendar_date_from" + onclick="showCalendar('date_from')"> + <span>Calendar</span> + </button> + <span id="con_calendar_date_from"></span> + to + <input name="date_to" size="10" id="date_to" + value="[% date_to FILTER html %]" + onchange="updateCalendarFromField(this)"> + <button type="button" class="calendar_button" + id="button_calendar_date_to" + onclick="showCalendar('date_to')"> + <span>Calendar</span> + </button> + <span id="con_calendar_date_to"></span> + <script type="text/javascript"> + createCalendar('date_from') + createCalendar('date_to') + </script> + <input type="submit" name="change" value="Change"> +</p> +<p> + <a href="#recently_opened">Recently Opened</a> + <span class="separator"> | </span> + <a href="#recently_closed">Recently Closed</a> +</p> + +<div class="yui-skin-sam"> + <a name="recently_opened"></a> + <b>[% recently_opened.size FILTER html %] Recently Opened [% terms.Bugs %]</b> + <div id="recently_opened"> + <table id="recently_opened_table" cellspacing="3" cellpadding="0" border="0" width="100%"> + <thead> + <tr bgcolor="#CCCCCC"> + [% FOREACH column = [ "ID", "Status", "Version", "Component", "Severity" "Summary" ] %] + <th>[% column FILTER html %]</th> + [% END %] + </tr> + </thead> + <tbody> + [% FOREACH bug = recently_opened %] + [% count = loop.count() %] + <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]"> + <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]"> + [% bug.id FILTER html %]</a></td> + <td align="center">[% bug.status FILTER html %]</td> + <td align="center">[% bug.version FILTER html %]</td> + <td align="center">[% bug.component FILTER html %]</td> + <td align="center">[% bug.severity FILTER html %]</td> + <td>[% bug.summary FILTER html %]</td> + </tr> + [% END %] + </tbody> + </table> + </div> + <br> + <a name="recently_closed"></a> + <b>[% recently_closed.size FILTER html %] Recently Closed [% terms.Bugs %]</b> + (<a href="#top">back to top</a>) + <div id="recently_closed"> + <table id="recently_closed_table" cellspacing="3" cellpadding="0" border="0" width="100%"> + <thead> + <tr bgcolor="#CCCCCC"> + [% FOREACH column = [ "ID", "Status", "Version", "Component", "Severity" "Summary" ] %] + <th>[% column FILTER html %]</th> + [% END %] + </tr> + </thead> + <tbody> + [% FOREACH bug = recently_closed %] + [% count = loop.count() %] + <tr class="[%+ count % 2 == 1 ? "bz_row_odd" : "bz_row_even" -%]"> + <td align="center"><a href="[% urlbase FILTER none %]show_bug.cgi?id=[% bug.id FILTER uri %]"> + [% bug.id FILTER html %]</a></td> + <td align="center">[% bug.status FILTER html %]</td> + <td align="center">[% bug.version FILTER html %]</td> + <td align="center">[% bug.component FILTER html %]</td> + <td align="center">[% bug.severity FILTER html %]</td> + <td>[% bug.summary FILTER html %]</td> + </tr> + [% END %] + </tbody> + </table> + </div> +</div> diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl new file mode 100644 index 000000000..fad1cafa5 --- /dev/null +++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl @@ -0,0 +1,54 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +<script type="text/javascript"> +<!-- +var column_defs = [ + { key:"milestone", label:"Milestone", sortable:true }, + { key:"percentage complete", label:"Percentage Complete", sortable:false }, + { key:"links", label:"Links", sortable:false }, +]; +var fields = [ + { key:"milestone" }, + { key:"percentage complete" }, + { key:"links" } +]; + +addStatListener("bug_milestones", "bug_milestones_table", column_defs, fields, {}); +--> +</script> + +<div class="yui-skin-sam"> +<div id="bug_milestones"> + <table id="bug_milestones_table" border="0" cellspacing="3" cellpadding="0"> + <thead> + <tr> + <th>Milestone</th> + <th>Percentage Complete</th> + <th>Links</th> + </tr> + </thead> + <tbody> + [% FOREACH milestone = by_roadmap %] + <tr> + <td>[% milestone.name FILTER html %]</td> + <td width="70%"> + [% INCLUDE bar_graph count = milestone.closed_bugs full_bug_count = milestone.total_bugs %] + </td> + <td> + <a href="[% milestone.link_closed FILTER html %]"> + [% milestone.closed_bugs FILTER html %]</a> of + <a href="[% milestone.link_total FILTER html %]"> + [% milestone.total_bugs FILTER html %]</a> bugs have been closed + </td> + </tr> + [% END %] + </tbody> + </table> + </div> +</div> diff --git a/extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl b/extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl new file mode 100644 index 000000000..c1b0a8854 --- /dev/null +++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl @@ -0,0 +1,205 @@ +[%# This Source Code Form is subject to the terms of the Mozilla Public + # License, v. 2.0. If a copy of the MPL was not distributed with this + # file, You can obtain one at http://mozilla.org/MPL/2.0/. + # + # This Source Code Form is "Incompatible With Secondary Licenses", as + # defined by the Mozilla Public License, v. 2.0. + #%] + +<script type="text/javascript"> +<!-- +var column_defs = [ + { key:"name", label:"Name", sortable:true }, + { key:"count", label:"Count", sortable:true }, + { key:"percentage", label:"Percentage", sortable:false } +]; +var fields = [ + { key:"name" }, + { key:"count", parser:"number" }, + { key:"percentage" } +]; +addStatListener("bug_counts", "bug_counts_table", column_defs, fields, {}); +addStatListener("status_counts", "status_counts_table", column_defs, fields, {}); +addStatListener("priority_counts", "priority_counts_table", column_defs, fields, {}); +addStatListener("severity_counts", "severity_counts_table", column_defs, fields, {}); +addStatListener("assignee_counts", "assignee_counts_table", column_defs, fields, {}); +--> +</script> + +<p> + <a href="#counts">Counts</a> + <span class="separator"> | </span> + <a href="#status">Status</a> + <span class="separator"> | </span> + <a href="#priority">Priority</a> + <span class="separator"> | </span> + <a href="#severity">Severity</a> + <span class="separator"> | </span> + <a href="#assignee">Assignee</a> +</p> + +<div class="yui-skin-sam"> + <a name="counts"></a> + <b>[% terms.Bug %] Counts</b> + <div id="bug_counts"> + <table id="bug_counts_table" border="0" cellspacing="3" cellpadding="0"> + <thead> + <tr> + <th>Name</th> + <th>Count</th> + <th>Percentage</th> + </tr> + </thead> + <tbody> + <tr> + <td><a href="[% bug_link_all FILTER html %]">Total [% terms.Bugs %]</a></td> + <td>[% total_bugs FILTER html %]</td> + <td> </td> + </tr> + <tr> + <td><a href="[% bug_link_open FILTER html %]">Open [% terms.Bugs %]</a></td> + <td>[% total_open_bugs FILTER html %]</td> + <td width="70%"> + [% INCLUDE bar_graph count = total_open_bugs full_bug_count = total_bugs %] + </td> + </tr> + <tr> + <td><a href="[% bug_link_closed FILTER html %]">Closed [% terms.Bugs %]</a></td> + <td>[% total_closed_bugs FILTER html %]</td> + <td width="70%"> + [% INCLUDE bar_graph count = total_closed_bugs full_bug_count = total_bugs %] + </td> + </tr> + </tbody> + </table> + </div> + <br> + <a name="status"></a> + <b>Status</b> + (<a href="#top">back to top</a>) + <div id="status_counts"> + <table id="status_counts_table" border="0" cellspacing="3" cellpadding="0"> + <thead> + <tr> + <th>Name</th> + <th>Count</th> + <th>Percentage</th> + </tr> + </thead> + <tbody> + [% FOREACH col = by_status %] + [% NEXT IF col.0 == 'CLOSED' %] + <tr> + <td> + <a href="[% bug_link_all FILTER html %]&bug_status=[% col.0 FILTER uri %]"> + [% col.0 FILTER html %]</a> + </td> + <td> + [% col.1 FILTER html %] + </td> + <td width="70%"> + [% INCLUDE bar_graph count = col.1 %] + </td> + </tr> + [% END %] + </tbody> + </table> + </div> + <br> + <a name="priority"></a> + <b>Priority</b> + (<a href="#top">back to top</a>) + <div id="priority_counts"> + <table id="priority_counts_table" border="0" cellspacing="3" cellpadding="0"> + <thead> + <tr> + <th>Name</th> + <th>Count</th> + <th>Percentage</th> + </tr> + </thead> + </tbody> + [% FOREACH col = by_priority %] + <tr> + <td> + <a href="[% bug_link FILTER html %]&priority=[% col.0 FILTER uri %]"> + [% col.0 FILTER html %]</a> + </td> + <td> + [% col.1 FILTER html %] + </td> + <td width="70%"> + [% INCLUDE bar_graph count = col.1 %] + </td> + </tr> + [% END %] + </tbody> + </table> + </div> + <br> + <a name="severity"></a> + <b>Severity</b> + (<a href="#top">back to top</a>) + <div id="severity_counts"> + <table id="severity_counts_table" border="0" cellspacing="3" cellpadding="0"> + <thead> + <tr> + <th>Name</th> + <th>Count</th> + <th>Percentage</th> + </tr> + </thead> + <tbody> + [% FOREACH col = by_severity %] + <tr> + <td> + <a href="[% bug_link FILTER html %]&bug_severity=[% col.0 FILTER uri %]"> + [% col.0 FILTER html %]</a> + </td> + <td align="right"> + [% col.1 FILTER html %] + </td> + <td width="70%"> + [% INCLUDE bar_graph count = col.1 %] + </td> + </tr> + [% END %] + </tbody> + </table> + </div> + <br> + <a name="assignee"></a> + <b>Assignee</b> + (<a href="#top">back to top</a>) + <div id="assignee_counts"> + <table id="assignee_counts_table" border="0" cellspacing="3" cellpadding="0"> + <thead> + <tr> + <th>Name</th> + <th>Count</th> + <th>Percentage</th> + </tr> + </thead> + <tbody> + [% FOREACH col = by_assignee %] + <tr> + <td> + [% IF user.id %] + <a href="[% bug_link FILTER html %]&emailassigned_to1=1&emailtype1=exact&email1=[% col.0.email FILTER uri %]"> + [% col.0.email FILTER html %]</a> + [% ELSE %] + [% col.0.realname || "No Name" FILTER html %] + [% END %] + </td> + <td> + [% col.1 FILTER html %] + </td> + <td width="70%"> + [% INCLUDE bar_graph count = col.1 %] + </td> + </tr> + [% END %] + </tbody> + </table> + </div> +</div> diff --git a/extensions/ProductDashboard/web/images/spacer.gif b/extensions/ProductDashboard/web/images/spacer.gif Binary files differnew file mode 100644 index 000000000..fc2560981 --- /dev/null +++ b/extensions/ProductDashboard/web/images/spacer.gif diff --git a/extensions/ProductDashboard/web/js/productdashboard.js b/extensions/ProductDashboard/web/js/productdashboard.js new file mode 100644 index 000000000..e7276b7b2 --- /dev/null +++ b/extensions/ProductDashboard/web/js/productdashboard.js @@ -0,0 +1,94 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This Source Code Form is "Incompatible With Secondary Licenses", as + * defined by the Mozilla Public License, v. 2.0. + */ + +function addStatListener (div_name, table_name, column_defs, fields, options) { + YAHOO.util.Event.addListener(window, "load", function() { + YAHOO.example.StatsFromMarkup = new function() { + this.myDataSource = new YAHOO.util.DataSource(YAHOO.util.Dom.get(table_name)); + this.myDataSource.responseType = YAHOO.util.DataSource.TYPE_HTMLTABLE; + this.myDataSource.responseSchema = { fields:fields }; + this.myDataTable = new YAHOO.widget.DataTable(div_name, column_defs, this.myDataSource, options); + this.myDataTable.subscribe("rowMouseoverEvent", this.myDataTable.onEventHighlightRow); + this.myDataTable.subscribe("rowMouseoutEvent", this.myDataTable.onEventUnhighlightRow); + }; + }); +} + +// Custom sort handler to sort by bug id inside an anchor tag +var sortBugIdLinks = function(a, b, desc) { + // Deal with empty values + if (!YAHOO.lang.isValue(a)) { + return (!YAHOO.lang.isValue(b)) ? 0 : 1; + } + else if(!YAHOO.lang.isValue(b)) { + return -1; + } + // Now we need to pull out the ID text and convert to Numbers + // First we do 'a' + var container = document.createElement("bug_id_link"); + container.innerHTML = a.getData("id"); + var anchors = container.getElementsByTagName("a"); + var text = anchors[0].textContent; + if (text === undefined) text = anchors[0].innerText; + var new_a = new Number(text); + // Then we do 'b' + container.innerHTML = b.getData("id"); + anchors = container.getElementsByTagName("a"); + text = anchors[0].textContent; + if (text == undefined) text = anchors[0].innerText; + var new_b = new Number(text); + + if (!desc) { + return YAHOO.util.Sort.compare(new_a, new_b); + } + else { + return YAHOO.util.Sort.compare(new_b, new_a); + } +} + +// Custom sort handler for bug severities +var sortBugSeverity = function(a, b, desc) { + // Deal with empty values + if (!YAHOO.lang.isValue(a)) { + return (!YAHOO.lang.isValue(b)) ? 0 : 1; + } + else if(!YAHOO.lang.isValue(b)) { + return -1; + } + + var new_a = new Number(severities[YAHOO.lang.trim(a.getData('bug_severity'))]); + var new_b = new Number(severities[YAHOO.lang.trim(b.getData('bug_severity'))]); + + if (!desc) { + return YAHOO.util.Sort.compare(new_a, new_b); + } + else { + return YAHOO.util.Sort.compare(new_b, new_a); + } +} + +// Custom sort handler for bug priorities +var sortBugPriority = function(a, b, desc) { + // Deal with empty values + if (!YAHOO.lang.isValue(a)) { + return (!YAHOO.lang.isValue(b)) ? 0 : 1; + } + else if(!YAHOO.lang.isValue(b)) { + return -1; + } + + var new_a = new Number(priorities[YAHOO.lang.trim(a.getData('priority'))]); + var new_b = new Number(priorities[YAHOO.lang.trim(b.getData('priority'))]); + + if (!desc) { + return YAHOO.util.Sort.compare(new_a, new_b); + } + else { + return YAHOO.util.Sort.compare(new_b, new_a); + } +} diff --git a/extensions/ProductDashboard/web/styles/productdashboard.css b/extensions/ProductDashboard/web/styles/productdashboard.css new file mode 100644 index 000000000..8f431006a --- /dev/null +++ b/extensions/ProductDashboard/web/styles/productdashboard.css @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This Source Code Form is "Incompatible With Secondary Licenses", as + * defined by the Mozilla Public License, v. 2.0. */ + +#product_dashboard_links { + float: right; + border: 1px solid; + padding-right: 25px; +} diff --git a/template/en/default/global/common-links.html.tmpl b/template/en/default/global/common-links.html.tmpl index 769d41e7e..ec8608eed 100644 --- a/template/en/default/global/common-links.html.tmpl +++ b/template/en/default/global/common-links.html.tmpl @@ -55,6 +55,8 @@ [% END %] [%-# Work around FF bug: keep this on one line %]</li> + [% Hook.process('action-links') %] + [% IF user.login %] <li><span class="separator">| </span><a href="userprefs.cgi">Preferences</a></li> [% IF user.in_group('tweakparams') || user.in_group('editusers') || user.can_bless |