diff options
Diffstat (limited to 'extensions/RequestNagger/Extension.pm')
-rw-r--r-- | extensions/RequestNagger/Extension.pm | 343 |
1 files changed, 343 insertions, 0 deletions
diff --git a/extensions/RequestNagger/Extension.pm b/extensions/RequestNagger/Extension.pm new file mode 100644 index 000000000..af9eb1783 --- /dev/null +++ b/extensions/RequestNagger/Extension.pm @@ -0,0 +1,343 @@ +# 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; + +use strict; +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 +# + +sub db_schema_abstract_schema { + my ($self, $args) = @_; + $args->{'schema'}->{'nag_watch'} = { + FIELDS => [ + id => { + TYPE => 'MEDIUMSERIAL', + NOTNULL => 1, + PRIMARYKEY => 1, + }, + nagged_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => { + TABLE => 'profiles', + COLUMN => 'userid', + DELETE => 'CASCADE', + } + }, + watcher_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => { + TABLE => 'profiles', + COLUMN => 'userid', + DELETE => 'CASCADE', + } + }, + ], + INDEXES => [ + nag_watch_idx => { + FIELDS => [ 'nagged_id', 'watcher_id' ], + TYPE => 'UNIQUE', + }, + ], + }; + $args->{'schema'}->{'nag_defer'} = { + FIELDS => [ + id => { + TYPE => 'MEDIUMSERIAL', + NOTNULL => 1, + PRIMARYKEY => 1, + }, + flag_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => { + TABLE => 'flags', + COLUMN => 'id', + DELETE => 'CASCADE', + } + }, + defer_until => { + TYPE => 'DATETIME', + NOTNULL => 1, + }, + ], + INDEXES => [ + nag_defer_idx => { + FIELDS => [ 'flag_id' ], + TYPE => 'UNIQUE', + }, + ], + }; +} + +sub install_update_db { + my $dbh = Bugzilla->dbh; + $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; |