diff options
-rwxr-xr-x | bin/send-alerts | 5 | ||||
-rwxr-xr-x | bin/send-questionnaires | 5 | ||||
-rwxr-xr-x | bin/send-reports | 5 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/Result/Problem.pm | 5 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/ResultSet/AlertType.pm | 280 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/ResultSet/Problem.pm | 309 | ||||
-rw-r--r-- | perllib/FixMyStreet/DB/ResultSet/Questionnaire.pm | 108 | ||||
-rw-r--r-- | perllib/FixMyStreet/Geocode/Bing.pm | 1 | ||||
-rw-r--r-- | perllib/FixMyStreet/Script/Alerts.pm | 293 | ||||
-rw-r--r-- | perllib/FixMyStreet/Script/Questionnaires.pm | 117 | ||||
-rw-r--r-- | perllib/FixMyStreet/Script/Reports.pm | 313 | ||||
-rw-r--r-- | t/app/controller/admin.t | 4 |
12 files changed, 745 insertions, 700 deletions
diff --git a/bin/send-alerts b/bin/send-alerts index eaebe9b66..5c8a5412c 100755 --- a/bin/send-alerts +++ b/bin/send-alerts @@ -17,14 +17,13 @@ BEGIN { require "$d/../setenv.pl"; } -use CGI; # XXX use CronFns; use FixMyStreet; -use FixMyStreet::DB; +use FixMyStreet::Script::Alerts; my $site = CronFns::site(FixMyStreet->config('BASE_URL')); CronFns::language($site); -FixMyStreet::DB->resultset('AlertType')->email_alerts(); +FixMyStreet::Script::Alerts::send(); diff --git a/bin/send-questionnaires b/bin/send-questionnaires index 15b8027f3..85eef810c 100755 --- a/bin/send-questionnaires +++ b/bin/send-questionnaires @@ -17,15 +17,14 @@ BEGIN { require "$d/../setenv.pl"; } -use CGI; # XXX Awkward kludge use CronFns; use FixMyStreet; -use FixMyStreet::DB; +use FixMyStreet::Script::Questionnaires; my %params; ( $params{verbose}, $params{nomail} ) = CronFns::options(); $params{site} = CronFns::site(FixMyStreet->config('BASE_URL')); CronFns::language($params{site}); -FixMyStreet::DB->resultset('Questionnaire')->send_questionnaires( \%params ); +FixMyStreet::Script::Questionnaires::send( \%params ); diff --git a/bin/send-reports b/bin/send-reports index 2dfe415fa..81be691e7 100755 --- a/bin/send-reports +++ b/bin/send-reports @@ -17,13 +17,12 @@ BEGIN { require "$d/../setenv.pl"; } -use CGI; # XXX use CronFns; use FixMyStreet; -use FixMyStreet::DB; +use FixMyStreet::Script::Reports; my $site = CronFns::site(FixMyStreet->config('BASE_URL')); CronFns::language($site); -FixMyStreet::DB->resultset('Problem')->send_reports(); +FixMyStreet::Script::Reports::send(); diff --git a/perllib/FixMyStreet/DB/Result/Problem.pm b/perllib/FixMyStreet/DB/Result/Problem.pm index 2eb6c6fc7..f2df41f09 100644 --- a/perllib/FixMyStreet/DB/Result/Problem.pm +++ b/perllib/FixMyStreet/DB/Result/Problem.pm @@ -477,6 +477,11 @@ sub url { return "/report/" . $self->id; } +sub admin_url { + my ($self, $cobrand) = @_; + return $cobrand->admin_base_url . '/report_edit/' . $self->id; +} + =head2 get_photo_params Returns a hashref of details of the attached photo, if any, for use in templates. diff --git a/perllib/FixMyStreet/DB/ResultSet/AlertType.pm b/perllib/FixMyStreet/DB/ResultSet/AlertType.pm index d4cf9ba8b..a801600ea 100644 --- a/perllib/FixMyStreet/DB/ResultSet/AlertType.pm +++ b/perllib/FixMyStreet/DB/ResultSet/AlertType.pm @@ -4,286 +4,10 @@ use base 'DBIx::Class::ResultSet'; use strict; use warnings; -use mySociety::DBHandle qw(dbh); -use mySociety::Gaze; -use mySociety::Locale; -use mySociety::MaPit; -use IO::String; -use RABX; - -use FixMyStreet::Cobrand; -use FixMyStreet::Email; - -# Child must have confirmed, id, email, state(!) columns -# If parent/child, child table must also have name and text -# and foreign key to parent must be PARENT_id sub email_alerts ($) { my ( $rs ) = @_; - my $schema = $rs->result_source->schema; - - my $q = $rs->search( { ref => { -not_like => '%local_problems%' } } ); - while (my $alert_type = $q->next) { - my $ref = $alert_type->ref; - my $head_table = $alert_type->head_table; - my $item_table = $alert_type->item_table; - my $query = 'select alert.id as alert_id, alert.user_id as alert_user_id, alert.lang as alert_lang, alert.cobrand as alert_cobrand, - alert.cobrand_data as alert_cobrand_data, alert.parameter as alert_parameter, alert.parameter2 as alert_parameter2, '; - if ($head_table) { - $query .= " - $item_table.id as item_id, $item_table.text as item_text, - $item_table.name as item_name, $item_table.anonymous as item_anonymous, - $item_table.confirmed as item_confirmed, - $head_table.* - from alert, $item_table, $head_table - where alert.parameter::integer = $head_table.id - and $item_table.${head_table}_id = $head_table.id - "; - } else { - $query .= " $item_table.*, - $item_table.id as item_id - from alert, $item_table - where 1 = 1"; - } - $query .= " - and alert_type='$ref' and whendisabled is null and $item_table.confirmed >= whensubscribed - and $item_table.confirmed >= current_timestamp - '7 days'::interval - and (select whenqueued from alert_sent where alert_sent.alert_id = alert.id and alert_sent.parameter::integer = $item_table.id) is null - and $item_table.user_id <> alert.user_id - and " . $alert_type->item_where . " - and alert.confirmed = 1 - order by alert.id, $item_table.confirmed"; - # XXX Ugh - needs work - $query =~ s/\?/alert.parameter/ if ($query =~ /\?/); - $query =~ s/\?/alert.parameter2/ if ($query =~ /\?/); - - $query = dbh()->prepare($query); - $query->execute(); - my $last_alert_id; - my %data = ( template => $alert_type->template, data => '', schema => $schema ); - while (my $row = $query->fetchrow_hashref) { - - my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($row->{alert_cobrand})->new(); - $cobrand->set_lang_and_domain( $row->{alert_lang}, 1, FixMyStreet->path_to('locale')->stringify ); - - # Cobranded and non-cobranded messages can share a database. In this case, the conf file - # should specify a vhost to send the reports for each cobrand, so that they don't get sent - # more than once if there are multiple vhosts running off the same database. The email_host - # call checks if this is the host that sends mail for this cobrand. - next unless $cobrand->email_host; - - # this is for the new_updates alerts - next if $row->{non_public} and $row->{user_id} != $row->{alert_user_id}; - - $schema->resultset('AlertSent')->create( { - alert_id => $row->{alert_id}, - parameter => $row->{item_id}, - } ); - if ($last_alert_id && $last_alert_id != $row->{alert_id}) { - _send_aggregated_alert_email(%data); - %data = ( template => $alert_type->template, data => '', schema => $schema ); - } - - # create problem status message for the templates - if ( FixMyStreet::DB::Result::Problem::fixed_states()->{$row->{state}} ) { - $data{state_message} = _("This report is currently marked as fixed."); - } elsif ( FixMyStreet::DB::Result::Problem::closed_states()->{$row->{state}} ) { - $data{state_message} = _("This report is currently marked as closed.") - } else { - $data{state_message} = _("This report is currently marked as open."); - } - - my $url = $cobrand->base_url_for_report($row); - # this is currently only for new_updates - if ($row->{item_text}) { - if ( $cobrand->moniker ne 'zurich' && $row->{alert_user_id} == $row->{user_id} ) { - # This is an alert to the same user who made the report - make this a login link - # Don't bother with Zurich which has no accounts - my $user = $schema->resultset('User')->find( { - id => $row->{alert_user_id} - } ); - $data{alert_email} = $user->email; - my $token_obj = $schema->resultset('Token')->create( { - scope => 'alert_to_reporter', - data => { - id => $row->{id}, - } - } ); - $data{problem_url} = $url . "/R/" . $token_obj->token; - } else { - $data{problem_url} = $url . "/report/" . $row->{id}; - } - $data{data} .= $row->{item_name} . ' : ' if $row->{item_name} && !$row->{item_anonymous}; - if ( $cobrand->include_time_in_update_alerts ) { - my $parser = DateTime::Format::Pg->new(); - my $dt = $parser->parse_timestamp( $row->{item_confirmed} ); - # We need to always set this otherwise we end up with the DateTime - # object being in the floating timezone in which case applying a - # subsequent timezone set will have no effect. - # this is basically recreating the code from the inflate wrapper - # in the database model. - FixMyStreet->set_time_zone($dt); - $data{data} .= $cobrand->prettify_dt( $dt, 'alert' ) . "\n\n"; - } - $data{data} .= $row->{item_text} . "\n\n------\n\n"; - # this is ward and council problems - } else { - $data{data} .= $url . "/report/" . $row->{id} . " - $row->{title}\n\n"; - if ( exists $row->{geocode} && $row->{geocode} && $ref =~ /ward|council/ ) { - my $nearest_st = _get_address_from_gecode( $row->{geocode} ); - $data{data} .= $nearest_st if $nearest_st; - } - $data{data} .= "\n\n------\n\n"; - } - if (!$data{alert_user_id}) { - %data = (%data, %$row); - if ($ref eq 'area_problems' || $ref eq 'council_problems' || $ref eq 'ward_problems') { - my $va_info = mySociety::MaPit::call('area', $row->{alert_parameter}); - $data{area_name} = $va_info->{name}; - } - if ($ref eq 'ward_problems') { - my $va_info = mySociety::MaPit::call('area', $row->{alert_parameter2}); - $data{ward_name} = $va_info->{name}; - } - } - $data{cobrand} = $row->{alert_cobrand}; - $data{cobrand_data} = $row->{alert_cobrand_data}; - $data{lang} = $row->{alert_lang}; - $last_alert_id = $row->{alert_id}; - } - if ($last_alert_id) { - _send_aggregated_alert_email(%data); - } - } - - # Nearby done separately as the table contains the parameters - my $template = $rs->find( { ref => 'local_problems' } )->template; - my $query = $schema->resultset('Alert')->search( { - alert_type => 'local_problems', - whendisabled => undef, - confirmed => 1 - }, { - order_by => 'id' - } ); - while (my $alert = $query->next) { - my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($alert->cobrand)->new(); - next unless $cobrand->email_host; - next if $alert->is_from_abuser; - - my $longitude = $alert->parameter; - my $latitude = $alert->parameter2; - my $d = mySociety::Gaze::get_radius_containing_population($latitude, $longitude, 200000); - # Convert integer to GB locale string (with a ".") - $d = mySociety::Locale::in_gb_locale { - sprintf("%f", int($d*10+0.5)/10); - }; - my $states = "'" . join( "', '", FixMyStreet::DB::Result::Problem::visible_states() ) . "'"; - my %data = ( - template => $template, - data => '', - alert_id => $alert->id, - alert_email => $alert->user->email, - lang => $alert->lang, - cobrand => $alert->cobrand, - cobrand_data => $alert->cobrand_data, - schema => $schema, - ); - my $q = "select problem.id, problem.bodies_str, problem.postcode, problem.geocode, problem.title from problem_find_nearby(?, ?, ?) as nearby, problem, users - where nearby.problem_id = problem.id - and problem.user_id = users.id - and problem.state in ($states) - and problem.non_public = 'f' - and problem.confirmed >= ? and problem.confirmed >= current_timestamp - '7 days'::interval - and (select whenqueued from alert_sent where alert_sent.alert_id = ? and alert_sent.parameter::integer = problem.id) is null - and users.email <> ? - order by confirmed desc"; - $q = dbh()->prepare($q); - $q->execute($latitude, $longitude, $d, $alert->whensubscribed, $alert->id, $alert->user->email); - while (my $row = $q->fetchrow_hashref) { - $schema->resultset('AlertSent')->create( { - alert_id => $alert->id, - parameter => $row->{id}, - } ); - my $url = $cobrand->base_url_for_report($row); - $data{data} .= $url . "/report/" . $row->{id} . " - $row->{title}\n\n"; - if ( exists $row->{geocode} && $row->{geocode} ) { - my $nearest_st = _get_address_from_gecode( $row->{geocode} ); - $data{data} .= $nearest_st if $nearest_st; - } - $data{data} .= "\n\n------\n\n"; - } - _send_aggregated_alert_email(%data) if $data{data}; - } -} - -sub _send_aggregated_alert_email(%) { - my %data = @_; - - my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($data{cobrand})->new(); - - $cobrand->set_lang_and_domain( $data{lang}, 1, FixMyStreet->path_to('locale')->stringify ); - - if (!$data{alert_email}) { - my $user = $data{schema}->resultset('User')->find( { - id => $data{alert_user_id} - } ); - $data{alert_email} = $user->email; - } - - my ($domain) = $data{alert_email} =~ m{ @ (.*) \z }x; - return if $data{schema}->resultset('Abuse')->search( { - email => [ $data{alert_email}, $domain ] - } )->first; - - my $token = $data{schema}->resultset("Token")->new_result( { - scope => 'alert', - data => { - id => $data{alert_id}, - type => 'unsubscribe', - email => $data{alert_email}, - } - } ); - $data{unsubscribe_url} = $cobrand->base_url( $data{cobrand_data} ) . '/A/' . $token->token; - - my $template = FixMyStreet->get_email_template($cobrand->moniker, $data{lang}, "$data{template}.txt"); - - my $result = FixMyStreet::Email::send_cron( - $data{schema}, - { - _template_ => $template, - _parameters_ => \%data, - To => $data{alert_email}, - }, - undef, - 0, - $cobrand, - $data{lang} - ); - - unless ($result) { - $token->insert(); - } else { - print "Failed to send alert $data{alert_id}!"; - } -} - -sub _get_address_from_gecode { - my $geocode = shift; - - return '' unless defined $geocode; - utf8::encode($geocode) if utf8::is_utf8($geocode); - my $h = new IO::String($geocode); - my $data = RABX::wire_rd($h); - - my $str = ''; - - my $address = $data->{resourceSets}[0]{resources}[0]{address}; - my @address; - push @address, $address->{addressLine} if $address->{addressLine} && $address->{addressLine} ne 'Street'; - push @address, $address->{locality} if $address->{locality}; - $str .= sprintf(_("Nearest road to the pin placed on the map (automatically generated by Bing Maps): %s\n\n"), - join( ', ', @address ) ) if @address; - - return $str; + require FixMyStreet::Script::Alerts; + FixMyStreet::Script::Alerts::send(@_); } 1; diff --git a/perllib/FixMyStreet/DB/ResultSet/Problem.pm b/perllib/FixMyStreet/DB/ResultSet/Problem.pm index 0d7b34daf..f82f0135c 100644 --- a/perllib/FixMyStreet/DB/ResultSet/Problem.pm +++ b/perllib/FixMyStreet/DB/ResultSet/Problem.pm @@ -4,15 +4,9 @@ use base 'DBIx::Class::ResultSet'; use strict; use warnings; -use CronFns; - -use Utils; -use mySociety::MaPit; - -use FixMyStreet; -use FixMyStreet::Cobrand; -use FixMyStreet::Email; -use FixMyStreet::SendReport; +use Memcached; +use mySociety::Locale; +use FixMyStreet::DB; my $site_key; @@ -236,303 +230,10 @@ sub categories_summary { return \%categories; } -sub get_admin_url { - my ($rs, $cobrand, $row) = @_; - return $cobrand->admin_base_url . '/report_edit/' . $row->id; -} - sub send_reports { my ( $rs, $site_override ) = @_; - - # Set up site, language etc. - my ($verbose, $nomail, $debug_mode) = CronFns::options(); - - my $base_url = FixMyStreet->config('BASE_URL'); - my $site = $site_override || CronFns::site($base_url); - - my $states = [ 'confirmed', 'fixed' ]; - $states = [ 'unconfirmed', 'confirmed', 'in progress', 'planned', 'closed', 'investigating' ] if $site eq 'zurich'; - my $unsent = $rs->search( { - state => $states, - whensent => undef, - bodies_str => { '!=', undef }, - } ); - my (%notgot, %note); - - my $send_report = FixMyStreet::SendReport->new(); - my $senders = $send_report->get_senders; - - my $debug_unsent_count = 0; - debug_print("starting to loop through unsent problem reports...") if $debug_mode; - while (my $row = $unsent->next) { - - my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($row->cobrand)->new(); - - if ($debug_mode) { - $debug_unsent_count++; - print "\n"; - debug_print("state=" . $row->state . ", bodies_str=" . $row->bodies_str . ($row->cobrand? ", cobrand=" . $row->cobrand : ""), $row->id); - } - - # Cobranded and non-cobranded messages can share a database. In this case, the conf file - # should specify a vhost to send the reports for each cobrand, so that they don't get sent - # more than once if there are multiple vhosts running off the same database. The email_host - # call checks if this is the host that sends mail for this cobrand. - if (! $cobrand->email_host()) { - debug_print("skipping because this host does not send reports for cobrand " . $cobrand->moniker, $row->id) if $debug_mode; - next; - } - - $cobrand->set_lang_and_domain($row->lang, 1); - if ( $row->is_from_abuser) { - $row->update( { state => 'hidden' } ); - debug_print("hiding because its sender is flagged as an abuser", $row->id) if $debug_mode; - next; - } elsif ( $row->title =~ /app store test/i ) { - $row->update( { state => 'hidden' } ); - debug_print("hiding because it is an app store test message", $row->id) if $debug_mode; - next; - } - - # Template variables for the email - my $email_base_url = $cobrand->base_url_for_report($row); - my %h = map { $_ => $row->$_ } qw/id title detail name category latitude longitude used_map/; - map { $h{$_} = $row->user->$_ || '' } qw/email phone/; - $h{confirmed} = DateTime::Format::Pg->format_datetime( $row->confirmed->truncate (to => 'second' ) ) - if $row->confirmed; - - $h{query} = $row->postcode; - $h{url} = $email_base_url . $row->url; - $h{admin_url} = $rs->get_admin_url($cobrand, $row); - $h{phone_line} = $h{phone} ? _('Phone:') . " $h{phone}\n\n" : ''; - if ($row->photo) { - $h{has_photo} = _("This web page also contains a photo of the problem, provided by the user.") . "\n\n"; - $h{image_url} = $email_base_url . '/photo/' . $row->id . '.full.jpeg'; - } else { - $h{has_photo} = ''; - $h{image_url} = ''; - } - $h{fuzzy} = $row->used_map ? _('To view a map of the precise location of this issue') - : _('The user could not locate the problem on a map, but to see the area around the location they entered'); - $h{closest_address} = ''; - - if ( $row->used_map ) { - $h{closest_address} = $cobrand->find_closest( $h{latitude}, $h{longitude}, $row ); - } - - if ( $cobrand->allow_anonymous_reports && - $row->user->email eq $cobrand->anonymous_account->{'email'} - ) { - $h{anonymous_report} = 1; - $h{user_details} = _('This report was submitted anonymously'); - } else { - $h{user_details} = sprintf(_('Name: %s'), $row->name) . "\n\n"; - $h{user_details} .= sprintf(_('Email: %s'), $row->user->email) . "\n\n"; - } - - $h{easting_northing} = ''; - - if ($cobrand->can('process_additional_metadata_for_email')) { - $cobrand->process_additional_metadata_for_email($row, \%h); - } - - my $bodies = $rs->result_source->schema->resultset('Body')->search( - { id => $row->bodies_str_ids }, - { order_by => 'name' }, - ); - - my $missing; - if ($row->bodies_missing) { - my @missing = $rs->result_source->schema->resultset("Body")->search( - { id => [ split /,/, $row->bodies_missing ] }, - { order_by => 'name' } - )->get_column('name')->all; - $missing = join(' / ', @missing) if @missing; - } - - my @dear; - my %reporters = (); - while (my $body = $bodies->next) { - my $sender_info = $cobrand->get_body_sender( $body, $row->category ); - my $sender = "FixMyStreet::SendReport::" . $sender_info->{method}; - - if ( ! exists $senders->{ $sender } ) { - warn sprintf "No such sender [ $sender ] for body %s ( %d )", $body->name, $body->id; - next; - } - $reporters{ $sender } ||= $sender->new(); - - if ( $reporters{ $sender }->should_skip( $row ) ) { - debug_print("skipped by sender " . $sender_info->{method} . " (might be due to previous failed attempts?)", $row->id) if $debug_mode; - } else { - debug_print("OK, adding recipient body " . $body->id . ":" . $body->name . ", " . $body->send_method, $row->id) if $debug_mode; - push @dear, $body->name; - $reporters{ $sender }->add_body( $body, $sender_info->{config} ); - } - - # If we are in the UK include eastings and northings, and nearest stuff - if ( $cobrand->country eq 'GB' && !$h{easting} ) { - my $coordsyst = 'G'; - my $first_area = $body->body_areas->first->area_id; - my $area_info = mySociety::MaPit::call('area', $first_area); - $coordsyst = 'I' if $area_info->{type} eq 'LGD'; - - ( $h{easting}, $h{northing} ) = Utils::convert_latlon_to_en( $h{latitude}, $h{longitude}, $coordsyst ); - - # email templates don't have conditionals so we need to format this here - $h{easting_northing} = "Easting/Northing"; - $h{easting_northing} .= " (IE)" if $coordsyst eq 'I'; - $h{easting_northing} .= ": $h{easting}/$h{northing}\n\n"; - } - } - - unless ( keys %reporters ) { - die 'Report not going anywhere for ID ' . $row->id . '!'; - } - - unless (@dear) { - debug_print("can't send because sender count is zero", $row->id) if $debug_mode; - next; - } - - if ($h{category} eq _('Other')) { - $h{category_footer} = _('this type of local problem'); - $h{category_line} = ''; - } else { - $h{category_footer} = "'" . $h{category} . "'"; - $h{category_line} = sprintf(_("Category: %s"), $h{category}) . "\n\n"; - } - - if ( $row->subcategory ) { - $h{subcategory_line} = sprintf(_("Subcategory: %s"), $row->subcategory) . "\n\n"; - } else { - $h{subcategory_line} = "\n\n"; - } - - $h{bodies_name} = join(_(' and '), @dear); - if ($h{category} eq _('Other')) { - $h{multiple} = @dear>1 ? "[ " . _("This email has been sent to both councils covering the location of the problem, as the user did not categorise it; please ignore it if you're not the correct council to deal with the issue, or let us know what category of problem this is so we can add it to our system.") . " ]\n\n" - : ''; - } else { - $h{multiple} = @dear>1 ? "[ " . _("This email has been sent to several councils covering the location of the problem, as the category selected is provided for all of them; please ignore it if you're not the correct council to deal with the issue.") . " ]\n\n" - : ''; - } - $h{missing} = ''; - if ($missing) { - $h{missing} = '[ ' - . sprintf(_('We realise this problem might be the responsibility of %s; however, we don\'t currently have any contact details for them. If you know of an appropriate contact address, please do get in touch.'), $missing) - . " ]\n\n"; - } - - if (FixMyStreet->config('STAGING_SITE') && !FixMyStreet->config('SEND_REPORTS_ON_STAGING')) { - # on a staging server send emails to ourselves rather than the bodies - %reporters = map { $_ => $reporters{$_} } grep { /FixMyStreet::SendReport::(Email|EmptyHomes)/ } keys %reporters; - unless (%reporters) { - %reporters = ( 'FixMyStreet::SendReport::Email' => FixMyStreet::SendReport::Email->new() ); - } - } - - # Multiply results together, so one success counts as a success. - my $result = -1; - - for my $sender ( keys %reporters ) { - debug_print("sending using " . $sender, $row->id) if $debug_mode; - $result *= $reporters{ $sender }->send( $row, \%h ); - if ( $reporters{ $sender }->unconfirmed_counts) { - foreach my $e (keys %{ $reporters{ $sender }->unconfirmed_counts } ) { - foreach my $c (keys %{ $reporters{ $sender }->unconfirmed_counts->{$e} }) { - $notgot{$e}{$c} += $reporters{ $sender }->unconfirmed_counts->{$e}{$c}; - } - } - %note = ( - %note, - %{ $reporters{ $sender }->unconfirmed_notes } - ); - } - } - - unless ($result) { - $row->update( { - whensent => \'current_timestamp', - lastupdate => \'current_timestamp', - } ); - if ( $cobrand->report_sent_confirmation_email && !$h{anonymous_report}) { - _send_report_sent_email( $row, \%h, $nomail, $cobrand ); - } - debug_print("send successful: OK", $row->id) if $debug_mode; - } else { - my @errors; - for my $sender ( keys %reporters ) { - unless ( $reporters{ $sender }->success ) { - push @errors, $reporters{ $sender }->error; - } - } - $row->update_send_failed( join( '|', @errors ) ); - debug_print("send FAILED: " . join( '|', @errors ), $row->id) if $debug_mode; - } - } - if ($debug_mode) { - print "\n"; - if ($debug_unsent_count) { - debug_print("processed all unsent reports (total: $debug_unsent_count)"); - } else { - debug_print("no unsent reports were found (must have whensent=null and suitable bodies_str & state) -- nothing to send"); - } - } - - if ($verbose || $debug_mode) { - print "Council email addresses that need checking:\n" if keys %notgot; - foreach my $e (keys %notgot) { - foreach my $c (keys %{$notgot{$e}}) { - print " " . $notgot{$e}{$c} . " problem, to $e category $c (" . $note{$e}{$c}. ")\n"; - } - } - my $sending_errors = ''; - my $unsent = $rs->search( { - state => [ 'confirmed', 'fixed' ], - whensent => undef, - bodies_str => { '!=', undef }, - send_fail_count => { '>', 0 } - } ); - while (my $row = $unsent->next) { - my $base_url = FixMyStreet->config('BASE_URL'); - $sending_errors .= "* " . $base_url . "/report/" . $row->id . ", failed " - . $row->send_fail_count . " times, last at " . $row->send_fail_timestamp - . ", reason " . $row->send_fail_reason . "\n"; - } - if ($sending_errors) { - print "The following reports had problems sending:\n$sending_errors"; - } - } -} - -sub _send_report_sent_email { - my $row = shift; - my $h = shift; - my $nomail = shift; - my $cobrand = shift; - - my $template = FixMyStreet->get_email_template($row->cobrand, $row->lang, 'confirm_report_sent.txt'); - - FixMyStreet::Email::send_cron( - $row->result_source->schema, - { - _template_ => $template, - _parameters_ => $h, - To => $row->user->email, - From => [ FixMyStreet->config('CONTACT_EMAIL'), $cobrand->contact_name ], - }, - FixMyStreet->config('CONTACT_EMAIL'), - $nomail, - $cobrand - ); -} - -sub debug_print { - my $msg = shift; - my $id = shift || ''; - $id = "report $id: " if $id; - print "[] $id$msg\n"; + require FixMyStreet::Script::Reports; + FixMyStreet::Script::Reports::send($site_override); } 1; diff --git a/perllib/FixMyStreet/DB/ResultSet/Questionnaire.pm b/perllib/FixMyStreet/DB/ResultSet/Questionnaire.pm index aca330960..6a1ce0d78 100644 --- a/perllib/FixMyStreet/DB/ResultSet/Questionnaire.pm +++ b/perllib/FixMyStreet/DB/ResultSet/Questionnaire.pm @@ -3,115 +3,11 @@ use base 'DBIx::Class::ResultSet'; use strict; use warnings; -use Encode; -use Utils; - -use FixMyStreet::Email; -use FixMyStreet::Cobrand; sub send_questionnaires { my ( $rs, $params ) = @_; - $rs->send_questionnaires_period( '4 weeks', $params ); - $rs->send_questionnaires_period( '26 weeks', $params ) - if $params->{site} eq 'emptyhomes'; -} - -sub send_questionnaires_period { - my ( $rs, $period, $params ) = @_; - - # Select all problems that need a questionnaire email sending - my $q_params = { - state => [ FixMyStreet::DB::Result::Problem::visible_states() ], - whensent => [ - '-and', - { '!=', undef }, - { '<', \"current_timestamp - '$period'::interval" }, - ], - send_questionnaire => 1, - }; - # FIXME Do these a bit better... - if ($params->{site} eq 'emptyhomes' && $period eq '4 weeks') { - $q_params->{'(select max(whensent) from questionnaire where me.id=problem_id)'} = undef; - } elsif ($params->{site} eq 'emptyhomes' && $period eq '26 weeks') { - $q_params->{'(select max(whensent) from questionnaire where me.id=problem_id)'} = { '!=', undef }; - } else { - $q_params->{'-or'} = [ - '(select max(whensent) from questionnaire where me.id=problem_id)' => undef, - '(select max(whenanswered) from questionnaire where me.id=problem_id)' => { '<', \"current_timestamp - '$period'::interval" } - ]; - } - - my $unsent = $rs->result_source->schema->resultset('Problem')->search( $q_params, { - order_by => { -desc => 'confirmed' } - } ); - - while (my $row = $unsent->next) { - - my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($row->cobrand)->new(); - $cobrand->set_lang_and_domain($row->lang, 1); - - # Not all cobrands send questionnaires - next unless $cobrand->send_questionnaires; - next if $row->is_from_abuser; - - # Cobranded and non-cobranded messages can share a database. In this case, the conf file - # should specify a vhost to send the reports for each cobrand, so that they don't get sent - # more than once if there are multiple vhosts running off the same database. The email_host - # call checks if this is the host that sends mail for this cobrand. - next unless $cobrand->email_host; - - my $template; - if ($params->{site} eq 'emptyhomes') { - ($template = $period) =~ s/ //; - $template = Utils::read_file( FixMyStreet->path_to( "templates/email/emptyhomes/" . $row->lang . "/questionnaire-$template.txt" )->stringify ); - } else { - $template = FixMyStreet->get_email_template($cobrand->moniker, $row->lang, 'questionnaire.txt'); - } - - my %h = map { $_ => $row->$_ } qw/name title detail category/; - $h{created} = Utils::prettify_duration( time() - $row->confirmed->epoch, 'week' ); - - my $questionnaire = $rs->create( { - problem_id => $row->id, - whensent => \'current_timestamp', - } ); - - # We won't send another questionnaire unless they ask for it (or it was - # the first EHA questionnaire. - $row->send_questionnaire( 0 ) - if $params->{site} ne 'emptyhomes' || $period eq '26 weeks'; - - my $token = $rs->result_source->schema->resultset("Token")->new_result( { - scope => 'questionnaire', - data => $questionnaire->id, - } ); - $h{url} = $cobrand->base_url($row->cobrand_data) . '/Q/' . $token->token; - - print "Sending questionnaire " . $questionnaire->id . ", problem " - . $row->id . ", token " . $token->token . " to " - . $row->user->email . "\n" - if $params->{verbose}; - - my $result = FixMyStreet::Email::send_cron( - $rs->result_source->schema, - { - _template_ => $template, - _parameters_ => \%h, - To => [ [ $row->user->email, $row->name ] ], - }, - undef, - $params->{nomail}, - $cobrand - ); - unless ($result) { - print " ...success\n" if $params->{verbose}; - $row->update(); - $token->insert(); - } else { - print " ...failed\n" if $params->{verbose}; - $questionnaire->delete; - } - } + require FixMyStreet::Script::Questionnaires; + FixMyStreet::Script::Questionnaires::send($params); } sub timeline { diff --git a/perllib/FixMyStreet/Geocode/Bing.pm b/perllib/FixMyStreet/Geocode/Bing.pm index 8cad8be7c..a846f3348 100644 --- a/perllib/FixMyStreet/Geocode/Bing.pm +++ b/perllib/FixMyStreet/Geocode/Bing.pm @@ -8,6 +8,7 @@ package FixMyStreet::Geocode::Bing; use strict; +use FixMyStreet::Geocode; use Utils; # string STRING CONTEXT diff --git a/perllib/FixMyStreet/Script/Alerts.pm b/perllib/FixMyStreet/Script/Alerts.pm new file mode 100644 index 000000000..fea897a24 --- /dev/null +++ b/perllib/FixMyStreet/Script/Alerts.pm @@ -0,0 +1,293 @@ +package FixMyStreet::Script::Alerts; + +use strict; +use warnings; + +use DateTime::Format::Pg; +use IO::String; + +use mySociety::DBHandle qw(dbh); +use mySociety::Gaze; +use mySociety::Locale; +use mySociety::MaPit; +use RABX; + +use FixMyStreet::Cobrand; +use FixMyStreet::DB; +use FixMyStreet::Email; + +FixMyStreet->configure_mysociety_dbhandle; + +# Child must have confirmed, id, email, state(!) columns +# If parent/child, child table must also have name and text +# and foreign key to parent must be PARENT_id +sub send() { + my $rs = FixMyStreet::DB->resultset('AlertType'); + my $schema = $rs->result_source->schema; + + my $q = $rs->search( { ref => { -not_like => '%local_problems%' } } ); + while (my $alert_type = $q->next) { + my $ref = $alert_type->ref; + my $head_table = $alert_type->head_table; + my $item_table = $alert_type->item_table; + my $query = 'select alert.id as alert_id, alert.user_id as alert_user_id, alert.lang as alert_lang, alert.cobrand as alert_cobrand, + alert.cobrand_data as alert_cobrand_data, alert.parameter as alert_parameter, alert.parameter2 as alert_parameter2, '; + if ($head_table) { + $query .= " + $item_table.id as item_id, $item_table.text as item_text, + $item_table.name as item_name, $item_table.anonymous as item_anonymous, + $item_table.confirmed as item_confirmed, + $head_table.* + from alert, $item_table, $head_table + where alert.parameter::integer = $head_table.id + and $item_table.${head_table}_id = $head_table.id + "; + } else { + $query .= " $item_table.*, + $item_table.id as item_id + from alert, $item_table + where 1 = 1"; + } + $query .= " + and alert_type='$ref' and whendisabled is null and $item_table.confirmed >= whensubscribed + and $item_table.confirmed >= current_timestamp - '7 days'::interval + and (select whenqueued from alert_sent where alert_sent.alert_id = alert.id and alert_sent.parameter::integer = $item_table.id) is null + and $item_table.user_id <> alert.user_id + and " . $alert_type->item_where . " + and alert.confirmed = 1 + order by alert.id, $item_table.confirmed"; + # XXX Ugh - needs work + $query =~ s/\?/alert.parameter/ if ($query =~ /\?/); + $query =~ s/\?/alert.parameter2/ if ($query =~ /\?/); + + $query = dbh()->prepare($query); + $query->execute(); + my $last_alert_id; + my %data = ( template => $alert_type->template, data => '', schema => $schema ); + while (my $row = $query->fetchrow_hashref) { + + my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($row->{alert_cobrand})->new(); + $cobrand->set_lang_and_domain( $row->{alert_lang}, 1, FixMyStreet->path_to('locale')->stringify ); + + # Cobranded and non-cobranded messages can share a database. In this case, the conf file + # should specify a vhost to send the reports for each cobrand, so that they don't get sent + # more than once if there are multiple vhosts running off the same database. The email_host + # call checks if this is the host that sends mail for this cobrand. + next unless $cobrand->email_host; + + # this is for the new_updates alerts + next if $row->{non_public} and $row->{user_id} != $row->{alert_user_id}; + + $schema->resultset('AlertSent')->create( { + alert_id => $row->{alert_id}, + parameter => $row->{item_id}, + } ); + if ($last_alert_id && $last_alert_id != $row->{alert_id}) { + _send_aggregated_alert_email(%data); + %data = ( template => $alert_type->template, data => '', schema => $schema ); + } + + # create problem status message for the templates + if ( FixMyStreet::DB::Result::Problem::fixed_states()->{$row->{state}} ) { + $data{state_message} = _("This report is currently marked as fixed."); + } elsif ( FixMyStreet::DB::Result::Problem::closed_states()->{$row->{state}} ) { + $data{state_message} = _("This report is currently marked as closed.") + } else { + $data{state_message} = _("This report is currently marked as open."); + } + + my $url = $cobrand->base_url_for_report($row); + # this is currently only for new_updates + if ($row->{item_text}) { + if ( $cobrand->moniker ne 'zurich' && $row->{alert_user_id} == $row->{user_id} ) { + # This is an alert to the same user who made the report - make this a login link + # Don't bother with Zurich which has no accounts + my $user = $schema->resultset('User')->find( { + id => $row->{alert_user_id} + } ); + $data{alert_email} = $user->email; + my $token_obj = $schema->resultset('Token')->create( { + scope => 'alert_to_reporter', + data => { + id => $row->{id}, + } + } ); + $data{problem_url} = $url . "/R/" . $token_obj->token; + } else { + $data{problem_url} = $url . "/report/" . $row->{id}; + } + $data{data} .= $row->{item_name} . ' : ' if $row->{item_name} && !$row->{item_anonymous}; + if ( $cobrand->include_time_in_update_alerts ) { + my $parser = DateTime::Format::Pg->new(); + my $dt = $parser->parse_timestamp( $row->{item_confirmed} ); + # We need to always set this otherwise we end up with the DateTime + # object being in the floating timezone in which case applying a + # subsequent timezone set will have no effect. + # this is basically recreating the code from the inflate wrapper + # in the database model. + FixMyStreet->set_time_zone($dt); + $data{data} .= $cobrand->prettify_dt( $dt, 'alert' ) . "\n\n"; + } + $data{data} .= $row->{item_text} . "\n\n------\n\n"; + # this is ward and council problems + } else { + $data{data} .= $url . "/report/" . $row->{id} . " - $row->{title}\n\n"; + if ( exists $row->{geocode} && $row->{geocode} && $ref =~ /ward|council/ ) { + my $nearest_st = _get_address_from_gecode( $row->{geocode} ); + $data{data} .= $nearest_st if $nearest_st; + } + $data{data} .= "\n\n------\n\n"; + } + if (!$data{alert_user_id}) { + %data = (%data, %$row); + if ($ref eq 'area_problems' || $ref eq 'council_problems' || $ref eq 'ward_problems') { + my $va_info = mySociety::MaPit::call('area', $row->{alert_parameter}); + $data{area_name} = $va_info->{name}; + } + if ($ref eq 'ward_problems') { + my $va_info = mySociety::MaPit::call('area', $row->{alert_parameter2}); + $data{ward_name} = $va_info->{name}; + } + } + $data{cobrand} = $row->{alert_cobrand}; + $data{cobrand_data} = $row->{alert_cobrand_data}; + $data{lang} = $row->{alert_lang}; + $last_alert_id = $row->{alert_id}; + } + if ($last_alert_id) { + _send_aggregated_alert_email(%data); + } + } + + # Nearby done separately as the table contains the parameters + my $template = $rs->find( { ref => 'local_problems' } )->template; + my $query = $schema->resultset('Alert')->search( { + alert_type => 'local_problems', + whendisabled => undef, + confirmed => 1 + }, { + order_by => 'id' + } ); + while (my $alert = $query->next) { + my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($alert->cobrand)->new(); + next unless $cobrand->email_host; + next if $alert->is_from_abuser; + + my $longitude = $alert->parameter; + my $latitude = $alert->parameter2; + my $d = mySociety::Gaze::get_radius_containing_population($latitude, $longitude, 200000); + # Convert integer to GB locale string (with a ".") + $d = mySociety::Locale::in_gb_locale { + sprintf("%f", int($d*10+0.5)/10); + }; + my $states = "'" . join( "', '", FixMyStreet::DB::Result::Problem::visible_states() ) . "'"; + my %data = ( + template => $template, + data => '', + alert_id => $alert->id, + alert_email => $alert->user->email, + lang => $alert->lang, + cobrand => $alert->cobrand, + cobrand_data => $alert->cobrand_data, + schema => $schema, + ); + my $q = "select problem.id, problem.bodies_str, problem.postcode, problem.geocode, problem.title from problem_find_nearby(?, ?, ?) as nearby, problem, users + where nearby.problem_id = problem.id + and problem.user_id = users.id + and problem.state in ($states) + and problem.non_public = 'f' + and problem.confirmed >= ? and problem.confirmed >= current_timestamp - '7 days'::interval + and (select whenqueued from alert_sent where alert_sent.alert_id = ? and alert_sent.parameter::integer = problem.id) is null + and users.email <> ? + order by confirmed desc"; + $q = dbh()->prepare($q); + $q->execute($latitude, $longitude, $d, $alert->whensubscribed, $alert->id, $alert->user->email); + while (my $row = $q->fetchrow_hashref) { + $schema->resultset('AlertSent')->create( { + alert_id => $alert->id, + parameter => $row->{id}, + } ); + my $url = $cobrand->base_url_for_report($row); + $data{data} .= $url . "/report/" . $row->{id} . " - $row->{title}\n\n"; + if ( exists $row->{geocode} && $row->{geocode} ) { + my $nearest_st = _get_address_from_gecode( $row->{geocode} ); + $data{data} .= $nearest_st if $nearest_st; + } + $data{data} .= "\n\n------\n\n"; + } + _send_aggregated_alert_email(%data) if $data{data}; + } +} + +sub _send_aggregated_alert_email(%) { + my %data = @_; + + my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($data{cobrand})->new(); + + $cobrand->set_lang_and_domain( $data{lang}, 1, FixMyStreet->path_to('locale')->stringify ); + + if (!$data{alert_email}) { + my $user = $data{schema}->resultset('User')->find( { + id => $data{alert_user_id} + } ); + $data{alert_email} = $user->email; + } + + my ($domain) = $data{alert_email} =~ m{ @ (.*) \z }x; + return if $data{schema}->resultset('Abuse')->search( { + email => [ $data{alert_email}, $domain ] + } )->first; + + my $token = $data{schema}->resultset("Token")->new_result( { + scope => 'alert', + data => { + id => $data{alert_id}, + type => 'unsubscribe', + email => $data{alert_email}, + } + } ); + $data{unsubscribe_url} = $cobrand->base_url( $data{cobrand_data} ) . '/A/' . $token->token; + + my $template = FixMyStreet->get_email_template($cobrand->moniker, $data{lang}, "$data{template}.txt"); + + my $result = FixMyStreet::Email::send_cron( + $data{schema}, + { + _template_ => $template, + _parameters_ => \%data, + To => $data{alert_email}, + }, + undef, + 0, + $cobrand, + $data{lang} + ); + + unless ($result) { + $token->insert(); + } else { + print "Failed to send alert $data{alert_id}!"; + } +} + +sub _get_address_from_gecode { + my $geocode = shift; + + return '' unless defined $geocode; + utf8::encode($geocode) if utf8::is_utf8($geocode); + my $h = new IO::String($geocode); + my $data = RABX::wire_rd($h); + + my $str = ''; + + my $address = $data->{resourceSets}[0]{resources}[0]{address}; + my @address; + push @address, $address->{addressLine} if $address->{addressLine} && $address->{addressLine} ne 'Street'; + push @address, $address->{locality} if $address->{locality}; + $str .= sprintf(_("Nearest road to the pin placed on the map (automatically generated by Bing Maps): %s\n\n"), + join( ', ', @address ) ) if @address; + + return $str; +} + +1; diff --git a/perllib/FixMyStreet/Script/Questionnaires.pm b/perllib/FixMyStreet/Script/Questionnaires.pm new file mode 100644 index 000000000..2d676f15d --- /dev/null +++ b/perllib/FixMyStreet/Script/Questionnaires.pm @@ -0,0 +1,117 @@ +package FixMyStreet::Script::Questionnaires; + +use strict; +use warnings; +use Utils; +use FixMyStreet::DB; +use FixMyStreet::Email; +use FixMyStreet::Cobrand; + +sub send { + my ( $params ) = @_; + send_questionnaires_period( '4 weeks', $params ); + send_questionnaires_period( '26 weeks', $params ) + if $params->{site} eq 'emptyhomes'; +} + +sub send_questionnaires_period { + my ( $period, $params ) = @_; + + my $rs = FixMyStreet::DB->resultset('Questionnaire'); + + # Select all problems that need a questionnaire email sending + my $q_params = { + state => [ FixMyStreet::DB::Result::Problem::visible_states() ], + whensent => [ + '-and', + { '!=', undef }, + { '<', \"current_timestamp - '$period'::interval" }, + ], + send_questionnaire => 1, + }; + # FIXME Do these a bit better... + if ($params->{site} eq 'emptyhomes' && $period eq '4 weeks') { + $q_params->{'(select max(whensent) from questionnaire where me.id=problem_id)'} = undef; + } elsif ($params->{site} eq 'emptyhomes' && $period eq '26 weeks') { + $q_params->{'(select max(whensent) from questionnaire where me.id=problem_id)'} = { '!=', undef }; + } else { + $q_params->{'-or'} = [ + '(select max(whensent) from questionnaire where me.id=problem_id)' => undef, + '(select max(whenanswered) from questionnaire where me.id=problem_id)' => { '<', \"current_timestamp - '$period'::interval" } + ]; + } + + my $unsent = FixMyStreet::DB->resultset('Problem')->search( $q_params, { + order_by => { -desc => 'confirmed' } + } ); + + while (my $row = $unsent->next) { + + my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($row->cobrand)->new(); + $cobrand->set_lang_and_domain($row->lang, 1); + + # Not all cobrands send questionnaires + next unless $cobrand->send_questionnaires; + next if $row->is_from_abuser; + + # Cobranded and non-cobranded messages can share a database. In this case, the conf file + # should specify a vhost to send the reports for each cobrand, so that they don't get sent + # more than once if there are multiple vhosts running off the same database. The email_host + # call checks if this is the host that sends mail for this cobrand. + next unless $cobrand->email_host; + + my $template; + if ($params->{site} eq 'emptyhomes') { + ($template = $period) =~ s/ //; + $template = Utils::read_file( FixMyStreet->path_to( "templates/email/emptyhomes/" . $row->lang . "/questionnaire-$template.txt" )->stringify ); + } else { + $template = FixMyStreet->get_email_template($cobrand->moniker, $row->lang, 'questionnaire.txt'); + } + + my %h = map { $_ => $row->$_ } qw/name title detail category/; + $h{created} = Utils::prettify_duration( time() - $row->confirmed->epoch, 'week' ); + + my $questionnaire = $rs->create( { + problem_id => $row->id, + whensent => \'current_timestamp', + } ); + + # We won't send another questionnaire unless they ask for it (or it was + # the first EHA questionnaire. + $row->send_questionnaire( 0 ) + if $params->{site} ne 'emptyhomes' || $period eq '26 weeks'; + + my $token = FixMyStreet::DB->resultset("Token")->new_result( { + scope => 'questionnaire', + data => $questionnaire->id, + } ); + $h{url} = $cobrand->base_url($row->cobrand_data) . '/Q/' . $token->token; + + print "Sending questionnaire " . $questionnaire->id . ", problem " + . $row->id . ", token " . $token->token . " to " + . $row->user->email . "\n" + if $params->{verbose}; + + my $result = FixMyStreet::Email::send_cron( + $rs->result_source->schema, + { + _template_ => $template, + _parameters_ => \%h, + To => [ [ $row->user->email, $row->name ] ], + }, + undef, + $params->{nomail}, + $cobrand + ); + unless ($result) { + print " ...success\n" if $params->{verbose}; + $row->update(); + $token->insert(); + } else { + print " ...failed\n" if $params->{verbose}; + $questionnaire->delete; + } + } +} + +1; diff --git a/perllib/FixMyStreet/Script/Reports.pm b/perllib/FixMyStreet/Script/Reports.pm new file mode 100644 index 000000000..55b1bb21c --- /dev/null +++ b/perllib/FixMyStreet/Script/Reports.pm @@ -0,0 +1,313 @@ +package FixMyStreet::Script::Reports; + +use strict; +use warnings; + +use CronFns; +use DateTime::Format::Pg; + +use Utils; +use mySociety::MaPit; + +use FixMyStreet; +use FixMyStreet::Cobrand; +use FixMyStreet::DB; +use FixMyStreet::Email; +use FixMyStreet::SendReport; + +sub send(;$) { + my ($site_override) = @_; + my $rs = FixMyStreet::DB->resultset('Problem'); + + # Set up site, language etc. + my ($verbose, $nomail, $debug_mode) = CronFns::options(); + + my $base_url = FixMyStreet->config('BASE_URL'); + my $site = $site_override || CronFns::site($base_url); + + my $states = [ 'confirmed', 'fixed' ]; + $states = [ 'unconfirmed', 'confirmed', 'in progress', 'planned', 'closed', 'investigating' ] if $site eq 'zurich'; + my $unsent = $rs->search( { + state => $states, + whensent => undef, + bodies_str => { '!=', undef }, + } ); + my (%notgot, %note); + + my $send_report = FixMyStreet::SendReport->new(); + my $senders = $send_report->get_senders; + + my $debug_unsent_count = 0; + debug_print("starting to loop through unsent problem reports...") if $debug_mode; + while (my $row = $unsent->next) { + + my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($row->cobrand)->new(); + + if ($debug_mode) { + $debug_unsent_count++; + print "\n"; + debug_print("state=" . $row->state . ", bodies_str=" . $row->bodies_str . ($row->cobrand? ", cobrand=" . $row->cobrand : ""), $row->id); + } + + # Cobranded and non-cobranded messages can share a database. In this case, the conf file + # should specify a vhost to send the reports for each cobrand, so that they don't get sent + # more than once if there are multiple vhosts running off the same database. The email_host + # call checks if this is the host that sends mail for this cobrand. + if (! $cobrand->email_host()) { + debug_print("skipping because this host does not send reports for cobrand " . $cobrand->moniker, $row->id) if $debug_mode; + next; + } + + $cobrand->set_lang_and_domain($row->lang, 1); + if ( $row->is_from_abuser) { + $row->update( { state => 'hidden' } ); + debug_print("hiding because its sender is flagged as an abuser", $row->id) if $debug_mode; + next; + } elsif ( $row->title =~ /app store test/i ) { + $row->update( { state => 'hidden' } ); + debug_print("hiding because it is an app store test message", $row->id) if $debug_mode; + next; + } + + # Template variables for the email + my $email_base_url = $cobrand->base_url_for_report($row); + my %h = map { $_ => $row->$_ } qw/id title detail name category latitude longitude used_map/; + map { $h{$_} = $row->user->$_ || '' } qw/email phone/; + $h{confirmed} = DateTime::Format::Pg->format_datetime( $row->confirmed->truncate (to => 'second' ) ) + if $row->confirmed; + + $h{query} = $row->postcode; + $h{url} = $email_base_url . $row->url; + $h{admin_url} = $row->admin_url($cobrand); + $h{phone_line} = $h{phone} ? _('Phone:') . " $h{phone}\n\n" : ''; + if ($row->photo) { + $h{has_photo} = _("This web page also contains a photo of the problem, provided by the user.") . "\n\n"; + $h{image_url} = $email_base_url . '/photo/' . $row->id . '.full.jpeg'; + } else { + $h{has_photo} = ''; + $h{image_url} = ''; + } + $h{fuzzy} = $row->used_map ? _('To view a map of the precise location of this issue') + : _('The user could not locate the problem on a map, but to see the area around the location they entered'); + $h{closest_address} = ''; + + if ( $row->used_map ) { + $h{closest_address} = $cobrand->find_closest( $h{latitude}, $h{longitude}, $row ); + } + + if ( $cobrand->allow_anonymous_reports && + $row->user->email eq $cobrand->anonymous_account->{'email'} + ) { + $h{anonymous_report} = 1; + $h{user_details} = _('This report was submitted anonymously'); + } else { + $h{user_details} = sprintf(_('Name: %s'), $row->name) . "\n\n"; + $h{user_details} .= sprintf(_('Email: %s'), $row->user->email) . "\n\n"; + } + + $h{easting_northing} = ''; + + if ($cobrand->can('process_additional_metadata_for_email')) { + $cobrand->process_additional_metadata_for_email($row, \%h); + } + + my $bodies = FixMyStreet::DB->resultset('Body')->search( + { id => $row->bodies_str_ids }, + { order_by => 'name' }, + ); + + my $missing; + if ($row->bodies_missing) { + my @missing = FixMyStreet::DB->resultset("Body")->search( + { id => [ split /,/, $row->bodies_missing ] }, + { order_by => 'name' } + )->get_column('name')->all; + $missing = join(' / ', @missing) if @missing; + } + + my @dear; + my %reporters = (); + while (my $body = $bodies->next) { + my $sender_info = $cobrand->get_body_sender( $body, $row->category ); + my $sender = "FixMyStreet::SendReport::" . $sender_info->{method}; + + if ( ! exists $senders->{ $sender } ) { + warn sprintf "No such sender [ $sender ] for body %s ( %d )", $body->name, $body->id; + next; + } + $reporters{ $sender } ||= $sender->new(); + + if ( $reporters{ $sender }->should_skip( $row ) ) { + debug_print("skipped by sender " . $sender_info->{method} . " (might be due to previous failed attempts?)", $row->id) if $debug_mode; + } else { + debug_print("OK, adding recipient body " . $body->id . ":" . $body->name . ", " . $body->send_method, $row->id) if $debug_mode; + push @dear, $body->name; + $reporters{ $sender }->add_body( $body, $sender_info->{config} ); + } + + # If we are in the UK include eastings and northings, and nearest stuff + if ( $cobrand->country eq 'GB' && !$h{easting} ) { + my $coordsyst = 'G'; + my $first_area = $body->body_areas->first->area_id; + my $area_info = mySociety::MaPit::call('area', $first_area); + $coordsyst = 'I' if $area_info->{type} eq 'LGD'; + + ( $h{easting}, $h{northing} ) = Utils::convert_latlon_to_en( $h{latitude}, $h{longitude}, $coordsyst ); + + # email templates don't have conditionals so we need to format this here + $h{easting_northing} = "Easting/Northing"; + $h{easting_northing} .= " (IE)" if $coordsyst eq 'I'; + $h{easting_northing} .= ": $h{easting}/$h{northing}\n\n"; + } + } + + unless ( keys %reporters ) { + die 'Report not going anywhere for ID ' . $row->id . '!'; + } + + unless (@dear) { + debug_print("can't send because sender count is zero", $row->id) if $debug_mode; + next; + } + + if ($h{category} eq _('Other')) { + $h{category_footer} = _('this type of local problem'); + $h{category_line} = ''; + } else { + $h{category_footer} = "'" . $h{category} . "'"; + $h{category_line} = sprintf(_("Category: %s"), $h{category}) . "\n\n"; + } + + if ( $row->subcategory ) { + $h{subcategory_line} = sprintf(_("Subcategory: %s"), $row->subcategory) . "\n\n"; + } else { + $h{subcategory_line} = "\n\n"; + } + + $h{bodies_name} = join(_(' and '), @dear); + if ($h{category} eq _('Other')) { + $h{multiple} = @dear>1 ? "[ " . _("This email has been sent to both councils covering the location of the problem, as the user did not categorise it; please ignore it if you're not the correct council to deal with the issue, or let us know what category of problem this is so we can add it to our system.") . " ]\n\n" + : ''; + } else { + $h{multiple} = @dear>1 ? "[ " . _("This email has been sent to several councils covering the location of the problem, as the category selected is provided for all of them; please ignore it if you're not the correct council to deal with the issue.") . " ]\n\n" + : ''; + } + $h{missing} = ''; + if ($missing) { + $h{missing} = '[ ' + . sprintf(_('We realise this problem might be the responsibility of %s; however, we don\'t currently have any contact details for them. If you know of an appropriate contact address, please do get in touch.'), $missing) + . " ]\n\n"; + } + + if (FixMyStreet->config('STAGING_SITE') && !FixMyStreet->config('SEND_REPORTS_ON_STAGING')) { + # on a staging server send emails to ourselves rather than the bodies + %reporters = map { $_ => $reporters{$_} } grep { /FixMyStreet::SendReport::(Email|EmptyHomes)/ } keys %reporters; + unless (%reporters) { + %reporters = ( 'FixMyStreet::SendReport::Email' => FixMyStreet::SendReport::Email->new() ); + } + } + + # Multiply results together, so one success counts as a success. + my $result = -1; + + for my $sender ( keys %reporters ) { + debug_print("sending using " . $sender, $row->id) if $debug_mode; + $result *= $reporters{ $sender }->send( $row, \%h ); + if ( $reporters{ $sender }->unconfirmed_counts) { + foreach my $e (keys %{ $reporters{ $sender }->unconfirmed_counts } ) { + foreach my $c (keys %{ $reporters{ $sender }->unconfirmed_counts->{$e} }) { + $notgot{$e}{$c} += $reporters{ $sender }->unconfirmed_counts->{$e}{$c}; + } + } + %note = ( + %note, + %{ $reporters{ $sender }->unconfirmed_notes } + ); + } + } + + unless ($result) { + $row->update( { + whensent => \'current_timestamp', + lastupdate => \'current_timestamp', + } ); + if ( $cobrand->report_sent_confirmation_email && !$h{anonymous_report}) { + _send_report_sent_email( $row, \%h, $nomail, $cobrand ); + } + debug_print("send successful: OK", $row->id) if $debug_mode; + } else { + my @errors; + for my $sender ( keys %reporters ) { + unless ( $reporters{ $sender }->success ) { + push @errors, $reporters{ $sender }->error; + } + } + $row->update_send_failed( join( '|', @errors ) ); + debug_print("send FAILED: " . join( '|', @errors ), $row->id) if $debug_mode; + } + } + if ($debug_mode) { + print "\n"; + if ($debug_unsent_count) { + debug_print("processed all unsent reports (total: $debug_unsent_count)"); + } else { + debug_print("no unsent reports were found (must have whensent=null and suitable bodies_str & state) -- nothing to send"); + } + } + + if ($verbose || $debug_mode) { + print "Council email addresses that need checking:\n" if keys %notgot; + foreach my $e (keys %notgot) { + foreach my $c (keys %{$notgot{$e}}) { + print " " . $notgot{$e}{$c} . " problem, to $e category $c (" . $note{$e}{$c}. ")\n"; + } + } + my $sending_errors = ''; + my $unsent = $rs->search( { + state => [ 'confirmed', 'fixed' ], + whensent => undef, + bodies_str => { '!=', undef }, + send_fail_count => { '>', 0 } + } ); + while (my $row = $unsent->next) { + my $base_url = FixMyStreet->config('BASE_URL'); + $sending_errors .= "* " . $base_url . "/report/" . $row->id . ", failed " + . $row->send_fail_count . " times, last at " . $row->send_fail_timestamp + . ", reason " . $row->send_fail_reason . "\n"; + } + if ($sending_errors) { + print "The following reports had problems sending:\n$sending_errors"; + } + } +} + +sub _send_report_sent_email { + my $row = shift; + my $h = shift; + my $nomail = shift; + my $cobrand = shift; + + my $template = FixMyStreet->get_email_template($row->cobrand, $row->lang, 'confirm_report_sent.txt'); + + FixMyStreet::Email::send_cron( + $row->result_source->schema, + { + _template_ => $template, + _parameters_ => $h, + To => $row->user->email, + From => [ FixMyStreet->config('CONTACT_EMAIL'), $cobrand->contact_name ], + }, + FixMyStreet->config('CONTACT_EMAIL'), + $nomail, + $cobrand + ); +} + +sub debug_print { + my $msg = shift; + my $id = shift || ''; + $id = "report $id: " if $id; + print "[] $id$msg\n"; +} + +1; diff --git a/t/app/controller/admin.t b/t/app/controller/admin.t index e2dd5df19..96565e82b 100644 --- a/t/app/controller/admin.t +++ b/t/app/controller/admin.t @@ -1240,9 +1240,7 @@ subtest "Check admin_base_url" => sub { my $rs = FixMyStreet::App->model('DB::Problem'); my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($report->cobrand)->new(); - is (FixMyStreet::App->model('DB::Problem')->get_admin_url( - $cobrand, - $report), + is ($report->admin_url($cobrand), (sprintf 'https://secure.mysociety.org/admin/bci/report_edit/%d', $report_id), 'get_admin_url OK'); }; |