diff options
Diffstat (limited to 'extensions/MyDashboard')
-rw-r--r-- | extensions/MyDashboard/Config.pm | 14 | ||||
-rw-r--r-- | extensions/MyDashboard/Extension.pm | 126 | ||||
-rw-r--r-- | extensions/MyDashboard/lib/Queries.pm | 259 | ||||
-rw-r--r-- | extensions/MyDashboard/lib/TimeAgo.pm | 179 | ||||
-rw-r--r-- | extensions/MyDashboard/lib/Util.pm | 50 | ||||
-rw-r--r-- | extensions/MyDashboard/lib/WebService.pm | 111 | ||||
-rw-r--r-- | extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl | 11 | ||||
-rw-r--r-- | extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl | 15 | ||||
-rw-r--r-- | extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl | 12 | ||||
-rw-r--r-- | extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl | 150 | ||||
-rw-r--r-- | extensions/MyDashboard/web/js/flags.js | 189 | ||||
-rw-r--r-- | extensions/MyDashboard/web/js/query.js | 167 | ||||
-rw-r--r-- | extensions/MyDashboard/web/styles/mydashboard.css | 73 |
13 files changed, 1356 insertions, 0 deletions
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..082f1c562 --- /dev/null +++ b/extensions/MyDashboard/Extension.pm @@ -0,0 +1,126 @@ +# 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::Saved; + +use Bugzilla::Extension::MyDashboard::Queries qw(QUERY_DEFS); + +our $VERSION = BUGZILLA_VERSION; + +################ +# 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, Bugzilla->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'; + + # require user to be logged in for this page + Bugzilla->login(LOGIN_REQUIRED); + + $vars->{queries} = [ QUERY_DEFS ]; +} + +######### +# 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}) { + if (defined $params->{'in_mydashboard_' . $q->id}) { + $sth_insert_fp->execute($q->id, $user->id) if !$q->in_mydashboard; + } + else { + $sth_delete_fp->execute($q->id, $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/Queries.pm b/extensions/MyDashboard/lib/Queries.pm new file mode 100644 index 000000000..69af01d79 --- /dev/null +++ b/extensions/MyDashboard/lib/Queries.pm @@ -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. + +package Bugzilla::Extension::MyDashboard::Queries; + +use strict; + +use Bugzilla; +use Bugzilla::Bug; +use Bugzilla::CGI; +use Bugzilla::Search; +use Bugzilla::Flag; +use Bugzilla::Status qw(is_open_state); +use Bugzilla::Util qw(format_time datetime_from); + +use Bugzilla::Extension::MyDashboard::Util qw(open_states quoted_open_states); +use Bugzilla::Extension::MyDashboard::TimeAgo qw(time_ago); + +use DateTime; + +use base qw(Exporter); +our @EXPORT = qw( + QUERY_ORDER + SELECT_COLUMNS + QUERY_DEFS + query_bugs + query_flags +); + +# Default sort order +use constant QUERY_ORDER => ("changeddate desc", "bug_id"); + +# List of columns that we will be selecting. In the future this should be configurable +# Share with buglist.cgi? +use constant SELECT_COLUMNS => qw( + bug_id + bug_status + short_desc + changeddate +); + +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' => ['UNCONFIRMED', '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 'UNCONFIRMED' && $_->name ne 'NEW', 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 + } + }); + } + + if ($user->showmybugslink) { + my $query = Bugzilla->params->{mybugstemplate}; + my $login = $user->login; + $query =~ s/%userid%/$login/; + $query =~ s/^buglist.cgi\?//; + push(@query_defs, { + name => 'mybugs', + heading => "My Bugs", + saved => 1, + params => $query, + }); + } + + foreach my $q (@{$user->queries}) { + next if !$q->in_mydashboard; + push(@query_defs, { name => $q->name, + saved => 1, + params => $q->url }); + } + + return @query_defs; +} + +sub query_bugs { + my $qdef = shift; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $date_now = DateTime->now(time_zone => $user->timezone); + + ## HACK to remove POST + delete $ENV{REQUEST_METHOD}; + + my $params = new Bugzilla::CGI($qdef->{params}); + + my $search = new Bugzilla::Search( fields => [ SELECT_COLUMNS ], + params => scalar $params->Vars, + order => [ QUERY_ORDER ]); + my $data = $search->data; + + my @bugs; + foreach my $row (@$data) { + my $bug = {}; + foreach my $column (SELECT_COLUMNS) { + $bug->{$column} = shift @$row; + if ($column eq 'changeddate') { + $bug->{$column} = format_time($bug->{$column}); + my $date_then = datetime_from($bug->{$column}); + $bug->{'changeddate_fancy'} = time_ago($date_then, $date_now); + } + } + push(@bugs, $bug); + } + + return (\@bugs, $params->canonicalise_query()); +} + +sub query_flags { + my ($type, $include_closed) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + my $date_now = DateTime->now(time_zone => $user->timezone); + + ($type ne 'requestee' || $type ne 'requester') + || ThrowCodeError('param_required', { param => 'type' }); + + my $match_params = { status => '?' }; + + if ($type eq 'requestee') { + $match_params->{'requestee_id'} = $user->id; + } + else { + $match_params->{'setter_id'} = $user->id; + } + + my $matched = Bugzilla::Flag->match($match_params); + + return [] if !@$matched; + + my @unfiltered_flags; + my %all_bugs; # Use hash to filter out duplicates + foreach my $flag (@$matched) { + next if ($flag->attach_id && $flag->attachment->isprivate && !$user->is_insider); + + my $data = { + id => $flag->id, + type => $flag->type->name, + status => $flag->status, + attach_id => $flag->attach_id, + is_patch => $flag->attach_id ? $flag->attachment->ispatch : 0, + bug_id => $flag->bug_id, + requester => $flag->setter->login, + requestee => $flag->requestee ? $flag->requestee->login : '', + updated => $flag->modification_date, + }; + push(@unfiltered_flags, $data); + + # Record bug id for later retrieval of status/summary + $all_bugs{$flag->{'bug_id'}}++; + } + + # Filter the bug list based on permission to see the bug + my %visible_bugs = map { $_ => 1 } @{ $user->visible_bugs([ keys %all_bugs ]) }; + + return [] if !scalar keys %visible_bugs; + + # Get all bug statuses and summaries in one query instead of loading + # many separate bug objects + my $bug_rows = $dbh->selectall_arrayref("SELECT bug_id, bug_status, short_desc + FROM bugs + WHERE " . $dbh->sql_in('bug_id', [ keys %visible_bugs ]), + { Slice => {} }); + foreach my $row (@$bug_rows) { + $visible_bugs{$row->{'bug_id'}} = { + bug_status => $row->{'bug_status'}, + short_desc => $row->{'short_desc'} + }; + } + + # Now drop out any flags for bugs the user cannot see + # or if the user did not want to see closed bugs + my @filtered_flags; + foreach my $flag (@unfiltered_flags) { + # Skip this flag if the bug is not visible to the user + next if !$visible_bugs{$flag->{'bug_id'}}; + + # Skip closed unless user requested closed bugs + next if (!$include_closed + && !is_open_state($visible_bugs{$flag->{'bug_id'}}->{'bug_status'})); + + # Include bug status and summary with each flag + $flag->{'bug_status'} = $visible_bugs{$flag->{'bug_id'}}->{'bug_status'}; + $flag->{'bug_summary'} = $visible_bugs{$flag->{'bug_id'}}->{'short_desc'}; + + # Format the updated date specific to the user's timezone + # and add the fancy human readable version + $flag->{'updated'} = format_time($flag->{'updated'}); + my $date_then = datetime_from($flag->{'updated'}); + $flag->{'updated_epoch'} = $date_then->epoch; + $flag->{'updated_fancy'} = time_ago($date_then, $date_now); + + push(@filtered_flags, $flag); + } + + return [] if !@filtered_flags; + + # Sort by most recently updated + return [ sort { $b->{'updated_epoch'} <=> $a->{'updated_epoch'} } @filtered_flags ]; +} + +1; diff --git a/extensions/MyDashboard/lib/TimeAgo.pm b/extensions/MyDashboard/lib/TimeAgo.pm new file mode 100644 index 000000000..78badf5fa --- /dev/null +++ b/extensions/MyDashboard/lib/TimeAgo.pm @@ -0,0 +1,179 @@ +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') + } + + my $dur = $now->subtract_datetime_absolute($time)->in_units('seconds'); + + foreach my $range ( @ranges ) { + if ( $dur <= $range->[0] ) { + if ( $range->[2] ) { + return $range->[2]->($time, $now) + } + 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<time> function + +=head1 DESCRIPTION + +DateTime::Duration::Fuzzy is inspired from the timeAgo jQuery module +L<http://timeago.yarp.com/>. + +It takes two DateTime objects -- first one representing a moment in the past +and second optional one representine the present, and returns a human-friendly +fuzzy expression of the time gone. + +=head2 functions + +=over 4 + +=item time_ago($then, $now) + +The only exportable function. + +First obligatory parameter is a DateTime object. + +Second optional parameter is also a DateTime object. +If it's not provided, then I<now> as the C<time> function returns is +substituted. + +Returns a string expression of the interval between the two DateTime +objects, like C<several hours ago>, C<yesterday> or <last century>. + +=back + +=head2 performance + +On 64bit machines, it is asvisable to 'use integer', which makes +the calculations faster. You can turn this on by setting the +C<ARCH_64BIT> environmental variable to a true value. + +If you do this on a 32bit machine, you will get wrong results for +intervals starting with "several decades ago". + +=head1 AUTHOR + +Jan Oldrich Kruza, C<< <sixtease at cpan.org> >> + +=head1 LICENSE AND COPYRIGHT + +Copyright 2010 Jan Oldrich Kruza. + +This program is free software; you can redistribute it and/or modify it +under the terms of either: the GNU General Public License as published +by the Free Software Foundation; or the Artistic License. + +See http://dev.perl.org/licenses/ for more information. + +=cut diff --git a/extensions/MyDashboard/lib/Util.pm b/extensions/MyDashboard/lib/Util.pm new file mode 100644 index 000000000..fa7cf83b0 --- /dev/null +++ b/extensions/MyDashboard/lib/Util.pm @@ -0,0 +1,50 @@ +# 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::Util; + +use strict; + +use Bugzilla::CGI; +use Bugzilla::Search; +use Bugzilla::Status; + +use base qw(Exporter); +@Bugzilla::Extension::MyDashboard::Util::EXPORT = qw( + open_states + closed_states + quoted_open_states + quoted_closed_states +); + +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; +} + +1; diff --git a/extensions/MyDashboard/lib/WebService.pm b/extensions/MyDashboard/lib/WebService.pm new file mode 100644 index 000000000..69f43f322 --- /dev/null +++ b/extensions/MyDashboard/lib/WebService.pm @@ -0,0 +1,111 @@ +# 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::WebService; + +use strict; +use warnings; + +use base qw(Bugzilla::WebService Bugzilla::WebService::Bug); + +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Util qw(detaint_natural trick_taint template_var); +use Bugzilla::WebService::Util qw(validate); + +use Bugzilla::Extension::MyDashboard::Queries qw(QUERY_DEFS query_bugs query_flags); + +use constant READ_ONLY => qw( + run_bug_query + run_flag_query +); + +sub run_bug_query { + my($self, $params) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->login(LOGIN_REQUIRED); + + defined $params->{query} + || ThrowCodeError('param_required', + { function => 'MyDashboard.run_bug_query', + param => 'query' }); + + my $result; + foreach my $qdef (QUERY_DEFS) { + next if $qdef->{name} ne $params->{query}; + my ($bugs, $query_string) = query_bugs($qdef); + + # Add last changes to each bug + foreach my $b (@$bugs) { + my $last_changes = {}; + my $activity = $self->history({ ids => [ $b->{bug_id} ], + start_time => $b->{changeddate} }); + if (@{$activity->{bugs}[0]{history}}) { + my $change_set = $activity->{bugs}[0]{history}[0]; + $last_changes->{activity} = $change_set->{changes}; + foreach my $change (@{ $last_changes->{activity} }) { + $change->{field_desc} + = template_var('field_descs')->{$change->{field_name}} || $change->{field_name}; + } + $last_changes->{email} = $change_set->{who}; + $last_changes->{when} = $self->datetime_format_inbound($change_set->{when}); + } + my $last_comment_id = $dbh->selectrow_array(" + SELECT comment_id FROM longdescs + WHERE bug_id = ? AND bug_when >= ?", + undef, $b->{bug_id}, $b->{changeddate}); + if ($last_comment_id) { + my $comments = $self->comments({ comment_ids => [ $last_comment_id ] }); + my $comment = $comments->{comments}{$last_comment_id}; + $last_changes->{comment} = $comment->{text}; + $last_changes->{email} = $comment->{creator} if !$last_changes->{email}; + $last_changes->{when} + = $self->datetime_format_inbound($comment->{creation_time}) if !$last_changes->{when}; + } + $b->{last_changes} = $last_changes; + } + + $query_string =~ s/^POSTDATA=&//; + $qdef->{bugs} = $bugs; + $qdef->{buffer} = $query_string; + $result = $qdef; + last; + } + + return { result => $result }; +} + +sub run_flag_query { + my ($self, $params) =@_; + my $user = Bugzilla->login(LOGIN_REQUIRED); + + my $type = $params->{type}; + $type || ThrowCodeError('param_required', + { function => 'MyDashboard.run_flag_query', + param => 'type' }); + + my $include_closed = $params->{include_closed} || 0; + my $results = query_flags($type, $include_closed); + + return { result => { $type => $results }}; +} + +1; + +__END__ + +=head1 NAME + +Bugzilla::Extension::MyDashboard::Webservice - The MyDashboard WebServices API + +=head1 DESCRIPTION + +This module contains API methods that are useful to user's of bugzilla.mozilla.org. + +=head1 METHODS + +See L<Bugzilla::WebService> for a description of how parameters are passed, +and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean. diff --git a/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl b/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl new file mode 100644 index 000000000..c822ab040 --- /dev/null +++ b/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-header.html.tmpl @@ -0,0 +1,11 @@ +[%# 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. + #%] + +<th> + My Dashboard +</th> diff --git a/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl b/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl new file mode 100644 index 000000000..cd6a36705 --- /dev/null +++ b/extensions/MyDashboard/template/en/default/hook/account/prefs/saved-searches-saved-row.html.tmpl @@ -0,0 +1,15 @@ +[%# 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. + #%] + +<td align="center"> + <input type="checkbox" + name="in_mydashboard_[% q.id FILTER html %]" + value="1" + alt="[% q.name FILTER html %]" + [% " checked" IF q.in_mydashboard %]> +</td> diff --git a/extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl b/extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.html.tmpl new file mode 100644 index 000000000..518743ccf --- /dev/null +++ b/extensions/MyDashboard/template/en/default/hook/global/common-links-action-links.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 user.login %] + <li><span class="separator"> | </span> + <a href="[% urlbase FILTER none %]page.cgi?id=mydashboard.html">My Dashboard</a></li> +[% END %] diff --git a/extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl b/extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl new file mode 100644 index 000000000..8222f6749 --- /dev/null +++ b/extensions/MyDashboard/template/en/default/pages/mydashboard.html.tmpl @@ -0,0 +1,150 @@ +[%# 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 %] + +[% PROCESS global/header.html.tmpl + title = "My Dashboard" + style_urls = [ "extensions/MyDashboard/web/styles/mydashboard.css", + "extensions/ProdCompSearch/web/styles/prod_comp_search.css" ] + javascript_urls = [ "js/yui3/yui/yui-min.js", + "extensions/MyDashboard/web/js/query.js", + "extensions/MyDashboard/web/js/flags.js", + "extensions/ProdCompSearch/web/js/prod_comp_search.js" ] +%] + +[% standard_queries = [] %] +[% saved_queries = [] %] +[% FOREACH q = queries %] + [% standard_queries.push(q) IF !q.saved %] + [% saved_queries.push(q) IF q.saved %] +[% END %] + +<script id="last-changes-template" type="text/x-handlebars-template"> + <div id="last_changes"> + {{#if email}} + <div id="last_changes_header"> + Last Changes :: {{email}} :: {{when}} + </div> + {{#if activity}} + <table id="activity"> + {{#each activity}} + <tr> + <td class="field_label">{{field_desc}}:</td> + <td class="field_data"> + {{#if removed}} + {{#unless added}} + Removed: + {{/unless}} + {{removed}} + {{/if}} + {{#if added}} + {{#if removed}} + → + {{/if}} + {{/if}} + {{#if added}} + {{#unless removed}} + Added: + {{/unless}} + {{added}} + {{/if}} + </td> + </tr> + {{/each}} + </table> + {{/if}} + {{#if comment}} + <pre class='bz_comment_text'>{{comment}}</pre> + {{/if}} + {{else}} + This is a new [% terms.bug %] and no changes have been made yet. + {{/if}} + </div> +</script> + +<script type="text/javascript"> + [% IF Param('splinter_base') %] + MyDashboard.splinter_base = '[% Bugzilla.splinter_review_base FILTER js %]'; + [% END %] +</script> + +<div id="mydashboard"> + <div class="yui3-skin-sam"> + <div id="left"> + <div id="query_list_container"> + Choose query: + <select id="query" name="query"> + <optgroup id="standard_queries" label="Standard"> + [% FOREACH r = standard_queries %] + <option value="[% r.name FILTER html %]">[% r.heading || r.name FILTER html %]</option> + [% END%] + </optgroup> + <optgroup id="saved_queries" label="Saved"> + [% FOREACH r = saved_queries %] + <option value="[% r.name FILTER html %]">[% r.heading || r.name FILTER html %]</option> + [% END %] + </optgroup> + </select> + <small> + (<a href="userprefs.cgi?tab=saved-searches">add or remove saved searches</a>) + </small> + </div> + + <div id="query_container"> + <div class="query_heading"></div> + <div class="query_description"></div> + <span id="query_count_refresh" class="bz_default_hidden"> + <span class="items_found" id="query_bugs_found">0 [% terms.bugs %] found</span> + | <a class="refresh" href="javascript:void(0);" id="query_refresh">Refresh</a> + </span> + <div id="query_pagination_top"></div> + <div id="query_table"></div> + </div> + </div> + + <div id="right"> + <div id="prod_comp_search_main"> + [% PROCESS prodcompsearch/form.html.tmpl + input_label = "File a $terms.Bug:" + script_name = "enter_bug.cgi" + new_tab = 1 + %] + </div> + + <div id="requestee_container"> + <div class="query_heading"> + Flags Requested of You + </div> + <span id="requestee_count_refresh" class="bz_default_hidden"> + <span class="items_found" id="requestee_flags_found">0 flags found</span> + | <a class="refresh" href="javascript:void(0);" id="requestee_refresh">Refresh</a> + | <input type="checkbox" id="requestee_closed"> + <label for="requestee_closed">Include Closed</label> + </span> + <div id="requestee_table"></div> + </div> + + <div id="requester_container"> + <div class="query_heading"> + Flags You Have Requested + </div> + <span id="requester_count_refresh" class="bz_default_hidden"> + <span class="items_found" id="requester_flags_found">0 flags found</span> + | <a class="refresh" href="javascript:void(0);" id="requester_refresh">Refresh</a> + | <input type="checkbox" id="requester_closed"> + <label for="requester_closed">Include Closed</label> + </span> + <div id="requester_table"></div> + </div> + </div> + <div style="clear:both;"></div> + </div> +</div> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/MyDashboard/web/js/flags.js b/extensions/MyDashboard/web/js/flags.js new file mode 100644 index 000000000..9980c00fb --- /dev/null +++ b/extensions/MyDashboard/web/js/flags.js @@ -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. + */ + +// Flag tables +YUI({ + base: 'js/yui3/', + combine: false +}).use("node", "datatable", "datatable-sort", "json-stringify", "escape", + "datatable-datasource", "datasource-io", "datasource-jsonschema", function (Y) { + // Common + var counter = 0; + var dataSource = { + requestee: null, + requester: null + }; + var dataTable = { + requestee: null, + requester: null + }; + + var updateFlagTable = function (type) { + if (!type) return; + + var include_closed = Y.one('#' + type + '_closed').get('checked') ? 1 : 0; + + counter = counter + 1; + + var callback = { + success: function(e) { + if (e.response) { + Y.one('#' + type + '_count_refresh').removeClass('bz_default_hidden'); + Y.one("#" + type + "_flags_found").setHTML( + e.response.results.length + ' flags found'); + dataTable[type].set('data', e.response.results); + } + }, + failure: function(o) { + var resp = o.responseText; + alert("IO request failed : " + resp); + } + }; + + var json_object = { + version: "1.1", + method: "MyDashboard.run_flag_query", + id: counter, + params: { type : type, include_closed: include_closed } + }; + + var stringified = Y.JSON.stringify(json_object); + + Y.one('#' + type + '_count_refresh').addClass('bz_default_hidden'); + + dataTable[type].set('data', []); + dataTable[type].render("#" + type + "_table"); + dataTable[type].showMessage('loadingMessage'); + + dataSource[type].sendRequest({ + request: stringified, + cfg: { + method: "POST", + headers: { 'Content-Type': 'application/json' } + }, + callback: callback + }); + }; + + var bugLinkFormatter = function (o) { + var bug_closed = ""; + if (o.data.bug_status == 'RESOLVED' || o.data.bug_status == 'VERIFIED') { + bug_closed = "bz_closed"; + } + return '<a href="show_bug.cgi?id=' + encodeURIComponent(o.value) + + '" target="_blank" ' + 'title="' + Y.Escape.html(o.data.bug_status) + ' - ' + + Y.Escape.html(o.data.bug_summary) + '" class="' + Y.Escape.html(bug_closed) + + '">' + o.value + '</a>'; + }; + + var updatedFormatter = function (o) { + return '<span title="' + Y.Escape.html(o.value) + '">' + + Y.Escape.html(o.data.updated_fancy) + '</span>'; + }; + + var requesteeFormatter = function (o) { + return o.value + ? Y.Escape.html(o.value) + : '<i>anyone</i>'; + }; + + var flagNameFormatter = function (o) { + if (o.data.attach_id && o.data.is_patch && MyDashboard.splinter_base) { + return '<a href="' + MyDashboard.splinter_base + + (MyDashboard.splinter_base.indexOf('?') == -1 ? '?' : '&') + + 'bug=' + encodeURIComponent(o.data.bug_id) + + '&attachment=' + encodeURIComponent(o.data.attach_id) + + '" title="Review this patch">' + + Y.Escape.html(o.value) + '</a>'; + } + else { + return Y.Escape.html(o.value); + } + }; + + // Requestee + dataSource.requestee = new Y.DataSource.IO({ source: 'jsonrpc.cgi' }); + dataTable.requestee = new Y.DataTable({ + columns: [ + { key: "requester", label: "Requester", sortable: true }, + { key: "type", label: "Flag", sortable: true, + formatter: flagNameFormatter, allowHTML: true }, + { key: "bug_id", label: "Bug", sortable: true, + formatter: bugLinkFormatter, allowHTML: true }, + { key: "updated", label: "Updated", sortable: true, + formatter: updatedFormatter, allowHTML: true } + ], + strings: { + emptyMessage: 'No flag data found.', + } + }); + + dataTable.requestee.plug(Y.Plugin.DataTableSort); + + dataTable.requestee.plug(Y.Plugin.DataTableDataSource, { + datasource: dataSource, + initialRequest: updateFlagTable("requestee"), + }); + + dataSource.requestee.plug(Y.Plugin.DataSourceJSONSchema, { + schema: { + resultListLocator: "result.result.requestee", + resultFields: ["requester", "type", "attach_id", "is_patch", "bug_id", + "bug_status", "bug_summary", "updated", "updated_fancy"] + } + }); + + dataTable.requestee.render("#requestee_table"); + + Y.one('#requestee_refresh').on('click', function(e) { + updateFlagTable('requestee'); + }); + Y.one('#requestee_closed').on('change', function(e) { + updateFlagTable('requestee'); + }); + + // Requester + dataSource.requester = new Y.DataSource.IO({ source: 'jsonrpc.cgi' }); + dataTable.requester = new Y.DataTable({ + columns: [ + { key:"requestee", label:"Requestee", sortable:true, + formatter: requesteeFormatter, allowHTML: true }, + { key:"type", label:"Flag", sortable:true, + formatter: flagNameFormatter, allowHTML: true }, + { key:"bug_id", label:"Bug", sortable:true, + formatter: bugLinkFormatter, allowHTML: true }, + { key: "updated", label: "Updated", sortable: true, + formatter: updatedFormatter, allowHTML: true } + ], + strings: { + emptyMessage: 'No flag data found.', + } + }); + + dataTable.requester.plug(Y.Plugin.DataTableSort); + + dataTable.requester.plug(Y.Plugin.DataTableDataSource, { + datasource: dataSource, + initialRequest: updateFlagTable("requester"), + }); + + dataSource.requester.plug(Y.Plugin.DataSourceJSONSchema, { + schema: { + resultListLocator: "result.result.requester", + resultFields: ["requestee", "type", "attach_id", "is_patch", "bug_id", + "bug_status", "bug_summary", "updated", "updated_fancy"] + } + }); + + Y.one('#requester_refresh').on('click', function(e) { + updateFlagTable('requester'); + }); + Y.one('#requester_closed').on('change', function(e) { + updateFlagTable('requester'); + }); +}); diff --git a/extensions/MyDashboard/web/js/query.js b/extensions/MyDashboard/web/js/query.js new file mode 100644 index 000000000..7d25cc33e --- /dev/null +++ b/extensions/MyDashboard/web/js/query.js @@ -0,0 +1,167 @@ +/* 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 (typeof(MyDashboard) == 'undefined') { + var MyDashboard = {}; +} + +// Main query code +YUI({ + base: 'js/yui3/', + combine: false, + groups: { + gallery: { + combine: false, + base: 'js/yui3/', + patterns: { 'gallery-': {} } + } + } +}).use("node", "datatable", "datatable-sort", "datatable-message", "json-stringify", + "datatable-datasource", "datasource-io", "datasource-jsonschema", "cookie", + "gallery-datatable-row-expansion-bmo", "handlebars", "escape", function (Y) { + var counter = 0, + dataSource = null, + dataTable = null, + default_query = "assignedbugs"; + + // Grab last used query name from cookie or use default + var query_cookie = Y.Cookie.get("my_dashboard_query"); + if (query_cookie) { + var cookie_value_found = 0; + Y.one("#query").get("options").each( function() { + if (this.get("value") == query_cookie) { + this.set('selected', true); + default_query = query_cookie; + cookie_value_found = 1; + } + }); + if (!cookie_value_found) { + Y.Cookie.set("my_dashboard_query", ""); + } + } + + var updateQueryTable = function(query_name) { + if (!query_name) return; + + counter = counter + 1; + + var callback = { + success: function(e) { + if (e.response) { + Y.one('#query_count_refresh').removeClass('bz_default_hidden'); + Y.one("#query_container .query_description").setHTML(e.response.meta.description); + Y.one("#query_container .query_heading").setHTML(e.response.meta.heading); + Y.one("#query_bugs_found").setHTML( + '<a href="buglist.cgi?' + e.response.meta.buffer + + '" target="_blank">' + e.response.results.length + ' bugs found</a>'); + dataTable.set('data', e.response.results); + } + }, + failure: function(o) { + var resp = o.responseText; + alert("IO request failed : " + resp); + } + }; + + var json_object = { + version: "1.1", + method: "MyDashboard.run_bug_query", + id: counter, + params: { query : query_name } + }; + + var stringified = Y.JSON.stringify(json_object); + + Y.one('#query_count_refresh').addClass('bz_default_hidden'); + + dataTable.set('data', []); + dataTable.render("#query_table"); + dataTable.showMessage('loadingMessage'); + + dataSource.sendRequest({ + request: stringified, + cfg: { + method: "POST", + headers: { 'Content-Type': 'application/json' } + }, + callback: callback + }); + }; + + var updatedFormatter = function (o) { + return '<span title="' + Y.Escape.html(o.value) + '">' + + Y.Escape.html(o.data.changeddate_fancy) + '</span>'; + }; + + dataSource = new Y.DataSource.IO({ source: 'jsonrpc.cgi' }); + + dataSource.plug(Y.Plugin.DataSourceJSONSchema, { + schema: { + resultListLocator: "result.result.bugs", + resultFields: ["bug_id", "changeddate", "changeddate_fancy", + "bug_status", "short_desc", "last_changes"], + metaFields: { + description: "result.result.description", + heading: "result.result.heading", + buffer: "result.result.buffer" + } + } + }); + + dataTable = new Y.DataTable({ + columns: [ + { key: Y.Plugin.DataTableRowExpansion.column_key, label: ' ', sortable: false }, + { key: "bug_id", label: "Bug", allowHTML: true, + formatter: '<a href="show_bug.cgi?id={value}" target="_blank">{value}</a>' }, + { key: "changeddate", label: "Updated", formatter: updatedFormatter, allowHTML: true }, + { key: "bug_status", label: "Status" }, + { key: "short_desc", label: "Summary" }, + ], + sortable: true + }); + + var last_changes_source = Y.one('#last-changes-template').getHTML(), + last_changes_template = Y.Handlebars.compile(last_changes_source); + + dataTable.plug(Y.Plugin.DataTableRowExpansion, { + uniqueIdKey: 'bug_id', + template: function(data) { + var last_changes = {}; + if (data.last_changes.email) { + last_changes = { + activity: data.last_changes.activity, + email: data.last_changes.email, + when: data.last_changes.when, + comment: data.last_changes.comment, + }; + } + return last_changes_template(last_changes); + } + }); + + dataTable.plug(Y.Plugin.DataTableSort); + + dataTable.plug(Y.Plugin.DataTableDataSource, { + datasource: dataSource, + initialRequest: updateQueryTable(default_query), + }); + + Y.one('#query').on('change', function(e) { + var index = e.target.get('selectedIndex'); + var selected_value = e.target.get("options").item(index).getAttribute('value'); + updateQueryTable(selected_value); + Y.Cookie.set("my_dashboard_query", selected_value, { expires: new Date("January 12, 2025") }); + }); + + Y.one('#query_refresh').on('click', function(e) { + var query_select = Y.one('#query'); + var index = query_select.get('selectedIndex'); + var selected_value = query_select.get("options").item(index).getAttribute('value'); + updateQueryTable(selected_value); + }); +}); diff --git a/extensions/MyDashboard/web/styles/mydashboard.css b/extensions/MyDashboard/web/styles/mydashboard.css new file mode 100644 index 000000000..050fd95c2 --- /dev/null +++ b/extensions/MyDashboard/web/styles/mydashboard.css @@ -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. */ + +#mydashboard { + min-width: 900px; +} + +#mydashboard .yui3-skin-sam .yui3-datatable-table { + width: 100%; +} + +.yui3-datatable-col-changeddate, +.yui3-datatable-col-created { + white-space: nowrap; +} + +.query_heading { + font-size: 18px; + font-weight: strong; + padding-bottom: 5px; + padding-top: 5px; + color: rgb(72, 72, 72); +} + +.query_description { + font-size: 90%; + font-style: italic; + padding-bottom: 5px; + color: rgb(109, 117, 129); +} + +#mydashboard_container { + margin: 0 auto; +} + +#left { + float: left; + width: 58%; +} + +#right { + float: right; + width: 40%; +} + +.items_found, .refresh { + font-size: 80%; +} + +#query_list_container { + text-align:center; +} + +#query_list_container, +#prod_comp_search_main { + padding: 20px !important; + height: 40px; +} + +#last_changes_header { + font-size: 12px; + font-weight: bold; + padding-bottom: 5px; + border-bottom: 1px solid rgb(200, 200, 186); +} + +#last_changes .field_label { + text-align: left; +} |