diff options
25 files changed, 1367 insertions, 2 deletions
@@ -43,6 +43,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 ^request_defer(.*) page.cgi?id=request_defer.html$1 [QSA] RewriteRule ^favicon\.ico$ extensions/BMO/web/images/favicon.ico RewriteRule ^form[\.:]itrequest$ enter_bug.cgi?product=Infrastructure+\%26+Operations&format=itrequest RewriteRule ^form[\.:](mozlist|poweredby|presentation|trademark|recoverykey)$ enter_bug.cgi?product=mozilla.org&format=$1 diff --git a/extensions/RequestNagger/Extension.pm b/extensions/RequestNagger/Extension.pm index a8dc4a5c2..af9eb1783 100644 --- a/extensions/RequestNagger/Extension.pm +++ b/extensions/RequestNagger/Extension.pm @@ -13,9 +13,244 @@ use warnings; use base qw(Bugzilla::Extension); use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Extension::RequestNagger::TimeAgo qw(time_ago); +use Bugzilla::Flag; +use Bugzilla::Install::Filesystem; +use Bugzilla::User::Setting; +use Bugzilla::Util qw(datetime_from detaint_natural); +use DateTime; our $VERSION = '1'; +BEGIN { + *Bugzilla::Flag::age = \&_flag_age; + *Bugzilla::Flag::deferred = \&_flag_deferred; + *Bugzilla::Product::nag_interval = \&_product_nag_interval; +} + +sub _flag_age { + return time_ago(datetime_from($_[0]->modification_date)); +} + +sub _flag_deferred { + my ($self) = @_; + if (!exists $self->{deferred}) { + my $dbh = Bugzilla->dbh; + my ($defer_until) = $dbh->selectrow_array( + "SELECT defer_until FROM nag_defer WHERE flag_id=?", + undef, + $self->id + ); + $self->{deferred} = $defer_until ? datetime_from($defer_until) : undef; + } + return $self->{deferred}; +} + +sub _product_nag_interval { $_[0]->{nag_interval} } + +sub object_columns { + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::Product')) { + push @$columns, 'nag_interval'; + } +} + +sub object_update_columns { + my ($self, $args) = @_; + my ($object, $columns) = @$args{qw(object columns)}; + if ($object->isa('Bugzilla::Product')) { + push @$columns, 'nag_interval'; + } +} + +sub object_before_create { + my ($self, $args) = @_; + my ($class, $params) = @$args{qw(class params)}; + return unless $class->isa('Bugzilla::Product'); + my $interval = _check_nag_interval(Bugzilla->cgi->param('nag_interval')); + $params->{nag_interval} = $interval; +} + +sub object_end_of_set_all { + my ($self, $args) = @_; + my ($object, $params) = @$args{qw(object params)}; + return unless $object->isa('Bugzilla::Product'); + my $interval = _check_nag_interval(Bugzilla->cgi->param('nag_interval')); + $object->set('nag_interval', $interval); +} + +sub _check_nag_interval { + my ($value) = @_; + detaint_natural($value) + || ThrowUserError('invalid_parameter', { name => 'request reminding interval', err => 'must be numeric' }); + return $value < 0 ? 0 : $value * 24; +} + +sub page_before_template { + my ($self, $args) = @_; + my ($vars, $page) = @$args{qw(vars page_id)}; + return unless $page eq 'request_defer.html'; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $input = Bugzilla->input_params; + + # load flag + my $flag_id = scalar($input->{flag}) + || ThrowUserError('request_nagging_flag_invalid'); + detaint_natural($flag_id) + || ThrowUserError('request_nagging_flag_invalid'); + my $flag = Bugzilla::Flag->new({ id => $flag_id, cache => 1 }) + || ThrowUserError('request_nagging_flag_invalid'); + + # you can only defer flags directed at you + $user->can_see_bug($flag->bug->id) + || ThrowUserError("bug_access_denied", { bug_id => $flag->bug->id }); + $flag->status eq '?' + || ThrowUserError('request_nagging_flag_set'); + $flag->requestee + || ThrowUserError('request_nagging_flag_wind'); + $flag->requestee->id == $user->id + || ThrowUserError('request_nagging_flag_not_owned'); + + my $date = DateTime->now()->truncate(to => 'day'); + my $defer_until; + if ($input->{'defer-until'} + && $input->{'defer-until'} =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/) + { + $defer_until = DateTime->new(year => $1, month => $2, day => $3); + if ($defer_until > $date->clone->add(days => 7)) { + $defer_until = undef; + } + } + + if ($input->{save} && $defer_until) { + $self->_defer_until($flag_id, $defer_until); + $vars->{saved} = "1"; + $vars->{defer_until} = $defer_until; + } + else { + my @dates; + foreach my $i (1..7) { + $date->add(days => 1); + unshift @dates, { days => $i, date => $date->clone }; + } + $vars->{defer_until} = \@dates; + } + + $vars->{flag} = $flag; +} + +sub _defer_until { + my ($self, $flag_id, $defer_until) = @_; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + my ($defer_id) = $dbh->selectrow_array("SELECT id FROM nag_defer WHERE flag_id=?", undef, $flag_id); + if ($defer_id) { + $dbh->do("UPDATE nag_defer SET defer_until=? WHERE id=?", undef, $defer_until->ymd, $flag_id); + } else { + $dbh->do("INSERT INTO nag_defer(flag_id, defer_until) VALUES (?, ?)", undef, $flag_id, $defer_until->ymd); + } + + $dbh->bz_commit_transaction(); +} + +# +# hooks +# + +sub object_end_of_update { + my ($self, $args) = @_; + if ($args->{object}->isa("Bugzilla::Flag") && exists $args->{changes}) { + # any change to the flag (setting, clearing, or retargetting) will clear the deferals + my $flag = $args->{object}; + Bugzilla->dbh->do("DELETE FROM nag_defer WHERE flag_id=?", undef, $flag->id); + } +} + +sub user_preferences { + my ($self, $args) = @_; + my $tab = $args->{'current_tab'}; + return unless $tab eq 'request_nagging'; + + my $save = $args->{'save_changes'}; + my $vars = $args->{'vars'}; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + my %watching = + map { $_ => 1 } + @{ $dbh->selectcol_arrayref( + "SELECT profiles.login_name + FROM nag_watch + INNER JOIN profiles ON nag_watch.nagged_id = profiles.userid + WHERE nag_watch.watcher_id = ? + ORDER BY profiles.login_name", + undef, + $user->id + ) }; + + if ($save) { + my $input = Bugzilla->input_params; + Bugzilla::User::match_field({ 'add_watching' => {'type' => 'multi'} }); + + $dbh->bz_start_transaction(); + + # user preference + if (my $value = $input->{request_nagging}) { + my $settings = $user->settings; + my $setting = new Bugzilla::User::Setting('request_nagging'); + if ($value eq 'default') { + $settings->{request_nagging}->reset_to_default; + } + else { + $setting->validate_value($value); + $settings->{request_nagging}->set($value); + } + } + + # watching + if ($input->{remove_watched_users}) { + my $del_watching = ref($input->{del_watching}) ? $input->{del_watching} : [ $input->{del_watching} ]; + foreach my $login (@$del_watching) { + my $u = Bugzilla::User->new({ name => $login, cache => 1 }) + || next; + next unless exists $watching{$u->login}; + $dbh->do( + "DELETE FROM nag_watch WHERE watcher_id=? AND nagged_id=?", + undef, + $user->id, $u->id + ); + delete $watching{$u->login}; + } + } + if ($input->{add_watching}) { + my $add_watching = ref($input->{add_watching}) ? $input->{add_watching} : [ $input->{add_watching} ]; + foreach my $login (@$add_watching) { + my $u = Bugzilla::User->new({ name => $login, cache => 1 }) + || next; + next if exists $watching{$u->login}; + $dbh->do( + "INSERT INTO nag_watch(watcher_id, nagged_id) VALUES(?, ?)", + undef, + $user->id, $u->id + ); + $watching{$u->login} = 1; + } + } + + $dbh->bz_commit_transaction(); + } + + $vars->{watching} = [ sort keys %watching ]; + + my $handled = $args->{'handled'}; + $$handled = 1; +} + # # installation # @@ -90,4 +325,19 @@ sub install_update_db { $dbh->bz_add_column('products', 'nag_interval', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 7 * 24 }); } +sub install_filesystem { + my ($self, $args) = @_; + my $files = $args->{'files'}; + my $extensions_dir = bz_locations()->{'extensionsdir'}; + my $script_name = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/send-request-nags.pl"; + $files->{$script_name} = { + perms => Bugzilla::Install::Filesystem::WS_EXECUTE + }; +} + +sub install_before_final_checks { + my ($self, $args) = @_; + add_setting('request_nagging', ['on', 'off'], 'on'); +} + __PACKAGE__->NAME; diff --git a/extensions/RequestNagger/bin/send-request.nags.pl b/extensions/RequestNagger/bin/send-request.nags.pl new file mode 100644 index 000000000..c62d91f03 --- /dev/null +++ b/extensions/RequestNagger/bin/send-request.nags.pl @@ -0,0 +1,183 @@ +#!/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($RealBin); +use lib "$RealBin/../../.."; + +use Bugzilla; +BEGIN { Bugzilla->extensions() } + +use Bugzilla::Attachment; +use Bugzilla::Bug; +use Bugzilla::Constants; +use Bugzilla::Error; +use Bugzilla::Extension::RequestNagger::Constants; +use Bugzilla::Mailer; +use Bugzilla::User; +use Bugzilla::Util qw(format_time); +use Email::MIME; +use Sys::Hostname; + +Bugzilla->usage_mode(USAGE_MODE_CMDLINE); + +my $DO_NOT_NAG = grep { $_ eq '-d' } @ARGV; + +my $dbh = Bugzilla->dbh; +my $date = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); +$date = format_time($date, '%a, %d %b %Y %T %z', 'UTC'); + +# delete expired defers +$dbh->do("DELETE FROM nag_defer WHERE defer_until <= CURRENT_DATE()"); +Bugzilla->switch_to_shadow_db(); + +# send nags to requestees +send_nags( + sql => REQUESTEE_NAG_SQL, + template => 'requestee', + recipient_field => 'requestee_id', + date => $date, +); + +# send nags to watchers +send_nags( + sql => WATCHING_NAG_SQL, + template => 'watching', + recipient_field => 'watcher_id', + date => $date, +); + +sub send_nags { + my (%args) = @_; + my $rows = $dbh->selectall_arrayref($args{sql}, { Slice => {} }); + + # iterate over rows, sending email when the current recipient changes + my $requests = []; + my $current_recipient; + foreach my $request (@$rows) { + # send previous user's requests + if (!$current_recipient || $request->{$args{recipient_field}} != $current_recipient->id) { + send_email(%args, recipient => $current_recipient, requests => $requests); + $current_recipient = Bugzilla::User->new({ id => $request->{$args{recipient_field}}, cache => 1 }); + $requests = []; + } + + # check group membership + $request->{requestee} = Bugzilla::User->new({ id => $request->{requestee_id}, cache => 1 }); + my $group; + foreach my $type (FLAG_TYPES) { + next unless $type->{type} eq $request->{flag_type}; + $group = $type->{group}; + last; + } + next unless $request->{requestee}->in_group($group); + + # check bug visibility + next unless $current_recipient->can_see_bug($request->{bug_id}); + + # create objects + $request->{bug} = Bugzilla::Bug->new({ id => $request->{bug_id}, cache => 1 }); + $request->{requester} = Bugzilla::User->new({ id => $request->{requester_id}, cache => 1 }); + $request->{flag} = Bugzilla::Flag->new({ id => $request->{flag_id}, cache => 1 }); + if ($request->{attach_id}) { + $request->{attachment} = Bugzilla::Attachment->new({ id => $request->{attach_id}, cache => 1 }); + # check attachment visibility + next if $request->{attachment}->isprivate && !$current_recipient->is_insider; + } + if (exists $request->{watcher_id}) { + $request->{watcher} = Bugzilla::User->new({ id => $request->{watcher_id}, cache => 1 }); + } + + # add this request to the current user's list + push(@$requests, $request); + } + send_email(%args, recipient => $current_recipient, requests => $requests); +} + +sub send_email { + my (%vars) = @_; + my $vars = \%vars; + return unless $vars->{recipient} && @{ $vars->{requests} }; + + # restructure the list to group by requestee then flag type + my $request_list = delete $vars->{requests}; + my $requests = {}; + my %seen_types; + foreach my $request (@{ $request_list }) { + # by requestee + my $requestee_login = $request->{requestee}->login; + $requests->{$requestee_login} ||= { + requestee => $request->{requestee}, + types => {}, + typelist => [], + }; + + # by flag type + my $types = $requests->{$requestee_login}->{types}; + my $flag_type = $request->{flag_type}; + $types->{$flag_type} ||= []; + + push @{ $types->{$flag_type} }, $request; + $seen_types{$requestee_login}{$flag_type} = 1; + } + foreach my $requestee_login (keys %seen_types) { + my @flag_types; + foreach my $flag_type (map { $_->{type} } FLAG_TYPES) { + push @flag_types, $flag_type if $seen_types{$requestee_login}{$flag_type}; + } + $requests->{$requestee_login}->{typelist} = \@flag_types; + } + $vars->{requests} = $requests; + + # generate email + my $template = Bugzilla->template_inner($vars->{recipient}->setting('lang')); + my $template_file = $vars->{template}; + + my ($header, $text); + $template->process("email/request_nagging-$template_file-header.txt.tmpl", $vars, \$header) + || ThrowTemplateError($template->error()); + $header .= "\n"; + $template->process("email/request_nagging-$template_file.txt.tmpl", $vars, \$text) + || ThrowTemplateError($template->error()); + + my @parts = ( + Email::MIME->create( + attributes => { content_type => "text/plain" }, + body => $text, + ) + ); + if ($vars->{recipient}->setting('email_format') eq 'html') { + my $html; + $template->process("email/request_nagging-$template_file.html.tmpl", $vars, \$html) + || ThrowTemplateError($template->error()); + push @parts, Email::MIME->create( + attributes => { content_type => "text/html" }, + body => $html, + ); + } + + my $email = Email::MIME->new($header); + $email->header_set('X-Generated-By' => hostname()); + if (scalar(@parts) == 1) { + $email->content_type_set($parts[0]->content_type); + } else { + $email->content_type_set('multipart/alternative'); + } + $email->parts_set(\@parts); + + # send + if ($DO_NOT_NAG) { + print $email->as_string, "\n"; + } else { + MessageToMTA($email); + } +} + diff --git a/extensions/RequestNagger/lib/Constants.pm b/extensions/RequestNagger/lib/Constants.pm new file mode 100644 index 000000000..ff31b94e0 --- /dev/null +++ b/extensions/RequestNagger/lib/Constants.pm @@ -0,0 +1,111 @@ +# 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::RequestNagger::Constants; + +use strict; +use base qw(Exporter); + +our @EXPORT = qw( + FLAG_TYPES + REQUESTEE_NAG_SQL + WATCHING_NAG_SQL +); + +# the order of this array determines the order used in email +use constant FLAG_TYPES => ( + { + type => 'review', # flag_type.name + group => 'everyone', # the user must be a member of this group to receive reminders + }, + { + type => 'feedback', + group => 'everyone', + }, + { + type => 'needinfo', + group => 'editbugs', + }, +); + +sub REQUESTEE_NAG_SQL { + my $dbh = Bugzilla->dbh; + my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; + + return " + SELECT + flagtypes.name AS flag_type, + flags.id AS flag_id, + flags.bug_id, + flags.attach_id, + flags.modification_date, + requester.userid AS requester_id, + requestee.userid AS requestee_id + FROM + flags + INNER JOIN flagtypes ON flagtypes.id = flags.type_id + INNER JOIN profiles AS requester ON requester.userid = flags.setter_id + INNER JOIN profiles AS requestee ON requestee.userid = flags.requestee_id + INNER JOIN bugs ON bugs.bug_id = flags.bug_id + INNER JOIN products ON products.id = bugs.product_id + LEFT JOIN attachments ON attachments.attach_id = flags.attach_id + LEFT JOIN profile_setting ON profile_setting.setting_name = 'request_nagging' + LEFT JOIN nag_defer ON nag_defer.flag_id = flags.id + WHERE + " . $dbh->sql_in('flagtypes.name', \@flag_types_sql) . " + AND flags.status = '?' + AND products.nag_interval != 0 + AND TIMESTAMPDIFF(HOUR, flags.modification_date, CURRENT_DATE()) >= products.nag_interval + AND (profile_setting.setting_value IS NULL OR profile_setting.setting_value = 'on') + AND requestee.disable_mail = 0 + AND nag_defer.id IS NULL + ORDER BY + flags.requestee_id, + flagtypes.name, + flags.modification_date + "; +} + +sub WATCHING_NAG_SQL { + my $dbh = Bugzilla->dbh; + my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; + + return " + SELECT + nag_watch.watcher_id, + flagtypes.name AS flag_type, + flags.id AS flag_id, + flags.bug_id, + flags.attach_id, + flags.modification_date, + requester.userid AS requester_id, + requestee.userid AS requestee_id + FROM + flags + INNER JOIN flagtypes ON flagtypes.id = flags.type_id + INNER JOIN profiles AS requester ON requester.userid = flags.setter_id + INNER JOIN profiles AS requestee ON requestee.userid = flags.requestee_id + INNER JOIN bugs ON bugs.bug_id = flags.bug_id + INNER JOIN products ON products.id = bugs.product_id + LEFT JOIN attachments ON attachments.attach_id = flags.attach_id + LEFT JOIN nag_defer ON nag_defer.flag_id = flags.id + INNER JOIN nag_watch ON nag_watch.nagged_id = flags.requestee_id + INNER JOIN profiles AS watcher ON watcher.userid = nag_watch.watcher_id + WHERE + " . $dbh->sql_in('flagtypes.name', \@flag_types_sql) . " + AND flags.status = '?' + AND products.nag_interval != 0 + AND TIMESTAMPDIFF(HOUR, flags.modification_date, CURRENT_DATE()) >= products.nag_interval + AND watcher.disable_mail = 0 + ORDER BY + nag_watch.watcher_id, + flags.requestee_id, + flags.modification_date + "; +} + +1; diff --git a/extensions/RequestNagger/lib/TimeAgo.pm b/extensions/RequestNagger/lib/TimeAgo.pm new file mode 100644 index 000000000..3dfbbeaac --- /dev/null +++ b/extensions/RequestNagger/lib/TimeAgo.pm @@ -0,0 +1,186 @@ +# 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::RequestNagger::TimeAgo; + +use strict; +use utf8; +use DateTime; +use Carp; +use Exporter qw(import); + +use if $ENV{ARCH_64BIT}, 'integer'; + +our @EXPORT_OK = qw(time_ago); + +our $VERSION = '0.06'; + +my @ranges = ( + [ -1, 'in the future' ], + [ 60, 'just now' ], + [ 900, 'a few minutes ago'], # 15*60 + [ 3000, 'less than an hour ago'], # 50*60 + [ 4500, 'about an hour ago'], # 75*60 + [ 7200, 'more than an hour ago'], # 2*60*60 + [ 21600, 'several hours ago'], # 6*60*60 + [ 86400, 'today', sub { # 24*60*60 + my $time = shift; + my $now = shift; + if ( $time->day < $now->day + or $time->month < $now->month + or $time->year < $now->year + ) { + return 'yesterday' + } + if ($time->hour < 5) { + return 'tonight' + } + if ($time->hour < 10) { + return 'this morning' + } + if ($time->hour < 15) { + return 'today' + } + if ($time->hour < 19) { + return 'this afternoon' + } + return 'this evening' + }], + [ 172800, 'yesterday'], # 2*24*60*60 + [ 604800, 'this week'], # 7*24*60*60 + [ 1209600, 'last week'], # 2*7*24*60*60 + [ 2678400, 'this month', sub { # 31*24*60*60 + my $time = shift; + my $now = shift; + if ($time->year == $now->year and $time->month == $now->month) { + return 'this month' + } + return 'last month' + }], + [ 5356800, 'last month'], # 2*31*24*60*60 + [ 24105600, 'several months ago'], # 9*31*24*60*60 + [ 31536000, 'about a year ago'], # 365*24*60*60 + [ 34214400, 'last year'], # (365+31)*24*60*60 + [ 63072000, 'more than a year ago'], # 2*365*24*60*60 + [ 283824000, 'several years ago'], # 9*365*24*60*60 + [ 315360000, 'about a decade ago'], # 10*365*24*60*60 + [ 630720000, 'last decade'], # 20*365*24*60*60 + [ 2838240000, 'several decades ago'], # 90*365*24*60*60 + [ 3153600000, 'about a century ago'], # 100*365*24*60*60 + [ 6307200000, 'last century'], # 200*365*24*60*60 + [ 6622560000, 'more than a century ago'], # 210*365*24*60*60 + [ 28382400000, 'several centuries ago'], # 900*365*24*60*60 + [ 31536000000, 'about a millenium ago'], # 1000*365*24*60*60 + [ 63072000000, 'more than a millenium ago'], # 2000*365*24*60*60 +); + +sub time_ago { + my ($time, $now) = @_; + + if (not defined $time or not $time->isa('DateTime')) { + croak('DateTime::Duration::Fuzzy::time_ago needs a DateTime object as first parameter') + } + if (not defined $now) { + $now = DateTime->now(); + } + if (not $now->isa('DateTime')) { + croak('Invalid second parameter provided to DateTime::Duration::Fuzzy::time_ago; it must be a DateTime object if provided') + } + + my $dur = $now->subtract_datetime_absolute($time)->in_units('seconds'); + + foreach my $range ( @ranges ) { + if ( $dur <= $range->[0] ) { + if ( $range->[2] ) { + return $range->[2]->($time, $now) + } + return $range->[1] + } + } + + return 'millenia ago' +} + +1 + +__END__ + +=head1 NAME + +DateTime::Duration::Fuzzy -- express dates as fuzzy human-friendly strings + +=head1 SYNOPSIS + + use DateTime::Duration::Fuzzy qw(time_ago); + use DateTime; + + my $now = DateTime->new( + year => 2010, month => 12, day => 12, + hour => 19, minute => 59, + ); + my $then = DateTime->new( + year => 2010, month => 12, day => 12, + hour => 15, + ); + print time_ago($then, $now); + # outputs 'several hours ago' + + print time_ago($then); + # $now taken from C<time> function + +=head1 DESCRIPTION + +DateTime::Duration::Fuzzy is inspired from the timeAgo jQuery module +L<http://timeago.yarp.com/>. + +It takes two DateTime objects -- first one representing a moment in the past +and second optional one representine the present, and returns a human-friendly +fuzzy expression of the time gone. + +=head2 functions + +=over 4 + +=item time_ago($then, $now) + +The only exportable function. + +First obligatory parameter is a DateTime object. + +Second optional parameter is also a DateTime object. +If it's not provided, then I<now> as the C<time> function returns is +substituted. + +Returns a string expression of the interval between the two DateTime +objects, like C<several hours ago>, C<yesterday> or <last century>. + +=back + +=head2 performance + +On 64bit machines, it is asvisable to 'use integer', which makes +the calculations faster. You can turn this on by setting the +C<ARCH_64BIT> environmental variable to a true value. + +If you do this on a 32bit machine, you will get wrong results for +intervals starting with "several decades ago". + +=head1 AUTHOR + +Jan Oldrich Kruza, C<< <sixtease at cpan.org> >> + +=head1 LICENSE AND COPYRIGHT + +Copyright 2010 Jan Oldrich Kruza. + +This program is free software; you can redistribute it and/or modify it +under the terms of either: the GNU General Public License as published +by the Free Software Foundation; or the Artistic License. + +See http://dev.perl.org/licenses/ for more information. + +=cut diff --git a/extensions/RequestNagger/template/en/default/account/prefs/request_nagging.html.tmpl b/extensions/RequestNagger/template/en/default/account/prefs/request_nagging.html.tmpl new file mode 100644 index 000000000..98317e328 --- /dev/null +++ b/extensions/RequestNagger/template/en/default/account/prefs/request_nagging.html.tmpl @@ -0,0 +1,56 @@ +[%# 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. + #%] + +<label for="request_nagging"> + Send me reminders for outstanding requests: +</label> +<select name="request_nagging" id="request_nagging"> + <option value="default" [% "selected" IF user.settings.request_nagging.is_default %]> + Site Default (On) + </option> + <option value="on" [% "selected" IF !user.settings.request_nagging.is_default + && user.settings.request_nagging.value == "on" %]> + On + </option> + <option value="off" [% "selected" IF !user.settings.request_nagging.is_default + && user.settings.request_nagging.value == "off" %]> + Off + </option> +</select> + +<h4>User Request Reminder Watching</h4> + +<p> + If you watch a user, you will receive a report of their outstanding + requests. +</p> + +<p> + [% IF watching.size %] + You are watching everyone in the following list:<br> + <select id="del_watching" name="del_watching" multiple="multiple" size="5"> + [% FOREACH u = watching %] + <option value="[% u FILTER html %]">[% u FILTER html %]</option> + [% END %] + </select><br> + <input type="checkbox" id="remove_watched_users" name="remove_watched_users"> + <label for="remove_watched_users">Remove selected users from my watch list</label> + [% ELSE %] + <i>You are currently not watching any users.</i> + [% END %] +</p> + +<p>Add users to my watch list (comma separated list): + [% INCLUDE global/userselect.html.tmpl + id => "add_watching" + name => "add_watching" + value => "" + size => 60 + multiple => 5 + %] +</p> diff --git a/extensions/RequestNagger/template/en/default/email/request_nagging-requestee-header.txt.tmpl b/extensions/RequestNagger/template/en/default/email/request_nagging-requestee-header.txt.tmpl new file mode 100644 index 000000000..3fc2ea66b --- /dev/null +++ b/extensions/RequestNagger/template/en/default/email/request_nagging-requestee-header.txt.tmpl @@ -0,0 +1,19 @@ +[%# 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/field-descs.none.tmpl" %] +[% PROCESS "global/reason-descs.none.tmpl" %] +From: [% Param('mailfrom') %] +To: [% recipient.email %] +Subject: [[% terms.Bugzilla %]] Your Outstanding Requests + ([% FOREACH type = requests.item(recipient.email).typelist %] + [%- requests.item(recipient.email).types.item(type).size %] [%+ type %] + [% ", " UNLESS loop.last %] + [% END %]) +Date: [% date %] +X-Bugzilla-Type: nag diff --git a/extensions/RequestNagger/template/en/default/email/request_nagging-requestee.html.tmpl b/extensions/RequestNagger/template/en/default/email/request_nagging-requestee.html.tmpl new file mode 100644 index 000000000..b1b0eff7e --- /dev/null +++ b/extensions/RequestNagger/template/en/default/email/request_nagging-requestee.html.tmpl @@ -0,0 +1,91 @@ +[%# 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/field-descs.none.tmpl" %] + +<!doctype html> +<html> + +<head> + <title>[[% terms.Bugzilla %]] Your Outstanding Requests</title> +</head> + +<body bgcolor="#ffffff"> + +<p> + The following is a list of requests people have made of you, which are + currently outstanding. To avoid disappointing others, please deal with them as + quickly as possible. +</p> + +[% requests = requests.item(recipient.login) %] +[% FOREACH type = requests.typelist %] + + <h3> + [% type FILTER upper FILTER html %] requests + <span style="font-size: x-small; font-weight: normal"> + (<a href="[% urlbase FILTER none %]buglist.cgi?bug_id= + [% FOREACH request = requests.types.$type %] + [% request.bug.id FILTER none %] + [% "%2C" UNLESS loop.last %] + [% END %]">buglist</a>) + </span> + </h3> + + <ul> + [% FOREACH request = requests.types.$type %] + <li> + <a href="[% urlbase FILTER none %]show_bug.cgi?id=[% request.bug.id FILTER none %]" + title="[% request.bug.bug_status FILTER html %] + [% request.bug.product FILTER html %] :: [% request.bug.component FILTER html %]"> + [% request.bug.id FILTER none %] - [% request.bug.short_desc FILTER html %] + </a><br> + <b>[%+ request.flag.age FILTER html %]</b> from [% request.requester.identity FILTER html %]<br> + <div style="font-size: x-small"> + [% IF request.attachment %] + <a href="[% urlbase FILTER none %]attachment.cgi?id=[% request.attachment.id FILTER none %]">Details</a> + [% IF request.attachment.ispatch %] + | <a href="[% urlbase FILTER none %]attachment.cgi?id=[% request.attachment.id FILTER none %]&action=diff">Diff</a> + | <a href="[% urlbase FILTER none %]review?bug=[% request.bug.id FILTER none %]&attachment=[% request.attachment.id FILTER none %]">Review</a> + [% END %] + | + [% END %] + <a href="[% urlbase FILTER none %]request_defer?flag=[% request.flag.id FILTER none %]">Defer</a> + </div> + <br> + </li> + [% END %] + </ul> + +[% END %] + +<div> + <hr style="border: 1px dashed #969696"> + [% IF requests.types.item('review').size || requests.types.item('feedback').size %] + <a href="https://wiki.mozilla.org/BMO/Handling_Requests"> + Guidance on handling requests + </a><br> + [% END %] + <a href="[% urlbase FILTER none %]request.cgi?action=queue&requestee=[% recipient.login FILTER uri %]&group=type"> + See all your outstanding requests + </a><br> + <a href="[% urlbase FILTER none %]userprefs.cgi#request_nagging"> + Opt out of these emails + </a><br> +</div> + +<div style="font-size: 90%; color: #666666"> + <hr style="border: 1px dashed #969696"> + <b>You are receiving this mail because:</b> + <ul> + <li>You have outstanding requests.</li> + </ul> +</div> + +</body> +</html> diff --git a/extensions/RequestNagger/template/en/default/email/request_nagging-requestee.txt.tmpl b/extensions/RequestNagger/template/en/default/email/request_nagging-requestee.txt.tmpl new file mode 100644 index 000000000..83fbfcf71 --- /dev/null +++ b/extensions/RequestNagger/template/en/default/email/request_nagging-requestee.txt.tmpl @@ -0,0 +1,45 @@ +[%# 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/field-descs.none.tmpl" %] + +The following is a list of requests people have made of you, which are +currently outstanding. To avoid disappointing others, please deal with them as +quickly as possible. + +[% requests = requests.item(recipient.login) %] +[% FOREACH type = requests.typelist %] +:: [% type FILTER upper FILTER html %] requests + +[% FOREACH request = requests.types.$type %] +[[% terms.Bug %] [%+ request.bug.id %]] [% request.bug.short_desc %] + [%+ request.flag.age %] from [% request.requester.identity %] + [%+ urlbase %]show_bug.cgi?id=[% request.bug.id +%] + [% IF request.attachment && request.attachment.ispatch %] + Review: [% urlbase %]review?bug=[% request.bug.id %]&attachment=[% request.attachment.id %] + [% END %] + Defer: [% urlbase %]request_defer?flag=[% request.flag.id %] + +[% END %] +[% END %] + +:: + +[% IF requests.types.item('review').size || requests.types.item('feedback').size %] +Guidance on handling requests: + https://wiki.mozilla.org/BMO/Handling_Requests +[% END %] + +See all your outstanding requests: + [%+ urlbase %]request.cgi?action=queue&requestee=[% recipient.login FILTER uri %]&group=type + +Opt out of these emails: + [%+ urlbase %]userprefs.cgi#request_nagging + +-- +You are receiving this mail because: you have outstanding requests. diff --git a/extensions/RequestNagger/template/en/default/email/request_nagging-watching-header.txt.tmpl b/extensions/RequestNagger/template/en/default/email/request_nagging-watching-header.txt.tmpl new file mode 100644 index 000000000..5693f2be0 --- /dev/null +++ b/extensions/RequestNagger/template/en/default/email/request_nagging-watching-header.txt.tmpl @@ -0,0 +1,15 @@ +[%# 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/field-descs.none.tmpl" %] +[% PROCESS "global/reason-descs.none.tmpl" %] +From: [% Param('mailfrom') %] +To: [% recipient.email %] +Subject: [[% terms.Bugzilla %]] Outstanding Requests Report +Date: [% date %] +X-Bugzilla-Type: nag-watch diff --git a/extensions/RequestNagger/template/en/default/email/request_nagging-watching.html.tmpl b/extensions/RequestNagger/template/en/default/email/request_nagging-watching.html.tmpl new file mode 100644 index 000000000..e01167c5f --- /dev/null +++ b/extensions/RequestNagger/template/en/default/email/request_nagging-watching.html.tmpl @@ -0,0 +1,105 @@ +[%# 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/field-descs.none.tmpl" %] + +<!doctype html> +<html> + +<head> + <title>[[% terms.Bugzilla %]] Outstanding Requests Report</title> +</head> + +<body bgcolor="#ffffff"> + +<p> + The following is a list of people who you are watching that have outstanding + requests. +</p> + +<hr> + +[% FOREACH login = requests.keys.sort %] + [% requestee = requests.$login.requestee %] + [% requestee.identity FILTER html %] + <ul> + <li> + [%+ FOREACH type = requests.$login.typelist %] + [% requests.$login.types.item(type).size %] [%+ type FILTER html %] + [% ", " UNLESS loop.last %] + [% END %] + </li> + </ul> +[% END %] + +[% FOREACH login = requests.keys.sort %] + [% requestee = requests.$login.requestee %] + + [% bug_ids = [] %] + [% FOREACH type = requests.$login.typelist %] + [% FOREACH request = requests.$login.types.$type %] + [% bug_ids.push(request.bug.id) %] + [% END %] + [% END %] + + <hr> + <h3> + [% requestee.identity FILTER html %] + <span style="font-size: x-small; font-weight: normal"> + (<a href="[% urlbase FILTER none %]buglist.cgi?bug_id=[% bug_ids.join(",") FILTER uri %]">buglist</a>) + </span><br> + <span style="font-size: x-small; font-weight: normal"> + [% FOREACH type = requests.$login.typelist %] + [% requests.$login.types.item(type).size %] [%+ type FILTER html %] + [% ", " UNLESS loop.last %] + [% END %] + </span> + </h3> + + [% FOREACH type = requests.$login.typelist %] + + <h3>[% type FILTER upper FILTER html %] requests</h3> + + <ul> + [% FOREACH request = requests.$login.types.$type %] + <li> + <a href="[% urlbase FILTER none %]show_bug.cgi?id=[% request.bug.id FILTER none %]" + title="[% request.bug.bug_status FILTER html %] + [% request.bug.product FILTER html %] :: [% request.bug.component FILTER html %]"> + [% request.bug.id FILTER none %] - [% request.bug.short_desc FILTER html %] + </a><br> + <b>[%+ request.flag.age FILTER html %]</b> from [% request.requester.identity FILTER html %]<br> + [% IF request.flag.deferred %] + Deferred until [%+ request.flag.deferred.ymd FILTER html %]<br> + [% END %] + <br> + </li> + [% END %] + </ul> + + [% END %] + +[% END %] + +<div> + <hr style="border: 1px dashed #969696"> + <a href="[% urlbase FILTER none %]userprefs.cgi?tab=request_nagging"> + Change who you are watching + </a> +</div> + +<div style="font-size: 90%; color: #666666"> + <hr style="border: 1px dashed #969696"> + <b>You are receiving this mail because:</b> + <ul> + <li>you are watching someone with outstanding requests.</li> + </ul> +</div> + +</body> +</html> diff --git a/extensions/RequestNagger/template/en/default/email/request_nagging-watching.txt.tmpl b/extensions/RequestNagger/template/en/default/email/request_nagging-watching.txt.tmpl new file mode 100644 index 000000000..39ca8d004 --- /dev/null +++ b/extensions/RequestNagger/template/en/default/email/request_nagging-watching.txt.tmpl @@ -0,0 +1,47 @@ +[%# 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/field-descs.none.tmpl" %] + +The following is a list of people who you are watching that have outstanding +requests. + +[% FOREACH login = requests.keys.sort %] +[% requestee = requests.$login.requestee %] +:: +:: [% requestee.identity %] +:: [% FOREACH type = requests.$login.typelist %] + [%- requests.$login.types.item(type).size %] [%+ type %] + [% ", " UNLESS loop.last %] + [% END %] +:: + +[% FOREACH type = requests.$login.typelist %] +:: [% type FILTER upper FILTER html %] requests + +[% FOREACH request = requests.$login.types.$type %] +[[% terms.Bug %] [%+ request.bug.id %]] [% request.bug.short_desc %] + [%+ request.flag.age %] from [% request.requester.identity %] + [%+ urlbase %]show_bug.cgi?id=[% request.bug.id +%] + [% IF request.flag.deferred %] + Deferred until [%+ request.flag.deferred.ymd %] + [% END %] + +[% END %] +[% END %] + +[% END %] + +:: + +Change who you are watching + [%+ urlbase %]userprefs.cgi?tab=request_nagging + +-- +You are receiving this mail because: you are watching someone with outstanding +requests. diff --git a/extensions/RequestNagger/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl b/extensions/RequestNagger/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl new file mode 100644 index 000000000..ed3e29c64 --- /dev/null +++ b/extensions/RequestNagger/template/en/default/hook/account/prefs/prefs-tabs.html.tmpl @@ -0,0 +1,14 @@ +[%# 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. + #%] + +[% tabs = tabs.import([{ + name => "request_nagging", + label => "Request Reminders", + link => "userprefs.cgi?tab=request_nagging", + saveable => 1 + }]) %] diff --git a/extensions/RequestNagger/template/en/default/hook/admin/products/edit-common-rows.html.tmpl b/extensions/RequestNagger/template/en/default/hook/admin/products/edit-common-rows.html.tmpl new file mode 100644 index 000000000..795ca2ec0 --- /dev/null +++ b/extensions/RequestNagger/template/en/default/hook/admin/products/edit-common-rows.html.tmpl @@ -0,0 +1,16 @@ +[%# 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. + #%] + +<tr> + <th align="right">Remind for outstanding requests after:</th> + <td> + <input name="nag_interval" size="5" + value="[% product.id ? product.nag_interval / 24 : 7 %]"> + days (Setting this to 0 disables request reminding). + </td> +</tr> diff --git a/extensions/RequestNagger/template/en/default/hook/admin/products/updated-changes.html.tmpl b/extensions/RequestNagger/template/en/default/hook/admin/products/updated-changes.html.tmpl new file mode 100644 index 000000000..9baccce86 --- /dev/null +++ b/extensions/RequestNagger/template/en/default/hook/admin/products/updated-changes.html.tmpl @@ -0,0 +1,14 @@ +[%# 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. + #%] + +[% IF changes.nag_interval.defined %] + <p> + Changed request reminder interval from '[% changes.nag_interval.0 / 24 FILTER html %]' to + '[% product.nag_interval / 24 FILTER html %]'. + </p> +[% END %] diff --git a/extensions/RequestNagger/template/en/default/hook/bug/show-header-end.html.tmpl b/extensions/RequestNagger/template/en/default/hook/bug/show-header-end.html.tmpl new file mode 100644 index 000000000..d91877dba --- /dev/null +++ b/extensions/RequestNagger/template/en/default/hook/bug/show-header-end.html.tmpl @@ -0,0 +1,9 @@ +[%# 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. + #%] + +[% style_urls.push("extensions/RequestNagger/web/requestnagger.css") %] diff --git a/extensions/RequestNagger/template/en/default/hook/global/setting-descs-settings.none.tmpl b/extensions/RequestNagger/template/en/default/hook/global/setting-descs-settings.none.tmpl new file mode 100644 index 000000000..aaec920a9 --- /dev/null +++ b/extensions/RequestNagger/template/en/default/hook/global/setting-descs-settings.none.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. + #%] + +[% + setting_descs.request_nagging = "Send me reminders for outstanding requests" +%] diff --git a/extensions/RequestNagger/template/en/default/hook/global/user-error-errors.html.tmpl b/extensions/RequestNagger/template/en/default/hook/global/user-error-errors.html.tmpl new file mode 100644 index 000000000..12ef38370 --- /dev/null +++ b/extensions/RequestNagger/template/en/default/hook/global/user-error-errors.html.tmpl @@ -0,0 +1,25 @@ +[%# 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. + #%] + +[% IF error == "request_nagging_flag_invalid" %] + [% title = "Invalid Flag" %] + Invalid or missing Flag ID + +[% ELSIF error == "request_nagging_flag_set" %] + [% title = "Flag Already Set" %] + The requested Flag has been set, and is no longer pending. + +[% ELSIF error == "request_nagging_flag_wind" %] + [% title = "No Requestee" %] + The requested Flag does not have a requestee, and cannot be deferred. + +[% ELSIF error == "request_nagging_flag_not_owned" %] + [% title = "Not The Requestee" %] + You cannot defer Flags unless you are the requestee. + +[% END %] diff --git a/extensions/RequestNagger/template/en/default/pages/request_defer.html.tmpl b/extensions/RequestNagger/template/en/default/pages/request_defer.html.tmpl new file mode 100644 index 000000000..e89409ce1 --- /dev/null +++ b/extensions/RequestNagger/template/en/default/pages/request_defer.html.tmpl @@ -0,0 +1,101 @@ +[%# 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/header.html.tmpl + title = "Defer Request Reminder" + style_urls = [ "extensions/RequestNagger/web/style/requestnagger.css" ] + javascript_urls = [ "js/util.js" , "extensions/RequestNagger/web/js/requestnagger.js" ] +%] + +<h2>Defer Request Reminder</h2> + +[% IF saved %] + <div id="message"> + Request reminder deferral has been saved. + </div> +[% END %] + +<form method="post" action="page.cgi"> +<input type="hidden" name="id" value="request_defer.html"> +<input type="hidden" name="flag" value="[% flag.id FILTER none %]"> +<input type="hidden" name="save" value="1"> + +<table class="edit_form"> +<tr><td> + + <div class="flag-bug"> + <a href="show_bug.cgi?id=[% flag.bug.id FILTER none %]"> + [% terms.Bug %] [%+ flag.bug.id FILTER none %] + </a> + - + <a href="show_bug.cgi?id=[% flag.bug.id FILTER none %]"> + [% flag.bug.short_desc FILTER html %] + </a> + </div> + + [% IF flag.attachment %] + <div class="flag-attach"> + <div class="flag-attach-desc"> + <a href="attachment.cgi?id=[% flag.attachment.id FILTER none %]&action=edit"> + [% flag.attachment.description FILTER html %] + </a> + </div> + <div class="flag-attach-details"> + [% flag.attachment.filename FILTER html %] ([% flag.attachment.contenttype FILTER html %]), + [% IF flag.attachment.datasize %] + [%+ flag.attachment.datasize FILTER unitconvert %] + [% ELSE %] + <em>deleted</em> + [% END %], + created by [%+ INCLUDE global/user.html.tmpl who = flag.attachment.attacher %] + </div> + [% IF flag.attachment.ispatch %] + <div class="flag-attach-actions"> + <a href="attachment.cgi?id=[% flag.attachment.id FILTER none ~%] + &action=diff">Diff</a> | + <a href="review?bug=[% flag.bug.id FILTER none ~%] + &attachment=[% flag.attachment.id FILTER none %]">Review</a> + </div> + [% END %] + </div> + [% END %] + + <div class="flag-details"> + <span class="flag-type"> + [% flag.type.name FILTER html %] + </span> + requested by [%+ INCLUDE global/user.html.tmpl who = flag.setter %] + [% flag.age FILTER html %] + </div> + + [% IF saved %] + <div class="deferred"> + Deferred until [% defer_until.ymd FILTER html %]. + </div> + [% ELSE %] + <div class="defer"> + Defer[% "ed" IF flag.deferred %] for + <select name="defer-until" id="defer-until"> + [% FOREACH defer = defer_until %] + <option value="[% defer.date.ymd FILTER html %]" + [%+ "selected" IF flag.deferred.ymd == defer.date.ymd %] + > + [% defer.days FILTER html %] Day[% "s" UNLESS defer.days == 1 %] + </option> + [% END %] + </select> + <span id="defer-date"></span> + </div> + <input type="submit" value="Submit"> + [% END %] +</td></tr> +</table> + +</form> + +[% PROCESS global/footer.html.tmpl %] diff --git a/extensions/RequestNagger/web/js/requestnagger.js b/extensions/RequestNagger/web/js/requestnagger.js new file mode 100644 index 000000000..e5cc43deb --- /dev/null +++ b/extensions/RequestNagger/web/js/requestnagger.js @@ -0,0 +1,13 @@ +/* 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. */ + +YAHOO.util.Event.onDOMReady(function() { + YAHOO.util.Event.addListener('defer-until', 'change', function() { + YAHOO.util.Dom.get('defer-date').innerHTML = 'until ' + this.value; + }); + bz_fireEvent(YAHOO.util.Dom.get('defer-until'), 'change'); +}); diff --git a/extensions/RequestNagger/web/style/requestnagger.css b/extensions/RequestNagger/web/style/requestnagger.css new file mode 100644 index 000000000..c4870a08e --- /dev/null +++ b/extensions/RequestNagger/web/style/requestnagger.css @@ -0,0 +1,42 @@ +/* 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. */ + +.edit_form { + width: 100%; +} + +.flag-bug { + font-size: large; +} + +.flag-bug, .flag-attach, .flag-details { + margin-bottom: 1em; +} + +.flag-attach-details { + font-size: small; +} + +.flag-attach-actions { + font-size: small; +} + +.flag-attach-desc { + font-weight: bold; +} + +.flag-type { + font-weight: bold; +} + +.defer { + margin-bottom: 2em; +} + +.deferred { + font-weight: bold; +} diff --git a/extensions/RequestWhiner/disabled b/extensions/RequestWhiner/disabled new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/extensions/RequestWhiner/disabled diff --git a/skins/custom/global.css b/skins/custom/global.css index 27232c274..cc155124a 100644 --- a/skins/custom/global.css +++ b/skins/custom/global.css @@ -71,3 +71,7 @@ a.controller { width: 100%; text-align: right; } + +.highlighted { + background: lightyellow; +} diff --git a/template/en/default/account/prefs/settings.html.tmpl b/template/en/default/account/prefs/settings.html.tmpl index f8b6ba487..65e31359b 100644 --- a/template/en/default/account/prefs/settings.html.tmpl +++ b/template/en/default/account/prefs/settings.html.tmpl @@ -42,7 +42,7 @@ [% FOREACH name = setting_names %] [% default_name = name _ '-isdefault' %] [% default_val = settings.${name}.default_value %] - <tr> + <tr id="[% name FILTER html %]_row"> <td align="right"> [% setting_descs.$name OR name FILTER html %] </td> @@ -75,3 +75,10 @@ </table> [% END %] <br> + +<script> +YAHOO.util.Event.onDOMReady(function() { + var id = document.location.hash.substring(1) + '_row'; + YAHOO.util.Dom.addClass(id, 'highlighted'); +}); +</script> diff --git a/userprefs.cgi b/userprefs.cgi index e614d8111..b0969b93d 100755 --- a/userprefs.cgi +++ b/userprefs.cgi @@ -150,7 +150,7 @@ sub DoSettings { my $settings = $user->settings; $vars->{'settings'} = $settings; - my @setting_list = keys %$settings; + my @setting_list = sort keys %$settings; $vars->{'setting_names'} = \@setting_list; $vars->{'has_settings_enabled'} = 0; |