Bug 489028 - Record last-visited time of bugs when logged in

r=glob a=justdave
trunk
Dylan William Hardison 2014-04-07 02:41:11 -04:00 committed by Sync
parent a021d9ad90
commit a9520867da
19 changed files with 597 additions and 9 deletions

View File

@ -1 +1 @@
36f56bd9112c2e930fb5bdbee3b5c89334de5247
eab44b1aad3f243dd69b1d30519b73a1e537fda2

View File

@ -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>

View File

@ -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

View File

@ -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;
}

View File

@ -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

View File

@ -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'},
);

View File

@ -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 },

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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);

View File

@ -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.

View File

@ -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);

View File

@ -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)
},
};
})();

View File

@ -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."}
%]

View File

@ -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 %]

View File

@ -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",

View File

@ -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