diff options
-rw-r--r-- | .htaccess | 1 | ||||
-rwxr-xr-x | contrib/reorg-tools/movebugs.pl | 3 | ||||
-rwxr-xr-x | contrib/reorg-tools/movecomponent.pl | 9 | ||||
-rw-r--r-- | extensions/BMO/web/js/edituser_menu.js | 14 | ||||
-rw-r--r-- | extensions/UserProfile/Extension.pm | 276 | ||||
-rwxr-xr-x | extensions/UserProfile/bin/migrate.pl | 43 | ||||
-rwxr-xr-x | extensions/UserProfile/bin/update.pl | 36 | ||||
-rw-r--r-- | extensions/UserProfile/lib/Util.pm | 378 | ||||
-rw-r--r-- | extensions/UserProfile/template/en/default/hook/account/prefs/account-field.html.tmpl | 11 | ||||
-rw-r--r-- | extensions/UserProfile/template/en/default/pages/user_profile.html.tmpl | 181 |
10 files changed, 942 insertions, 10 deletions
@@ -42,6 +42,7 @@ Redirect permanent /duplicates.html https://bugzilla.mozilla.org/duplicates.cgi RewriteEngine On RewriteRule ^review(.*) page.cgi?id=splinter.html$1 [QSA] +RewriteRule ^user_?profile(.*) page.cgi?id=user_profile.html$1 [QSA] RewriteRule ^favicon\.ico$ extensions/BMO/web/images/favicon.ico RewriteRule ^form[\.:](itrequest|mozlist|poweredby|presentation|trademark|recoverykey)$ enter_bug.cgi?product=mozilla.org&format=$1 RewriteRule ^form[\.:]legal$ enter_bug.cgi?product=Legal&format=legal diff --git a/contrib/reorg-tools/movebugs.pl b/contrib/reorg-tools/movebugs.pl index 6d6a53b51..adc02a1e0 100755 --- a/contrib/reorg-tools/movebugs.pl +++ b/contrib/reorg-tools/movebugs.pl @@ -10,6 +10,7 @@ use lib "$FindBin::Bin/../../lib"; use Bugzilla; use Bugzilla::Constants; use Bugzilla::FlagType; +use Bugzilla::Hook; use Bugzilla::Util; Bugzilla->usage_mode(USAGE_MODE_CMDLINE); @@ -168,5 +169,7 @@ $dbh->do( undef, $user_id, $component_field_id, $old_component, $new_component); +Bugzilla::Hook::process('reorg_move_bugs', { bug_ids => $ra_ids } ); + $dbh->bz_commit_transaction(); diff --git a/contrib/reorg-tools/movecomponent.pl b/contrib/reorg-tools/movecomponent.pl index 8f8bc0abc..702dbc6f0 100755 --- a/contrib/reorg-tools/movecomponent.pl +++ b/contrib/reorg-tools/movecomponent.pl @@ -35,6 +35,7 @@ use lib qw(. lib); use Bugzilla; use Bugzilla::Constants; +use Bugzilla::Hook; use Bugzilla::Util; sub usage() { @@ -152,6 +153,10 @@ getc(); print "Moving '$component' from '$oldproduct' to '$newproduct'...\n\n"; $dbh->bz_start_transaction() if $doit; +my $ra_ids = $dbh->selectcol_arrayref( + "SELECT bug_id FROM bugs WHERE product_id=? AND component_id=?", + undef, $oldprodid, $compid); + # Bugs table $dbh->do("UPDATE bugs SET product_id = ? WHERE component_id = ?", undef, @@ -187,7 +192,5 @@ $dbh->do("INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, undef, ($userid, $fieldid, $oldproduct, $newproduct, $compid)); +Bugzilla::Hook::process('reorg_move_bugs', { bug_ids => $ra_ids } ) if $doit; $dbh->bz_commit_transaction() if $doit; - -exit(0); - diff --git a/extensions/BMO/web/js/edituser_menu.js b/extensions/BMO/web/js/edituser_menu.js index 4f6d6ec69..383418430 100644 --- a/extensions/BMO/web/js/edituser_menu.js +++ b/extensions/BMO/web/js/edituser_menu.js @@ -3,6 +3,7 @@ var usermenu_widget; YAHOO.util.Event.onDOMReady(function() { usermenu_widget = new YAHOO.widget.Menu('usermenu_widget', { position : 'dynamic' }); usermenu_widget.addItems([ + { text: 'Profile', url: '#', target: '_blank' }, { text: 'Activity', url: '#', target: '_blank' }, { text: 'Mail', url: '#', target: '_blank' }, { text: 'Edit', url: '#', target: '_blank' } @@ -16,15 +17,14 @@ function show_usermenu(event, id, email, show_edit) { if (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey) return true; usermenu_widget.getItem(0).cfg.setProperty('url', - 'page.cgi?id=user_activity.html&action=run' + - '&from=' + YAHOO.util.Date.format(new Date(new Date() - (1000 * 60 * 60 * 24 * 14)), {format: '%Y-%m-%d'}) + - '&to=' + YAHOO.util.Date.format(new Date(), {format: '%Y-%m-%d'}) + - '&who=' + encodeURIComponent(email)); - usermenu_widget.getItem(1).cfg.setProperty('url', 'mailto:' + encodeURIComponent(email)); + 'user_profile?login=' + encodeURIComponent(email)); + usermenu_widget.getItem(1).cfg.setProperty('url', + 'page.cgi?id=user_activity.html&action=run&from=-14d&who=' + encodeURIComponent(email)); + usermenu_widget.getItem(2).cfg.setProperty('url', 'mailto:' + encodeURIComponent(email)); if (show_edit) { - usermenu_widget.getItem(2).cfg.setProperty('url', 'editusers.cgi?action=edit&userid=' + id); + usermenu_widget.getItem(3).cfg.setProperty('url', 'editusers.cgi?action=edit&userid=' + id); } else { - usermenu_widget.removeItem(2); + usermenu_widget.removeItem(3); } usermenu_widget.cfg.setProperty('xy', YAHOO.util.Event.getXY(event)); usermenu_widget.show(); diff --git a/extensions/UserProfile/Extension.pm b/extensions/UserProfile/Extension.pm index 550f7d0ac..10eb08c2b 100644 --- a/extensions/UserProfile/Extension.pm +++ b/extensions/UserProfile/Extension.pm @@ -13,10 +13,272 @@ use warnings; use base qw(Bugzilla::Extension); use Bugzilla::Constants; +use Bugzilla::Extension::UserProfile::Util; +use Bugzilla::Install::Filesystem; +use Bugzilla::User; +use Scalar::Util qw(blessed); our $VERSION = '1'; # +# user methods +# + +BEGIN { + *Bugzilla::User::last_activity_ts = \&_user_last_activity_ts; + *Bugzilla::User::set_last_activity_ts = \&_user_set_last_activity_ts; + *Bugzilla::User::last_statistics_ts = \&_user_last_statistics_ts; + *Bugzilla::User::clear_last_statistics_ts = \&_user_clear_last_statistics_ts; +} + +sub _user_last_activity_ts { $_[0]->{last_activity_ts} } +sub _user_set_last_activity_ts { $_[0]->set('last_activity_ts', $_[1]) } +sub _user_last_statistics_ts { $_[0]->{last_statistics_ts} } +sub _user_clear_last_statistics_ts { $_[0]->set('last_statistics_ts', undef) } + +# +# hooks +# + +sub bug_end_of_create { + my ($self, $args) = @_; + $self->_bug_touched($args); +} + +sub bug_end_of_update { + my ($self, $args) = @_; + $self->_bug_touched($args); +} + +sub _bug_touched { + my ($self, $args) = @_; + my $bug = $args->{bug}; + + my $user = Bugzilla->user; + my ($assigned_to, $qa_contact); + + # bug update + if (exists $args->{changes}) { + return unless + scalar(keys %{ $args->{changes} }) + || exists $args->{bug}->{added_comments}; + + # if the assignee or qa-contact is changed to someone other than the + # current user, update them + if (exists $args->{changes}->{assigned_to} + && $args->{changes}->{assigned_to}->[1] ne $user->login) + { + $assigned_to = $bug->assigned_to; + } + if (exists $args->{changes}->{qa_contact} + && $args->{changes}->{qa_contact}->[1] ne $user->login) + { + $qa_contact = $bug->qa_contact; + } + + # if the product is changed, we need to recount everyone involved with + # this bug + if (exists $args->{changes}->{product}) { + tag_for_recount_from_bug($bug->id); + } + + } + # new bug + else { + # if the assignee or qa-contact is created set to someone other than + # the current user, update them + if ($bug->assigned_to->id != $user->id) { + $assigned_to = $bug->assigned_to; + } + if ($bug->qa_contact && $bug->qa_contact->id != $user->id) { + $qa_contact = $bug->qa_contact; + } + } + + # update user's last_activity_ts + $user->set_last_activity_ts($args->{timestamp}); + $user->update(); + + # clear the last_statistics_ts for assignee/qa-contact to force a recount + # at the next poll + if ($assigned_to) { + $assigned_to->clear_last_statistics_ts(); + $assigned_to->update(); + } + if ($qa_contact) { + $qa_contact->clear_last_statistics_ts(); + $qa_contact->update(); + } +} + +sub object_end_of_create { + my ($self, $args) = @_; + $self->_object_touched($args); +} + +sub object_end_of_update { + my ($self, $args) = @_; + $self->_object_touched($args); +} + +sub _object_touched { + my ($self, $args) = @_; + my $object = $args->{object} + or return; + return if exists $args->{changes} && !scalar(keys %{ $args->{changes} }); + + if ($object->isa('Bugzilla::Attachment')) { + # if an attachment is created or updated, that counts as user activity + my $user = Bugzilla->user; + my $timestamp = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $user->set_last_activity_ts($timestamp); + $user->update(); + } + elsif ($object->isa('Bugzilla::Product') && exists $args->{changes}->{name}) { + # if a product is renamed by an admin, rename in the + # profiles_statistics_products table + Bugzilla->dbh->do( + "UPDATE profiles_statistics_products SET product=? where product=?", + undef, + $args->{changes}->{name}->[1], $args->{changes}->{name}->[0], + ); + } +} + +sub reorg_move_bugs { + my ($self, $args) = @_; + my $bug_ids = $args->{bug_ids}; + printf "Touching user profile data for %s bugs.\n", scalar(@$bug_ids); + my $count = 0; + foreach my $bug_id (@$bug_ids) { + $count += tag_for_recount_from_bug($bug_id); + } + print "Updated $count users.\n"; +} + +sub webservice_user_get { + my ($self, $args) = @_; + my ($service, $users) = @$args{qw(webservice users)}; + + my $dbh = Bugzilla->dbh; + my $ids = [ + map { blessed($_->{id}) ? $_->{id}->value : $_->{id} } + grep { exists $_->{id} } + @$users + ]; + return unless @$ids; + my $timestamps = $dbh->selectall_hashref( + "SELECT userid,last_activity_ts FROM profiles WHERE " . $dbh->sql_in('userid', $ids), + 'userid', + ); + foreach my $user (@$users) { + my $id = blessed($user->{id}) ? $user->{id}->value : $user->{id}; + $user->{last_activity} = $service->type('dateTime', $timestamps->{$id}->{last_activity_ts}); + } +} + +sub page_before_template { + my ($self, $args) = @_; + my ($vars, $page) = @$args{qw(vars page_id)}; + return unless $page eq 'user_profile.html'; + my $user = Bugzilla->user; + + # check login + my $target; + my $input = Bugzilla->input_params; + my $limit = Bugzilla->params->{'maxusermatches'} + 1; + if (!$input->{login}) { + $target = Bugzilla->login(LOGIN_REQUIRED); + } else { + my $users = Bugzilla::User::match($input->{login}, $limit, 1); + if (scalar(@$users) == 1) { + # always allow singular matches without confirmation + $target = $users->[0]; + } else { + Bugzilla::User::match_field({ 'login' => {'type' => 'single'} }); + $target = Bugzilla::User->check($input->{login}); + } + } + + # load statistics into $vars + my $dbh = Bugzilla->switch_to_shadow_db; + + my $stats = $dbh->selectall_hashref( + "SELECT name, count + FROM profiles_statistics + WHERE user_id = ?", + "name", + undef, + $target->id, + ); + map { $stats->{$_} = $stats->{$_}->{count} } keys %$stats; + + my $statuses = $dbh->selectall_hashref( + "SELECT status, count + FROM profiles_statistics_status + WHERE user_id = ?", + "status", + undef, + $target->id, + ); + map { $statuses->{$_} = $statuses->{$_}->{count} } keys %$statuses; + + my $products = $dbh->selectall_arrayref( + "SELECT product, count + FROM profiles_statistics_products + WHERE user_id = ? + ORDER BY product = '', count DESC", + { Slice => {} }, + $target->id, + ); + + # ensure there's always an "other" product entry + my ($other_product) = grep { $_->{product} eq '' } @$products; + if (!$other_product) { + $other_product = { product => '', count => 0 }; + push @$products, $other_product; + } + + # load product objects and validate product visibility + foreach my $product (@$products) { + next if $product->{product} eq ''; + my $product_obj = Bugzilla::Product->new({ name => $product->{product} }); + if (!$product_obj || !$user->can_see_product($product_obj->name)) { + # products not accessible to current user are moved into "other" + $other_product->{count} += $product->{count}; + $product->{count} = 0; + } else { + $product->{product} = $product_obj; + } + } + + # set other's name, and remove empty products + $other_product->{product} = { name => 'Other' }; + $products = [ grep { $_->{count} } @$products ]; + + $vars->{stats} = $stats; + $vars->{statuses} = $statuses; + $vars->{products} = $products; + $vars->{target} = $target; +} + +sub object_columns { + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::User')) { + push(@$columns, qw(last_activity_ts last_statistics_ts)); + } +} + +sub object_update_columns { + my ($self, $args) = @_; + my ($object, $columns) = @$args{qw(object columns)}; + if ($object->isa('Bugzilla::User')) { + push(@$columns, qw(last_activity_ts last_statistics_ts)); + } +} + +# # installation # @@ -126,4 +388,18 @@ sub install_update_db { $dbh->bz_add_column('profiles', 'last_statistics_ts', { TYPE => 'DATETIME' }); } +sub install_filesystem { + my ($self, $args) = @_; + my $files = $args->{'files'}; + my $extensions_dir = bz_locations()->{'extensionsdir'}; + my $script_name = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/update.pl"; + $files->{$script_name} = { + perms => Bugzilla::Install::Filesystem::WS_EXECUTE + }; + $script_name = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/migrate.pl"; + $files->{$script_name} = { + perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE + }; +} + __PACKAGE__->NAME; diff --git a/extensions/UserProfile/bin/migrate.pl b/extensions/UserProfile/bin/migrate.pl new file mode 100755 index 000000000..147edef9c --- /dev/null +++ b/extensions/UserProfile/bin/migrate.pl @@ -0,0 +1,43 @@ +#!/usr/bin/perl + +# 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. + +use strict; +use warnings; +$| = 1; + +use FindBin qw($Bin); +use lib "$Bin/../../.."; + +use Bugzilla; +BEGIN { Bugzilla->extensions() } + +use Bugzilla::Constants; +use Bugzilla::Extension::UserProfile::Util; +use Bugzilla::Install::Util qw(indicate_progress); + +Bugzilla->usage_mode(USAGE_MODE_CMDLINE); +my $dbh = Bugzilla->dbh; + +my $user_ids = $dbh->selectcol_arrayref( + "SELECT userid + FROM profiles + WHERE last_activity_ts IS NULL + ORDER BY userid" +); + +my ($current, $total) = (1, scalar(@$user_ids)); +foreach my $user_id (@$user_ids) { + indicate_progress({ current => $current++, total => $total, every => 25 }); + my $ts = last_user_activity($user_id); + next unless $ts; + $dbh->do( + "UPDATE profiles SET last_activity_ts = ? WHERE userid = ?", + undef, + $ts, $user_id); +} diff --git a/extensions/UserProfile/bin/update.pl b/extensions/UserProfile/bin/update.pl new file mode 100755 index 000000000..4cdb08fe7 --- /dev/null +++ b/extensions/UserProfile/bin/update.pl @@ -0,0 +1,36 @@ +#!/usr/bin/perl + +# 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. + +use strict; +use warnings; + +use FindBin qw($Bin); +use lib "$Bin/../../.."; + +use Bugzilla; +BEGIN { Bugzilla->extensions() } + +use Bugzilla::Constants; +use Bugzilla::Extension::UserProfile::Util; + +Bugzilla->usage_mode(USAGE_MODE_CMDLINE); + +my $user_ids = Bugzilla->dbh->selectcol_arrayref( + "SELECT userid + FROM profiles + WHERE last_activity_ts IS NOT NULL + AND (last_statistics_ts IS NULL + OR last_activity_ts > last_statistics_ts) + ORDER BY userid", + { Slice => {} } +); + +foreach my $user_id (@$user_ids) { + update_statistics_by_user($user_id); +} diff --git a/extensions/UserProfile/lib/Util.pm b/extensions/UserProfile/lib/Util.pm new file mode 100644 index 000000000..93313eee5 --- /dev/null +++ b/extensions/UserProfile/lib/Util.pm @@ -0,0 +1,378 @@ +# 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::UserProfile::Util; + +use strict; +use warnings; + +use base qw(Exporter); +our @EXPORT = qw( update_statistics_by_user + tag_for_recount_from_bug + last_user_activity ); + +use Bugzilla; + +sub update_statistics_by_user { + my ($user_id) = @_; + + # run all our queries on the slaves + + my $dbh = Bugzilla->switch_to_shadow_db(); + + my $now = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + + # grab the current values + + my $last_statistics_ts = _get_last_statistics_ts($user_id); + + my $statistics = _get_stats($user_id, 'profiles_statistics', 'name'); + my $by_status = _get_stats($user_id, 'profiles_statistics_status', 'status'); + my $by_product = _get_stats($user_id, 'profiles_statistics_products', 'product'); + + # bugs filed + _update_statistics($statistics, 'bugs_filed', [ $user_id ], <<EOF); + SELECT COUNT(*) + FROM bugs + WHERE bugs.reporter = ? +EOF + + # comments made + _update_statistics($statistics, 'comments', [ $user_id ], <<EOF); + SELECT COUNT(*) + FROM longdescs + WHERE who = ? +EOF + + # commented on + _update_statistics($statistics, 'commented_on', [ $user_id ], <<EOF); + SELECT COUNT(*) FROM ( + SELECT longdescs.bug_id + FROM longdescs + WHERE who = ? + GROUP BY longdescs.bug_id + ) AS temp +EOF + + # confirmed + _update_statistics($statistics, 'confirmed', [ $user_id, _field_id('bug_status') ], <<EOF); + SELECT COUNT(*) + FROM bugs_activity + WHERE who = ? + AND fieldid = ? + AND removed = 'UNCONFIRMED' AND added = 'NEW' +EOF + + # patches submitted + _update_statistics($statistics, 'patches', [ $user_id ], <<EOF); + SELECT COUNT(*) + FROM attachments + WHERE submitter_id = ? + AND ispatch = 1 +EOF + + # patches reviewed + _update_statistics($statistics, 'reviews', [ $user_id ], <<EOF); + SELECT COUNT(*) + FROM flags + INNER JOIN attachments ON attachments.attach_id = flags.attach_id + WHERE setter_id = ? + AND attachments.ispatch = 1 + AND status IN ('+', '-') +EOF + + # assigned to + _update_statistics($statistics, 'assigned', [ $user_id ], <<EOF); + SELECT COUNT(*) + FROM bugs + WHERE assigned_to = ? +EOF + + # qa contact + _update_statistics($statistics, 'qa_contact', [ $user_id ], <<EOF); + SELECT COUNT(*) + FROM bugs + WHERE qa_contact = ? +EOF + + # bugs touched + _update_statistics($statistics, 'touched', [ $user_id, $user_id], <<EOF); + SELECT COUNT(*) FROM ( + SELECT bugs_activity.bug_id + FROM bugs_activity + WHERE who = ? + GROUP BY bugs_activity.bug_id + UNION + SELECT longdescs.bug_id + FROM longdescs + WHERE who = ? + GROUP BY longdescs.bug_id + ) temp +EOF + + # activity by status/resolution, and product + _activity_by_status($by_status, $user_id); + _activity_by_product($by_product, $user_id); + + # if nothing is dirty, no need to do anything else + if ($last_statistics_ts) { + return unless _has_dirty($statistics) + || _has_dirty($by_status) + || _has_dirty($by_product); + } + + # switch back to the main db for updating + + $dbh = Bugzilla->switch_to_main_db(); + $dbh->bz_start_transaction(); + + # commit updated statistics + + _set_stats($statistics, $user_id, 'profiles_statistics', 'name') + if _has_dirty($statistics); + _set_stats($by_status, $user_id, 'profiles_statistics_status', 'status') + if _has_dirty($by_status); + _set_stats($by_product, $user_id, 'profiles_statistics_products', 'product') + if _has_dirty($by_product); + + # update the user's last_statistics_ts + _set_last_statistics_ts($user_id, $now); + + $dbh->bz_commit_transaction(); +} + +sub tag_for_recount_from_bug { + my ($bug_id) = @_; + my $dbh = Bugzilla->dbh; + # get a list of all users associated with this bug + my $user_ids = $dbh->selectcol_arrayref(<<EOF, undef, $bug_id, _field_id('cc'), $bug_id); + SELECT DISTINCT user_id + FROM ( + SELECT DISTINCT who AS user_id + FROM bugs_activity + WHERE bug_id = ? + AND fieldid <> ? + UNION ALL + SELECT DISTINCT who AS user_id + FROM longdescs + WHERE bug_id = ? + ) tmp +EOF + # clear last_statistics_ts + $dbh->do( + "UPDATE profiles SET last_statistics_ts=NULL WHERE " . $dbh->sql_in('userid', $user_ids) + ); + return scalar(@$user_ids); +} + +sub last_user_activity { + # last comment, or change to a bug (excluding CC changes) + my ($user_id) = @_; + return Bugzilla->dbh->selectrow_array(<<EOF, undef, $user_id, $user_id, _field_id('cc')); + SELECT MAX(bug_when) + FROM ( + SELECT MAX(bug_when) AS bug_when + FROM longdescs + WHERE who = ? + UNION ALL + SELECT MAX(bug_when) AS bug_when + FROM bugs_activity + WHERE who = ? + AND fieldid <> ? + ) tmp +EOF +} + +# for performance reasons hit the db directly rather than using the user object + +sub _get_last_statistics_ts { + my ($user_id) = @_; + return Bugzilla->dbh->selectrow_array( + "SELECT last_statistics_ts FROM profiles WHERE userid = ?", + undef, $user_id + ); +} + +sub _set_last_statistics_ts { + my ($user_id, $timestamp) = @_; + Bugzilla->dbh->do( + "UPDATE profiles SET last_statistics_ts = ? WHERE userid = ?", + undef, + $timestamp, $user_id, + ); +} + +sub _update_statistics { + my ($statistics, $name, $values, $sql) = @_; + my ($count) = Bugzilla->dbh->selectrow_array($sql, undef, @$values); + if (!exists $statistics->{$name}) { + $statistics->{$name} = { + id => 0, + count => $count, + dirty => 1, + }; + } elsif ($statistics->{$name}->{count} != $count) { + $statistics->{$name}->{count} = $count; + $statistics->{$name}->{dirty} = 1; + }; +} + +sub _activity_by_status { + my ($by_status, $user_id) = @_; + my $dbh = Bugzilla->dbh; + + # we actually track both status and resolution changes as statuses + my @values = ($user_id, _field_id('bug_status'), $user_id, _field_id('resolution')); + my $rows = $dbh->selectall_arrayref(<<EOF, { Slice => {} }, @values); + SELECT added AS status, COUNT(*) AS count + FROM bugs_activity + WHERE who = ? + AND fieldid = ? + GROUP BY added + UNION ALL + SELECT CONCAT('RESOLVED/', added) AS status, COUNT(*) AS count + FROM bugs_activity + WHERE who = ? + AND fieldid = ? + AND added != '' + GROUP BY added +EOF + + foreach my $row (@$rows) { + my $status = $row->{status}; + if (!exists $by_status->{$status}) { + $by_status->{$status} = { + id => 0, + count => $row->{count}, + dirty => 1, + }; + } elsif ($by_status->{$status}->{count} != $row->{count}) { + $by_status->{$status}->{count} = $row->{count}; + $by_status->{$status}->{dirty} = 1; + } + } +} + +sub _activity_by_product { + my ($by_product, $user_id) = @_; + my $dbh = Bugzilla->dbh; + + my %products; + + # changes + my $rows = $dbh->selectall_arrayref(<<EOF, { Slice => {} }, $user_id); + SELECT products.name AS product, count(*) AS count + FROM bugs_activity + INNER JOIN bugs ON bugs.bug_id = bugs_activity.bug_id + INNER JOIN products ON products.id = bugs.product_id + WHERE who = ? + GROUP BY bugs.product_id +EOF + map { $products{$_->{product}} += $_->{count} } @$rows; + + # comments + $rows = $dbh->selectall_arrayref(<<EOF, { Slice => {} }, $user_id); + SELECT products.name AS product, count(*) AS count + FROM longdescs + INNER JOIN bugs ON bugs.bug_id = longdescs.bug_id + INNER JOIN products ON products.id = bugs.product_id + WHERE who = ? + GROUP BY bugs.product_id +EOF + map { $products{$_->{product}} += $_->{count} } @$rows; + + # store only the top 10 and 'other' (which is an empty string) + my @sorted = sort { $products{$b} <=> $products{$a} } keys %products; + my @other; + @other = splice(@sorted, 10) if scalar(@sorted) > 10; + map { $products{''} += $products{$_} } @other; + push @sorted, '' if $products{''}; + + # update by_product + foreach my $product (@sorted) { + if (!exists $by_product->{$product}) { + $by_product->{$product} = { + id => 0, + count => $products{$product}, + dirty => 1, + }; + } elsif ($by_product->{$product}->{count} != $products{$product}) { + $by_product->{$product}->{count} = $products{$product}; + $by_product->{$product}->{dirty} = 1; + } + } + foreach my $product (keys %$by_product) { + if (!grep { $_ eq $product } @sorted) { + delete $by_product->{$product}; + } + } +} + +our $_field_id_cache; +sub _field_id { + my ($name) = @_; + if (!$_field_id_cache) { + my $rows = Bugzilla->dbh->selectall_arrayref("SELECT id, name FROM fielddefs"); + foreach my $row (@$rows) { + $_field_id_cache->{$row->[1]} = $row->[0]; + } + } + return $_field_id_cache->{$name}; +} + +sub _get_stats { + my ($user_id, $table, $name_field) = @_; + my $result = {}; + my $rows = Bugzilla->dbh->selectall_arrayref( + "SELECT * FROM $table WHERE user_id = ?", + { Slice => {} }, + $user_id, + ); + foreach my $row (@$rows) { + unless (defined $row->{$name_field}) { + print "$user_id $table $name_field\n"; + die; + } + $result->{$row->{$name_field}} = { + id => $row->{id}, + count => $row->{count}, + dirty => 0, + } + } + return $result; +} + +sub _set_stats { + my ($statistics, $user_id, $table, $name_field) = @_; + my $dbh = Bugzilla->dbh; + foreach my $name (keys %$statistics) { + next unless $statistics->{$name}->{dirty}; + if ($statistics->{$name}->{id}) { + $dbh->do( + "UPDATE $table SET count = ? WHERE user_id = ? AND $name_field = ?", + undef, + $statistics->{$name}->{count}, $user_id, $name, + ); + } else { + $dbh->do( + "INSERT INTO $table(user_id, $name_field, count) VALUES (?, ?, ?)", + undef, + $user_id, $name, $statistics->{$name}->{count}, + ); + } + } +} + +sub _has_dirty { + my ($statistics) = @_; + foreach my $name (keys %$statistics) { + return 1 if $statistics->{$name}->{dirty}; + } + return 0; +} + +1; diff --git a/extensions/UserProfile/template/en/default/hook/account/prefs/account-field.html.tmpl b/extensions/UserProfile/template/en/default/hook/account/prefs/account-field.html.tmpl new file mode 100644 index 000000000..f2e3aad01 --- /dev/null +++ b/extensions/UserProfile/template/en/default/hook/account/prefs/account-field.html.tmpl @@ -0,0 +1,11 @@ +[%# 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. + #%] + +<a href="user_profile?login=[% user.login FILTER uri %]"> + [% terms.Bugzilla %] User Profile +</a><br><hr> diff --git a/extensions/UserProfile/template/en/default/pages/user_profile.html.tmpl b/extensions/UserProfile/template/en/default/pages/user_profile.html.tmpl new file mode 100644 index 000000000..1fb6a0b67 --- /dev/null +++ b/extensions/UserProfile/template/en/default/pages/user_profile.html.tmpl @@ -0,0 +1,181 @@ +[%# 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. + #%] + +[% PROCESS global/variables.none.tmpl %] + +[% inline_styles = BLOCK %] + #login_autocomplete { + float: left; + } + + #user_profile_table th { + text-align: right; + padding-right: 1em; + vertical-align: middle; + } + + #user_profile_table .numeric { + text-align: right; + } + + #user_profile_table .product_span { + white-space: nowrap; + } + + #what { + margin-top: 2em; + } + + #updated { + font-style: italic; + font-size: x-small; + } +[% END %] + +[% PROCESS global/header.html.tmpl + title = "User Profile: " _ target.identity + style = inline_styles + yui = [ 'autocomplete' ] + javascript_urls = [ "js/field.js" ] +%] + +<table id="user_profile_table"> + +<tr> + <th>Email</th> + <td colspan="2"> + <form action="user_profile"> + [% INCLUDE global/userselect.html.tmpl + id => "login" + name => "login" + value => target.email + size => 40 + emptyok => 0 + %] + <input type="submit" value="Show"> + </form> + </td> +</tr> + +<tr> + <th>Name</th> + <td colspan="2">[% target.name FILTER html %]</td> +</tr> + +<tr> + <td> </td> + <td> </td> + <td width="100%"> </td> +</tr> + +<tr> + <th>Last activity</th> + <td colspan="2"> + <a href="page.cgi?id=user_activity.html&action=run&who=[% target.login FILTER uri %]&from=-4w"> + [% target.last_activity_ts FILTER time %] + </a> + </td> +</tr> +<tr> + <th>[% terms.Bugs %] filed</th> + <td class="numeric"> + <a href="buglist.cgi?query_format=advanced&emailtype1=exact&emailreporter1=1&email1=[% target.login FILTER uri %]" + target="_blank"> + [% stats.bugs_filed || 0 FILTER html %] + </a> + </td> +</tr> +<tr> + <th>Comments made</th> + <td class="numeric">[% stats.comments || 0 FILTER html %]</td> +</tr> +<tr> + <th>Assigned to</th> + <td class="numeric"> + <a href="buglist.cgi?query_format=advanced&emailtype1=exact&emailassigned_to1=1&email1=[% target.login FILTER uri %]" + target="_blank"> + [% stats.assigned || 0 FILTER html %] + </a> + </td> +</tr> +<tr> + <th>Commented on</th> + <td class="numeric"> + <a href="buglist.cgi?query_format=advanced&emailtype1=exact&emaillongdesc1=1&email1=[% target.login FILTER uri %]" + target="_blank"> + [% stats.commented_on || 0 FILTER html %] + </a> + </td> +</tr> +<tr> + <th>QA-Contact</th> + <td class="numeric"> + <a href="buglist.cgi?query_format=advanced&emailtype1=exact&emailqa_contact1=1&email1=[% target.login FILTER uri %]" + target="_blank"> + [% stats.qa_contact || 0 FILTER html %] + </a> + </td> +</tr> +<tr> + <th>Patches submitted</th> + <td class="numeric">[% stats.patches || 0 FILTER html %]</td> +</tr> +<tr> + <th>Patches reviewed</th> + <td class="numeric">[% stats.reviews || 0 FILTER html %]</td> +</tr> +<tr> + <th>[% terms.Bugs %] poked</th> + <td class="numeric">[% stats.touched || 0 FILTER html %]</td> +</tr> + +<tr> + <td> </td> +</tr> + +<tr> + <th>Statuses changed</td> + <td colspan="2"> + RESOLVED ([% statuses.item('RESOLVED') || 0 FILTER html %]), + FIXED ([% statuses.item('RESOLVED/FIXED') || 0 FILTER html %]), + VERIFIED ([% statuses.item('VERIFIED') || 0 FILTER html %]), + INVALID ([% statuses.item('RESOLVED/INVALID') || 0 FILTER html %]) + </td> +</tr> + +<tr> + <th>Activity by product</td> + <td colspan="2"> + [% FOREACH p = products %] + <span class="product_span"> + [% IF p.product.id %] + <a href="describecomponents.cgi?product=[% p.product.name FILTER uri %]" + target="_blank"> + [% END %] + [% p.product.name FILTER html %] ([% p.count || 0 FILTER html %]) + [% "</a>" IF p.product.id %] + [% "," UNLESS loop.last ~%] + </span> + [%+ END %] + </td> +</tr> + +</table> + +<div id="what"> + <a href="https://wiki.mozilla.org/BMO/User_profile_fields" target="_blank"> + What do these fields mean? + </a> +</div> + +<div id="updated"> + This information is updated daily +</div> + +[% PROCESS global/footer.html.tmpl %] + |