summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDylan William Hardison <dylan@hardison.net>2014-05-20 08:19:51 +0200
committerDylan William Hardison <dylan@hardison.net>2014-05-28 16:52:47 +0200
commitb6b83df873a1509797235738e00f9e6307eca876 (patch)
tree2599c58a67bf67499d5c2d64c2854f8c7a4ef1e2
parent038e6854b32ae3155018188d80f306599a1e9644 (diff)
downloadbugzilla-b6b83df873a1509797235738e00f9e6307eca876.tar.gz
bugzilla-b6b83df873a1509797235738e00f9e6307eca876.tar.xz
Bug 1000917 - Backport upstream bug 489028 to bmo/4.2 to allow user last visit searching
-rw-r--r--Bugzilla/Bug.pm18
-rw-r--r--Bugzilla/BugUserLastVisit.pm76
-rw-r--r--Bugzilla/Config/Admin.pm7
-rw-r--r--Bugzilla/DB/Schema.pm19
-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
-rw-r--r--clean-bug-user-last-visit.pl38
-rwxr-xr-xcontrib/sanitizeme.pl6
-rw-r--r--extensions/MyDashboard/lib/Queries.pm10
-rw-r--r--js/bug.js46
-rw-r--r--template/en/default/admin/params/admin.html.tmpl7
-rw-r--r--template/en/default/bug/show-header.html.tmpl5
-rw-r--r--template/en/default/global/field-descs.none.tmpl1
-rw-r--r--template/en/default/global/user-error.html.tmpl5
20 files changed, 674 insertions, 71 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/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index d8f3e175a..cbcb5b26c 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -1714,6 +1714,25 @@ use constant ABSTRACT_SCHEMA => {
],
},
+ bug_user_last_visit => {
+ FIELDS => [
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1,
+ PRIMARYKEY => 1},
+ user_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
+ bug_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE'}},
+ last_visit_ts => {TYPE => 'DATETIME', NOTNULL => 1},
+ ],
+ INDEXES => [
+ bug_user_last_visit_idx => {FIELDS => ['user_id', 'bug_id'],
+ TYPE => 'UNIQUE'}
+ ],
+ },
};
# Foreign Keys are added in Bugzilla::DB::bz_add_field_tables
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..288d4677b 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.
diff --git a/clean-bug-user-last-visit.pl b/clean-bug-user-last-visit.pl
new file mode 100644
index 000000000..9884b7c48
--- /dev/null
+++ b/clean-bug-user-last-visit.pl
@@ -0,0 +1,38 @@
+#!/usr/bin/perl -wT
+# 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.
+
+=head1 NAME
+
+clean-bug-user-last-visit.pl
+
+=head1 DESCRIPTION
+
+This utility script cleans out entries from the bug_user_last_visit table that
+are older than (a configurable) number of days.
+
+It takes no arguments and produces no output except in the case of errors.
+
+=cut
+
+use 5.10.1;
+use strict;
+use warnings;
+use lib qw(. lib);
+
+use Bugzilla;
+use Bugzilla::Constants;
+
+Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
+
+my $dbh = Bugzilla->dbh;
+my $sql = 'DELETE FROM bug_user_last_visit WHERE last_visit_ts < '
+ . $dbh->sql_date_math('NOW()',
+ '-',
+ Bugzilla->params->{last_visit_keep_days},
+ 'DAY');
+$dbh->do($sql);
diff --git a/contrib/sanitizeme.pl b/contrib/sanitizeme.pl
index 28c2f38f2..c80069912 100755
--- a/contrib/sanitizeme.pl
+++ b/contrib/sanitizeme.pl
@@ -86,6 +86,7 @@ eval {
delete_security_groups();
delete_sensitive_user_data();
delete_attachment_data() unless $keep_attachments;
+ delete_bug_user_last_visit();
disable_email_delivery() unless $enable_email;
print "All done!\n";
$dbh->bz_rollback_transaction() if $dry_run;
@@ -201,6 +202,11 @@ sub delete_attachment_data {
$dbh->do("UPDATE attach_data SET thedata = ''");
}
+sub delete_bug_user_last_visit {
+ print "Removing all entries from bug_user_last_visit...\n"
+ $dbh->do('TRUNCATE TABLE bug_user_last_visit');
+}
+
sub disable_email_delivery {
# turn off email delivery for all users.
print "Turning off email delivery...\n";
diff --git a/extensions/MyDashboard/lib/Queries.pm b/extensions/MyDashboard/lib/Queries.pm
index 566b5b340..58a6e122c 100644
--- a/extensions/MyDashboard/lib/Queries.pm
+++ b/extensions/MyDashboard/lib/Queries.pm
@@ -91,6 +91,16 @@ sub QUERY_DEFS {
'email1' => $user->login
}
},
+ {
+ name => 'lastvisitedbugs',
+ heading => 'Updated Since Last Visit',
+ description => 'Bugs updated since list visited',
+ params => {
+ o1 => 'lessthan',
+ v1 => '%last_changed%',
+ f1 => 'last_visit_ts',
+ },
+ },
);
if (Bugzilla->params->{'useqacontact'}) {
diff --git a/js/bug.js b/js/bug.js
index 06ef03da1..fc7beea17 100644
--- a/js/bug.js
+++ b/js/bug.js
@@ -129,3 +129,49 @@ YAHOO.bugzilla.dupTable = {
[dt, data.product_name]);
}
};
+
+(function(){
+ 'use strict';
+ var JSON = YAHOO.lang.JSON;
+
+ YAHOO.bugzilla.bugUserLastVisit = {
+ update: function(bug_id) {
+ var args = JSON.stringify({
+ version: "1.1",
+ method: 'BugUserLastVisit.update',
+ params: { ids: bug_id },
+ });
+ var callbacks = {
+ failure: function(res) {
+ if (console)
+ console.log("failed to update last visited: "
+ + res.responseText);
+ },
+ };
+
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ YAHOO.util.Connect.asyncRequest('POST', 'jsonrpc.cgi', callbacks,
+ args)
+ },
+
+ get: function(done) {
+ var args = JSON.stringify({
+ version: "1.1",
+ method: 'BugUserLastVisit.get',
+ params: { },
+ });
+ var callbacks = {
+ success: function(res) { done(JSON.parse(res.responseText)) },
+ failure: function(res) {
+ if (console)
+ console.log("failed to get last visited: "
+ + res.responseText);
+ },
+ };
+
+ YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
+ YAHOO.util.Connect.asyncRequest('POST', 'jsonrpc.cgi', callbacks,
+ args)
+ },
+ };
+})();
diff --git a/template/en/default/admin/params/admin.html.tmpl b/template/en/default/admin/params/admin.html.tmpl
index dd83ebb2e..f84dbc701 100644
--- a/template/en/default/admin/params/admin.html.tmpl
+++ b/template/en/default/admin/params/admin.html.tmpl
@@ -37,5 +37,8 @@
"$terms.Bugzilla will issue a warning in case you'd run into inconsistencies " _
"when you're about to do so, but such deletions remain kinda scary. " _
"So, you have to turn on this option before any such deletions " _
- "will ever happen." }
-%] \ No newline at end of file
+ "will ever happen."
+
+ last_visit_keep_days => "This option controls how many days $terms.Bugzilla will " _
+ "remember when users visit specific ${terms.bugs}."}
+%]
diff --git a/template/en/default/bug/show-header.html.tmpl b/template/en/default/bug/show-header.html.tmpl
index 9f2127d23..306407a65 100644
--- a/template/en/default/bug/show-header.html.tmpl
+++ b/template/en/default/bug/show-header.html.tmpl
@@ -41,6 +41,7 @@
[% yui = ['autocomplete', 'calendar'] %]
[% yui.push('container') IF user.can_tag_comments %]
[% javascript_urls = [ "js/util.js", "js/field.js" ] %]
+[% javascript_urls.push("js/bug.js") IF user.id %]
[% javascript_urls.push('js/comment-tagging.js')
IF user.id && Param('comment_taggers_group') %]
[% IF bug.defined %]
@@ -65,6 +66,10 @@
}
YAHOO.util.Event.onDOMReady(function() {
initDirtyFieldTracking();
+
+ [% IF user.id AND user.is_involved_in_bug(bug) %]
+ YAHOO.bugzilla.bugUserLastVisit.update([% bug.bug_id FILTER none %]);
+ [% END %]
});
[% javascript FILTER none %]
[% END %]
diff --git a/template/en/default/global/field-descs.none.tmpl b/template/en/default/global/field-descs.none.tmpl
index 731ba37ef..721e1dc49 100644
--- a/template/en/default/global/field-descs.none.tmpl
+++ b/template/en/default/global/field-descs.none.tmpl
@@ -111,6 +111,7 @@
"everconfirmed" => "Ever confirmed",
"flagtypes.name" => "Flags",
"keywords" => "Keywords",
+ "last_visit_ts" => "Last Visit",
"longdesc" => "Comment",
"longdescs.count" => "Number of Comments",
"longdescs.isprivate" => "Comment is private",
diff --git a/template/en/default/global/user-error.html.tmpl b/template/en/default/global/user-error.html.tmpl
index 54d0f696a..b04deb4d6 100644
--- a/template/en/default/global/user-error.html.tmpl
+++ b/template/en/default/global/user-error.html.tmpl
@@ -1817,6 +1817,11 @@
Sorry, but you are not allowed to (un)mark comments or attachments
as private.
+ [% ELSIF error == "user_not_involved" %]
+ [% title = "User Not Involved with $terms.Bug" %]
+ Sorry, but you are not involved with [% terms.Bug %] [%+
+ bug_id FILTER bug_link(bug_id) FILTER none %].
+
[% ELSIF error == "webdot_too_large" %]
[% title = "Dependency Graph Too Large" %]
The dependency graph contains too many [% terms.bugs %] to display (more