aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDylan William Hardison <dylan@hardison.net>2014-04-07 02:41:11 -0400
committerDylan William Hardison <dylan@hardison.net>2014-04-22 16:37:52 -0400
commiteab44b1aad3f243dd69b1d30519b73a1e537fda2 (patch)
tree45e67469b8c6905a545dc5f2bd8bbe03bd41ea7c
parent36f56bd9112c2e930fb5bdbee3b5c89334de5247 (diff)
downloadbugs-eab44b1aad3f243dd69b1d30519b73a1e537fda2.tar
bugs-eab44b1aad3f243dd69b1d30519b73a1e537fda2.tar.gz
bugs-eab44b1aad3f243dd69b1d30519b73a1e537fda2.tar.bz2
bugs-eab44b1aad3f243dd69b1d30519b73a1e537fda2.tar.xz
bugs-eab44b1aad3f243dd69b1d30519b73a1e537fda2.zip
Bug 489028 - Record last-visited time of bugs when logged in
r=glob a=justdave
-rw-r--r--Bugzilla/Bug.pm24
-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.pm48
-rw-r--r--Bugzilla/User.pm53
-rw-r--r--Bugzilla/WebService/BugUserLastVisit.pm208
-rw-r--r--Bugzilla/WebService/Constants.pm13
-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
-rw-r--r--js/bug.js46
-rw-r--r--template/en/default/admin/params/admin.html.tmpl5
-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
18 files changed, 596 insertions, 8 deletions
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index b53847790..b66ad1e26 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -28,6 +28,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);
@@ -4081,6 +4082,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 {
@@ -4407,6 +4425,7 @@ sub _multi_select_accessor {
1;
+__END__
=head1 B<Methods>
=over
@@ -4415,6 +4434,11 @@ sub _multi_select_accessor {
Ensures the accessors for custom fields are always created.
+=item C<update_user_last_visit($user, $last_visit)>
+
+Creates or updates a L<Bugzilla::BugUserLastVisit> for this bug and the supplied
+$user, the timestamp given as $last_visit.
+
=back
=head1 B<Methods in need of POD>
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 811f7029e..97f8ea390 100644
--- a/Bugzilla/Config/Admin.pm
+++ b/Bugzilla/Config/Admin.pm
@@ -33,6 +33,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 30ed55b9a..cf404f4ca 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -1713,6 +1713,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 6289f110a..98cf389a1 100644
--- a/Bugzilla/Field.pm
+++ b/Bugzilla/Field.pm
@@ -258,6 +258,8 @@ use constant DEFAULT_FIELDS => (
type => FIELD_TYPE_BUG_URLS},
{name => 'tag', desc => 'Personal Tags', buglist => 1,
type => FIELD_TYPE_KEYWORDS},
+ {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 0f9df67a8..4056d4994 100644
--- a/Bugzilla/Install/Filesystem.pm
+++ b/Bugzilla/Install/Filesystem.pm
@@ -152,6 +152,7 @@ sub FILESYSTEM {
'jobqueue.pl' => { perms => OWNER_EXECUTE },
'migrate.pl' => { perms => OWNER_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 43de32774..1c008bdfb 100644
--- a/Bugzilla/Search.pm
+++ b/Bugzilla/Search.pm
@@ -320,6 +320,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 },
@@ -349,6 +353,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) {
@@ -514,7 +522,14 @@ sub COLUMN_JOINS {
from => 'map_bug_tag.tag_id',
to => 'id',
},
- }
+ },
+ 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;
};
@@ -587,6 +602,7 @@ sub COLUMNS {
'longdescs.count' => 'COUNT(DISTINCT map_longdescs_count.comment_id)',
tag => $dbh->sql_group_concat('DISTINCT map_tag.name'),
+ last_visit_ts => 'bug_user_last_visit.last_visit_ts',
);
# Backward-compatibility for old field names. Goes new_name => old_name.
@@ -2141,6 +2157,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, @_);
}
@@ -2622,6 +2653,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 5cebe728b..0b644a3b4 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -19,9 +19,11 @@ use Bugzilla::Product;
use Bugzilla::Classification;
use Bugzilla::Field;
use Bugzilla::Group;
+use Bugzilla::BugUserLastVisit;
use DateTime::TimeZone;
use List::Util qw(max);
+use List::MoreUtils qw(any);
use Scalar::Util qw(blessed);
use URI;
use URI::QueryParam;
@@ -729,6 +731,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
@@ -2767,6 +2791,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 4085c2cbd..e18e2b8ec 100644
--- a/Bugzilla/WebService/Constants.pm
+++ b/Bugzilla/WebService/Constants.pm
@@ -266,12 +266,13 @@ sub WS_DISPATCH {
Bugzilla::Hook::process('webservice', { dispatch => \%hook_dispatch });
my $dispatch = {
- 'Bugzilla' => 'Bugzilla::WebService::Bugzilla',
- 'Bug' => 'Bugzilla::WebService::Bug',
- 'Classification' => 'Bugzilla::WebService::Classification',
- 'Group' => 'Bugzilla::WebService::Group',
- 'Product' => 'Bugzilla::WebService::Product',
- 'User' => 'Bugzilla::WebService::User',
+ 'Bugzilla' => 'Bugzilla::WebService::Bugzilla',
+ 'Bug' => 'Bugzilla::WebService::Bug',
+ 'Classification' => 'Bugzilla::WebService::Classification',
+ 'Group' => 'Bugzilla::WebService::Group',
+ 'Product' => 'Bugzilla::WebService::Product',
+ 'User' => 'Bugzilla::WebService::User',
+ 'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit',
%hook_dispatch
};
return $dispatch;
diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm
index 7aac95170..ba8304a5f 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/js/bug.js b/js/bug.js
index fe2e1f53b..abefbb22d 100644
--- a/js/bug.js
+++ b/js/bug.js
@@ -189,3 +189,49 @@ function set_assign_to(use_qa_contact) {
}
}
}
+
+(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 eb023de75..dfe513d05 100644
--- a/template/en/default/admin/params/admin.html.tmpl
+++ b/template/en/default/admin/params/admin.html.tmpl
@@ -24,5 +24,8 @@
"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." }
+ "will ever happen."
+
+ last_visit_keep_days => "This option controls how many days Bugzilla will " _
+ "remember when users visit specific bugs."}
%]
diff --git a/template/en/default/bug/show-header.html.tmpl b/template/en/default/bug/show-header.html.tmpl
index c340def17..6dfc19dfb 100644
--- a/template/en/default/bug/show-header.html.tmpl
+++ b/template/en/default/bug/show-header.html.tmpl
@@ -26,6 +26,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 %]
@@ -52,6 +53,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 cbb6b641b..f4e17c3f8 100644
--- a/template/en/default/global/field-descs.none.tmpl
+++ b/template/en/default/global/field-descs.none.tmpl
@@ -96,6 +96,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 0b5d66764..dcdf70783 100644
--- a/template/en/default/global/user-error.html.tmpl
+++ b/template/en/default/global/user-error.html.tmpl
@@ -1871,6 +1871,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