summaryrefslogtreecommitdiffstats
path: root/Bugzilla
diff options
context:
space:
mode:
Diffstat (limited to 'Bugzilla')
-rw-r--r--Bugzilla/Bug.pm18
-rw-r--r--Bugzilla/BugUserLastVisit.pm76
-rw-r--r--Bugzilla/Config/Admin.pm7
-rw-r--r--Bugzilla/Field.pm2
-rw-r--r--Bugzilla/Install/Filesystem.pm1
-rw-r--r--Bugzilla/Search.pm189
-rw-r--r--Bugzilla/User.pm53
-rw-r--r--Bugzilla/WebService/BugUserLastVisit.pm208
-rw-r--r--Bugzilla/WebService/Constants.pm1
-rw-r--r--Bugzilla/WebService/Server/REST.pm1
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm52
11 files changed, 539 insertions, 69 deletions
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index 344939333..fe259be27 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -50,6 +50,7 @@ use Bugzilla::Group;
use Bugzilla::Status;
use Bugzilla::Comment;
use Bugzilla::BugUrl;
+use Bugzilla::BugUserLastVisit;
use List::MoreUtils qw(firstidx uniq part);
use List::Util qw(min max first);
@@ -4143,6 +4144,23 @@ sub LogActivityEntry {
}
}
+# Update bug_user_last_visit table
+sub update_user_last_visit {
+ my ($self, $user, $last_visit_ts) = @_;
+ my $lv = Bugzilla::BugUserLastVisit->match({ bug_id => $self->id,
+ user_id => $user->id })->[0];
+
+ if ($lv) {
+ $lv->set(last_visit_ts => $last_visit_ts);
+ $lv->update;
+ }
+ else {
+ Bugzilla::BugUserLastVisit->create({ bug_id => $self->id,
+ user_id => $user->id,
+ last_visit_ts => $last_visit_ts });
+ }
+}
+
# Convert WebService API and email_in.pl field names to internal DB field
# names.
sub map_fields {
diff --git a/Bugzilla/BugUserLastVisit.pm b/Bugzilla/BugUserLastVisit.pm
new file mode 100644
index 000000000..e8e483405
--- /dev/null
+++ b/Bugzilla/BugUserLastVisit.pm
@@ -0,0 +1,76 @@
+package Bugzilla::BugUserLastVisit;
+
+use 5.10.1;
+use strict;
+
+use parent qw(Bugzilla::Object);
+
+#####################################################################
+# Overriden Constants that are used as methods
+#####################################################################
+
+use constant DB_TABLE => 'bug_user_last_visit';
+use constant DB_COLUMNS => qw( id user_id bug_id last_visit_ts );
+use constant UPDATE_COLUMNS => qw( last_visit_ts );
+use constant VALIDATORS => {};
+use constant LIST_ORDER => 'id';
+use constant NAME_FIELD => 'id';
+
+# turn off auditing and exclude these objects from memcached
+use constant { AUDIT_CREATES => 0,
+ AUDIT_UPDATES => 0,
+ AUDIT_REMOVES => 0,
+ USE_MEMCACHED => 0 };
+
+#####################################################################
+# Provide accessors for our columns
+#####################################################################
+
+sub id { return $_[0]->{id} }
+sub bug_id { return $_[0]->{bug_id} }
+sub user_id { return $_[0]->{user_id} }
+sub last_visit_ts { return $_[0]->{last_visit_ts} }
+
+1;
+__END__
+
+=head1 NAME
+
+Bugzilla::BugUserLastVisit - Model for BugUserLastVisit bug search data
+
+=head1 SYNOPSIS
+
+ use Bugzilla::BugUserLastVisit;
+
+ my $lv = Bugzilla::BugUserLastVisit->new($id);
+
+ # Class Functions
+ $user = Bugzilla::BugUserLastVisit->create({
+ bug_id => $bug_id,
+ user_id => $user_id,
+ last_visit_ts => $last_visit_ts
+ });
+
+=head1 DESCRIPTION
+
+This package handles Bugzilla BugUserLastVisit.
+
+C<Bugzilla::BugUserLastVisit> is an implementation of L<Bugzilla::Object>, and
+thus provides all the methods of L<Bugzilla::Object> in addition to the methods
+listed below.
+
+=head1 METHODS
+
+=head2 Accessor Methods
+
+=over
+
+=item C<id>
+
+=item C<bug_id>
+
+=item C<user_id>
+
+=item C<last_visit_ts>
+
+=back
diff --git a/Bugzilla/Config/Admin.pm b/Bugzilla/Config/Admin.pm
index e6141cf9e..769e3170b 100644
--- a/Bugzilla/Config/Admin.pm
+++ b/Bugzilla/Config/Admin.pm
@@ -56,6 +56,13 @@ sub get_param_list {
name => 'allowuserdeletion',
type => 'b',
default => 0
+ },
+
+ {
+ name => 'last_visit_keep_days',
+ type => 't',
+ default => 10,
+ checker => \&check_numeric
});
return @param_list;
}
diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm
index 3e69d152d..0a26b9320 100644
--- a/Bugzilla/Field.pm
+++ b/Bugzilla/Field.pm
@@ -266,6 +266,8 @@ use constant DEFAULT_FIELDS => (
{name => 'see_also', desc => "See Also",
type => FIELD_TYPE_BUG_URLS},
{name => 'tag', desc => 'Tags'},
+ {name => 'last_visit_ts', desc => 'Last Visit', buglist => 1,
+ type => FIELD_TYPE_DATETIME},
{name => 'comment_tag', desc => 'Comment Tag'},
);
diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm
index fc05aaacd..9c2d3026f 100644
--- a/Bugzilla/Install/Filesystem.pm
+++ b/Bugzilla/Install/Filesystem.pm
@@ -163,6 +163,7 @@ sub FILESYSTEM {
'sentry.pl' => { perms => WS_EXECUTE },
'metrics.pl' => { perms => WS_EXECUTE },
'install-module.pl' => { perms => OWNER_EXECUTE },
+ 'clean-bug-user-last-visit.pl' => { perms => WS_EXECUTE },
'Bugzilla.pm' => { perms => CGI_READ },
"$localconfig*" => { perms => CGI_READ },
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm
index 0665fbd96..519ebe69f 100644
--- a/Bugzilla/Search.pm
+++ b/Bugzilla/Search.pm
@@ -349,6 +349,10 @@ use constant OPERATOR_FIELD_OVERRIDE => {
changedafter => \&_work_time_changedbefore_after,
_default => \&_work_time,
},
+ last_visit_ts => {
+ _non_changed => \&_last_visit_ts,
+ _default => \&_last_visit_ts_invalid_operator,
+ },
# Custom Fields
FIELD_TYPE_FREETEXT, { _non_changed => \&_nullable },
@@ -376,6 +380,10 @@ sub SPECIAL_PARSING {
creation_ts => \&_datetime_translate,
deadline => \&_date_translate,
delta_ts => \&_datetime_translate,
+
+ # last_visit field that accept both a 1d, 1w, 1m, 1y format and the
+ # %last_changed% pronoun.
+ last_visit_ts => \&_last_visit_datetime,
};
foreach my $field (Bugzilla->active_custom_fields) {
if ($field->type == FIELD_TYPE_DATETIME) {
@@ -454,79 +462,91 @@ use constant COLUMN_DEPENDS => {
# certain columns in the buglist. For the most part, Search.pm uses
# DB::Schema to figure out what needs to be joined, but for some
# fields it needs a little help.
-use constant COLUMN_JOINS => {
- actual_time => {
- table => '(SELECT bug_id, SUM(work_time) AS total'
- . ' FROM longdescs GROUP BY bug_id)',
- join => 'INNER',
- },
- assigned_to => {
- from => 'assigned_to',
- to => 'userid',
- table => 'profiles',
- join => 'INNER',
- },
- reporter => {
- from => 'reporter',
- to => 'userid',
- table => 'profiles',
- join => 'INNER',
- },
- qa_contact => {
- from => 'qa_contact',
- to => 'userid',
- table => 'profiles',
- },
- component => {
- from => 'component_id',
- to => 'id',
- table => 'components',
- join => 'INNER',
- },
- product => {
- from => 'product_id',
- to => 'id',
- table => 'products',
- join => 'INNER',
- },
- classification => {
- table => 'classifications',
- from => 'map_product.classification_id',
- to => 'id',
- join => 'INNER',
- },
- 'flagtypes.name' => {
- as => 'map_flags',
- table => 'flags',
- extra => ['map_flags.attach_id IS NULL'],
- then_to => {
- as => 'map_flagtypes',
- table => 'flagtypes',
- from => 'map_flags.type_id',
+sub COLUMN_JOINS {
+ my $user = Bugzilla->user;
+
+ my $joins = {
+ actual_time => {
+ table => '(SELECT bug_id, SUM(work_time) AS total'
+ . ' FROM longdescs GROUP BY bug_id)',
+ join => 'INNER',
+ },
+ assigned_to => {
+ from => 'assigned_to',
+ to => 'userid',
+ table => 'profiles',
+ join => 'INNER',
+ },
+ reporter => {
+ from => 'reporter',
+ to => 'userid',
+ table => 'profiles',
+ join => 'INNER',
+ },
+ qa_contact => {
+ from => 'qa_contact',
+ to => 'userid',
+ table => 'profiles',
+ },
+ component => {
+ from => 'component_id',
to => 'id',
+ table => 'components',
+ join => 'INNER',
},
- },
- keywords => {
- table => 'keywords',
- then_to => {
- as => 'map_keyworddefs',
- table => 'keyworddefs',
- from => 'map_keywords.keywordid',
+ product => {
+ from => 'product_id',
to => 'id',
+ table => 'products',
+ join => 'INNER',
},
- },
- blocked => {
- table => 'dependencies',
- to => 'dependson',
- },
- dependson => {
- table => 'dependencies',
- to => 'blocked',
- },
- 'longdescs.count' => {
- table => 'longdescs',
- join => 'INNER',
- },
+ classification => {
+ table => 'classifications',
+ from => 'map_product.classification_id',
+ to => 'id',
+ join => 'INNER',
+ },
+ 'flagtypes.name' => {
+ as => 'map_flags',
+ table => 'flags',
+ extra => ['map_flags.attach_id IS NULL'],
+ then_to => {
+ as => 'map_flagtypes',
+ table => 'flagtypes',
+ from => 'map_flags.type_id',
+ to => 'id',
+ },
+ },
+ keywords => {
+ table => 'keywords',
+ then_to => {
+ as => 'map_keyworddefs',
+ table => 'keyworddefs',
+ from => 'map_keywords.keywordid',
+ to => 'id',
+ },
+ },
+ blocked => {
+ table => 'dependencies',
+ to => 'dependson',
+ },
+ dependson => {
+ table => 'dependencies',
+ to => 'blocked',
+ },
+ 'longdescs.count' => {
+ table => 'longdescs',
+ join => 'INNER',
+ },
+ last_visit_ts => {
+ as => 'bug_user_last_visit',
+ table => 'bug_user_last_visit',
+ extra => ['bug_user_last_visit.user_id = ' . $user->id],
+ from => 'bug_id',
+ to => 'bug_id',
+ },
+ };
+ return $joins;
};
# This constant defines the columns that can be selected in a query
@@ -595,6 +615,7 @@ sub COLUMNS {
dependson => $dbh->sql_group_concat('DISTINCT map_dependson.dependson'),
'longdescs.count' => 'COUNT(DISTINCT map_longdescs_count.comment_id)',
+ last_visit_ts => 'bug_user_last_visit.last_visit_ts',
);
# Backward-compatibility for old field names. Goes new_name => old_name.
@@ -2202,6 +2223,21 @@ sub _datetime_translate {
return shift->_timestamp_translate(0, @_);
}
+sub _last_visit_datetime {
+ my ($self, $args) = @_;
+ my $value = $args->{value};
+
+ $self->_datetime_translate($args);
+ if ($value eq $args->{value}) {
+ # Failed to translate a datetime. let's try the pronoun expando.
+ if ($value eq '%last_changed%') {
+ $self->_add_extra_column('changeddate');
+ $args->{value} = $args->{quoted} = 'bugs.delta_ts';
+ }
+ }
+}
+
+
sub _date_translate {
return shift->_timestamp_translate(1, @_);
}
@@ -2726,6 +2762,21 @@ sub _percentage_complete {
$self->_add_extra_column('actual_time');
}
+sub _last_visit_ts {
+ my ($self, $args) = @_;
+
+ $args->{full_field} = $self->COLUMNS->{last_visit_ts}->{name};
+ $self->_add_extra_column('last_visit_ts');
+}
+
+sub _last_visit_ts_invalid_operator {
+ my ($self, $args) = @_;
+
+ ThrowUserError('search_field_operator_invalid',
+ { field => $args->{field},
+ operator => $args->{operator} });
+}
+
sub _days_elapsed {
my ($self, $args) = @_;
my $dbh = Bugzilla->dbh;
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index d9c3756b0..a796ee765 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -51,9 +51,11 @@ use Bugzilla::Classification;
use Bugzilla::Field;
use Bugzilla::Group;
use Bugzilla::Hook;
+use Bugzilla::BugUserLastVisit;
use DateTime::TimeZone;
use List::Util qw(max);
+use List::MoreUtils qw(any);
use Scalar::Util qw(blessed);
use Storable qw(dclone);
use URI;
@@ -749,6 +751,28 @@ sub groups {
return $self->{groups};
}
+sub last_visited {
+ my ($self) = @_;
+
+ return Bugzilla::BugUserLastVisit->match({ user_id => $self->id });
+}
+
+sub is_involved_in_bug {
+ my ($self, $bug) = @_;
+ my $user_id = $self->id;
+ my $user_login = $self->login;
+
+ return unless $user_id;
+ return 1 if $user_id == $bug->assigned_to->id;
+ return 1 if $user_id == $bug->reporter->id;
+
+ if (Bugzilla->params->{'useqacontact'} and $bug->qa_contact) {
+ return 1 if $user_id == $bug->qa_contact->id;
+ }
+
+ return any { $user_login eq $_ } @{ $bug->cc };
+}
+
# It turns out that calling ->id on objects a few hundred thousand
# times is pretty slow. (It showed up as a significant time contributor
# when profiling xt/search.t.) So we cache the group ids separately from
@@ -2694,6 +2718,35 @@ Returns true if the user can attach tags to comments.
i.e. if the 'comment_taggers_group' parameter is set and the user belongs to
this group.
+=item C<last_visited>
+
+Returns an arrayref L<Bugzilla::BugUserLastVisit> objects.
+
+=item C<is_involved_in_bug($bug)>
+
+Returns true if any of the following conditions are met, false otherwise.
+
+=over
+
+=item *
+
+User is the assignee of the bug
+
+=item *
+
+User is the reporter of the bug
+
+=item *
+
+User is the QA contact of the bug (if Bugzilla is configured to use a QA
+contact)
+
+=item *
+
+User is in the cc list for the bug.
+
+=back
+
=back
=head1 CLASS FUNCTIONS
diff --git a/Bugzilla/WebService/BugUserLastVisit.pm b/Bugzilla/WebService/BugUserLastVisit.pm
new file mode 100644
index 000000000..71b637fef
--- /dev/null
+++ b/Bugzilla/WebService/BugUserLastVisit.pm
@@ -0,0 +1,208 @@
+# 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::WebService::BugUserLastVisit;
+
+use 5.10.1;
+use strict;
+
+use parent qw(Bugzilla::WebService);
+
+use Bugzilla::Bug;
+use Bugzilla::Error;
+use Bugzilla::WebService::Util qw( validate filter );
+use Bugzilla::Constants;
+
+sub update {
+ my ($self, $params) = validate(@_, 'ids');
+ my $user = Bugzilla->user;
+ my $dbh = Bugzilla->dbh;
+
+ $user->login(LOGIN_REQUIRED);
+
+ my $ids = $params->{ids} // [];
+ ThrowCodeError('param_required', { param => 'ids' }) unless @$ids;
+
+ # Cache permissions for bugs. This highly reduces the number of calls to the
+ # DB. visible_bugs() is only able to handle bug IDs, so we have to skip
+ # aliases.
+ $user->visible_bugs([grep /^[0-9]$/, @$ids]);
+
+ $dbh->bz_start_transaction();
+ my @results;
+ my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()');
+ foreach my $bug_id (@$ids) {
+ my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 });
+
+ ThrowUserError('user_not_involved', { bug_id => $bug->id })
+ unless $user->is_involved_in_bug($bug);
+
+ $bug->update_user_last_visit($user, $last_visit_ts);
+
+ push(
+ @results,
+ $self->_bug_user_last_visit_to_hash(
+ $bug, $last_visit_ts, $params
+ ));
+ }
+ $dbh->bz_commit_transaction();
+
+ return \@results;
+}
+
+sub get {
+ my ($self, $params) = validate(@_, 'ids');
+ my $user = Bugzilla->user;
+ my $ids = $params->{ids};
+
+ $user->login(LOGIN_REQUIRED);
+
+ if ($ids) {
+ # Cache permissions for bugs. This highly reduces the number of calls to
+ # the DB. visible_bugs() is only able to handle bug IDs, so we have to
+ # skip aliases.
+ $user->visible_bugs([grep /^[0-9]$/, @$ids]);
+ }
+
+ my @last_visits = @{ $user->last_visits };
+
+ if ($ids) {
+ # remove bugs that we arn't interested in if ids is passed in.
+ my %id_set = map { ($_ => 1) } @$ids;
+ @last_visits = grep { $id_set{ $_->bug_id } } @last_visits;
+ }
+
+ return [
+ map {
+ $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts,
+ $params)
+ } @last_visits
+ ];
+}
+
+sub _bug_user_last_visit_to_hash {
+ my ($self, $bug_id, $last_visit_ts, $params) = @_;
+
+ my %result = (id => $self->type('int', $bug_id),
+ last_visit_ts => $self->type('dateTime', $last_visit_ts));
+
+ return filter($params, \%result);
+}
+
+1;
+
+__END__
+=head1 NAME
+
+Bugzilla::WebService::BugUserLastVisit - Find and Store the last time a user
+visited a bug.
+
+=head1 METHODS
+
+See L<Bugzilla::WebService> for a description of how parameters are passed,
+and what B<STABLE>, B<UNSTABLE>, and B<EXPERIMENTAL> mean.
+
+Although the data input and output is the same for JSONRPC, XMLRPC and REST,
+the directions for how to access the data via REST is noted in each method
+where applicable.
+
+=head2 update
+
+B<EXPERIMENTAL>
+
+=over
+
+=item B<Description>
+
+Update the last visit time for the specified bug and current user.
+
+=item B<REST>
+
+To add a single bug id:
+
+ POST /rest/bug_user_last_visit/<bug-id>
+
+Tp add one or more bug ids at once:
+
+ POST /rest/bug_user_last_visit
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+=over
+
+=item C<ids> (array) - One or more bug ids to add.
+
+=back
+
+=item B<Returns>
+
+=over
+
+=item C<array> - An array of hashes containing the following:
+
+=over
+
+=item C<id> - (int) The bug id.
+
+=item C<last_visit_ts> - (string) The timestamp the user last visited the bug.
+
+=back
+
+=back
+
+=back
+
+=head2 get
+
+B<EXPERIMENTAL>
+
+=over
+
+=item B<Description>
+
+Get the last visited timestamp for one or more specified bug ids or get a
+list of the last 20 visited bugs and their timestamps.
+
+=item B<REST>
+
+To return the last visited timestamp for a single bug id:
+
+GET /rest/bug_visit/<bug-id>
+
+To return more than one bug timestamp or the last 20:
+
+GET /rest/bug_visit
+
+The returned data format is the same as below.
+
+=item B<Params>
+
+=over
+
+=item C<ids> (integer) - One or more optional bug ids to get.
+
+=back
+
+=item B<Returns>
+
+=over
+
+=item C<array> - An array of hashes containing the following:
+
+=over
+
+=item C<id> - (int) The bug id.
+
+=item C<last_visit_ts> - (string) The timestamp the user last visited the bug.
+
+=back
+
+=back
+
+=back
diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm
index c2a6d855c..c6bbeb9bd 100644
--- a/Bugzilla/WebService/Constants.pm
+++ b/Bugzilla/WebService/Constants.pm
@@ -277,6 +277,7 @@ sub WS_DISPATCH {
'User' => 'Bugzilla::WebService::User',
'Product' => 'Bugzilla::WebService::Product',
'Group' => 'Bugzilla::WebService::Group',
+ 'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit',
%hook_dispatch
};
return $dispatch;
diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm
index 5457b41db..96e4b3179 100644
--- a/Bugzilla/WebService/Server/REST.pm
+++ b/Bugzilla/WebService/Server/REST.pm
@@ -27,6 +27,7 @@ use Bugzilla::WebService::Server::REST::Resources::Classification;
use Bugzilla::WebService::Server::REST::Resources::Group;
use Bugzilla::WebService::Server::REST::Resources::Product;
use Bugzilla::WebService::Server::REST::Resources::User;
+use Bugzilla::WebService::Server::REST::Resources::BugUserLastVisit;
use Scalar::Util qw(blessed reftype);
use MIME::Base64 qw(decode_base64);
diff --git a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm
new file mode 100644
index 000000000..a434d4bef
--- /dev/null
+++ b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm
@@ -0,0 +1,52 @@
+# 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::WebService::Server::REST::Resources::BugUserLastVisit;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+BEGIN {
+ *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources;
+}
+
+sub _rest_resources {
+ return [
+ # bug-id
+ qr{^/bug_user_last_visit/(\d+)$}, {
+ GET => {
+ method => 'get',
+ params => sub {
+ return { ids => $_[0] };
+ },
+ },
+ POST => {
+ method => 'update',
+ params => sub {
+ return { ids => $_[0] };
+ },
+ },
+ },
+ ];
+}
+
+1;
+__END__
+
+=head1 NAME
+
+Bugzilla::Webservice::Server::REST::Resources::BugUserLastVisit - The
+BugUserLastVisit REST API
+
+=head1 DESCRIPTION
+
+This part of the Bugzilla REST API allows you to lookup and update the last time
+a user visited a bug.
+
+See L<Bugzilla::WebService::BugUserLastVisit> for more details on how to use
+this part of the REST API.