diff options
Diffstat (limited to 'extensions/ProductDashboard')
21 files changed, 1762 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..57192f195 --- /dev/null +++ b/extensions/ProductDashboard/Extension.pm @@ -0,0 +1,192 @@ +# 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. + scalar @{$user->get_selectable_products} + || ThrowUserError('no_products'); + + # 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 (!$product || !$user->can_enter_product($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'); + + $vars->{'open_bugs_percentage'} = int($vars->{'total_open_bugs'} / $vars->{'total_bugs'} * 100); + $vars->{'closed_bugs_percentage'} = int($vars->{'total_closed_bugs'} / $vars->{'total_bugs'} * 100); + + 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, 50); + $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); + $milestone_stats{'percentage'} = $milestone_stats{'total_bugs'} + ? int(($milestone_stats{'closed_bugs'} / $milestone_stats{'total_bugs'}) * 100) + : 0; + 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..b58298ea8 --- /dev/null +++ b/extensions/ProductDashboard/lib/Queries.pm @@ -0,0 +1,475 @@ +# 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::Util; +use Bugzilla::Component; +use Bugzilla::Version; +use Bugzilla::Milestone; + +use Bugzilla::Extension::ProductDashboard::Util qw(open_states closed_states + quoted_open_states quoted_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 (" . join(',', quoted_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 (" . join(',', quoted_closed_states()) . ") + 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 (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed'; + + return $dbh->selectall_arrayref("SELECT version, COUNT(bug_id), + ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100)) + FROM bugs + WHERE product_id = ? + $extra + GROUP BY version + ORDER BY COUNT(bug_id) DESC", + undef, $product->id, $product->id); +} + +sub by_milestone { + my ($product, $bug_status) = @_; + my $dbh = Bugzilla->dbh; + my $extra = ''; + + $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed'; + + return $dbh->selectall_arrayref("SELECT target_milestone, COUNT(bug_id), + ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100)) + FROM bugs + WHERE product_id = ? + $extra + GROUP BY target_milestone + ORDER BY COUNT(bug_id) DESC", + undef, $product->id, $product->id); +} + +sub by_priority { + my ($product, $bug_status) = @_; + my $dbh = Bugzilla->dbh; + my $extra = ''; + + $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed'; + + return $dbh->selectall_arrayref("SELECT priority, COUNT(bug_id), + ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100)) + FROM bugs + WHERE product_id = ? + $extra + GROUP BY priority + ORDER BY COUNT(bug_id) DESC", + undef, $product->id, $product->id); +} + +sub by_severity { + my ($product, $bug_status) = @_; + my $dbh = Bugzilla->dbh; + my $extra = ''; + + $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed'; + + return $dbh->selectall_arrayref("SELECT bug_severity, COUNT(bug_id), + ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100)) + FROM bugs + WHERE product_id = ? + $extra + GROUP BY bug_severity + ORDER BY COUNT(bug_id) DESC", + undef, $product->id, $product->id); +} + +sub by_component { + my ($product, $bug_status) = @_; + my $dbh = Bugzilla->dbh; + my $extra = ''; + + $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed'; + + return $dbh->selectall_arrayref("SELECT components.name, COUNT(bugs.bug_id), + ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100)) + 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, $product->id); +} + +sub by_value_summary { + my ($product, $type, $value, $bug_status) = @_; + my $dbh = Bugzilla->dbh; + + 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 (" . join(',', quoted_open_states()) . ") " if $bug_status eq 'open'; + $query .= "AND bugs.bug_status IN (" . join(',', quoted_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, $limit) = @_; + my $dbh = Bugzilla->dbh; + my $extra = ''; + + $limit = ($limit && detaint_natural($limit)) ? $dbh->sql_limit($limit) : ""; + + $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed'; + + my @result = map { [ Bugzilla::User->new($_->[0]), $_->[1], $_->[2] ] } + @{$dbh->selectall_arrayref("SELECT bugs.assigned_to AS userid, COUNT(bugs.bug_id), + ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100)) + 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 $limit", + undef, $product->id, $product->id)}; + + return \@result; +} + +sub by_status { + my ($product, $bug_status) = @_; + my $dbh = Bugzilla->dbh; + my $extra = ''; + + $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . join(',', quoted_closed_states()) . ")" if $bug_status eq 'closed'; + + return $dbh->selectall_arrayref("SELECT bugs.bug_status, COUNT(bugs.bug_id), + ROUND(((COUNT(bugs.bug_id) / ( SELECT COUNT(*) FROM bugs WHERE bugs.product_id = ? $extra)) * 100)) + FROM bugs + WHERE bugs.product_id = ? + $extra + GROUP BY bugs.bug_status + ORDER BY COUNT(bugs.bug_id) DESC", + undef, $product->id, $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 (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . join(',', quoted_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 = ($limit && detaint_natural($limit)) ? $dbh->sql_limit($limit) : ""; + + my $extra = ''; + $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . join(',', quoted_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 = ($limit && detaint_natural($limit)) ? $dbh->sql_limit($limit) : ""; + + my $extra = ''; + $extra = "AND bugs.bug_status IN (" . join(',', quoted_open_states()) . ")" if $bug_status eq 'open'; + $extra = "AND bugs.bug_status IN (" . join(',', quoted_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 = ($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, trick_taint($date_from), trick_taint($date_to)); + } + else { + $date_part = "AND bugs.creation_ts >= CURRENT_DATE() - INTERVAL ? DAY"; + 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 (" . join(',', quoted_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 = ($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_activity.bug_when >= ? AND bugs_activity.bug_when <= ?"; + push(@values, trick_taint($date_from), trick_taint($date_to)); + } + else { + $date_part = "AND bugs_activity.bug_when >= CURRENT_DATE() - INTERVAL ? DAY"; + 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 (" . join(',', quoted_closed_states()) . ") + AND bugs.bug_id = bugs_activity.bug_id + AND bugs_activity.added IN (" . join(',', quoted_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..5d9c161ef --- /dev/null +++ b/extensions/ProductDashboard/lib/Util.pm @@ -0,0 +1,95 @@ +# 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 + quoted_open_states + quoted_closed_states + bug_milestone_link_total + bug_milestone_link_open + bug_milestone_link_closed +); + +use Bugzilla::Status; +use Bugzilla::Util; + +our $_open_states; +sub open_states { + $_open_states ||= Bugzilla::Status->match({ is_open => 1, isactive => 1 }); + return wantarray ? @$_open_states : $_open_states; +} + +our $_quoted_open_states; +sub quoted_open_states { + my $dbh = Bugzilla->dbh; + $_quoted_open_states ||= [ map { $dbh->quote($_->name) } open_states() ]; + return wantarray ? @$_quoted_open_states : $_quoted_open_states; +} + +our $_closed_states; +sub closed_states { + $_closed_states ||= Bugzilla::Status->match({ is_open => 0, isactive => 1 }); + return wantarray ? @$_closed_states : $_closed_states; +} + +our $_quoted_closed_states; +sub quoted_closed_states { + my $dbh = Bugzilla->dbh; + $_quoted_closed_states ||= [ map { $dbh->quote($_->name) } closed_states() ]; + return wantarray ? @$_quoted_closed_states : $_quoted_closed_states; +} + +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__"; +} + +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/hook/global/user-error-errors.html.tmpl b/extensions/ProductDashboard/template/en/default/hook/global/user-error-errors.html.tmpl new file mode 100644 index 000000000..d8af64d31 --- /dev/null +++ b/extensions/ProductDashboard/template/en/default/hook/global/user-error-errors.html.tmpl @@ -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. + #%] + +[% IF error == "product_dashboard_invalid_recent_days" %] + [% title = "Invalid Recent Days" %] + Invalid value for recent days. +[% END %] 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..f48d8f812 --- /dev/null +++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl @@ -0,0 +1,234 @@ +[%# 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 %] + +[% javascript_urls = [ "js/util.js", "js/field.js" ] %] + +[% IF current_tab_name == 'summary' %] + [% javascript_urls.push("extensions/ProductDashboard/web/js/summary.js") %] + [% ELSIF current_tab_name == 'recents' %] + [% yui = [ "calendar" ] %] + [% javascript_urls.push("js/field.js") %] + [% javascript_urls.push("js/util.js") %] + [% javascript_urls.push("extensions/ProductDashboard/web/js/recents.js") %] +[% ELSIF current_tab_name == 'components' %] + [% javascript_urls.push("extensions/ProductDashboard/web/js/components.js") %] +[% ELSIF current_tab_name == 'duplicates' %] + [% javascript_urls.push("extensions/ProductDashboard/web/js/duplicates.js") %] +[% ELSIF current_tab_name == 'popularity' %] + [% javascript_urls.push("extensions/ProductDashboard/web/js/popularity.js") %] +[% ELSIF current_tab_name == 'roadmap' && Param('usetargetmilestone') %] + [% javascript_urls.push("extensions/ProductDashboard/web/js/roadmap.js") %] +[% END %] + +[% 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" ] +%] + +<script type="text/javascript"> +<!-- + PD = {}; + [%# Set up severities list for proper sorting %] + PD.severities = new Array(); + [% sort_count = 0 %] + [% FOREACH s = severities %] + PD.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 => "roadmap", + label => "Road Map", + link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=roadmap" + }, + ] +%] + +[% IF product.votesperuser %] + [% + tabs.push({ + name => "popularity", + label => "Popularity", + link => "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=popularity" + }) + %] +[% END %] + +[% 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="yui3-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 %] + + <strong>Choose product:</strong> + <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 %] + <div class="product_name"> + [% product.name FILTER html %] + </div> + + <div class="product_description"> + [% product.description FILTER none %] + </div> + + [% WRAPPER global/tabs.html.tmpl + tabs = tabs + current_tab = current_tab + %] + + [% 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..6b0e7240a --- /dev/null +++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl @@ -0,0 +1,146 @@ +[%# 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> + +<script> +<!-- + // Past due + [% IF user.is_timetracker %] + PD.past_due = [ + [% FOREACH bug = summary.past_due %] + { + id: '[% bug.id FILTER js %]', + bug_status: '[% bug.status FILTER js %]', + version: '[% bug.version FILTER js %]', + component: '[% bug.component FILTER js %]', + severity: '[% bug.severity FILTER js %]', + summary: '[% bug.summary FILTER js %]' + }, + [% END %] + ]; + [% END %] + + // Updated recently + PD.updated_recently = [ + [% FOREACH bug = summary.updated_recently %] + { + id: '[% bug.id FILTER js %]', + bug_status: '[% bug.status FILTER js %]', + version: '[% bug.version FILTER js %]', + component: '[% bug.component FILTER js %]', + severity: '[% bug.severity FILTER js %]', + summary: '[% bug.summary FILTER js %]' + }, + [% 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="yui3-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"></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"></div> +</div> + +[% ELSE %] + +<script type="text/javascript"> +<!-- + PD.product_name = '[% product.name FILTER js %]'; + PD.bug_status = '[% bug_status FILTER js %]'; + + // Component counts + PD.component_counts = [ + [% FOREACH col = by_component %] + { + name: "[% col.0 FILTER js %]", + count: [% col.1 || 0 FILTER js %], + percentage: [% col.2 || 0 FILTER js %], + link: '<a href="[% bug_link FILTER html %]&component=[% col.0 FILTER uri %]">Link</a>' + }, + [% END %] + ]; + + // Version counts + PD.version_counts = [ + [% FOREACH col = by_version %] + { + name: "[% col.0 FILTER js %]", + count: [% col.1 || 0 FILTER js %], + percentage: [% col.2 || 0 FILTER js %], + link: '<a href="[% bug_link FILTER html %]&version=[% col.0 FILTER uri %]">Link</a>' + }, + [% END %] + ]; + + [% IF Param('usetargetmilestone') %] + // Milestone counts + PD.milestone_counts = [ + [% FOREACH col = by_milestone %] + { + name: "[% col.0 FILTER js %]", + count: [% col.1 || 0 FILTER js %], + percentage: [% col.2 || 0 FILTER js %], + link: '<a href="[% bug_link FILTER html %]&target_milestone=[% col.0 FILTER uri %]">Link</a>' + }, + [% END %] + ]; + [% END %] +--> +</script> + +<h3>[% terms.Bug %] counts per component, version and milestone.</h3> + +<p> + <a href="#component">Component</a> | + <a href="#version">Version</a> | + <a href="#milestone">Milestone</a> +</p> + +<p>Click on a value to show a list of most recently updated [% terms.bugs %].</p> + +<div class="yui3-skin-sam"> + <a name="component"></a> + <b>Component</b> + <div id="component_counts"></div> + <br> + <a name="version"></a> + <b>Version</b> + (<a href="#top">back to top</a>) + <div id="version_counts"></div> + [% IF Param('usetargetmilestone') %] + <br> + <a name="milestone"></a> + <b>Milestone</b> + (<a href="#top">back to top</a>) + <div id="milestone_counts"></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..585cdc829 --- /dev/null +++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl @@ -0,0 +1,34 @@ +[%# 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"> + PD.duplicates = [ + [% FOREACH bug = by_duplicate %] + { + id: '[% bug.id FILTER js %]', + count: '[% bug.dupe_count FILTER js %]', + status: '[% bug.status FILTER js %]', + version: '[% bug.version FILTER js %]', + component: '[% bug.component FILTER js %]', + severity: '[% bug.severity FILTER js %]', + summary: '[% bug.summary FILTER js %]' + }, + [% END %] + ]; +</script> + +<h3>Most duplicated [% terms.bugs %]</h3> + +[% IF by_duplicate.size %] + <b>[% by_duplicate.size FILTER html %] [% terms.Bugs %] Found</b> + <div class="yui3-skin-sam"> + <div id="duplicates"></div> + </div> +[% ELSE %] + <b>No duplicate [% terms.bugs %] found.</b> +[% 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..933f26c81 --- /dev/null +++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl @@ -0,0 +1,38 @@ +[%# 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"> + PD.popularity = [ + [% FOREACH bug = by_popularity %] + { + id: '[% bug.id FILTER js %]', + count: '[% bug.votes FILTER js %]', + status: '[% bug.status FILTER js %]', + version: '[% bug.version FILTER js %]', + component: '[% bug.component FILTER js %]', + severity: '[% bug.severity FILTER js %]', + summary: '[% bug.summary FILTER js %]' + }, + [% END %] + ]; +</script> + +<h3>Most voted on [% terms.bugs %]</h3> + +[% IF by_popularity.size %] + <b>[% by_popularity.size FILTER html %] [% terms.Bugs %] Found</b> + <div class="yui3-skin-sam"> + <div id="popularity"></div> + </div> +[% ELSE %] + <b>No [% terms.bugs %] found.</b> +[% 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..66320e174 --- /dev/null +++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl @@ -0,0 +1,87 @@ +[%# 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"> + PD.recents = {}; + + // Recently opened + PD.recents.opened = [ + [% FOREACH bug = recently_opened %] + { + id: '[% bug.id FILTER js %]', + status: '[% bug.status FILTER js %]', + version: '[% bug.version FILTER js %]', + component: '[% bug.component FILTER js %]', + severity: '[% bug.severity FILTER js %]', + summary: '[% bug.summary FILTER js %]' + }, + [% END %] + ]; + + // Recently closed + PD.recents.closed = [ + [% FOREACH bug = recently_closed %] + { + id: '[% bug.id FILTER js %]', + status: '[% bug.status FILTER js %]', + version: '[% bug.version FILTER js %]', + component: '[% bug.component FILTER js %]', + severity: '[% bug.severity FILTER js %]', + summary: '[% bug.summary FILTER js %]' + }, + [% END %] + ]; +</script> + +<h3>Most recently opened and closed [% terms.bugs %]</h3> + +<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"></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"></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..b31827fbd --- /dev/null +++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl @@ -0,0 +1,27 @@ +[%# 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"> +<!-- + PD.roadmap = [ + [% FOREACH milestone = by_roadmap %] + { + name: '[% milestone.name FILTER js %]', + percentage: '[% milestone.percentage FILTER js %]', + link: '<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> [% terms.bugs %] have been closed', + }, + [% END %] + ]; +--> +</script> + +<h3>Percentage of [% terms.bug %] closure per milestone</h3> + +<div class="yui3-skin-sam"> + <div id="bug_milestones"></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..5afba25a9 --- /dev/null +++ b/extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl @@ -0,0 +1,122 @@ +[%# 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> + PD.summary = {}; + + // global counts + PD.summary.bug_counts = [ + { + name: "Total [% terms.Bugs %]", + count: [% total_bugs || 0 FILTER js %], + percentage: 100, + link: '<a href="[% bug_link_all FILTER js %]">Link</a>', + }, + { + name: "Open [% terms.Bugs %]", + count: [% total_open_bugs || 0 FILTER js %], + percentage: [% open_bugs_percentage FILTER js %], + link: '<a href="[% bug_link_open FILTER js %]">Link</a>', + }, + { + name: "Closed [% terms.Bugs %]", + count: [% total_closed_bugs || 0 FILTER js %], + percentage: [% closed_bugs_percentage FILTER js %], + link: '<a href="[% bug_link_closed FILTER js %]">Link</a>', + } + ]; + + // Status counts + PD.summary.status_counts = [ + [% FOREACH col = by_status %] + [% NEXT IF col.0 == 'CLOSED' %] + { + name: "[% col.0 FILTER js %]", + count: [% col.1 || 0 FILTER js %], + percentage: [% col.2 || 0 FILTER js %], + link: '<a href="[% bug_link_all FILTER js %]&bug_status=[% col.0 FILTER uri FILTER js %]">Link</a>' + }, + [% END %] + ]; + + // Priority counts + PD.summary.priority_counts = [ + [% FOREACH col = by_priority %] + { + name: "[% col.0 FILTER js %]", + count: [% col.1 || 0 FILTER js %], + percentage: [% col.2 || 0 FILTER js %], + link: '<a href="[% bug_link FILTER js %]&priority=[% col.0 FILTER uri FILTER js %]">Link</a>' + }, + [% END %] + ]; + + // Severity counts + PD.summary.severity_counts = [ + [% FOREACH col = by_severity %] + { + name: "[% col.0 FILTER js %]", + count: [% col.1 || 0 FILTER js %], + percentage: [% col.2 || 0 FILTER js %], + link: '<a href="[% bug_link FILTER js %]&bug_severity=[% col.0 FILTER uri FILTER js %]">Link</a>' + }, + [% END %] + ]; + + // Assignee counts + PD.summary.assignee_counts = [ + [% FOREACH col = by_assignee %] + { + name: "[% IF user.id %][% col.0.email FILTER js %][% ELSE %][% col.0.realname || 'No Name' FILTER js %][% END %]", + count: [% col.1 || 0 FILTER js %], + percentage: [% col.2 || 0 FILTER js %], + link: '[% IF user.id %]<a href="[% bug_link FILTER js %]&emailassigned_to1=1&emailtype1=exact&email1=[% col.0.email FILTER uri FILTER js %]">Link</a>[% END %]' + }, + [% END %] + ]; +</script> + +<h3>Summary of [% terms.bug %] counts</h3> + +<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="yui3-skin-sam"> + <a name="counts"></a> + <b>[% terms.Bug %] Counts</b> + <div id="bug_counts"></div> + <br> + <a name="status"></a> + <b>Status</b> + (<a href="#top">back to top</a>) + <div id="status_counts"></div> + <br> + <a name="priority"></a> + <b>Priority</b> + (<a href="#top">back to top</a>) + <div id="priority_counts"></div> + <br> + <a name="severity"></a> + <b>Severity</b> + (<a href="#top">back to top</a>) + <div id="severity_counts"></div> + <br> + <a name="assignee"></a> + <b>Assignee</b> + (<a href="#top">back to top</a>) + <div id="assignee_counts"></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/components.js b/extensions/ProductDashboard/web/js/components.js new file mode 100644 index 000000000..538b15457 --- /dev/null +++ b/extensions/ProductDashboard/web/js/components.js @@ -0,0 +1,75 @@ +/* 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. + */ + +YUI({ + base: 'js/yui3/', + combine: false +}).use("datatable", "datatable-sort", function(Y) { + if (typeof PD.updated_recently != 'undefined') { + var columns = [ + { key:"id", label:"ID", sortable:true, allowHTML: true, + formatter: '<a href="show_bug.cgi?id={value}" target="_blank">{value}</a>' }, + { key:"bug_status", label:"Status", sortable:true }, + { key:"version", label:"Version", sortable:true }, + { key:"component", label:"Component", sortable:true }, + { key:"severity", label:"Severity", sortable:true }, + { key:"summary", label:"Summary", sortable:false }, + ]; + + var updatedRecentlyDataTable = new Y.DataTable({ + columns: columns, + data: PD.updated_recently + }); + updatedRecentlyDataTable.render("#updated_recently"); + + if (typeof PD.past_due != 'undefined') { + var pastDueDataTable = new Y.DataTable({ + columns: columns, + data: PD.past_due + }); + pastDueDataTable.render('#past_due'); + } + } + + if (typeof PD.component_counts != 'undefined') { + var summary_url = '<a href="page.cgi?id=productdashboard.html&product=' + + encodeURIComponent(PD.product_name) + '&bug_status=' + + encodeURIComponent(PD.bug_status) + '&tab=components'; + + var columns = [ + { key:"name", label:"Name", sortable:true, allowHTML: true, + formatter: summary_url + '&component={value}">{value}</a>' }, + { key:"count", label:"Count", sortable:true }, + { key:"percentage", label:"Percentage", sortable:false, allowHTML: true, + formatter: '<div class="percentage"><div class="bar" style="width:{value}%"></div><div class="percent">{value}%</div></div>' }, + { key:"link", label:"Link", sortable:false, allowHTML: true } + ]; + + var componentsDataTable = new Y.DataTable({ + columns: columns, + data: PD.component_counts + }); + componentsDataTable.render("#component_counts"); + + columns[0].formatter = summary_url + '&version={value}">{value}</a>'; + var versionsDataTable = new Y.DataTable({ + columns: columns, + data: PD.version_counts + }); + versionsDataTable.render('#version_counts'); + + if (typeof PD.milestone_counts != 'undefined') { + columns[0].formatter = summary_url + '&target_milestone={value}">{value}</a>'; + var milestonesDataTable = new Y.DataTable({ + columns: columns, + data: PD.milestone_counts + }); + milestonesDataTable.render('#milestone_counts'); + } + } +}); diff --git a/extensions/ProductDashboard/web/js/duplicates.js b/extensions/ProductDashboard/web/js/duplicates.js new file mode 100644 index 000000000..5e3193a65 --- /dev/null +++ b/extensions/ProductDashboard/web/js/duplicates.js @@ -0,0 +1,28 @@ +/* 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. + */ + +YUI({ + base: 'js/yui3/', + combine: false +}).use("datatable", "datatable-sort", function (Y) { + var column_defs = [ + { key:"id", label:"ID", sortable:true, allowHTML: true, + formatter: '<a href="show_bug.cgi?id={value}" target="_blank">{value}</a>' }, + { key:"count", label:"Count", sortable:true }, + { key:"status", label:"Status", sortable:true }, + { key:"version", label:"Version", sortable:true }, + { key:"component", label:"Component", sortable:true }, + { key:"severity", label:"Severity", sortable:true }, + { key:"summary", label:"Summary", sortable:false }, + ]; + + var duplicatesDataTable = new Y.DataTable({ + columns: column_defs, + data: PD.duplicates + }).render('#duplicates'); +}); diff --git a/extensions/ProductDashboard/web/js/popularity.js b/extensions/ProductDashboard/web/js/popularity.js new file mode 100644 index 000000000..b78b67867 --- /dev/null +++ b/extensions/ProductDashboard/web/js/popularity.js @@ -0,0 +1,28 @@ +/* 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. + */ + +YUI({ + base: 'js/yui3/', + combine: false +}).use("datatable", "datatable-sort", function (Y) { + var column_defs = [ + { key:"id", label:"ID", sortable:true, allowHTML: true, + formatter: '<a href="show_bug.cgi?id={value}" target="_blank">{value}</a>' }, + { key:"count", label:"Count", sortable:true }, + { key:"status", label:"Status", sortable:true }, + { key:"version", label:"Version", sortable:true }, + { key:"component", label:"Component", sortable:true }, + { key:"severity", label:"Severity", sortable:true }, + { key:"summary", label:"Summary", sortable:false }, + ]; + + var popularityDataTable = new Y.DataTable({ + columns: column_defs, + data: PD.popularity + }).render('#popularity'); +}); diff --git a/extensions/ProductDashboard/web/js/recents.js b/extensions/ProductDashboard/web/js/recents.js new file mode 100644 index 000000000..84e1758b6 --- /dev/null +++ b/extensions/ProductDashboard/web/js/recents.js @@ -0,0 +1,32 @@ +/* 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. + */ + +YUI({ + base: 'js/yui3/', + combine: false +}).use("datatable", "datatable-sort", function (Y) { + var column_defs = [ + { key:"id", label:"ID", sortable:true, allowHTML: true, + formatter: '<a href="show_bug.cgi?id={value}" target="_blank">{value}</a>' }, + { key:"status", label:"Status", sortable:true }, + { key:"version", label:"Version", sortable:true }, + { key:"component", label:"Component", sortable:true }, + { key:"severity", label:"Severity", sortable:true }, + { key:"summary", label:"Summary", sortable:false }, + ]; + + var recentlyOpenedDataTable = new Y.DataTable({ + columns: column_defs, + data: PD.recents.opened + }).render('#recently_opened'); + + var recentlyClosedDataTable = new Y.DataTable({ + columns: column_defs, + data: PD.recents.closed + }).render('#recently_closed'); +}); diff --git a/extensions/ProductDashboard/web/js/roadmap.js b/extensions/ProductDashboard/web/js/roadmap.js new file mode 100644 index 000000000..1bef5b091 --- /dev/null +++ b/extensions/ProductDashboard/web/js/roadmap.js @@ -0,0 +1,24 @@ +/* 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. + */ + +YUI({ + base: 'js/yui3/', + combine: false +}).use("datatable", "datatable-sort", function (Y) { + var column_defs = [ + { key: 'name', label: 'Name', sortable: true }, + { key: 'percentage', label: 'Percentage', sortable: false, allowHTML: true, + formatter: '<div class="percentage"><div class="bar" style="width:{value}%"></div><div class="percent">{value}%</div></div>' }, + { key: 'link', label: 'Links', allowHTML: true, sortable: false } + ]; + + var roadmapDataTable = new Y.DataTable({ + columns: column_defs, + data: PD.roadmap, + }).render('#bug_milestones'); +}); diff --git a/extensions/ProductDashboard/web/js/summary.js b/extensions/ProductDashboard/web/js/summary.js new file mode 100644 index 000000000..59d000d7b --- /dev/null +++ b/extensions/ProductDashboard/web/js/summary.js @@ -0,0 +1,45 @@ +/* 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. + */ + +YUI({ + base: 'js/yui3/', + combine: false +}).use("datatable", "datatable-sort", function (Y) { + var column_defs = [ + { key: 'name', label: 'Name', sortable: true }, + { key: 'count', label: 'Count', sortable: true }, + { key: 'percentage', label: 'Percentage', sortable: true, allowHTML: true, + formatter: '<div class="percentage"><div class="bar" style="width:{value}%"></div><div class="percent">{value}%</div></div>' }, + { key: 'link', label: 'Link', allowHTML: true } + ]; + + var bugsCountDataTable = new Y.DataTable({ + columns: column_defs, + data: PD.summary.bug_counts + }).render('#bug_counts'); + + var statusCountsDataTable = new Y.DataTable({ + columns: column_defs, + data: PD.summary.status_counts + }).render('#status_counts'); + + var priorityCountsDataTable = new Y.DataTable({ + columns: column_defs, + data: PD.summary.priority_counts + }).render('#priority_counts'); + + var severityCountsDataTable = new Y.DataTable({ + columns: column_defs, + data: PD.summary.severity_counts + }).render('#severity_counts'); + + var assigneeCountsDataTable = new Y.DataTable({ + columns: column_defs, + data: PD.summary.assignee_counts + }).render('#assignee_counts'); +}); diff --git a/extensions/ProductDashboard/web/styles/productdashboard.css b/extensions/ProductDashboard/web/styles/productdashboard.css new file mode 100644 index 000000000..c0c45cf38 --- /dev/null +++ b/extensions/ProductDashboard/web/styles/productdashboard.css @@ -0,0 +1,45 @@ +/* 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; + padding-right: 25px; + border: 1px solid rgb(116, 126, 147); +} + +.product_name { + font-size: 2em; + margin: 10px 0 10px 0; + color: rgb(109, 117, 129); +} + +.product_description { + font-size: 90%; + font-style: italic; + padding-bottom: 5px; + margin-bottom: 10px; +} + +.percentage { + position:relative; + width: 200px; + border: 1px solid rgb(203, 203, 203); + position: relative; + padding: 3px; +} + +.bar{ + background-color: #00ff00; + height: 20px; +} + +.percent{ + position: absolute; + display: inline-block; + top: 3px; + left: 48%; +} |