diff options
author | Struan Donald <struan@exo.org.uk> | 2020-09-10 14:49:42 +0100 |
---|---|---|
committer | M Somerville <matthew-github@dracos.co.uk> | 2020-10-14 09:23:30 +0100 |
commit | 98cc29c15b98a26f75adabb1c07acdd9513b4446 (patch) | |
tree | 088f82776e23748645ef59e3d8635712cb3cce1b | |
parent | ae686e9f8f16841d2cfcf00dd674503756d69536 (diff) |
[TfL] script to automatically close reports
Closes reports that meet the following criteria:
* status is action scheduled
* in a category with a fixed - council auto response template
* over n days in action scheduled state (n is an argument)
Reports matching these criteria are marked as fixed - council and the
relevant response template text is added as a comment.
Fixes mysociety/fixmystreet-commercial#1955
-rwxr-xr-x | bin/tfl/auto-close-reports | 48 | ||||
-rw-r--r-- | perllib/FixMyStreet/Script/TfL/AutoClose.pm | 129 | ||||
-rw-r--r-- | t/script/tfl/autoclose.t | 216 |
3 files changed, 393 insertions, 0 deletions
diff --git a/bin/tfl/auto-close-reports b/bin/tfl/auto-close-reports new file mode 100755 index 000000000..00d310811 --- /dev/null +++ b/bin/tfl/auto-close-reports @@ -0,0 +1,48 @@ +#!/usr/bin/env perl + +# Closes reports that meet the following criteria: +# * status is action scheduled +# * in a category with a fixed - council auto response template +# * over n days in action scheduled state (n is an argument) +# +# Reports matching these criteria are marked as fixed - council and the +# relevant response template text is added as a comment. + +use warnings; +use v5.14; +use utf8; + +BEGIN { + use File::Basename qw(dirname); + use File::Spec; + my $d = dirname(File::Spec->rel2abs($0)); + require "$d/../../setenv.pl"; +} + +use FixMyStreet; +use Getopt::Long; +use FixMyStreet::Script::TfL::AutoClose; + +my %h; +GetOptions(\%h, 'verbose|v', 'days|d', 'help|h', 'commit|c'); +pod2usage(0) if $h{help}; + +FixMyStreet::Script::TfL::AutoClose->new(%h)->close; + +__END__ + +=head1 NAME + +auto-close-reports - set action_scheduled reports to fixed + +=head1 SYNOPSIS + +auto-close-reports --commit + + Options: + --commit Actually close any reports + --days Number of days before autoclosing + --verbose Output how many reports have been closed + --help This help message + +=cut diff --git a/perllib/FixMyStreet/Script/TfL/AutoClose.pm b/perllib/FixMyStreet/Script/TfL/AutoClose.pm new file mode 100644 index 000000000..687a29e7f --- /dev/null +++ b/perllib/FixMyStreet/Script/TfL/AutoClose.pm @@ -0,0 +1,129 @@ +package FixMyStreet::Script::TfL::AutoClose; + +use v5.14; + +use Moo; +use CronFns; +use FixMyStreet; +use FixMyStreet::Cobrand; +use FixMyStreet::DB; +use Types::Standard qw(InstanceOf Maybe); + +has commit => ( is => 'ro', default => 0 ); + +has verbose => ( is => 'ro', default => 0 ); + +has body => ( + is => 'lazy', + isa => Maybe[InstanceOf['FixMyStreet::DB::Result::Body']], + default => sub { + my $body = FixMyStreet::DB->resultset('Body')->find({ name => 'TfL' }); + return $body; + } +); + +has days => ( + is => 'ro', + default => 28 +); + +sub close { + my $self = shift; + + die "Can't find body\n" unless $self->body; + warn "DRY RUN: use --commit to close reports\n" unless $self->commit; + my $categories = $self->categories; + $self->close_reports($categories); +} + +has newest => ( + is => 'lazy', + isa => InstanceOf['DateTime'], + default => sub { + my $self = shift; + my $days = $self->days * -1; + my $date = DateTime->now->add( days => $days )->truncate( to => 'day' ); + return $date; + } +); + +# get list of cateories that have a response template for the fixed +# state marked as auto-response. +sub categories { + my $self = shift; + + my $templates = FixMyStreet::DB->resultset('ResponseTemplate')->search({ + state => 'fixed - council', + auto_response => 1, + body_id => $self->body->id, + }); + + my %categories; + for my $template ( $templates->all ) { + map { $categories{$_->category} = $template; } $template->contacts->all; + } + + return \%categories; +} + +# find reports in relevant categories that have been set to action +# scheduled for 30 days. +sub close_reports { + my ($self, $categories) = @_; + + my $dtf = FixMyStreet::DB->schema->storage->datetime_parser; + + my $reports = FixMyStreet::DB->resultset('Problem')->search({ + category => { -in => [ keys %$categories ] }, + 'me.state' => 'action scheduled', + bodies_str => $self->body->id, + 'comments.state' => 'confirmed', + 'comments.problem_state' => 'action scheduled', + }, + { + group_by => 'me.id', + join => [ 'comments' ], + having => \[ 'MIN(comments.confirmed) < ?', $dtf->format_datetime($self->newest) ] + }); + + my $count = 0; + for my $r ( $reports->all ) { + my $comments = FixMyStreet::DB->resultset('Comment')->search( + { problem_id => $r->id }, + { order_by => 'confirmed' } + ); + my $earliest; + while ( my $c = $comments->next ) { + if ( $c->problem_state ne 'action scheduled' ) { + $earliest = undef; + next; + } + $earliest = $c->confirmed unless defined $earliest; + } + next unless defined $earliest && $earliest < $self->newest; + if ($self->commit) { + $r->update({ + state => 'fixed - council', + lastupdate => \'current_timestamp', + }); + my $c = FixMyStreet::DB->resultset('Comment')->new( + { + problem => $r, + text => $categories->{$r->category}->text, + state => 'confirmed', + problem_state => 'fixed - council', + user => $self->body->comment_user, + confirmed => \'current_timestamp' + } + ); + $c->insert; + } + $count++; + } + + say "$count reports closed" if $self->verbose; + + return 1; +} + +1; diff --git a/t/script/tfl/autoclose.t b/t/script/tfl/autoclose.t new file mode 100644 index 000000000..91f46867c --- /dev/null +++ b/t/script/tfl/autoclose.t @@ -0,0 +1,216 @@ +use FixMyStreet::TestMech; +use DateTime; +use Test::Output; + +use_ok 'FixMyStreet::Script::TfL::AutoClose'; + +my $close = FixMyStreet::Script::TfL::AutoClose->new( commit => 1 ); +my $no_commit = FixMyStreet::Script::TfL::AutoClose->new(); +my $mech = FixMyStreet::TestMech->new; + + +my $area_id = 2651; +my $body = $mech->create_body_ok($area_id, 'TfL'); +my $body_user = $mech->create_user_ok('tfl@example.com', name => 'TfL', from_body => $body); +$body->update( { comment_user_id => $body_user->id } ); +my $c1 = $mech->create_contact_ok(category => 'Potholes', body_id => $body->id, email => 'p'); +my $c2 = $mech->create_contact_ok(category => 'Graffiti', body_id => $body->id, email => 'g'); +my $c3 = $mech->create_contact_ok(category => 'Flytipping', body_id => $body->id, email => 'f'); +my $t1 = FixMyStreet::DB->resultset('ResponseTemplate')->create({ + body_id => $body->id, title => "Not auto closed", text => "Text 1 ⛄", state => "fixed - council" } +); +my $t2 = FixMyStreet::DB->resultset('ResponseTemplate')->create({ + body_id => $body->id, title => "Auto closed", text => "Text 2", state => "fixed - council", auto_response => 1 +}); +my $t3 = FixMyStreet::DB->resultset('ResponseTemplate')->create({ + body_id => $body->id, title => "Investigating", text => "Text 3", state => "investigating", auto_response => 1 +}); +$t1->add_to_contacts($c1); +$t2->add_to_contacts($c2); +$t3->add_to_contacts($c3); + +is_deeply keys %{ $close->categories }, ("Graffiti"), "fetches correct category list"; + +my $now = DateTime->now; + +my %problems; +for my $p ( + { + category => 'Grafitti', + state => 'in progress', + test => 'category_not_state', + date => $now->clone->add( days => -30 ), + }, + { + category => 'Grafitti', + state => 'action scheduled', + test => 'category_state', + date => $now->clone->add( days => -30 ), + }, + { + category => 'Potholes', + state => 'action scheduled', + test => 'not_category_state', + date => $now->clone->add( days => -30 ), + }, + { + category => 'Grafitti', + state => 'action scheduled', + test => 'category_state_not_old', + date => $now->clone->add( days => -20 ), + } +) { + my $k = delete $p->{test}; + my $d = delete $p->{date}; + $p->{confirmed} = $d; + $p->{lastupdate} = $d; + ($problems{$k}) = $mech->create_problems_for_body( 1, $body->id, 'Title', $p); + my $c = FixMyStreet::DB->resultset('Comment')->create({ + problem => $problems{$k}, + text => 'comment', + state => 'confirmed', + problem_state => $p->{state}, + user => $body_user, + confirmed => $p->{lastupdate} + }); + is $problems{$k}->comments->count, 1, "comment added"; +} + +subtest "check that nothing saved without commit arg" => sub { + ok $no_commit->close_reports({ 'Grafitti' => $t2 }), "close reports ran"; + $_->discard_changes for values %problems; + + is $problems{category_not_state}->state, 'in progress', 'ignores incorrect state'; + is $problems{category_state}->state, 'action scheduled', 'not updated to fixed'; + is $problems{not_category_state}->state, 'action scheduled', 'ignores incorrect category'; + is $problems{category_state_not_old}->state, 'action scheduled', 'ignores newer reports'; + + is $problems{category_state}->comments->count, 1, "no comment added"; +}; + +subtest "check that reports are updated" => sub { + ok $close->close_reports({ 'Grafitti' => $t2 }), "close reports ran"; + + $_->discard_changes for values %problems; + + is $problems{category_not_state}->state, 'in progress', 'ignores incorrect state'; + is $problems{category_state}->state, 'fixed - council', 'updates to fixed'; + is $problems{not_category_state}->state, 'action scheduled', 'ignores incorrect category'; + is $problems{category_state_not_old}->state, 'action scheduled', 'ignores newer reports'; + + my $comment = ( $problems{category_state}->comments->search({}, { order_by => 'id' })->all )[-1]; + is $comment->text, "Text 2", "correct template used when closing"; +}; + +subtest "check that days argument works" => sub { + my $close_newer = FixMyStreet::Script::TfL::AutoClose->new( days => 19, commit => 1 ); + ok $close_newer->close_reports({ 'Grafitti' => $t2 }), "close reports ran"; + + $problems{category_state_not_old}->discard_changes; + is $problems{category_state_not_old}->state, 'fixed - council', 'updates to fixed'; +}; + +subtest 'check that uses oldest comment for time' => sub { + my $latest = $now->clone->add( days => -20 ); + my $oldest = $now->clone->add( days => -30 ); + my ($p) = $mech->create_problems_for_body( 1, $body->id, 'Title', { + category => 'Grafitti', + state => 'action scheduled', + user => $body_user, + lastupdate => $latest, + confirmed => $latest, + }); + + my $first = FixMyStreet::DB->resultset('Comment')->create({ + problem => $p, + problem_state => 'investigating', + text => 'comment', + state => 'confirmed', + user => $body_user, + confirmed => $oldest + }); + + FixMyStreet::DB->resultset('Comment')->create({ + problem => $p, + text => 'comment', + state => 'confirmed', + problem_state => 'action scheduled', + user => $body_user, + confirmed => $latest + }); + + ok $close->close_reports({ 'Grafitti' => $t2 }), "close reports ran"; + + $p->discard_changes; + is $p->state, 'action scheduled', 'ignores comments with wrong state'; + + + $first->update( { problem_state => 'action scheduled' }); + ok $close->close_reports({ 'Grafitti' => $t2 }), "close reports ran"; + + $p->discard_changes; + is $p->state, 'fixed - council', 'updates to fixed'; +}; + +subtest 'check that changing state accounted for' => sub { + my $latest = $now->clone->add( days => -20 ); + my $oldest = $now->clone->add( days => -30 ); + my ($p) = $mech->create_problems_for_body( 1, $body->id, 'Title', { + category => 'Grafitti', + state => 'action scheduled', + user => $body_user, + lastupdate => $latest, + confirmed => $latest, + }); + + my $investigating = FixMyStreet::DB->resultset('Comment')->create({ + problem => $p, + problem_state => 'investigating', + text => 'comment', + state => 'confirmed', + user => $body_user, + confirmed => $latest + }); + + FixMyStreet::DB->resultset('Comment')->create({ + problem => $p, + text => 'comment', + state => 'confirmed', + problem_state => 'action scheduled', + user => $body_user, + confirmed => $oldest + }); + + FixMyStreet::DB->resultset('Comment')->create({ + problem => $p, + text => 'comment', + state => 'confirmed', + problem_state => 'action scheduled', + user => $body_user, + confirmed => $now + }); + + ok $close->close_reports({ 'Grafitti' => $t2 }), "close reports ran"; + + $p->discard_changes; + is $p->state, 'action scheduled', 'change of state resets time'; + + + $investigating->update( { problem_state => 'action scheduled' }); + ok $close->close_reports({ 'Grafitti' => $t2 }), "close reports ran"; + + $p->discard_changes; + is $p->state, 'fixed - council', 'updates to fixed'; +}; +subtest 'check verbose works' => sub { + my $verbose = FixMyStreet::Script::TfL::AutoClose->new( commit => 1, verbose => 1 ); + stdout_is { $close->close_reports({ 'Grafitti' => $t2 }) } "", "No message displayed with verbose"; + stdout_is { $verbose->close_reports({ 'Grafitti' => $t2 }) } "0 reports closed\n", "Message displayed with verbose"; +}; + +subtest 'check dry run warning displayed' => sub { + stderr_is { $no_commit->close() } "DRY RUN: use --commit to close reports\n", "Dry run warning message displayed without commit"; + stderr_is { $close->close() } "", "No warning displayed with commit"; +}; + +done_testing; |