diff options
Diffstat (limited to 'extensions/MyDashboard/lib')
-rw-r--r-- | extensions/MyDashboard/lib/Queries.pm | 251 | ||||
-rw-r--r-- | extensions/MyDashboard/lib/TimeAgo.pm | 181 | ||||
-rw-r--r-- | extensions/MyDashboard/lib/Util.pm | 50 | ||||
-rw-r--r-- | extensions/MyDashboard/lib/WebService.pm | 111 |
4 files changed, 593 insertions, 0 deletions
diff --git a/extensions/MyDashboard/lib/Queries.pm b/extensions/MyDashboard/lib/Queries.pm new file mode 100644 index 000000000..e8813989b --- /dev/null +++ b/extensions/MyDashboard/lib/Queries.pm @@ -0,0 +1,251 @@ +# 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::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 $date_now = DateTime->now; + + ## 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}, '%Y-%m-%d %H:%M'); + 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 = shift; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + my $date_now = DateTime->now; + + ($type ne 'requestee' || $type ne 'requester') + || ThrowCodeError('param_required', { param => 'type' }); + + 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.bug_status AS bug_status, + bugs.short_desc AS bug_summary, + flags.attach_id AS attach_id, + attachments.description AS attach_summary, + requesters.login_name AS requester, + requestees.login_name 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 flags.creation_date, flagtypes.name"; + + my $flags = []; + if ($type eq 'requestee') { + $flags = $dbh->selectall_arrayref($query . + " AND requestees.login_name = ? " . + $group_order_by, + { Slice => {} }, $user->login); + } + + if ($type eq 'requester') { + $flags = $dbh->selectall_arrayref($query . + " AND requesters.login_name = ? " . + $group_order_by, + { Slice => {} }, $user->login); + } + + # Format the created date specific to the user's timezone and add the fancy version + foreach my $flag (@$flags) { + $flag->{'created'} = format_time($flag->{'created'}, '%Y-%m-%d %H:%M'); + my $date_then = datetime_from($flag->{'created'}); + $flag->{'created_fancy'} = time_ago($date_then, $date_now); + } + + return $flags; +} + +1; diff --git a/extensions/MyDashboard/lib/TimeAgo.pm b/extensions/MyDashboard/lib/TimeAgo.pm new file mode 100644 index 000000000..b870a292e --- /dev/null +++ b/extensions/MyDashboard/lib/TimeAgo.pm @@ -0,0 +1,181 @@ +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 $now_clone = $now->clone->set_time_zone(Bugzilla->user->timezone); + my $time_clone = $time->clone->set_time_zone(Bugzilla->user->timezone); + 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<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..1f3b7ce06 --- /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); + + defined $params->{type} + || ThrowCodeError('param_required', + { function => 'MyDashboard.run_flag_query', + param => 'type' }); + + my $type = $params->{type}; + my $results = query_flags($type); + + 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. |