From b0f7006e467249d5cbff2b84febe0b658744b559 Mon Sep 17 00:00:00 2001 From: Dave Lawrence Date: Thu, 9 Aug 2012 19:06:02 -0400 Subject: Initial import of MyDashboard extension --- extensions/MyDashboard/Config.pm | 14 + extensions/MyDashboard/Extension.pm | 379 +++++++++++++++++++++ extensions/MyDashboard/lib/TimeAgo.pm | 182 ++++++++++ extensions/MyDashboard/lib/WebService.pm | 98 ++++++ .../prefs/saved-searches-saved-header.html.tmpl | 11 + .../prefs/saved-searches-saved-row.html.tmpl | 15 + .../global/common-links-action-links.html.tmpl | 11 + .../default/mydashboard/prod-comp-search.html.tmpl | 43 +++ .../en/default/pages/mydashboard.html.tmpl | 289 ++++++++++++++++ .../en/default/pages/mydashboard_old.html.tmpl | 326 ++++++++++++++++++ extensions/MyDashboard/web/js/mydashboard.js | 126 +++++++ extensions/MyDashboard/web/js/prod_comp_search.js | 85 +++++ extensions/MyDashboard/web/styles/mydashboard.css | 90 +++++ .../MyDashboard/web/styles/prod_comp_search.css | 22 ++ 14 files changed, 1691 insertions(+) create mode 100644 extensions/MyDashboard/Config.pm create mode 100644 extensions/MyDashboard/Extension.pm create mode 100644 extensions/MyDashboard/lib/TimeAgo.pm create mode 100644 extensions/MyDashboard/lib/WebService.pm create mode 100644 extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl create mode 100644 extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl create mode 100644 extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl create mode 100644 extensions/MyDashboard/template/en/default/mydashboard/prod-comp-search.html.tmpl create mode 100644 extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl create mode 100644 extensions/MyDashboard/template/en/default/pages/mydashboard_old.html.tmpl create mode 100644 extensions/MyDashboard/web/js/mydashboard.js create mode 100644 extensions/MyDashboard/web/js/prod_comp_search.js create mode 100644 extensions/MyDashboard/web/styles/mydashboard.css create mode 100644 extensions/MyDashboard/web/styles/prod_comp_search.css (limited to 'extensions') diff --git a/extensions/MyDashboard/Config.pm b/extensions/MyDashboard/Config.pm new file mode 100644 index 000000000..7c14936ff --- /dev/null +++ b/extensions/MyDashboard/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::MyDashboard; + +use strict; + +use constant NAME => 'MyDashboard'; + +__PACKAGE__->NAME; diff --git a/extensions/MyDashboard/Extension.pm b/extensions/MyDashboard/Extension.pm new file mode 100644 index 000000000..869d3c81e --- /dev/null +++ b/extensions/MyDashboard/Extension.pm @@ -0,0 +1,379 @@ +# 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::MyDashboard; + +use strict; + +use base qw(Bugzilla::Extension); + +use Bugzilla; +use Bugzilla::Constants; +use Bugzilla::Search; +use Bugzilla::Util; +use Bugzilla::Status; +use Bugzilla::Field; +use Bugzilla::Search::Saved; + +use Bugzilla::Extension::MyDashboard::TimeAgo qw(time_ago); + +use DateTime; + +our $VERSION = BUGZILLA_VERSION; + +sub QUERY_DEFS { + my $user = Bugzilla->user; + + my @query_defs = ( + { + name => 'assignedbugs', + heading => 'Assigned to You', + description => 'The bug has been assigned to you and it is not resolved or closed yet.', + params => { + 'bug_status' => ['__open__'], + 'emailassigned_to1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + { + name => 'newbugs', + heading => 'New Reported by You', + description => 'You reported the bug but nobody has accepted it yet.', + params => { + 'bug_status' => ['NEW'], + 'emailreporter1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + { + name => 'inprogressbugs', + heading => "In Progress Reported by You", + description => 'You reported the bug, the developer accepted the bug and is hopefully working on it.', + params => { + 'bug_status' => [ map { $_->name } grep($_->name ne 'NEW' && $_->name ne 'MODIFIED', _open_states()) ], + 'emailreporter1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + { + name => 'openccbugs', + heading => "You Are CC'd On", + description => 'You are in the CC list of the bug, so you are watching it.', + params => { + 'bug_status' => ['__open__'], + 'emailcc1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + ); + + if (Bugzilla->params->{'useqacontact'}) { + push(@query_defs, { + name => 'qacontactbugs', + heading => 'You Are QA Contact', + description => 'You are the qa contact on this bug and it is not resolved or closed yet.', + params => { + 'bug_status' => ['__open__'], + 'emailqa_contact1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }); + } + + return @query_defs; +} + +################ +# Installation # +################ + +sub db_schema_abstract_schema { + my ($self, $args) = @_; + + my $schema = $args->{schema}; + + $schema->{'mydashboard'} = { + FIELDS => [ + namedquery_id => {TYPE => 'INT3', NOTNULL => 1, + REFERENCES => {TABLE => 'namedqueries', + COLUMN => 'id', + DELETE => 'CASCADE'}}, + user_id => {TYPE => 'INT3', NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', + COLUMN => 'userid', + DELETE => 'CASCADE'}}, + ], + INDEXES => [ + mydashboard_namedquery_id_idx => {FIELDS => [qw(namedquery_id user_id)], + TYPE => 'UNIQUE'}, + mydashboard_user_id_idx => ['user_id'], + ], + }; +} + +########### +# Objects # +########### + +BEGIN { + *Bugzilla::Search::Saved::in_mydashboard = \&_in_mydashboard; +} + +sub _in_mydashboard { + my ($self) = @_; + my $dbh = Bugzilla->dbh; + return $self->{'in_mydashboard'} if exists $self->{'in_mydashboard'}; + $self->{'in_mydashboard'} = $dbh->selectrow_array(" + SELECT 1 FROM mydashboard WHERE namedquery_id = ? AND user_id = ?", + undef, $self->id, $self->user->id); + return $self->{'in_mydashboard'}; +} + +############# +# Templates # +############# + +sub page_before_template { + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; + + return if $page ne 'mydashboard.html'; + + # If we're using bug groups to restrict bug entry, we need to know who the + # user is right from the start. + my $user = Bugzilla->login(LOGIN_REQUIRED); + + # Switch to shadow db since we are just reading information + Bugzilla->switch_to_shadow_db(); + + _active_product_counts($vars); + _standard_saved_queries($vars); + _flags_requested($vars); + + $vars->{'severities'} = get_legal_field_values('bug_severity'); +} + +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; +} + +sub _active_product_counts { + my ($vars) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + my @enterable_products = @{$user->get_enterable_products()}; + $vars->{'products'} + = $dbh->selectall_arrayref("SELECT products.name AS product, count(*) AS count + FROM bugs,products + WHERE bugs.product_id=products.id + AND products.isactive = 1 + AND bugs.bug_status IN (" . join(',', _quoted_open_states()) . ") + AND products.id IN (" . join(',', map { $_->id } @enterable_products) . ") + GROUP BY products.name ORDER BY count DESC", { Slice => {} }); + + $vars->{'products_buffer'} = "&" . join('&', map { "bug_status=" . $_->name } _open_states()); +} + +sub _standard_saved_queries { + my ($vars) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + # Default sort order + my $order = ["bug_id"]; + + # List of columns that we will be selecting. In the future this should be configurable + # Share with buglist.cgi? + my @select_columns = ('bug_id','product','bug_status','bug_severity','version', 'component','short_desc', 'changeddate'); + + # Define the columns that can be selected in a query + my $columns = Bugzilla::Search::COLUMNS; + + # Weed out columns that don't actually exist and detaint along the way. + @select_columns = grep($columns->{$_} && trick_taint($_), @select_columns); + + ### Standard query definitions + my @query_defs = QUERY_DEFS; + + ### Saved query definitions + ### These are enabled through the userprefs.cgi UI + foreach my $q (@{$user->queries}) { + next if !$q->in_mydashboard; + push(@query_defs, { name => $q->name, + heading => $q->name, + saved => 1, + params => $q->url }); + } + + my $date_now = DateTime->now(time_zone => Bugzilla->local_timezone); + + ### Collect the query results for display in the template + + my @results; + foreach my $qdef (@query_defs) { + my $params = new Bugzilla::CGI($qdef->{params}); + + my $search = new Bugzilla::Search( fields => \@select_columns, + params => $params, + order => $order ); + my $query = $search->sql(); + + my $sth = $dbh->prepare($query); + $sth->execute(); + + my $rows = $sth->fetchall_arrayref(); + + my @bugs; + foreach my $row (@$rows) { + my $bug = {}; + foreach my $column (@select_columns) { + $bug->{$column} = shift @$row; + if ($column eq 'changeddate') { + my $date_then = datetime_from($bug->{$column}); + $bug->{'updated'} = time_ago($date_then, $date_now); + } + } + push(@bugs, $bug); + } + + $qdef->{bugs} = \@bugs; + $qdef->{buffer} = $params->canonicalise_query(); + + push(@results, $qdef); + } + + $vars->{'results'} = \@results; +} + +sub _flags_requested { + my ($vars) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + my $attach_join_clause = "flags.attach_id = attachments.attach_id"; + if (Bugzilla->params->{insidergroup} && !$user->in_group(Bugzilla->params->{insidergroup})) { + $attach_join_clause .= " AND attachments.isprivate < 1"; + } + + my $query = + # Select columns describing each flag, the bug/attachment on which + # it has been set, who set it, and of whom they are requesting it. + " SELECT flags.id AS id, + flagtypes.name AS type, + flags.status AS status, + flags.bug_id AS bug_id, + bugs.short_desc AS bug_summary, + flags.attach_id AS attach_id, + attachments.description AS attach_summary, + requesters.realname AS requester, + requestees.realname AS requestee, + " . $dbh->sql_date_format('flags.creation_date', '%Y.%m.%d %H:%i') . " AS created + FROM flags + LEFT JOIN attachments + ON ($attach_join_clause) + INNER JOIN flagtypes + ON flags.type_id = flagtypes.id + INNER JOIN bugs + ON flags.bug_id = bugs.bug_id + LEFT JOIN profiles AS requesters + ON flags.setter_id = requesters.userid + LEFT JOIN profiles AS requestees + ON flags.requestee_id = requestees.userid + LEFT JOIN bug_group_map AS bgmap + ON bgmap.bug_id = bugs.bug_id + LEFT JOIN cc AS ccmap + ON ccmap.who = " . $user->id . " + AND ccmap.bug_id = bugs.bug_id "; + + # Limit query to pending requests and open bugs only + $query .= " WHERE bugs.bug_status IN (" . join(',', _quoted_open_states()) . ") + AND flags.status = '?' "; + + # Weed out bug the user does not have access to + $query .= " AND ((bgmap.group_id IS NULL) + OR bgmap.group_id IN (" . $user->groups_as_string . ") + OR (ccmap.who IS NOT NULL AND cclist_accessible = 1) + OR (bugs.reporter = " . $user->id . " AND bugs.reporter_accessible = 1) + OR (bugs.assigned_to = " . $user->id .") "; + if (Bugzilla->params->{useqacontact}) { + $query .= " OR (bugs.qa_contact = " . $user->id . ") "; + } + $query .= ") "; + + # Order the records (within each group). + my $group_order_by = " GROUP BY flags.bug_id ORDER BY flagtypes.name, flags.creation_date"; + + my $requestee_list = $dbh->selectall_arrayref($query . + " AND requestees.login_name = ? " . + $group_order_by, + { Slice => {} }, $user->login); + $vars->{'requestee_list'} = $requestee_list; + my $requester_list = $dbh->selectall_arrayref($query . + " AND requesters.login_name = ? " . + $group_order_by, + { Slice => {} }, $user->login); + $vars->{'requester_list'} = $requester_list; +} + +######### +# Hooks # +######### + +sub user_preferences { + my ($self, $args) = @_; + my $tab = $args->{'current_tab'}; + return unless $tab eq 'saved-searches'; + + my $save = $args->{'save_changes'}; + my $handled = $args->{'handled'}; + my $vars = $args->{'vars'}; + + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $params = Bugzilla->input_params; + + if ($save) { + my $sth_insert_fp = $dbh->prepare('INSERT INTO mydashboard + (namedquery_id, user_id) + VALUES (?, ?)'); + my $sth_delete_fp = $dbh->prepare('DELETE FROM mydashboard + WHERE namedquery_id = ? + AND user_id = ?'); + foreach my $q (@{$user->queries}, @{$user->queries_available}) { + if (defined $params->{'in_mydashboard_' . $q->id}) { + $sth_insert_fp->execute($q->id, $q->user->id) if !$q->in_mydashboard; + } + else { + $sth_delete_fp->execute($q->id, $q->user->id) if $q->in_mydashboard; + } + } + } +} + +sub webservice { + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{MyDashboard} = "Bugzilla::Extension::MyDashboard::WebService"; +} + +__PACKAGE__->NAME; diff --git a/extensions/MyDashboard/lib/TimeAgo.pm b/extensions/MyDashboard/lib/TimeAgo.pm new file mode 100644 index 000000000..f213986d6 --- /dev/null +++ b/extensions/MyDashboard/lib/TimeAgo.pm @@ -0,0 +1,182 @@ +package Bugzilla::Extension::MyDashboard::TimeAgo; + +use strict; +use utf8; +use DateTime; +use Carp; +use Exporter qw(import); + +use if $ENV{ARCH_64BIT}, 'integer'; + +our @EXPORT_OK = qw(time_ago); + +our $VERSION = '0.06'; + +my @ranges = ( + [ -1, 'in the future' ], + [ 60, 'just now' ], + [ 900, 'a few minutes ago'], # 15*60 + [ 3000, 'less than an hour ago'], # 50*60 + [ 4500, 'about an hour ago'], # 75*60 + [ 7200, 'more than an hour ago'], # 2*60*60 + [ 21600, 'several hours ago'], # 6*60*60 + [ 86400, 'today', sub { # 24*60*60 + my $time = shift; + my $now = shift; + if ( $time->day < $now->day + or $time->month < $now->month + or $time->year < $now->year + ) { + return 'yesterday' + } + if ($time->hour < 5) { + return 'tonight' + } + if ($time->hour < 10) { + return 'this morning' + } + if ($time->hour < 15) { + return 'today' + } + if ($time->hour < 19) { + return 'this afternoon' + } + return 'this evening' + }], + [ 172800, 'yesterday'], # 2*24*60*60 + [ 604800, 'this week'], # 7*24*60*60 + [ 1209600, 'last week'], # 2*7*24*60*60 + [ 2678400, 'this month', sub { # 31*24*60*60 + my $time = shift; + my $now = shift; + if ($time->year == $now->year and $time->month == $now->month) { + return 'this month' + } + return 'last month' + }], + [ 5356800, 'last month'], # 2*31*24*60*60 + [ 24105600, 'several months ago'], # 9*31*24*60*60 + [ 31536000, 'about a year ago'], # 365*24*60*60 + [ 34214400, 'last year'], # (365+31)*24*60*60 + [ 63072000, 'more than a year ago'], # 2*365*24*60*60 + [ 283824000, 'several years ago'], # 9*365*24*60*60 + [ 315360000, 'about a decade ago'], # 10*365*24*60*60 + [ 630720000, 'last decade'], # 20*365*24*60*60 + [ 2838240000, 'several decades ago'], # 90*365*24*60*60 + [ 3153600000, 'about a century ago'], # 100*365*24*60*60 + [ 6307200000, 'last century'], # 200*365*24*60*60 + [ 6622560000, 'more than a century ago'], # 210*365*24*60*60 + [ 28382400000, 'several centuries ago'], # 900*365*24*60*60 + [ 31536000000, 'about a millenium ago'], # 1000*365*24*60*60 + [ 63072000000, 'more than a millenium ago'], # 2000*365*24*60*60 +); + +sub time_ago { + my ($time, $now) = @_; + + if (not defined $time or not $time->isa('DateTime')) { + croak('DateTime::Duration::Fuzzy::time_ago needs a DateTime object as first parameter') + } + if (not defined $now) { + $now = DateTime->now(); + } + if (not $now->isa('DateTime')) { + croak('Invalid second parameter provided to DateTime::Duration::Fuzzy::time_ago; it must be a DateTime object if provided') + } + + # Use clones in UTC for safe date calculation + my $now_clone = $now->clone->set_time_zone('UTC'); + my $time_clone = $time->clone->set_time_zone('UTC'); + my $dur = $now_clone->subtract_datetime_absolute( $time_clone )->in_units('seconds'); + + foreach my $range ( @ranges ) { + if ( $dur <= $range->[0] ) { + if ( $range->[2] ) { + return $range->[2]->( $time_clone, $now_clone ) + } + return $range->[1] + } + } + + return 'millenia ago' +} + +1 + +__END__ + +=head1 NAME + +DateTime::Duration::Fuzzy -- express dates as fuzzy human-friendly strings + +=head1 SYNOPSIS + + use DateTime::Duration::Fuzzy qw(time_ago); + use DateTime; + + my $now = DateTime->new( + year => 2010, month => 12, day => 12, + hour => 19, minute => 59, + ); + my $then = DateTime->new( + year => 2010, month => 12, day => 12, + hour => 15, + ); + print time_ago($then, $now); + # outputs 'several hours ago' + + print time_ago($then); + # $now taken from C