summaryrefslogtreecommitdiffstats
path: root/extensions/MyDashboard/lib
diff options
context:
space:
mode:
Diffstat (limited to 'extensions/MyDashboard/lib')
-rw-r--r--extensions/MyDashboard/lib/Queries.pm251
-rw-r--r--extensions/MyDashboard/lib/TimeAgo.pm181
-rw-r--r--extensions/MyDashboard/lib/Util.pm50
-rw-r--r--extensions/MyDashboard/lib/WebService.pm111
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.