From cb14de16717a12c32632d15a95e34b36edb7d178 Mon Sep 17 00:00:00 2001 From: Dave Lawrence Date: Tue, 7 Aug 2012 14:32:38 -0400 Subject: Initial checkin of Product Dashboard --- extensions/ProductDashboard/Config.pm | 14 + extensions/ProductDashboard/Extension.pm | 189 +++++++++ extensions/ProductDashboard/lib/Queries.pm | 463 +++++++++++++++++++++ extensions/ProductDashboard/lib/Util.pm | 96 +++++ .../global/common-links-action-links.html.tmpl | 9 + .../en/default/pages/productdashboard.html.tmpl | 209 ++++++++++ .../pages/productdashboard/components.html.tmpl | 259 ++++++++++++ .../pages/productdashboard/duplicates.html.tmpl | 73 ++++ .../pages/productdashboard/popularity.html.tmpl | 73 ++++ .../pages/productdashboard/recents.html.tmpl | 132 ++++++ .../pages/productdashboard/roadmap.html.tmpl | 54 +++ .../pages/productdashboard/summary.html.tmpl | 205 +++++++++ extensions/ProductDashboard/web/images/spacer.gif | Bin 0 -> 43 bytes .../ProductDashboard/web/js/productdashboard.js | 94 +++++ .../web/styles/productdashboard.css | 12 + 15 files changed, 1882 insertions(+) create mode 100644 extensions/ProductDashboard/Config.pm create mode 100644 extensions/ProductDashboard/Extension.pm create mode 100644 extensions/ProductDashboard/lib/Queries.pm create mode 100644 extensions/ProductDashboard/lib/Util.pm create mode 100644 extensions/ProductDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl create mode 100644 extensions/ProductDashboard/template/en/default/pages/productdashboard.html.tmpl create mode 100644 extensions/ProductDashboard/template/en/default/pages/productdashboard/components.html.tmpl create mode 100644 extensions/ProductDashboard/template/en/default/pages/productdashboard/duplicates.html.tmpl create mode 100644 extensions/ProductDashboard/template/en/default/pages/productdashboard/popularity.html.tmpl create mode 100644 extensions/ProductDashboard/template/en/default/pages/productdashboard/recents.html.tmpl create mode 100644 extensions/ProductDashboard/template/en/default/pages/productdashboard/roadmap.html.tmpl create mode 100644 extensions/ProductDashboard/template/en/default/pages/productdashboard/summary.html.tmpl create mode 100644 extensions/ProductDashboard/web/images/spacer.gif create mode 100644 extensions/ProductDashboard/web/js/productdashboard.js create mode 100644 extensions/ProductDashboard/web/styles/productdashboard.css (limited to 'extensions/ProductDashboard') 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. + #%] + +
  • | Product Dashboard
  • 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" ] +%] + + + +[% 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 %] + +
    + + +
    + + + + [% IF summary.keys %] + + [% END %] + + [% IF product %] + + + + [% END %] + +

    Product: [% product.name FILTER html %]

    + + + + + + [% IF product %] + +

    [% product.description FILTER none %]

    + + [% WRAPPER global/tabs.html.tmpl + tabs = tabs + current_tab = current_tab + %] + +

    [% current_tab.label FILTER html %]

    + + [% 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 %] + +
    +
    + +[% 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 %] +
    + + + + + +
    + + + + +
    + + + +
    +
       [% percentage_bugs FILTER html %]%
    +
    +[% 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 %] + +

    Summary for [% summary.type FILTER html %]: [% summary.value FILTER html %]

    + + + + + + [% IF user.is_timetracker %] +

    + Past Due | + Updated Recently +

    + [% END %] + +
    + + [% IF user.is_timetracker %] + + [% summary.past_due.size FILTER html %] Past Due [% terms.Bugs %] (deadline is before today's date) + (full list) +
    + + + + [% FOREACH column = [ "ID", "Status", "Version", "Component", "Severity" "Summary" ] %] + + [% END %] + + + + [% FOREACH bug = summary.past_due %] + [% count = loop.count() %] + + + + + + + + + [% END %] + +
    [% column FILTER html %]
    + [% bug.id FILTER html %][% bug.status FILTER html %][% bug.version FILTER html %][% bug.component FILTER html %][% bug.severity FILTER html %][% bug.summary FILTER html %]
    +
    +
    + [% END %] + + + [% summary.updated_recently.size FILTER html %] Most Recently Updated [% terms.Bugs %] + [% IF user.is_timetracker %](back to top)[% END %] + (full list) +
    + + + + [% FOREACH column = [ "ID", "Status", "Version", "Component", "Severity" "Summary" ] %] + + [% END %] + + + + [% FOREACH bug = summary.updated_recently %] + [% count = loop.count() %] + + + + + + + + + [% END %] + +
    [% column FILTER html %]
    + [% bug.id FILTER html %][% bug.status FILTER html %][% bug.version FILTER html %][% bug.component FILTER html %][% bug.severity FILTER html %][% bug.summary FILTER html %]
    +
    +
    + +[% ELSE %] + + [% summary_url = "page.cgi?id=productdashboard.html&product=$url_filtered_product&bug_status=$url_filtered_status&tab=components" %] + + + +

    + Component | + Version | + Milestone +

    + +

    Click on value to show additional information

    + +
    + + Component +
    + + + + + + + + + + + [% FOREACH col = by_component %] + + + + + + + [% END %] + +
    NameCountPercentage[% terms.Bug %] List
    + + [% col.0 FILTER html %] + + [% col.1 FILTER html %] + + [% INCLUDE bar_graph count = col.1 %] + + View +
    +
    +
    + + Version + (back to top) +
    + + + + + + + + + + + [% FOREACH col = by_version %] + + + + + + + [% END %] + +
    NameCountPercentage[% terms.Bug %] List
    + + [% col.0 FILTER html %] + + [% col.1 FILTER html %] + + [% INCLUDE bar_graph count = col.1 %] + + View +
    +
    + + [% IF Param('usetargetmilestone') %] +
    + + Milestone + (back to top) +
    + + + + + + + + + + + [% FOREACH col = by_milestone %] + + + + + + + [% END %] + +
    NameCountPercentage[% terms.Bug %] List
    + + [% col.0 FILTER html %] + + [% col.1 FILTER html %] + + [% INCLUDE bar_graph count = col.1 %] + + View +
    +
    + [% END %] +
    + +[% 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. + #%] + + + + + +[% IF by_duplicate.size %] + [% by_duplicate.size FILTER html %] [% terms.Bugs %] Found +
    +
    + + + + [% FOREACH column = [ "ID", "Dupe Count", "Status", "Version" + "Component", "Severity" "Summary" ] %] + + + [% END %] + + + + [% FOREACH bug = by_duplicate %] + [% count = loop.count() %] + + + + + + + + + + [% END %] + +
    [% column FILTER html %]
    + [% bug.id FILTER html %][% bug.dupe_count FILTER html %][% bug.status FILTER html %][% bug.version FILTER html %][% bug.component FILTER html %][% bug.severity FILTER html %][% bug.summary FILTER html %]
    +
    +
    +[% ELSE %] +

    No duplicate [% terms.bugs %] found.

    +[% 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. + #%] + + + + + +[% IF by_popularity.size %] + [% by_popularity.size FILTER html %] [% terms.Bugs %] Found +
    +
    + + + + [% FOREACH column = [ "ID", "Count", "Status", "Version" + "Component", "Severity" "Summary" ] %] + + + [% END %] + + + + [% FOREACH bug = by_popularity %] + [% count = loop.count() %] + + + + + + + + + + [% END %] + +
    [% column FILTER html %]
    + [% bug.id FILTER html %][% bug.votes FILTER html %][% bug.status FILTER html %][% bug.version FILTER html %][% bug.component FILTER html %][% bug.severity FILTER html %][% bug.summary FILTER html %]
    +
    +
    +[% ELSE %] +

    No [% terms.bugs %] found.

    +[% 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. + #%] + + + + + +

    + Activity within the last + days (between 1 and 100) or from + + + + to + + + + + +

    +

    + Recently Opened + | + Recently Closed +

    + +
    + + [% recently_opened.size FILTER html %] Recently Opened [% terms.Bugs %] +
    + + + + [% FOREACH column = [ "ID", "Status", "Version", "Component", "Severity" "Summary" ] %] + + [% END %] + + + + [% FOREACH bug = recently_opened %] + [% count = loop.count() %] + + + + + + + + + [% END %] + +
    [% column FILTER html %]
    + [% bug.id FILTER html %][% bug.status FILTER html %][% bug.version FILTER html %][% bug.component FILTER html %][% bug.severity FILTER html %][% bug.summary FILTER html %]
    +
    +
    + + [% recently_closed.size FILTER html %] Recently Closed [% terms.Bugs %] + (back to top) +
    + + + + [% FOREACH column = [ "ID", "Status", "Version", "Component", "Severity" "Summary" ] %] + + [% END %] + + + + [% FOREACH bug = recently_closed %] + [% count = loop.count() %] + + + + + + + + + [% END %] + +
    [% column FILTER html %]
    + [% bug.id FILTER html %][% bug.status FILTER html %][% bug.version FILTER html %][% bug.component FILTER html %][% bug.severity FILTER html %][% bug.summary FILTER html %]
    +
    +
    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. + #%] + + + +
    +
    + + + + + + + + + + [% FOREACH milestone = by_roadmap %] + + + + + + [% END %] + +
    MilestonePercentage CompleteLinks
    [% milestone.name FILTER html %] + [% INCLUDE bar_graph count = milestone.closed_bugs full_bug_count = milestone.total_bugs %] + + + [% milestone.closed_bugs FILTER html %] of  + + [% milestone.total_bugs FILTER html %] bugs have been closed +
    +
    +
    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. + #%] + + + +

    + Counts + | + Status + | + Priority + | + Severity + | + Assignee +

    + +
    + + [% terms.Bug %] Counts +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameCountPercentage
    Total [% terms.Bugs %][% total_bugs FILTER html %] 
    Open [% terms.Bugs %][% total_open_bugs FILTER html %] + [% INCLUDE bar_graph count = total_open_bugs full_bug_count = total_bugs %] +
    Closed [% terms.Bugs %][% total_closed_bugs FILTER html %] + [% INCLUDE bar_graph count = total_closed_bugs full_bug_count = total_bugs %] +
    +
    +
    + + Status + (back to top) +
    + + + + + + + + + + [% FOREACH col = by_status %] + [% NEXT IF col.0 == 'CLOSED' %] + + + + + + [% END %] + +
    NameCountPercentage
    + + [% col.0 FILTER html %] + + [% col.1 FILTER html %] + + [% INCLUDE bar_graph count = col.1 %] +
    +
    +
    + + Priority + (back to top) +
    + + + + + + + + + + [% FOREACH col = by_priority %] + + + + + + [% END %] + +
    NameCountPercentage
    + + [% col.0 FILTER html %] + + [% col.1 FILTER html %] + + [% INCLUDE bar_graph count = col.1 %] +
    +
    +
    + + Severity + (back to top) +
    + + + + + + + + + + [% FOREACH col = by_severity %] + + + + + + [% END %] + +
    NameCountPercentage
    + + [% col.0 FILTER html %] + + [% col.1 FILTER html %] + + [% INCLUDE bar_graph count = col.1 %] +
    +
    +
    + + Assignee + (back to top) +
    + + + + + + + + + + [% FOREACH col = by_assignee %] + + + + + + [% END %] + +
    NameCountPercentage
    + [% IF user.id %] + + [% col.0.email FILTER html %] + [% ELSE %] + [% col.0.realname || "No Name" FILTER html %] + [% END %] + + [% col.1 FILTER html %] + + [% INCLUDE bar_graph count = col.1 %] +
    +
    +
    diff --git a/extensions/ProductDashboard/web/images/spacer.gif b/extensions/ProductDashboard/web/images/spacer.gif new file mode 100644 index 000000000..fc2560981 Binary files /dev/null and b/extensions/ProductDashboard/web/images/spacer.gif differ 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; +} -- cgit v1.2.3-24-g4f1b