diff options
Diffstat (limited to 'perllib/FixMyStreet/Cobrand')
24 files changed, 1457 insertions, 461 deletions
diff --git a/perllib/FixMyStreet/Cobrand/BathNES.pm b/perllib/FixMyStreet/Cobrand/BathNES.pm index 06095734b..e8e2c5427 100644 --- a/perllib/FixMyStreet/Cobrand/BathNES.pm +++ b/perllib/FixMyStreet/Cobrand/BathNES.pm @@ -90,14 +90,6 @@ sub send_questionnaires { 0 } sub default_map_zoom { 3 } -sub category_extra_hidden { - my ($self, $meta) = @_; - my $code = $meta->{code}; - # These two are used in the non-Open311 'Street light fault' category. - return 1 if $code eq 'unitid' || $code eq 'asset_details'; - return $self->SUPER::category_extra_hidden($meta); -} - sub available_permissions { my $self = shift; @@ -171,9 +163,8 @@ sub categories_restriction { # Do a manual prefetch of all staff users for contributed_by lookup sub _dashboard_user_lookup { my $self = shift; - my $c = $self->{c}; - my @user_ids = $c->model('DB::User')->search( + my @user_ids = FixMyStreet::DB->resultset('User')->search( { from_body => { '!=' => undef } }, { columns => [ 'id', 'email' ] })->all; @@ -182,23 +173,22 @@ sub _dashboard_user_lookup { } sub dashboard_export_updates_add_columns { - my $self = shift; - my $c = $self->{c}; + my ($self, $csv) = @_; - return unless $c->user->has_body_permission_to('export_extra_columns'); + return unless $csv->user->has_body_permission_to('export_extra_columns'); - push @{$c->stash->{csv}->{headers}}, "Staff User"; - push @{$c->stash->{csv}->{headers}}, "User Email"; - push @{$c->stash->{csv}->{columns}}, "staff_user"; - push @{$c->stash->{csv}->{columns}}, "user_email"; + $csv->add_csv_columns( + staff_user => 'Staff User', + user_email => 'User Email', + ); - $c->stash->{csv}->{objects} = $c->stash->{csv}->{objects}->search(undef, { + $csv->objects_attrs({ '+columns' => ['user.email'], join => 'user', }); my $user_lookup = $self->_dashboard_user_lookup; - $c->stash->{csv}->{extra_data} = sub { + $csv->csv_extra_data(sub { my $report = shift; my $staff_user = ''; @@ -210,38 +200,28 @@ sub dashboard_export_updates_add_columns { user_email => $report->user->email || '', staff_user => $staff_user, }; - }; + }); } sub dashboard_export_problems_add_columns { - my $self = shift; - my $c = $self->{c}; - - return unless $c->user->has_body_permission_to('export_extra_columns'); - - $c->stash->{csv}->{headers} = [ - @{ $c->stash->{csv}->{headers} }, - "User Email", - "User Phone", - "Staff User", - "Attribute Data", - ]; - - $c->stash->{csv}->{columns} = [ - @{ $c->stash->{csv}->{columns} }, - "user_email", - "user_phone", - "staff_user", - "attribute_data", - ]; - - $c->stash->{csv}->{objects} = $c->stash->{csv}->{objects}->search(undef, { + my ($self, $csv) = @_; + + return unless $csv->user->has_body_permission_to('export_extra_columns'); + + $csv->add_csv_columns( + user_email => 'User Email', + user_phone => 'User Phone', + staff_user => 'Staff User', + attribute_data => "Attribute Data", + ); + + $csv->objects_attrs({ '+columns' => ['user.email', 'user.phone'], join => 'user', }); my $user_lookup = $self->_dashboard_user_lookup; - $c->stash->{csv}->{extra_data} = sub { + $csv->csv_extra_data(sub { my $report = shift; my $staff_user = ''; @@ -255,7 +235,7 @@ sub dashboard_export_problems_add_columns { staff_user => $staff_user, attribute_data => $attribute_data, }; - }; + }); } 1; diff --git a/perllib/FixMyStreet/Cobrand/Bexley.pm b/perllib/FixMyStreet/Cobrand/Bexley.pm index 481926e72..063a225b7 100644 --- a/perllib/FixMyStreet/Cobrand/Bexley.pm +++ b/perllib/FixMyStreet/Cobrand/Bexley.pm @@ -3,10 +3,6 @@ use parent 'FixMyStreet::Cobrand::Whitelabel'; use strict; use warnings; -use Encode; -use JSON::MaybeXS; -use LWP::Simple qw($ua); -use Path::Tiny; use Time::Piece; sub council_area_id { 2494 } @@ -54,7 +50,7 @@ sub open311_munge_update_params { $params->{service_request_id_ext} = $comment->problem->id; - my $contact = $comment->problem->category_row; + my $contact = $comment->problem->contact; $params->{service_code} = $contact->email; } @@ -88,8 +84,8 @@ sub open311_config { $params->{multi_photos} = 1; } -sub open311_extra_data { - my ($self, $row, $h, $extra, $contact) = @_; +sub open311_extra_data_include { + my ($self, $row, $h, $contact) = @_; my $open311_only; if ($contact->email =~ /^Confirm/) { @@ -103,7 +99,7 @@ sub open311_extra_data { if (!$row->get_extra_field_value('site_code')) { if (my $ref = $self->lookup_site_code($row, 'NSG_REF')) { - push @$extra, { name => 'site_code', value => $ref, description => 'Site code' }; + $row->update_extra_field({ name => 'site_code', value => $ref, description => 'Site code' }); } } } elsif ($contact->email =~ /^Uniform/) { @@ -112,7 +108,7 @@ sub open311_extra_data { # WFS service at the point we're sending the report over Open311. if (!$row->get_extra_field_value('uprn')) { if (my $ref = $self->lookup_site_code($row, 'UPRN')) { - push @$extra, { name => 'uprn', description => 'UPRN', value => $ref }; + $row->update_extra_field({ name => 'uprn', description => 'UPRN', value => $ref }); } } } else { # Symology @@ -121,7 +117,7 @@ sub open311_extra_data { # WFS service at the point we're sending the report over Open311. if (!$row->get_extra_field_value('NSGRef')) { if (my $ref = $self->lookup_site_code($row, 'NSG_REF')) { - push @$extra, { name => 'NSGRef', description => 'NSG Ref', value => $ref }; + $row->update_extra_field({ name => 'NSGRef', description => 'NSG Ref', value => $ref }); } } } @@ -202,9 +198,6 @@ sub open311_post_send { $self->open311_config($row, $h, {}, $contact); # Populate NSGRef again if needed - my $extra_data = join "; ", map { "$_->{description}: $_->{value}" } @{$row->get_extra_fields}; - $h->{additional_information} = $extra_data; - $sender->send($row, $h); } @@ -216,71 +209,21 @@ sub email_list { return @to; } -sub dashboard_export_problems_add_columns { - my $self = shift; - my $c = $self->{c}; - - my %groups; - if ($c->stash->{body}) { - %groups = FixMyStreet::DB->resultset('Contact')->search({ - body_id => $c->stash->{body}->id, - })->group_lookup; - } - - splice @{$c->stash->{csv}->{headers}}, 5, 0, 'Subcategory'; - splice @{$c->stash->{csv}->{columns}}, 5, 0, 'subcategory'; - - $c->stash->{csv}->{extra_data} = sub { - my $report = shift; - - if ($groups{$report->category}) { - return { - category => $groups{$report->category}, - subcategory => $report->category, - }; - } - return {}; - }; -} - sub _is_out_of_hours { my $time = localtime; return 1 if $time->hour > 16 || ($time->hour == 16 && $time->min >= 45); return 1 if $time->hour < 8; return 1 if $time->wday == 1 || $time->wday == 7; - return 1 if _is_bank_holiday(); + return 1 if FixMyStreet::Cobrand::UK::is_public_holiday(); return 0; } -sub _is_bank_holiday { - my $json = _get_bank_holiday_json(); - my $today = localtime->date; - for my $event (@{$json->{'england-and-wales'}{events}}) { - if ($event->{date} eq $today) { - return 1; - } - } -} +sub update_anonymous_message { + my ($self, $update) = @_; + my $t = Utils::prettify_dt( $update->confirmed ); -sub _get_bank_holiday_json { - my $file = 'bank-holidays.json'; - my $cache_file = path(FixMyStreet->path_to("../data/$file")); - my $js; - if (-s $cache_file && -M $cache_file <= 7 && !FixMyStreet->config('STAGING_SITE')) { - # uncoverable statement - $js = $cache_file->slurp_utf8; - } else { - $ua->timeout(5); - $js = LWP::Simple::get("https://www.gov.uk/$file"); - # uncoverable branch false - $js = decode_utf8($js) if !utf8::is_utf8($js); - if ($js && !FixMyStreet->config('STAGING_SITE')) { - # uncoverable statement - $cache_file->spew_utf8($js); - } - } - $js = JSON->new->decode($js) if $js; - return $js; + my $staff = $update->user->from_body || $update->get_extra_metadata('is_body_user') || $update->get_extra_metadata('is_superuser'); + return sprintf('Posted anonymously by a non-staff user at %s', $t) if !$staff; } 1; diff --git a/perllib/FixMyStreet/Cobrand/Bristol.pm b/perllib/FixMyStreet/Cobrand/Bristol.pm index 6e3160c89..5e70c9456 100644 --- a/perllib/FixMyStreet/Cobrand/Bristol.pm +++ b/perllib/FixMyStreet/Cobrand/Bristol.pm @@ -52,7 +52,10 @@ sub categories_restriction { # Email categories with a devolved send_method, so can identify Open311 # categories as those which have a blank send_method. # Also Highways England categories have a blank send_method. - return $rs->search( { 'me.send_method' => undef } ); + return $rs->search( { -or => [ + 'me.send_method' => undef, # Open311 categories + 'me.send_method' => '', # Open311 categories that have been edited in the admin + ] } ); } sub open311_config { @@ -68,8 +71,10 @@ sub open311_contact_meta_override { $service->{group} = []; my %server_set = (easting => 1, northing => 1); + my %hidden_field = (usrn => 1, asset_id => 1); foreach (@$meta) { $_->{automated} = 'server_set' if $server_set{$_->{code}}; + $_->{automated} = 'hidden_field' if $hidden_field{$_->{code}}; } } diff --git a/perllib/FixMyStreet/Cobrand/Bromley.pm b/perllib/FixMyStreet/Cobrand/Bromley.pm index 8f82817a8..cd923c19d 100644 --- a/perllib/FixMyStreet/Cobrand/Bromley.pm +++ b/perllib/FixMyStreet/Cobrand/Bromley.pm @@ -6,8 +6,17 @@ use warnings; use utf8; use DateTime::Format::W3CDTF; use DateTime::Format::Flexible; +use File::Temp; +use Integrations::Echo; +use JSON::MaybeXS; +use Parallel::ForkManager; +use Sort::Key::Natural qw(natkeysort_inplace); +use Storable; use Try::Tiny; use FixMyStreet::DateRange; +use FixMyStreet::WorkingDays; +use Open311::GetServiceRequestUpdates; +use Memcached; sub council_area_id { return 2482; } sub council_area { return 'Bromley'; } @@ -171,11 +180,12 @@ sub open311_config { $params->{extended_description} = 0; } -sub open311_extra_data { - my ($self, $row, $h, $extra) = @_; +sub open311_extra_data_include { + my ($self, $row, $h) = @_; my $title = $row->title; + my $extra = $row->get_extra_fields; foreach (@$extra) { next unless $_->{value}; $title .= ' | ID: ' . $_->{value} if $_->{name} eq 'feature_id'; @@ -207,7 +217,11 @@ sub open311_extra_data { push @$open311_only, { name => 'fms_extra_title', value => $row->user->title }; } - return ($open311_only, [ 'feature_id', 'prow_reference' ]); + return $open311_only; +} + +sub open311_extra_data_exclude { + [ 'feature_id', 'prow_reference' ] } sub open311_config_updates { @@ -215,7 +229,7 @@ sub open311_config_updates { $params->{endpoints} = { service_request_updates => 'update.xml', update => 'update.xml' - }; + } if $params->{endpoint} =~ /bromley.gov.uk/; } sub open311_pre_send { @@ -228,6 +242,11 @@ sub open311_pre_send { } } +sub open311_pre_send_updates { + my ($self, $row) = @_; + return $self->open311_pre_send($row); +} + sub open311_munge_update_params { my ($self, $params, $comment, $body) = @_; delete $params->{update_id}; @@ -317,6 +336,8 @@ sub add_admin_subcategories { my $c = $self->{c}; my $user = $c->stash->{user}; + return $c->stash->{contacts} unless $user; # e.g. admin templates, not user + my @subcategories = @{$user->get_extra_metadata('subcategories') || []}; my %active_contacts = map { $_ => 1 } @subcategories; @@ -328,7 +349,7 @@ sub add_admin_subcategories { foreach (@{$subcats{$_->{id}}}) { push @new_contacts, { id => $_->{key}, - category => (" " x 4) . $_->{name}, + category => (" " x 4) . $_->{name}, # nbsp active => $active_contacts{$_->{key}}, }; } @@ -344,25 +365,637 @@ sub munge_load_and_group_problems { return unless $c->action eq 'dashboard/heatmap'; # Bromley subcategory stuff - if (!$where->{category}) { + if (!$where->{'me.category'}) { my $cats = $c->user->categories; my $subcats = $c->user->get_extra_metadata('subcategories') || []; - $where->{category} = [ @$cats, @$subcats ] if @$cats || @$subcats; + $where->{'me.category'} = [ @$cats, @$subcats ] if @$cats || @$subcats; } my %subcats = $self->subcategories; my $subcat; - my %chosen = map { $_ => 1 } @{$where->{category} || []}; + my %chosen = map { $_ => 1 } @{$where->{'me.category'} || []}; my @subcat = grep { $chosen{$_} } map { $_->{key} } map { @$_ } values %subcats; if (@subcat) { my %chosen = map { $_ => 1 } @subcat; $where->{'-or'} = { - category => [ grep { !$chosen{$_} } @{$where->{category}} ], - subcategory => \@subcat, + 'me.category' => [ grep { !$chosen{$_} } @{$where->{'me.category'}} ], + 'me.subcategory' => \@subcat, }; - delete $where->{category}; + delete $where->{'me.category'}; } } -1; +# We want to send confirmation emails only for Waste reports +sub report_sent_confirmation_email { + my ($self, $report) = @_; + my $contact = $report->contact or return; + return 'id' if grep { $_ eq 'Waste' } @{$report->contact->groups}; + return ''; +} + +sub munge_around_category_where { + my ($self, $where) = @_; + $where->{extra} = [ undef, { -not_like => '%Waste%' } ]; +} + +sub munge_reports_category_list { + my ($self, $categories) = @_; + @$categories = grep { grep { $_ ne 'Waste' } @{$_->groups} } @$categories; +} + +sub munge_report_new_contacts { + my ($self, $categories) = @_; + + return if $self->{c}->action =~ /^waste/; + + @$categories = grep { grep { $_ ne 'Waste' } @{$_->groups} } @$categories; + $self->SUPER::munge_report_new_contacts($categories); +} + +sub updates_disallowed { + my $self = shift; + my ($problem) = @_; + + # No updates on waste reports + return 'waste' if $problem->cobrand_data eq 'waste'; + + return $self->next::method(@_); +} + +sub bin_addresses_for_postcode { + my $self = shift; + my $pc = shift; + + my $echo = $self->feature('echo'); + $echo = Integrations::Echo->new(%$echo); + my $points = $echo->FindPoints($pc); + my $data = [ map { { + value => $_->{Id}, + label => FixMyStreet::Template::title($_->{Description}), + } } @$points ]; + natkeysort_inplace { $_->{label} } @$data; + return $data; +} + +sub look_up_property { + my $self = shift; + my $id = shift; + + my $cfg = $self->feature('echo'); + if ($cfg->{max_per_day}) { + my $today = DateTime->today->set_time_zone(FixMyStreet->local_time_zone)->ymd; + my $ip = $self->{c}->req->address; + my $key = FixMyStreet->test_mode ? "bromley-test" : "bromley-$ip-$today"; + my $count = Memcached::increment($key, 86400) || 0; + $self->{c}->detach('/page_error_403_access_denied', []) if $count > $cfg->{max_per_day}; + } + + my $calls = $self->call_api( + GetPointAddress => [ $id ], + GetServiceUnitsForObject => [ $id ], + GetEventsForObject => [ 'PointAddress', $id ], + ); + + $self->{api_serviceunits} = $calls->{"GetServiceUnitsForObject $id"}; + $self->{api_events} = $calls->{"GetEventsForObject PointAddress $id"}; + my $result = $calls->{"GetPointAddress $id"}; + return { + id => $result->{Id}, + uprn => $result->{SharedRef}{Value}{anyType}, + address => FixMyStreet::Template::title($result->{Description}), + latitude => $result->{Coordinates}{GeoPoint}{Latitude}, + longitude => $result->{Coordinates}{GeoPoint}{Longitude}, + }; +} + +my %irregulars = ( 1 => 'st', 2 => 'nd', 3 => 'rd', 11 => 'th', 12 => 'th', 13 => 'th'); +sub ordinal { + my $n = shift; + $irregulars{$n % 100} || $irregulars{$n % 10} || 'th'; +} + +sub construct_bin_date { + my $str = shift; + return unless $str; + my $offset = ($str->{OffsetMinutes} || 0) * 60; + my $zone = DateTime::TimeZone->offset_as_string($offset); + my $date = DateTime::Format::W3CDTF->parse_datetime($str->{DateTime}); + $date->set_time_zone($zone); + return $date; +} + +sub image_for_service { + my ($self, $service_id) = @_; + my $base = '/cobrands/bromley/images/container-images'; + my $images = { + 531 => "$base/refuse-black-sack", + 532 => "$base/refuse-black-sack", + 533 => "$base/large-communal-black", + 535 => "$base/kerbside-green-box-mix", + 536 => "$base/small-communal-mix", + 537 => "$base/kerbside-black-box-paper", + 541 => "$base/small-communal-paper", + 542 => "$base/food-green-caddy", + 544 => "$base/food-communal", + 545 => "$base/garden-waste-bin", + }; + return $images->{$service_id}; +} + +sub bin_services_for_address { + my $self = shift; + my $property = shift; + + my %service_name_override = ( + 531 => 'Non-Recyclable Refuse', + 532 => 'Non-Recyclable Refuse', + 533 => 'Non-Recyclable Refuse', + 535 => 'Mixed Recycling (Cans, Plastics & Glass)', + 536 => 'Mixed Recycling (Cans, Plastics & Glass)', + 537 => 'Paper & Cardboard', + 541 => 'Paper & Cardboard', + 542 => 'Food Waste', + 544 => 'Food Waste', + 545 => 'Garden Waste', + ); + + $self->{c}->stash->{containers} = { + 1 => 'Green Box (Plastic)', + 2 => 'Wheeled Bin (Plastic)', + 12 => 'Black Box (Paper)', + 13 => 'Wheeled Bin (Paper)', + 9 => 'Kitchen Caddy', + 10 => 'Outside Food Waste Container', + 45 => 'Wheeled Bin (Food)', + }; + my %service_to_containers = ( + 535 => [ 1 ], + 536 => [ 2 ], + 537 => [ 12 ], + 541 => [ 13 ], + 542 => [ 9, 10 ], + 544 => [ 45 ], + ); + my %request_allowed = map { $_ => 1 } keys %service_to_containers; + my %quantity_max = ( + 535 => 6, + 536 => 4, + 537 => 6, + 541 => 4, + 542 => 6, + 544 => 4, + ); + + my $result = $self->{api_serviceunits}; + return [] unless @$result; + + my $events = $self->{api_events}; + my $open = $self->_parse_open_events($events); + + my @to_fetch; + my %schedules; + my @task_refs; + foreach (@$result) { + next unless $_->{ServiceTasks}; + + my $servicetask = $_->{ServiceTasks}{ServiceTask}; + my $schedules = _parse_schedules($servicetask); + + next unless $schedules->{next} or $schedules->{last}; + $schedules{$_->{Id}} = $schedules; + push @to_fetch, GetEventsForObject => [ ServiceUnit => $_->{Id} ]; + push @task_refs, $schedules->{last}{ref} if $schedules->{last}; + } + push @to_fetch, GetTasks => \@task_refs if @task_refs; + + my $calls = $self->call_api(@to_fetch); + + my @out; + my %task_ref_to_row; + foreach (@$result) { + next unless $schedules{$_->{Id}}; + my $schedules = $schedules{$_->{Id}}; + my $servicetask = $_->{ServiceTasks}{ServiceTask}; + + my $events = $calls->{"GetEventsForObject ServiceUnit $_->{Id}"}; + my $open_unit = $self->_parse_open_events($events); + + my $containers = $service_to_containers{$_->{ServiceId}}; + my ($open_request) = grep { $_ } map { $open->{request}->{$_} } @$containers; + my $row = { + id => $_->{Id}, + service_id => $_->{ServiceId}, + service_name => $service_name_override{$_->{ServiceId}} || $_->{ServiceName}, + report_open => $open->{missed}->{$_->{ServiceId}} || $open_unit->{missed}->{$_->{ServiceId}}, + request_allowed => $request_allowed{$_->{ServiceId}}, + request_open => $open_request, + request_containers => $containers, + request_max => $quantity_max{$_->{ServiceId}}, + enquiry_open_events => $open->{enquiry}, + service_task_id => $servicetask->{Id}, + service_task_name => $servicetask->{TaskTypeName}, + service_task_type_id => $servicetask->{TaskTypeId}, + schedule => $servicetask->{ScheduleDescription}, + last => $schedules->{last}, + next => $schedules->{next}, + }; + if ($row->{last}) { + my $ref = join(',', @{$row->{last}{ref}}); + $task_ref_to_row{$ref} = $row; + } + push @out, $row; + } + if (%task_ref_to_row) { + my $tasks = $calls->{GetTasks}; + my $now = DateTime->now->set_time_zone(FixMyStreet->local_time_zone); + foreach (@$tasks) { + my $ref = join(',', @{$_->{Ref}{Value}{anyType}}); + my $completed = construct_bin_date($_->{CompletedDate}); + my $state = $_->{State}{Name} || ''; + my $task_type_id = $_->{TaskTypeId} || ''; + + my $orig_resolution = $_->{Resolution}{Name} || ''; + my $resolution = $orig_resolution; + my $resolution_id = $_->{Resolution}{Ref}{Value}{anyType}; + if ($resolution_id) { + my $template = FixMyStreet::DB->resultset('ResponseTemplate')->search({ + 'me.body_id' => $self->body->id, + 'me.external_status_code' => [ + "$resolution_id,$task_type_id,$state", + "$resolution_id,$task_type_id,", + "$resolution_id,,$state", + "$resolution_id,,", + $resolution_id, + ], + })->first; + $resolution = $template->text if $template; + } + + my $row = $task_ref_to_row{$ref}; + $row->{last}{state} = $state; + $row->{last}{completed} = $completed; + $row->{last}{resolution} = $resolution; + $row->{report_allowed} = within_working_days($row->{last}{date}, 2); + + # Special handling if last instance is today + if ($row->{last}{date}->ymd eq $now->ymd) { + # If it's before 5pm and outstanding, show it as in progress + if ($state eq 'Outstanding' && $now->hour < 17) { + $row->{next} = $row->{last}; + $row->{next}{state} = 'In progress'; + delete $row->{last}; + } + if (!$completed && $now->hour < 17) { + $row->{report_allowed} = 0; + } + } + + # If the task is ended and could not be done, do not allow reporting + if ($state eq 'Not Completed' || ($state eq 'Completed' && $orig_resolution eq 'Excess Waste')) { + $row->{report_allowed} = 0; + $row->{report_locked_out} = 1; + } + } + } + + return \@out; +} + +sub _parse_open_events { + my $self = shift; + my $events = shift; + my $open; + foreach (@$events) { + next if $_->{ResolvedDate}; + next if $_->{ResolutionCodeId} && $_->{ResolutionCodeId} != 584; # Out of Stock + my $event_type = $_->{EventTypeId}; + my $service_id = $_->{ServiceId}; + if ($event_type == 2104) { # Request + my $data = $_->{Data}{ExtensibleDatum}; + my $container; + DATA: foreach (@$data) { + if ($_->{ChildData}) { + foreach (@{$_->{ChildData}{ExtensibleDatum}}) { + if ($_->{DatatypeName} eq 'Container Type') { + $container = $_->{Value}; + last DATA; + } + } + } + } + my $report = $self->problems->search({ external_id => $_->{Guid} })->first; + $open->{request}->{$container} = $report ? { report => $report } : 1; + } elsif (2095 <= $event_type && $event_type <= 2103) { # Missed collection + my $report = $self->problems->search({ external_id => $_->{Guid} })->first; + $open->{missed}->{$service_id} = $report ? { report => $report } : 1; + } else { # General enquiry of some sort + $open->{enquiry}->{$event_type} = 1; + } + } + return $open; +} + +sub _parse_schedules { + my $servicetask = shift; + return unless $servicetask->{ServiceTaskSchedules}; + my $schedules = $servicetask->{ServiceTaskSchedules}{ServiceTaskSchedule}; + $schedules = [ $schedules ] unless ref $schedules eq 'ARRAY'; + + my $today = DateTime->now->set_time_zone(FixMyStreet->local_time_zone)->strftime("%F"); + my ($min_next, $max_last, $next_changed); + foreach my $schedule (@$schedules) { + my $end_date = construct_bin_date($schedule->{EndDate})->strftime("%F"); + next if $end_date lt $today; + + my $next = $schedule->{NextInstance}; + my $d = construct_bin_date($next->{CurrentScheduledDate}); + if ($d && (!$min_next || $d < $min_next->{date})) { + $next_changed = $next->{CurrentScheduledDate}{DateTime} ne $next->{OriginalScheduledDate}{DateTime}; + $min_next = { + date => $d, + ordinal => ordinal($d->day), + changed => $next_changed, + }; + } + + my $last = $schedule->{LastInstance}; + $d = construct_bin_date($last->{CurrentScheduledDate}); + # It is possible the last instance for this schedule has been rescheduled to + # be in the future. If so, we should treat it like it is a next instance. + if ($d && $d->strftime("%F") gt $today && (!$min_next || $d < $min_next->{date})) { + my $last_changed = $last->{CurrentScheduledDate}{DateTime} ne $last->{OriginalScheduledDate}{DateTime}; + $min_next = { + date => $d, + ordinal => ordinal($d->day), + changed => $last_changed, + }; + } elsif ($d && (!$max_last || $d > $max_last->{date})) { + my $last_changed = $last->{CurrentScheduledDate}{DateTime} ne $last->{OriginalScheduledDate}{DateTime}; + $max_last = { + date => $d, + ordinal => ordinal($d->day), + changed => $last_changed, + ref => $last->{Ref}{Value}{anyType}, + }; + } + } + + return { + next => $min_next, + last => $max_last, + }; +} + +sub bin_future_collections { + my $self = shift; + + my $services = $self->{c}->stash->{service_data}; + my @tasks; + my %names; + foreach (@$services) { + push @tasks, $_->{service_task_id}; + $names{$_->{service_task_id}} = $_->{service_name}; + } + + my $echo = $self->feature('echo'); + $echo = Integrations::Echo->new(%$echo); + my $result = $echo->GetServiceTaskInstances(@tasks); + + my $events = []; + foreach (@$result) { + my $task_id = $_->{ServiceTaskRef}{Value}{anyType}; + my $tasks = Integrations::Echo::force_arrayref($_->{Instances}, 'ScheduledTaskInfo'); + foreach (@$tasks) { + my $dt = construct_bin_date($_->{CurrentScheduledDate}); + my $summary = $names{$task_id} . ' collection'; + my $desc = ''; + push @$events, { date => $dt, summary => $summary, desc => $desc }; + } + } + return $events; +} + +=over + +=item within_working_days + +Given a DateTime object and a number, return true if today is less than or +equal to that number of working days (excluding weekends and bank holidays) +after the date. + +=cut + +sub within_working_days { + my ($dt, $days) = @_; + my $wd = FixMyStreet::WorkingDays->new(public_holidays => FixMyStreet::Cobrand::UK::public_holidays()); + $dt = $wd->add_days($dt, $days)->ymd; + my $today = DateTime->now->set_time_zone(FixMyStreet->local_time_zone)->ymd; + return $today le $dt; +} + +=item waste_fetch_events + +Loop through all open waste events to see if there have been any updates + +=back + +=cut + +sub waste_fetch_events { + my ($self, $verbose) = @_; + + my $body = $self->body; + my @contacts = $body->contacts->search({ + send_method => 'Open311', + endpoint => { '!=', '' }, + })->all; + die "Could not find any devolved contacts\n" unless @contacts; + + my %open311_conf = ( + endpoint => $contacts[0]->endpoint || '', + api_key => $contacts[0]->api_key || '', + jurisdiction => $contacts[0]->jurisdiction || '', + extended_statuses => $body->send_extended_statuses, + ); + my $cobrand = $body->get_cobrand_handler; + $cobrand->call_hook(open311_config_updates => \%open311_conf) + if $cobrand; + my $open311 = Open311->new(%open311_conf); + + my $updates = Open311::GetServiceRequestUpdates->new( + current_open311 => $open311, + current_body => $body, + system_user => $body->comment_user, + suppress_alerts => 0, + blank_updates_permitted => $body->blank_updates_permitted, + ); + + my $echo = $self->feature('echo'); + $echo = Integrations::Echo->new(%$echo); + + my $cfg = { + verbose => $verbose, + updates => $updates, + echo => $echo, + event_types => {}, + }; + + my $reports = $self->problems->search({ + external_id => { '!=', '' }, + state => [ FixMyStreet::DB::Result::Problem->open_states() ], + category => [ map { $_->category } @contacts ], + }); + + while (my $report = $reports->next) { + print 'Fetching data for report ' . $report->id . "\n" if $verbose; + + my $event = $cfg->{echo}->GetEvent($report->external_id); + my $request = $self->construct_waste_open311_update($cfg, $event) or next; + + next if !$request->{status} || $request->{status} eq 'confirmed'; # Still in initial state + next unless $self->waste_check_last_update( + $cfg, $report, $request->{status}, $request->{external_status_code}); + my $last_updated = construct_bin_date($event->{LastUpdatedDate}); + $request->{comment_time} = $last_updated; + + print " Updating report to state $request->{status}, $request->{description} ($request->{external_status_code})\n" if $cfg->{verbose}; + $cfg->{updates}->process_update($request, $report); + } +} + +sub construct_waste_open311_update { + my ($self, $cfg, $event) = @_; + + my $event_type = $cfg->{event_types}{$event->{EventTypeId}} ||= $self->waste_get_event_type($cfg, $event->{EventTypeId}); + my $state_id = $event->{EventStateId}; + my $resolution_id = $event->{ResolutionCodeId} || ''; + my $status = $event_type->{states}{$state_id}{state}; + my $description = $event_type->{resolution}{$resolution_id} || $event_type->{states}{$state_id}{name}; + return { + description => $description, + status => $status, + update_id => 'waste', + external_status_code => "$resolution_id,,", + }; +} + +sub waste_get_event_type { + my ($self, $cfg, $id) = @_; + + my $event_type = $cfg->{echo}->GetEventType($id); + + my $state_map = { + New => { New => 'confirmed' }, + Pending => { + Unallocated => 'investigating', + 'Allocated to Crew' => 'action scheduled', + }, + Closed => { + Completed => 'fixed - council', + 'Not Completed' => 'unable to fix', + Rejected => 'closed', + }, + }; + + my $states = $event_type->{Workflow}->{States}->{State}; + my $data; + foreach (@$states) { + my $core = $_->{CoreState}; # New/Pending/Closed + my $name = $_->{Name}; # New : Unallocated/Allocated to Crew : Completed/Not Completed/Rejected + $data->{states}{$_->{Id}} = { + core => $core, + name => $name, + state => $state_map->{$core}{$name}, + }; + my $codes = Integrations::Echo::force_arrayref($_->{ResolutionCodes}, 'StateResolutionCode'); + foreach (@$codes) { + my $name = $_->{Name}; + my $id = $_->{ResolutionCodeId}; + $data->{resolution}{$id} = $name; + } + } + return $data; +} + +# We only have the report's current state, no history, so must check current +# against latest received update to see if state the same, and skip if so +sub waste_check_last_update { + my ($self, $cfg, $report, $status, $resolution_id) = @_; + + my $latest = $report->comments->search( + { external_id => 'waste', }, + { order_by => { -desc => 'id' } } + )->first; + + if ($latest) { + my $state = $cfg->{updates}->current_open311->map_state($status); + my $code = $latest->get_extra_metadata('external_status_code') || ''; + if ($latest->problem_state eq $state && $code eq $resolution_id) { + print " Latest update matches fetched state, skipping\n" if $cfg->{verbose}; + return; + } + } + return 1; +} + +sub admin_templates_external_status_code_hook { + my ($self) = @_; + my $c = $self->{c}; + + my $res_code = $c->get_param('resolution_code') || ''; + my $task_type = $c->get_param('task_type') || ''; + my $task_state = $c->get_param('task_state') || ''; + + return "$res_code,$task_type,$task_state"; +} + +sub call_api { + my $self = shift; + + my $tmp = File::Temp->new; + my @cmd = ( + FixMyStreet->path_to('bin/fixmystreet.com/bromley-echo'), + '--out', $tmp, + '--calls', encode_json(\@_), + ); + + # We cannot fork directly under mod_fcgid, so + # call an external script that calls back in. + my $data; + if (FixMyStreet->test_mode) { + $data = $self->_parallel_api_calls(@_); + } else { + system(@cmd); + $data = Storable::fd_retrieve($tmp); + } + return $data; +} + +sub _parallel_api_calls { + my $self = shift; + my $echo = $self->feature('echo'); + $echo = Integrations::Echo->new(%$echo); + + my %calls; + my $pm = Parallel::ForkManager->new(FixMyStreet->test_mode ? 0 : 10); + $pm->run_on_finish(sub { + my ($pid, $exit_code, $ident, $exit_signal, $core_dump, $data) = @_; + %calls = ( %calls, %$data ); + }); + + while (@_) { + my $call = shift; + my $args = shift; + $pm->start and next; + my $result = $echo->$call(@$args); + my $key = "$call @$args"; + $key = $call if $call eq 'GetTasks'; + $pm->finish(0, { $key => $result }); + } + $pm->wait_all_children; + + return \%calls; +} + +1; diff --git a/perllib/FixMyStreet/Cobrand/Buckinghamshire.pm b/perllib/FixMyStreet/Cobrand/Buckinghamshire.pm index 117725273..f901c4e2f 100644 --- a/perllib/FixMyStreet/Cobrand/Buckinghamshire.pm +++ b/perllib/FixMyStreet/Cobrand/Buckinghamshire.pm @@ -45,16 +45,7 @@ sub send_questionnaires { return 0; } -sub open311_pre_send { - my ($self, $row, $open311) = @_; - - return unless $row->extra; - my $extra = $row->get_extra_fields; - if (@$extra) { - @$extra = grep { $_->{name} ne 'road-placement' } @$extra; - $row->set_extra_fields(@$extra); - } -} +sub open311_extra_data_exclude { [ 'road-placement' ] } sub open311_post_send { my ($self, $row, $h) = @_; @@ -103,6 +94,7 @@ sub report_new_munge_before_insert { my ($self, $report) = @_; return unless $report->category eq 'Flytipping'; + return unless $self->{c}->stash->{report}->to_body_named('Buckinghamshire'); my $placement = $self->{c}->get_param('road-placement'); return unless $placement && $placement eq 'off-road'; @@ -132,19 +124,17 @@ sub map_type { 'Buckinghamshire' } sub default_map_zoom { 3 } sub _dashboard_export_add_columns { - my $self = shift; - my $c = $self->{c}; + my ($self, $csv) = @_; - push @{$c->stash->{csv}->{headers}}, "Staff User"; - push @{$c->stash->{csv}->{columns}}, "staff_user"; + $csv->add_csv_columns( staff_user => 'Staff User' ); # All staff users, for contributed_by lookup - my @user_ids = $c->model('DB::User')->search( + my @user_ids = FixMyStreet::DB->resultset('User')->search( { from_body => $self->body->id }, { columns => [ 'id', 'email', ] })->all; my %user_lookup = map { $_->id => $_->email } @user_ids; - $c->stash->{csv}->{extra_data} = sub { + $csv->csv_extra_data(sub { my $report = shift; my $staff_user = ''; if (my $contributed_by = $report->get_extra_metadata('contributed_by')) { @@ -153,15 +143,15 @@ sub _dashboard_export_add_columns { return { staff_user => $staff_user, }; - }; + }); } sub dashboard_export_updates_add_columns { - shift->_dashboard_export_add_columns; + shift->_dashboard_export_add_columns(@_); } sub dashboard_export_problems_add_columns { - shift->_dashboard_export_add_columns; + shift->_dashboard_export_add_columns(@_); } # Enable adding/editing of parish councils in the admin diff --git a/perllib/FixMyStreet/Cobrand/CheshireEast.pm b/perllib/FixMyStreet/Cobrand/CheshireEast.pm index c5e5107f3..2a0423b7c 100644 --- a/perllib/FixMyStreet/Cobrand/CheshireEast.pm +++ b/perllib/FixMyStreet/Cobrand/CheshireEast.pm @@ -5,6 +5,7 @@ use strict; use warnings; use Moo; +with 'FixMyStreet::Roles::ConfirmOpen311'; with 'FixMyStreet::Roles::ConfirmValidation'; sub council_area_id { 21069 } @@ -56,39 +57,6 @@ sub abuse_reports_only { 1 } sub send_questionnaires { 0 } -sub open311_config { - my ($self, $row, $h, $params) = @_; - - $params->{multi_photos} = 1; -} - -sub open311_extra_data { - my ($self, $row, $h, $extra) = @_; - - my $open311_only = [ - { name => 'report_url', - value => $h->{url} }, - { name => 'title', - value => $row->title }, - { name => 'description', - value => $row->detail }, - ]; - - # Reports made via FMS.com or the app probably won't have a site code - # value because we don't display the adopted highways layer on those - # frontends. Instead we'll look up the closest asset from the WFS - # service at the point we're sending the report over Open311. - if (!$row->get_extra_field_value('site_code')) { - if (my $site_code = $self->lookup_site_code($row)) { - push @$extra, - { name => 'site_code', - value => $site_code }; - } - } - - return $open311_only; -} - # TODO These values may not be accurate sub lookup_site_code_config { { buffer => 200, # metres @@ -142,4 +110,7 @@ sub council_rss_alert_options { return ( \@options, undef ); } +# Make sure fetched report description isn't shown. +sub filter_report_description { "" } + 1; diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm index 695487268..e58bceb2a 100644 --- a/perllib/FixMyStreet/Cobrand/Default.pm +++ b/perllib/FixMyStreet/Cobrand/Default.pm @@ -523,13 +523,29 @@ sub allow_update_reporting { return 0; } =item updates_disallowed Returns a boolean indicating whether updates on a particular report are allowed -or not. Default behaviour is disallowed if "closed_updates" metadata is set. +or not. Default behaviour is disallowed if "closed_updates" metadata is set, or +if the report's category has its "updates_disallowed" flag set. =cut sub updates_disallowed { my ($self, $problem) = @_; return 1 if $problem->get_extra_metadata('closed_updates'); + return 1 if $problem->contact && $problem->contact->get_extra_metadata('updates_disallowed'); + return 0; +} + +=item reopening_disallowed + +Returns a boolean indicating whether reopening of a particular report is +allowed or not. Default behaviour is allowed unless the report's category +has its reopening_disallowed flag set. + +=cut + +sub reopening_disallowed { + my ($self, $problem) = @_; + return 1 if $problem->contact && $problem->contact->get_extra_metadata('reopening_disallowed'); return 0; } @@ -941,11 +957,12 @@ Get stats to display on the council reports page sub get_report_stats { return 0; } sub get_body_sender { - my ( $self, $body, $category ) = @_; + my ( $self, $body, $problem ) = @_; # look up via category + my $category = $problem->category; my $contact = $body->contacts->search( { category => $category } )->first; - if ( $body->can_be_devolved && $contact->send_method ) { + if ( $body->can_be_devolved && $contact && $contact->send_method ) { return { method => $contact->send_method, config => $contact, contact => $contact }; } @@ -1055,7 +1072,7 @@ sub can_support_problems { return 0; } =item default_map_zoom default_map_zoom is used when displaying a map overriding the -default of max-4 or max-3 depending on population density. +default that depends on population density. =cut @@ -1107,7 +1124,22 @@ pressed in the front end, rather than whenever a username is not provided. =cut -sub allow_anonymous_reports { 0; } +sub allow_anonymous_reports { + my ($self, $category_name) = @_; + + $category_name ||= $self->{c}->stash->{category}; + if ( $category_name && $self->can('body') and $self->body ) { + my $category_rs = FixMyStreet::DB->resultset("Contact")->search({ + body_id => $self->body->id, + category => $category_name + }); + if ( my $category = $category_rs->first ) { + return 'button' if $category->get_extra_metadata('anonymous_allowed'); + } + } + + return 0; +} =item anonymous_account @@ -1216,15 +1248,13 @@ sub get_geocoder { sub problem_as_hashref { my $self = shift; my $problem = shift; - my $ctx = shift; - return $problem->as_hashref( $ctx ); + return $problem->as_hashref; } sub updates_as_hashref { my $self = shift; my $problem = shift; - my $ctx = shift; return {}; } @@ -1256,14 +1286,6 @@ sub category_extra_hidden { return 0; } -sub traffic_management_options { - return [ - _("Yes"), - _("No"), - ]; -} - - =item display_days_ago_threshold Used to control whether a relative 'n days ago' or absolute date is shown diff --git a/perllib/FixMyStreet/Cobrand/EastSussex.pm b/perllib/FixMyStreet/Cobrand/EastSussex.pm index e6c2da6c5..b2fd58dc1 100644 --- a/perllib/FixMyStreet/Cobrand/EastSussex.pm +++ b/perllib/FixMyStreet/Cobrand/EastSussex.pm @@ -7,11 +7,10 @@ use warnings; sub council_area_id { return 2224; } sub open311_extra_data { - my ($self, $row, $h, $extra, $contact) = @_; + my ($self, $row, $h, $contact) = @_; $h->{es_original_detail} = $row->detail; - $contact = $row->category_row; my $fields = $contact->get_extra_fields; my $text = ''; for my $field ( @$fields ) { @@ -21,7 +20,7 @@ sub open311_extra_data { } } $row->detail($row->detail . $text); - return (); + return (undef, ['sect_label', 'road_name', 'area_name']); } sub open311_post_send { diff --git a/perllib/FixMyStreet/Cobrand/FixMyStreet.pm b/perllib/FixMyStreet/Cobrand/FixMyStreet.pm index dfb511f39..ae96924d8 100644 --- a/perllib/FixMyStreet/Cobrand/FixMyStreet.pm +++ b/perllib/FixMyStreet/Cobrand/FixMyStreet.pm @@ -35,15 +35,19 @@ sub restriction { return {}; } -# FixMyStreet needs to not show TfL reports... +# FixMyStreet needs to not show TfL reports or Bromley waste reports sub problems_restriction { my ($self, $rs) = @_; my $table = ref $rs eq 'FixMyStreet::DB::ResultSet::Nearby' ? 'problem' : 'me'; - return $rs->search({ "$table.cobrand" => { '!=' => 'tfl' } }); + return $rs->search({ + "$table.cobrand" => { '!=' => 'tfl' }, + "$table.cobrand_data" => { '!=' => 'waste' }, + }); } sub problems_sql_restriction { my $self = shift; return "AND cobrand != 'tfl'"; + # Doesn't need Bromley one as all waste reports non-public } sub relative_url_for_report { @@ -54,32 +58,40 @@ sub relative_url_for_report { sub munge_around_category_where { my ($self, $where) = @_; + my $iow = grep { $_->name eq 'Isle of Wight Council' } @{ $self->{c}->stash->{around_bodies} }; + if ($iow) { + # display all the categories on Isle of Wight at the moment as there's no way to + # do the expand bit later as we fetch it using ajax which uses a bounding box so + # can't determine the body + $where->{send_method} = [ { '!=' => 'Triage' }, undef ]; + } + my $bromley = grep { $_->name eq 'Bromley Council' } @{ $self->{c}->stash->{around_bodies} }; + if ($bromley) { + $where->{extra} = [ undef, { -not_like => '%Waste%' } ]; + } +} + +sub _iow_category_munge { + my ($self, $body, $categories) = @_; my $user = $self->{c}->user; - my @iow = grep { $_->name eq 'Isle of Wight Council' } @{ $self->{c}->stash->{around_bodies} }; - return unless @iow; - - # display all the categories on Isle of Wight at the moment as there's no way to - # do the expand bit later as we fetch it using ajax which uses a bounding box so - # can't determine the body - $where->{send_method} = [ { '!=' => 'Triage' }, undef ]; - return $where; + + if ( $user && ( $user->is_superuser || $user->belongs_to_body( $body->id ) ) ) { + @$categories = grep { !$_->send_method || $_->send_method ne 'Triage' } @$categories; + return; + } + + @$categories = grep { $_->send_method && $_->send_method eq 'Triage' } @$categories; } -sub munge_reports_categories_list { +sub munge_reports_category_list { my ($self, $categories) = @_; my %bodies = map { $_->body->name => $_->body } @$categories; - if ( $bodies{'Isle of Wight Council'} ) { - my $user = $self->{c}->user; - my $b = $bodies{'Isle of Wight Council'}; - - if ( $user && ( $user->is_superuser || $user->belongs_to_body( $b->id ) ) ) { - @$categories = grep { !$_->send_method || $_->send_method ne 'Triage' } @$categories; - return @$categories; - } - - @$categories = grep { $_->send_method && $_->send_method eq 'Triage' } @$categories; - return @$categories; + if ( my $body = $bodies{'Isle of Wight Council'} ) { + return $self->_iow_category_munge($body, $categories); + } + if ( $bodies{'Bromley Council'} ) { + @$categories = grep { grep { $_ ne 'Waste' } @{$_->groups} } @$categories; } } @@ -118,16 +130,12 @@ sub munge_report_new_contacts { my %bodies = map { $_->body->name => $_->body } @$contacts; - if ( $bodies{'Isle of Wight Council'} ) { - my $user = $self->{c}->user; - if ( $user && ( $user->is_superuser || $user->belongs_to_body( $bodies{'Isle of Wight Council'}->id ) ) ) { - @$contacts = grep { !$_->send_method || $_->send_method ne 'Triage' } @$contacts; - return; - } - - @$contacts = grep { $_->send_method && $_->send_method eq 'Triage' } @$contacts; + if ( my $body = $bodies{'Isle of Wight Council'} ) { + return $self->_iow_category_munge($body, $contacts); + } + if ( $bodies{'Bromley Council'} ) { + @$contacts = grep { grep { $_ ne 'Waste' } @{$_->groups} } @$contacts; } - if ( $bodies{'TfL'} ) { # Presented categories vary if we're on/off a red route my $tfl = FixMyStreet::Cobrand->get_class_for_moniker( 'tfl' )->new({ c => $self->{c} }); @@ -139,10 +147,10 @@ sub munge_report_new_contacts { sub munge_load_and_group_problems { my ($self, $where, $filter) = @_; - return unless $where->{category} && $self->{c}->stash->{body}->name eq 'Isle of Wight Council'; + return unless $where->{'me.category'} && $self->{c}->stash->{body}->name eq 'Isle of Wight Council'; my $iow = FixMyStreet::Cobrand->get_class_for_moniker( 'isleofwight' )->new({ c => $self->{c} }); - $where->{category} = $iow->expand_triage_cat_list($where->{category}, $self->{c}->stash->{body}); + $where->{'me.category'} = $iow->expand_triage_cat_list($where->{'me.category'}, $self->{c}->stash->{body}); } sub title_list { @@ -310,6 +318,19 @@ sub updates_disallowed { return $self->next::method(@_); } +sub problem_state_processed { + my ($self, $comment) = @_; + + my $state = $comment->problem_state || ''; + my $code = $comment->get_extra_metadata('external_status_code') || ''; + + my ($cfg) = $self->per_body_config('extra_state_mapping', $comment->problem); + + $state = ( $cfg->{$state}->{$code} || $state ) if $cfg->{$state}; + + return $state; +} + sub suppress_reporter_alerts { my $self = shift; my $c = $self->{c}; @@ -347,4 +368,13 @@ sub manifest { }; } +sub report_new_munge_before_insert { + my ($self, $report) = @_; + + # Make sure TfL reports are marked safety critical + $self->SUPER::report_new_munge_before_insert($report); + + FixMyStreet::Cobrand::Buckinghamshire::report_new_munge_before_insert($self, $report); +} + 1; diff --git a/perllib/FixMyStreet/Cobrand/Greenwich.pm b/perllib/FixMyStreet/Cobrand/Greenwich.pm index be260d0c0..4cc4e4163 100644 --- a/perllib/FixMyStreet/Cobrand/Greenwich.pm +++ b/perllib/FixMyStreet/Cobrand/Greenwich.pm @@ -44,8 +44,8 @@ sub reports_per_page { return 20; } sub admin_user_domain { 'royalgreenwich.gov.uk' } -sub open311_extra_data { - my ($self, $row, $h, $extra) = @_; +sub open311_extra_data_include { + my ($self, $row, $h) = @_; # Greenwich doesn't have category metadata to fill this return [ diff --git a/perllib/FixMyStreet/Cobrand/Hackney.pm b/perllib/FixMyStreet/Cobrand/Hackney.pm new file mode 100644 index 000000000..b8f92f1ea --- /dev/null +++ b/perllib/FixMyStreet/Cobrand/Hackney.pm @@ -0,0 +1,207 @@ +package FixMyStreet::Cobrand::Hackney; +use parent 'FixMyStreet::Cobrand::Whitelabel'; + +use strict; +use warnings; +use mySociety::EmailUtil qw(is_valid_email is_valid_email_list); + +sub council_area_id { return 2508; } +sub council_area { return 'Hackney'; } +sub council_name { return 'Hackney Council'; } +sub council_url { return 'hackney'; } +sub send_questionnaires { 0 } + +sub disambiguate_location { + my $self = shift; + my $string = shift; + + my $town = 'Hackney'; + + # Teale Street is on the boundary with Tower Hamlets and + # shows the 'please use fixmystreet.com' message, but Hackney + # do provide services on that road. + ($string, $town) = ('E2 9AA', '') if $string =~ /^teale\s+st/i; + + return { + %{ $self->SUPER::disambiguate_location() }, + string => $string, + town => $town, + centre => '51.552267,-0.063316', + bounds => [ 51.519814, -0.104511, 51.577784, -0.016527 ], + }; +} + +sub do_not_reply_email { shift->feature('do_not_reply_email') } + +sub verp_email_domain { shift->feature('verp_email_domain') } + +sub get_geocoder { + return 'OSM'; # default of Bing gives poor results, let's try overriding. +} + +sub geocoder_munge_query_params { + my ($self, $params) = @_; + + $params->{addressdetails} = 1; +} + +sub geocoder_munge_results { + my ($self, $result) = @_; + if (my $a = $result->{address}) { + if ($a->{road} && $a->{suburb} && $a->{postcode}) { + $result->{display_name} = "$a->{road}, $a->{suburb}, $a->{postcode}"; + return; + } + } + $result->{display_name} = '' unless $result->{display_name} =~ /Hackney/; + $result->{display_name} =~ s/, United Kingdom$//; + $result->{display_name} =~ s/, London, Greater London, England//; + $result->{display_name} =~ s/, London Borough of Hackney//; +} + + +sub open311_config { + my ($self, $row, $h, $params) = @_; + + $params->{multi_photos} = 1; +} + +sub open311_extra_data { + my ($self, $row, $h, $contact) = @_; + + my $open311_only = [ + { name => 'report_url', + value => $h->{url} }, + { name => 'title', + value => $row->title }, + { name => 'description', + value => $row->detail }, + { name => 'category', + value => $row->category }, + ]; + + # Make sure contact 'email' set correctly for Open311 + if (my $sent_to = $row->get_extra_metadata('sent_to')) { + $row->unset_extra_metadata('sent_to'); + my $code = $sent_to->{$contact->email}; + $contact->email($code) if $code; + } + + return $open311_only; +} + +sub map_type { 'OSM' } + +sub default_map_zoom { 6 } + +sub admin_user_domain { 'hackney.gov.uk' } + +sub social_auth_enabled { + my $self = shift; + + return $self->feature('oidc_login') ? 1 : 0; +} + +sub anonymous_account { + my $self = shift; + return { + email => $self->feature('anonymous_account') . '@' . $self->admin_user_domain, + name => 'Anonymous user', + }; +} + +sub open311_skip_existing_contact { + my ($self, $contact) = @_; + + # For Hackney we want the 'protected' flag to prevent any changes to this + # contact at all. + return $contact->get_extra_metadata("open311_protect") ? 1 : 0; +} + +sub open311_filter_contacts_for_deletion { + my ($self, $contacts) = @_; + + # Don't delete open311 protected contacts when importing + return $contacts->search({ + extra => { -not_like => '%T15:open311_protect,I1:1%' }, + }); +} + +sub problem_is_within_area_type { + my ($self, $problem, $type) = @_; + my $layer_map = { + park => "greenspaces:hackney_park", + estate => "housing:lbh_estate", + }; + my $layer = $layer_map->{$type}; + return unless $layer; + + my ($x, $y) = $problem->local_coords; + + my $cfg = { + url => "https://map.hackney.gov.uk/geoserver/wfs", + srsname => "urn:ogc:def:crs:EPSG::27700", + typename => $layer, + outputformat => "json", + filter => "<Filter xmlns:gml=\"http://www.opengis.net/gml\"><Intersects><PropertyName>geom</PropertyName><gml:Point srsName=\"27700\"><gml:coordinates>$x,$y</gml:coordinates></gml:Point></Intersects></Filter>", + }; + + my $features = $self->_fetch_features($cfg, $x, $y) || []; + return scalar @$features ? 1 : 0; +} + +sub get_body_sender { + my ( $self, $body, $problem ) = @_; + + my $contact = $body->contacts->search( { category => $problem->category } )->first; + + if (my ($park, $estate, $other) = $self->_split_emails($contact->email)) { + my $to = $other; + if ($self->problem_is_within_area_type($problem, 'park')) { + $to = $park; + } elsif ($self->problem_is_within_area_type($problem, 'estate')) { + $to = $estate; + } + $problem->set_extra_metadata(sent_to => { $contact->email => $to }); + if (is_valid_email($to)) { + return { method => 'Email', contact => $contact }; + } + } + return $self->SUPER::get_body_sender($body, $problem); +} + +# Translate email address to actual delivery address +sub munge_sendreport_params { + my ($self, $row, $h, $params) = @_; + + my $sent_to = $row->get_extra_metadata('sent_to') or return; + $row->unset_extra_metadata('sent_to'); + for my $recip (@{$params->{To}}) { + my ($email, $name) = @$recip; + $recip->[0] = $sent_to->{$email} if $sent_to->{$email}; + } +} + +sub _split_emails { + my ($self, $email) = @_; + + my $parts = join '\s*', qw(^ park : (.*?) ; estate : (.*?) ; other : (.*?) $); + my $regex = qr/$parts/i; + + if (my ($park, $estate, $other) = $email =~ $regex) { + return ($park, $estate, $other); + } + return (); +} + +sub validate_contact_email { + my ( $self, $email ) = @_; + + return 1 if is_valid_email_list($email); + + my @emails = grep { $_ } $self->_split_emails($email); + return unless @emails; + return 1 if is_valid_email_list(join(",", @emails)); +} + +1; diff --git a/perllib/FixMyStreet/Cobrand/HighwaysEngland.pm b/perllib/FixMyStreet/Cobrand/HighwaysEngland.pm index ed58eb4f7..c282ac5ea 100644 --- a/perllib/FixMyStreet/Cobrand/HighwaysEngland.pm +++ b/perllib/FixMyStreet/Cobrand/HighwaysEngland.pm @@ -29,6 +29,15 @@ sub users_restriction { FixMyStreet::Cobrand::UKCouncils::users_restriction($_[0 sub updates_restriction { FixMyStreet::Cobrand::UKCouncils::updates_restriction($_[0], $_[1]) } sub base_url { FixMyStreet::Cobrand::UKCouncils::base_url($_[0]) } +sub munge_problem_list { + my ($self, $problem) = @_; + $problem->anonymous(1); +} +sub munge_update_list { + my ($self, $update) = @_; + $update->anonymous(1); +} + sub admin_allow_user { my ( $self, $user ) = @_; return 1 if $user->is_superuser; diff --git a/perllib/FixMyStreet/Cobrand/Hounslow.pm b/perllib/FixMyStreet/Cobrand/Hounslow.pm index 2fc949546..90d3b17dc 100644 --- a/perllib/FixMyStreet/Cobrand/Hounslow.pm +++ b/perllib/FixMyStreet/Cobrand/Hounslow.pm @@ -65,8 +65,14 @@ sub categories_restriction { # Email categories with a devolved send_method, so can identify Open311 # categories as those which have a blank send_method. return $rs->search({ - 'me.send_method' => undef, 'body.name' => [ 'Hounslow Borough Council', 'Highways England' ], + -or => [ + 'me.send_method' => undef, + 'me.category' => { -in => [ + 'Pavement Overcrowding', + 'Streetspace Suggestions and Feedback', + ] }, + ], }); } @@ -120,40 +126,25 @@ sub open311_skip_report_fetch { sub filter_report_description { "" } sub setup_general_enquiries_stash { - my $self = shift; - - my @bodies = $self->{c}->model('DB::Body')->active->for_areas(( $self->council_area_id ))->all; - my %bodies = map { $_->id => $_ } @bodies; - my @contacts # - = $self->{c} # - ->model('DB::Contact') # - ->active - ->search( - { - 'me.body_id' => [ keys %bodies ] - }, - { - prefetch => 'body', - order_by => 'me.category', - } - )->all; - @contacts = grep { - my $group = $_->get_extra_metadata('group') || ''; - $group eq 'Other' || $group eq 'General Enquiries'; - } @contacts; - $self->{c}->stash->{bodies} = \%bodies; - $self->{c}->stash->{bodies_to_list} = \%bodies; - $self->{c}->stash->{contacts} = \@contacts; - $self->{c}->stash->{missing_details_bodies} = []; - $self->{c}->stash->{missing_details_body_names} = []; - - $self->{c}->set_param('title', "General Enquiry"); - # Can't use (0, 0) for lat lon so default to the rough location - # of Hounslow Highways HQ. - $self->{c}->stash->{latitude} = 51.469; - $self->{c}->stash->{longitude} = -0.35; - - return 1; + my $self = shift; + my $c = $self->{c}; + + $c->set_param('title', "General Enquiry"); + # Can't use (0, 0) for lat lon so default to the rough location + # of Hounslow Highways HQ. + $c->stash->{latitude} = 51.469; + $c->stash->{longitude} = -0.35; + + $c->stash->{all_areas} = { $self->council_area_id => { id => $self->council_area_id } }; + $c->forward('/report/new/setup_categories_and_bodies'); + + my $contacts = $c->stash->{contacts}; + @$contacts = grep { + my $groups = $_->groups; + grep { $_ eq 'Other' || $_ eq 'General Enquiries' } @$groups; + } @$contacts; + + return 1; } sub abuse_reports_only { 1 } @@ -171,4 +162,29 @@ sub lookup_site_code_config { { # their cobrand at all. sub cut_off_date { '2019-05-06' } +sub front_stats_data { + my ( $self ) = @_; + + my $recency = '1 week'; + my $shorter_recency = '3 days'; + + my $completed = $self->problems->recent_completed(); + my $updates = $self->problems->number_comments(); + my $new = $self->problems->recent_new( $recency ); + + if ( $new > $completed ) { + $recency = $shorter_recency; + $new = $self->problems->recent_new( $recency ); + } + + my $stats = { + completed => $completed, + updates => $updates, + new => $new, + recency => $recency, + }; + + return $stats; +} + 1; diff --git a/perllib/FixMyStreet/Cobrand/IsleOfWight.pm b/perllib/FixMyStreet/Cobrand/IsleOfWight.pm index db0a20b9c..72555b9e6 100644 --- a/perllib/FixMyStreet/Cobrand/IsleOfWight.pm +++ b/perllib/FixMyStreet/Cobrand/IsleOfWight.pm @@ -6,6 +6,7 @@ use warnings; use Moo; with 'FixMyStreet::Roles::ConfirmOpen311'; +with 'FixMyStreet::Roles::ConfirmValidation'; sub council_area_id { 2636 } sub council_area { 'Isle of Wight' } @@ -63,20 +64,18 @@ sub lookup_site_code_config { { accept_feature => sub { 1 } } } -sub open311_pre_send { - my ($self, $row, $open311) = @_; - - return unless $row->extra; - my $extra = $row->get_extra_fields; - if (@$extra) { - @$extra = grep { $_->{name} ne 'urgent' } @$extra; - $row->set_extra_fields(@$extra); - } -} +sub open311_extra_data_exclude { [ '^urgent$' ] } # Make sure fetched report description isn't shown. sub filter_report_description { "" } +around 'open311_config' => sub { + my ($orig, $self, $row, $h, $params) = @_; + + $params->{upload_files} = 1; + $self->$orig($row, $h, $params); +}; + sub open311_munge_update_params { my ($self, $params, $comment, $body) = @_; @@ -130,19 +129,18 @@ sub munge_around_category_where { my $b = $self->{c}->model('DB::Body')->for_areas( $self->council_area_id )->first; if ( $user && ( $user->is_superuser || $user->belongs_to_body( $b->id ) ) ) { $where->{send_method} = [ { '!=' => 'Triage' }, undef ]; - return $where; + return; } $where->{'send_method'} = 'Triage'; - return $where; } sub munge_load_and_group_problems { my ($self, $where, $filter) = @_; - return unless $where->{category}; + return unless $where->{'me.category'}; - $where->{category} = $self->_expand_triage_cat_list($where->{category}); + $where->{'me.category'} = $self->_expand_triage_cat_list($where->{'me.category'}); } sub munge_around_filter_category_list { @@ -176,10 +174,7 @@ sub expand_triage_cat_list { my %group_to_category; while ( my $cat = $all_cats->next ) { - next unless $cat->get_extra_metadata('group'); - my $groups = $cat->get_extra_metadata('group'); - $groups = ref $groups eq 'ARRAY' ? $groups : [ $groups ]; - for my $group ( @$groups ) { + for my $group ( @{$cat->groups} ) { $group_to_category{$group} //= []; push @{ $group_to_category{$group} }, $cat->category; } diff --git a/perllib/FixMyStreet/Cobrand/Lincolnshire.pm b/perllib/FixMyStreet/Cobrand/Lincolnshire.pm index ee40bb173..d1fe319e1 100644 --- a/perllib/FixMyStreet/Cobrand/Lincolnshire.pm +++ b/perllib/FixMyStreet/Cobrand/Lincolnshire.pm @@ -77,4 +77,11 @@ sub pin_colour { return 'yellow'; } +around 'open311_config' => sub { + my ($orig, $self, $row, $h, $params) = @_; + + $params->{upload_files} = 1; + $self->$orig($row, $h, $params); +}; + 1; diff --git a/perllib/FixMyStreet/Cobrand/Northamptonshire.pm b/perllib/FixMyStreet/Cobrand/Northamptonshire.pm index 3e32b0856..2543f701d 100644 --- a/perllib/FixMyStreet/Cobrand/Northamptonshire.pm +++ b/perllib/FixMyStreet/Cobrand/Northamptonshire.pm @@ -91,10 +91,10 @@ sub open311_config { $params->{multi_photos} = 1; } -sub open311_extra_data { - my ($self, $row, $h, $extra) = @_; +sub open311_extra_data_include { + my ($self, $row, $h) = @_; - return ([ + return [ { name => 'report_url', value => $h->{url} }, { name => 'title', @@ -103,10 +103,9 @@ sub open311_extra_data { value => $row->detail }, { name => 'category', value => $row->category }, - ], [ - 'emergency' - ]); + ]; } +sub open311_extra_data_exclude { [ 'emergency' ] } sub open311_get_update_munging { my ($self, $comment) = @_; diff --git a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm index 8ce12a81b..97174e1ce 100644 --- a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm +++ b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm @@ -122,8 +122,8 @@ sub open311_config { $params->{extended_description} = 'oxfordshire'; } -sub open311_extra_data { - my ($self, $row, $h, $extra) = @_; +sub open311_extra_data_include { + my ($self, $row, $h) = @_; return [ { name => 'external_id', value => $row->id }, @@ -138,6 +138,65 @@ sub open311_config_updates { $params->{use_customer_reference} = 1; } +sub open311_pre_send { + my ($self, $row, $open311) = @_; + + $self->{ox_original_detail} = $row->detail; + + if (my $fid = $row->get_extra_field_value('feature_id')) { + my $text = "Asset Id: $fid\n\n" . $row->detail; + $row->detail($text); + } +} + +sub open311_post_send { + my ($self, $row, $h, $contact) = @_; + + $row->detail($self->{ox_original_detail}); +} + +sub open311_munge_update_params { + my ($self, $params, $comment, $body) = @_; + + if ($comment->get_extra_metadata('defect_raised')) { + my $p = $comment->problem; + my ($e, $n) = $p->local_coords; + my $usrn = $p->get_extra_field_value('usrn'); + if (!$usrn) { + my $cfg = { + url => 'https://tilma.mysociety.org/mapserver/oxfordshire', + typename => "OCCRoads", + srsname => 'urn:ogc:def:crs:EPSG::27700', + accept_feature => sub { 1 }, + filter => "<Filter xmlns:gml=\"http://www.opengis.net/gml\"><DWithin><PropertyName>SHAPE_GEOMETRY</PropertyName><gml:Point><gml:coordinates>$e,$n</gml:coordinates></gml:Point><Distance units='m'>20</Distance></DWithin></Filter>", + }; + my $features = $self->_fetch_features($cfg); + my $feature = $self->_nearest_feature($cfg, $e, $n, $features); + if ($feature) { + my $props = $feature->{properties}; + $usrn = Utils::trim_text($props->{TYPE1_2_USRN}); + } + } + $params->{'attribute[usrn]'} = $usrn; + $params->{'attribute[raise_defect]'} = 1; + $params->{'attribute[easting]'} = $e; + $params->{'attribute[northing]'} = $n; + my $details = $comment->user->email . ' '; + if (my $traffic = $p->get_extra_metadata('traffic_information')) { + $details .= 'TM1 ' if $traffic eq 'Signs and Cones'; + $details .= 'TM2 ' if $traffic eq 'Stop and Go Boards'; + } + (my $type = $p->get_extra_metadata('defect_item_type')) =~ s/ .*//; + $details .= $type eq 'Sweep' ? 'S&F' : $type; + $details .= ' ' . ($p->get_extra_metadata('detailed_information') || ''); + $params->{'attribute[extra_details]'} = $details; + + foreach (qw(defect_item_category defect_item_type defect_item_detail defect_location_description)) { + $params->{"attribute[$_]"} = $p->get_extra_metadata($_); + } + } +} + sub should_skip_sending_update { my ($self, $update ) = @_; @@ -151,18 +210,20 @@ sub should_skip_sending_update { return 0; } -sub on_map_default_status { return 'open'; } -sub admin_user_domain { 'oxfordshire.gov.uk' } +sub report_inspect_update_extra { + my ( $self, $problem ) = @_; -sub traffic_management_options { - return [ - "Signs and Cones", - "Stop and Go Boards", - "High Speed Roads", - ]; + foreach (qw(defect_item_category defect_item_type defect_item_detail defect_location_description)) { + my $value = $self->{c}->get_param($_); + $problem->set_extra_metadata($_ => $value) if $value; + } } +sub on_map_default_status { return 'open'; } + +sub admin_user_domain { 'oxfordshire.gov.uk' } + sub admin_pages { my $self = shift; @@ -203,13 +264,11 @@ sub available_permissions { } sub dashboard_export_problems_add_columns { - my $self = shift; - my $c = $self->{c}; + my ($self, $csv) = @_; - push @{$c->stash->{csv}->{headers}}, "HIAMS/Exor Ref"; - push @{$c->stash->{csv}->{columns}}, "external_ref"; + $csv->add_csv_columns( external_ref => 'HIAMS/Exor Ref' ); - $c->stash->{csv}->{extra_data} = sub { + $csv->csv_extra_data(sub { my $report = shift; # Try and get a HIAMS reference first of all my $ref = $report->get_extra_metadata('customer_reference'); @@ -222,7 +281,7 @@ sub dashboard_export_problems_add_columns { return { external_ref => ( $ref || '' ), }; - }; + }); } 1; diff --git a/perllib/FixMyStreet/Cobrand/Peterborough.pm b/perllib/FixMyStreet/Cobrand/Peterborough.pm index 0ddaeacb6..b10367cfd 100644 --- a/perllib/FixMyStreet/Cobrand/Peterborough.pm +++ b/perllib/FixMyStreet/Cobrand/Peterborough.pm @@ -13,6 +13,7 @@ sub council_area { 'Peterborough' } sub council_name { 'Peterborough City Council' } sub council_url { 'peterborough' } sub map_type { 'MasterMap' } +sub default_map_zoom { 5 } sub send_questionnaires { 0 } @@ -31,6 +32,8 @@ sub disambiguate_location { sub get_geocoder { 'OSM' } +sub contact_extra_fields { [ 'display_name' ] } + sub geocoder_munge_results { my ($self, $result) = @_; $result->{display_name} = '' unless $result->{display_name} =~ /City of Peterborough/; @@ -40,30 +43,29 @@ sub geocoder_munge_results { sub admin_user_domain { "peterborough.gov.uk" } -around open311_extra_data => sub { - my ($orig, $self, $row, $h, $extra) = @_; +around open311_extra_data_include => sub { + my ($orig, $self, $row, $h) = @_; - my $open311_only = $self->$orig($row, $h, $extra); + my $open311_only = $self->$orig($row, $h); foreach (@$open311_only) { if ($_->{name} eq 'description') { my ($ref) = grep { $_->{name} =~ /pcc-Skanska-csc-ref/i } @{$row->get_extra_fields}; $_->{value} .= "\n\nSkanska CSC ref: $ref->{value}" if $ref; } } + if ( $row->geocode && $row->contact->email =~ /Bartec/ ) { + my $address = $row->geocode->{resourceSets}->[0]->{resources}->[0]->{address}; + my ($number, $street) = $address->{addressLine} =~ /\s*(\d*)\s*(.*)/; + push @$open311_only, ( + { name => 'postcode', value => $address->{postalCode} }, + { name => 'house_no', value => $number }, + { name => 'street', value => $street } + ); + } return $open311_only; }; - # remove categories which are informational only -sub open311_pre_send { - my ($self, $row, $open311) = @_; - - return unless $row->extra; - my $extra = $row->get_extra_fields; - if (@$extra) { - @$extra = grep { $_->{name} !~ /^(PCC-|emergency$|private_land$)/i } @$extra; - $row->set_extra_fields(@$extra); - } -} +sub open311_extra_data_exclude { [ '^PCC-', '^emergency$', '^private_land$' ] } sub lookup_site_code_config { { buffer => 50, # metres @@ -85,8 +87,37 @@ sub open311_munge_update_params { # Send the FMS problem ID with the update. $params->{service_request_id_ext} = $comment->problem->id; - my $contact = $comment->problem->category_row; + my $contact = $comment->problem->contact; $params->{service_code} = $contact->email; } +around 'open311_config' => sub { + my ($orig, $self, $row, $h, $params) = @_; + + $params->{upload_files} = 1; + $self->$orig($row, $h, $params); +}; + +sub dashboard_export_problems_add_columns { + my ($self, $csv) = @_; + + $csv->add_csv_columns( + usrn => 'USRN', + nearest_address => 'Nearest address', + ); + + $csv->csv_extra_data(sub { + my $report = shift; + + my $address = ''; + $address = $report->geocode->{resourceSets}->[0]->{resources}->[0]->{name} + if $report->geocode; + + return { + usrn => $report->get_extra_field_value('site_code'), + nearest_address => $address, + }; + }); +} + 1; diff --git a/perllib/FixMyStreet/Cobrand/Rutland.pm b/perllib/FixMyStreet/Cobrand/Rutland.pm index 63a20d893..bc8eff6d2 100644 --- a/perllib/FixMyStreet/Cobrand/Rutland.pm +++ b/perllib/FixMyStreet/Cobrand/Rutland.pm @@ -29,8 +29,8 @@ sub open311_config { $params->{multi_photos} = 1; } -sub open311_extra_data { - my ($self, $row, $h, $extra) = @_; +sub open311_extra_data_include { + my ($self, $row, $h) = @_; return [ { name => 'external_id', value => $row->id }, diff --git a/perllib/FixMyStreet/Cobrand/TfL.pm b/perllib/FixMyStreet/Cobrand/TfL.pm index b98ad1d8b..b04841c39 100644 --- a/perllib/FixMyStreet/Cobrand/TfL.pm +++ b/perllib/FixMyStreet/Cobrand/TfL.pm @@ -209,7 +209,7 @@ sub around_nearby_filter { sub state_groups_inspect { my $rs = FixMyStreet::DB->resultset("State"); - my @open = grep { $_ !~ /^(planned|action scheduled|for triage)$/ } FixMyStreet::DB::Result::Problem->open_states; + my @open = grep { $_ !~ /^(planned|investigating|for triage)$/ } FixMyStreet::DB::Result::Problem->open_states; my @closed = grep { $_ ne 'closed' } FixMyStreet::DB::Result::Problem->closed_states; [ [ $rs->display('confirmed'), \@open ], @@ -242,51 +242,32 @@ sub available_permissions { } sub dashboard_export_problems_add_columns { - my $self = shift; - my $c = $self->{c}; + my ($self, $csv) = @_; - my %groups; - if ($c->stash->{body}) { - %groups = FixMyStreet::DB->resultset('Contact')->search({ - body_id => $c->stash->{body}->id, - })->group_lookup; - } + $csv->modify_csv_header( Ward => 'Borough' ); + + $csv->add_csv_columns( + agent_responsible => "Agent responsible", + safety_critical => "Safety critical", + delivered_to => "Delivered to", + closure_email_at => "Closure email at", + reassigned_at => "Reassigned at", + reassigned_by => "Reassigned by", + ); + $csv->splice_csv_column('fixed', action_scheduled => 'Action scheduled'); - splice @{$c->stash->{csv}->{headers}}, 5, 0, 'Subcategory'; - splice @{$c->stash->{csv}->{columns}}, 5, 0, 'subcategory'; - - $c->stash->{csv}->{headers} = [ - map { $_ eq 'Ward' ? 'Borough' : $_ } @{ $c->stash->{csv}->{headers} }, - "Agent responsible", - "Safety critical", - "Delivered to", - "Closure email at", - "Reassigned at", - "Reassigned by", - ]; - - $c->stash->{csv}->{columns} = [ - @{ $c->stash->{csv}->{columns} }, - "agent_responsible", - "safety_critical", - "delivered_to", - "closure_email_at", - "reassigned_at", - "reassigned_by", - ]; - - if ($c->stash->{category}) { - my ($contact) = grep { $_->category eq $c->stash->{category} } @{$c->stash->{contacts}}; + if ($csv->category) { + my @contacts = $csv->body->contacts->search(undef, { order_by => [ 'category' ] } )->all; + my ($contact) = grep { $_->category eq $csv->category } @contacts; if ($contact) { foreach (@{$contact->get_metadata_for_storage}) { next if $_->{code} eq 'safety_critical'; - push @{$c->stash->{csv}->{columns}}, "extra.$_->{code}"; - push @{$c->stash->{csv}->{headers}}, $_->{description}; + $csv->add_csv_columns( "extra.$_->{code}" => $_->{description} ); } } } - $c->stash->{csv}->{extra_data} = sub { + $csv->csv_extra_data(sub { my $report = shift; my $agent = $report->shortlisted_user; @@ -315,8 +296,6 @@ sub dashboard_export_problems_add_columns { my $fields = { acknowledged => $report->whensent, agent_responsible => $agent ? $agent->name : '', - category => $groups{$report->category}, - subcategory => $report->category, user_name_display => $user_name_display, safety_critical => $safety_critical, delivered_to => join(',', @$delivered_to), @@ -329,7 +308,7 @@ sub dashboard_export_problems_add_columns { $fields->{"extra.$_->{name}"} = $_->{value}; } return $fields; - }; + }); } sub must_have_2fa { @@ -449,6 +428,13 @@ sub munge_surrounding_london { # Don't send any TfL categories %$bodies = map { $_->id => $_ } grep { $_->name ne 'TfL' } values %$bodies; } + + # Hackney doesn't have any of the council TfL categories so don't show + # any Hackney categories on red routes + my %bodies = map { $_->name => $_->id } values %$bodies; + if ( $bodies{'Hackney Council'} && $self->report_new_is_on_tlrn ) { + delete $bodies->{ $bodies{'Hackney Council'} }; + } } sub munge_red_route_categories { @@ -498,6 +484,7 @@ sub _tlrn_categories { [ "Mobile Crane Operation", "Other (TfL)", "Pavement Defect (uneven surface / cracked paving slab)", + "Pavement Overcrowding", "Pothole", "Pothole (minor)", "Roadworks", @@ -505,6 +492,7 @@ sub _tlrn_categories { [ "Single Light out (street light)", "Standing water", "Street Light - Equipment damaged, pole leaning", + "Streetspace Feedback", "Unstable hoardings", "Unstable scaffolding", "Worn out road markings", diff --git a/perllib/FixMyStreet/Cobrand/UK.pm b/perllib/FixMyStreet/Cobrand/UK.pm index a42ff58a6..988458e0f 100644 --- a/perllib/FixMyStreet/Cobrand/UK.pm +++ b/perllib/FixMyStreet/Cobrand/UK.pm @@ -2,7 +2,11 @@ package FixMyStreet::Cobrand::UK; use base 'FixMyStreet::Cobrand::Default'; use strict; +use Encode; use JSON::MaybeXS; +use LWP::UserAgent; +use Path::Tiny; +use Time::Piece; use mySociety::MaPit; use mySociety::VotingArea; use Utils; @@ -397,9 +401,9 @@ sub link_to_council_cobrand { $handler->moniker ne $self->{c}->cobrand->moniker ) { my $url = sprintf("%s%s", $handler->base_url, $problem->url); - return sprintf("<a href='%s'>%s</a>", $url, $problem->body( $self->{c} )); + return sprintf("<a href='%s'>%s</a>", $url, $problem->body); } else { - return $problem->body( $self->{c} ); + return $problem->body; } } @@ -407,12 +411,6 @@ sub lookup_by_ref_regex { return qr/^\s*(\d+)\s*$/; } -sub category_extra_hidden { - my ($self, $meta) = @_; - return 1 if $meta->{code} eq 'usrn' || $meta->{code} eq 'asset_id'; - return $self->SUPER::category_extra_hidden($meta); -} - sub report_new_munge_before_insert { my ($self, $report) = @_; @@ -422,4 +420,82 @@ sub report_new_munge_before_insert { } } +# To use recaptcha, add a RECAPTCHA key to your config, with subkeys secret and +# site_key, taken from the recaptcha site. This shows it to non-UK IP addresses +# on alert and report pages. + +sub requires_recaptcha { + my $self = shift; + my $c = $self->{c}; + + return 0 if $c->user_exists; + return 0 if !FixMyStreet->config('RECAPTCHA'); + return 0 unless $c->action =~ /^(alert|report|around)/; + return 0 if $c->user_country eq 'GB'; + return 1; +} + +sub check_recaptcha { + my $self = shift; + my $c = $self->{c}; + + return unless $self->requires_recaptcha; + + my $url = 'https://www.google.com/recaptcha/api/siteverify'; + my $res = LWP::UserAgent->new->post($url, { + secret => FixMyStreet->config('RECAPTCHA')->{secret}, + response => $c->get_param('g-recaptcha-response'), + remoteip => $c->req->address, + }); + $res = decode_json($res->content); + $c->detach('/page_error_400_bad_request', ['Bad recaptcha']) + unless $res->{success}; +} + +sub public_holidays { + my $nation = shift || 'england-and-wales'; + my $json = _get_bank_holiday_json(); + return [ map { $_->{date} } @{$json->{$nation}{events}} ]; +} + +sub is_public_holiday { + my %args = @_; + $args{date} ||= localtime; + $args{date} = $args{date}->date; + $args{nation} ||= 'england-and-wales'; + my $json = _get_bank_holiday_json(); + for my $event (@{$json->{$args{nation}}{events}}) { + if ($event->{date} eq $args{date}) { + return 1; + } + } +} + +sub _get_bank_holiday_json { + my $file = 'bank-holidays.json'; + my $cache_file = path(FixMyStreet->path_to("../data/$file")); + my $js; + if (-s $cache_file && -M $cache_file <= 7 && !FixMyStreet->config('STAGING_SITE')) { + # uncoverable statement + $js = $cache_file->slurp_utf8; + } else { + $js = _fetch_url("https://www.gov.uk/$file"); + # uncoverable branch false + $js = decode_utf8($js) if !utf8::is_utf8($js); + if ($js && !FixMyStreet->config('STAGING_SITE')) { + # uncoverable statement + $cache_file->spew_utf8($js); + } + } + $js = JSON->new->decode($js) if $js; + return $js; +} + +sub _fetch_url { + my $url = shift; + my $ua = LWP::UserAgent->new; + $ua->timeout(5); + $ua->get($url)->content; +} + 1; diff --git a/perllib/FixMyStreet/Cobrand/UKCouncils.pm b/perllib/FixMyStreet/Cobrand/UKCouncils.pm index 21dd2d455..0e8341d57 100644 --- a/perllib/FixMyStreet/Cobrand/UKCouncils.pm +++ b/perllib/FixMyStreet/Cobrand/UKCouncils.pm @@ -270,6 +270,19 @@ sub relative_url_for_report { return FixMyStreet->config('BASE_URL'); } +sub problem_state_processed { + my ($self, $comment) = @_; + + my $state = $comment->problem_state || ''; + my $code = $comment->get_extra_metadata('external_status_code') || ''; + + my $cfg = $self->feature('extra_state_mapping'); + + $state = ( $cfg->{$state}->{$code} || $state ) if $cfg->{$state}; + + return $state; +} + sub admin_allow_user { my ( $self, $user ) = @_; return 1 if $user->is_superuser; @@ -329,6 +342,13 @@ sub munge_report_new_contacts { } } +sub open311_extra_data { + my $self = shift; + my $include = $self->call_hook(open311_extra_data_include => @_); + my $exclude = $self->call_hook(open311_extra_data_exclude => @_); + push @$exclude, 'sect_label', 'road_name', 'area_name'; + return ($include, $exclude); +}; =head2 lookup_site_code @@ -392,7 +412,7 @@ sub _fetch_features_url { SRSNAME => $cfg->{srsname}, TYPENAME => $cfg->{typename}, VERSION => "1.1.0", - outputformat => "geojson", + outputformat => $cfg->{outputformat} || "geojson", $cfg->{filter} ? ( Filter => $cfg->{filter} ) : ( BBOX => $cfg->{bbox} ), ); @@ -405,7 +425,7 @@ sub _nearest_feature { # We have a list of features, and we want to find the one closest to the # report location. - my $site_code = ''; + my $chosen = ''; my $nearest; # We shouldn't receive anything aside from these geometry types, but belt and braces. @@ -432,14 +452,14 @@ sub _nearest_feature { for (my $i=0; $i<@$coordinates-1; $i++) { my $distance = $self->_distanceToLine($x, $y, $coordinates->[$i], $coordinates->[$i+1]); if ( !defined $nearest || $distance < $nearest ) { - $site_code = $feature->{properties}->{$cfg->{property}}; + $chosen = $feature; $nearest = $distance; } } } } - return $site_code; + return $cfg->{property} && $chosen ? $chosen->{properties}->{$cfg->{property}} : $chosen; } sub contact_name { diff --git a/perllib/FixMyStreet/Cobrand/Westminster.pm b/perllib/FixMyStreet/Cobrand/Westminster.pm index c9f31f7f9..e00a7c092 100644 --- a/perllib/FixMyStreet/Cobrand/Westminster.pm +++ b/perllib/FixMyStreet/Cobrand/Westminster.pm @@ -78,15 +78,15 @@ sub open311_config { $h->{account_id} = $id || '0'; } -sub open311_extra_data { - my ($self, $row, $h, $extra) = @_; +sub open311_extra_data_include { + my ($self, $row, $h) = @_; # Reports made via the app probably won't have a USRN because we don't # display the road layer. Instead we'll look up the closest asset from the # asset service at the point we're sending the report over Open311. if (!$row->get_extra_field_value('USRN')) { if (my $ref = $self->lookup_site_code($row, 'USRN')) { - push @$extra, { name => 'USRN', value => $ref }; + $row->update_extra_field({ name => 'USRN', value => $ref }); } } @@ -96,7 +96,7 @@ sub open311_extra_data { my ($uprn_field) = grep { $_->{name} eq 'UPRN' } @$fields; if ( $uprn_field && !$uprn_field->{value} ) { if (my $ref = $self->lookup_site_code($row, 'UPRN')) { - push @$extra, { name => 'UPRN', value => $ref }; + $row->update_extra_field({ name => 'UPRN', value => $ref }); } } diff --git a/perllib/FixMyStreet/Cobrand/Zurich.pm b/perllib/FixMyStreet/Cobrand/Zurich.pm index 3cf678f9c..c7b9f70ee 100644 --- a/perllib/FixMyStreet/Cobrand/Zurich.pm +++ b/perllib/FixMyStreet/Cobrand/Zurich.pm @@ -10,6 +10,8 @@ use DateTime::Format::Pg; use Try::Tiny; use FixMyStreet::Geocode::Zurich; +use FixMyStreet::Template; +use FixMyStreet::WorkingDays; use strict; use warnings; @@ -131,9 +133,8 @@ sub problem_has_user_response { sub problem_as_hashref { my $self = shift; my $problem = shift; - my $ctx = shift; - my $hashref = $problem->as_hashref( $ctx ); + my $hashref = $problem->as_hashref; if ( $problem->state eq 'submitted' ) { for my $var ( qw( photo is_fixed meta ) ) { @@ -171,7 +172,6 @@ sub problem_as_hashref { sub updates_as_hashref { my $self = shift; my $problem = shift; - my $ctx = shift; my $hashref = {}; @@ -179,10 +179,10 @@ sub updates_as_hashref { $hashref->{update_pp} = $self->prettify_dt( $problem->lastupdate ); if ( $problem->state ne 'external' ) { - $hashref->{details} = FixMyStreet::App::View::Web::add_links( + $hashref->{details} = FixMyStreet::Template::add_links( $problem->get_extra_metadata('public_response') || '' ); } else { - $hashref->{details} = sprintf( _('Assigned to %s'), $problem->body($ctx)->name ); + $hashref->{details} = sprintf( _('Assigned to %s'), $problem->body->name ); } } @@ -217,13 +217,13 @@ sub allow_photo_display { } sub get_body_sender { - my ( $self, $body, $category ) = @_; + my ( $self, $body, $problem ) = @_; return { method => 'Zurich' }; } # Report overdue functions -my %public_holidays = map { $_ => 1 } ( +my @public_holidays = ( # New Year's Day, Saint Berchtold, Good Friday, Easter Monday, # Sechseläuten, Labour Day, Ascension Day, Whit Monday, # Swiss National Holiday, Knabenschiessen, Christmas, St Stephen's Day @@ -249,53 +249,23 @@ my %public_holidays = map { $_ => 1 } ( '2021-09-13', ); -sub is_public_holiday { - my $dt = shift; - return $public_holidays{$dt->ymd}; -} - -sub is_weekend { - my $dt = shift; - return $dt->dow > 5; -} - -sub add_days { - my ( $dt, $days ) = @_; - $dt = $dt->clone; - while ( $days > 0 ) { - $dt->add ( days => 1 ); - next if is_public_holiday($dt) or is_weekend($dt); - $days--; - } - return $dt; -} - -sub sub_days { - my ( $dt, $days ) = @_; - $dt = $dt->clone; - while ( $days > 0 ) { - $dt->subtract ( days => 1 ); - next if is_public_holiday($dt) or is_weekend($dt); - $days--; - } - return $dt; -} - sub overdue { my ( $self, $problem ) = @_; my $w = $problem->created; return 0 unless $w; + my $wd = FixMyStreet::WorkingDays->new( public_holidays => \@public_holidays ); + # call with previous state if ( $problem->state eq 'submitted' ) { # One working day - $w = add_days( $w, 1 ); + $w = $wd->add_days( $w, 1 ); return $w < DateTime->now() ? 1 : 0; } elsif ( $problem->state eq 'confirmed' || $problem->state eq 'in progress' || $problem->state eq 'feedback pending' ) { # States which affect the subdiv_overdue statistic. TODO: this may no longer be required # Six working days from creation - $w = add_days( $w, 6 ); + $w = $wd->add_days( $w, 6 ); return $w < DateTime->now() ? 1 : 0; # call with new state @@ -303,7 +273,7 @@ sub overdue { # States which affect the closed_overdue statistic # Five working days from moderation (so 6 from creation) - $w = add_days( $w, 6 ); + $w = $wd->add_days( $w, 6 ); return $w < DateTime->now() ? 1 : 0; } } @@ -454,10 +424,21 @@ sub admin_type { return $type; } +sub _admin_index_order { + my $self = shift; + my $c = $self->{c}; + my $order = $c->get_param('o') || 'created'; + my $dir = defined $c->get_param('d') ? $c->get_param('d') : 1; + $c->stash->{order} = $order; + $c->stash->{dir} = $dir; + return $dir ? { -desc => $order } : $order; +} + sub admin { my $self = shift; my $c = $self->{c}; my $type = $c->stash->{admin_type}; + my $internal = $c->get_param('internal'); if ($type eq 'dm') { $c->stash->{template} = 'admin/index-dm.html'; @@ -466,22 +447,20 @@ sub admin { my @children = map { $_->id } $body->bodies->all; my @all = (@children, $body->id); - my $order = $c->get_param('o') || 'created'; - my $dir = defined $c->get_param('d') ? $c->get_param('d') : 1; - $c->stash->{order} = $order; - $c->stash->{dir} = $dir; - $order = { -desc => $order } if $dir; + my $order = $self->_admin_index_order; - # XXX No multiples or missing bodies + # No multiples or missing bodies $c->stash->{submitted} = $c->cobrand->problems->search({ state => [ 'submitted', 'confirmed' ], bodies_str => $c->stash->{body}->id, + non_public => $internal ? 1 : 0, }, { order_by => $order, }); $c->stash->{approval} = $c->cobrand->problems->search({ state => 'feedback pending', bodies_str => $c->stash->{body}->id, + non_public => $internal ? 1 : 0, }, { order_by => $order, }); @@ -490,6 +469,7 @@ sub admin { $c->stash->{other} = $c->cobrand->problems->search({ state => { -not_in => [ 'submitted', 'confirmed', 'feedback pending' ] }, bodies_str => \@all, + non_public => $internal ? 1 : 0, }, { order_by => $order, })->page( $page ); @@ -499,23 +479,20 @@ sub admin { $c->stash->{template} = 'admin/index-sdm.html'; my $body = $c->stash->{body}; + my $order = $self->_admin_index_order; - my $order = $c->get_param('o') || 'created'; - my $dir = defined $c->get_param('d') ? $c->get_param('d') : 1; - $c->stash->{order} = $order; - $c->stash->{dir} = $dir; - $order = { -desc => $order } if $dir; - - # XXX No multiples or missing bodies + # No multiples or missing bodies $c->stash->{reports_new} = $c->cobrand->problems->search( { state => 'in progress', bodies_str => $body->id, + non_public => $internal ? 1 : 0, }, { order_by => $order } ); $c->stash->{reports_unpublished} = $c->cobrand->problems->search( { state => 'feedback pending', bodies_str => $body->parent->id, + non_public => $internal ? 1 : 0, }, { order_by => $order } ); @@ -524,6 +501,7 @@ sub admin { $c->stash->{reports_published} = $c->cobrand->problems->search( { state => 'fixed - council', bodies_str => $body->parent->id, + non_public => $internal ? 1 : 0, }, { order_by => $order } )->page( $page ); @@ -544,6 +522,18 @@ sub category_options { $c->stash->{category_options} = \@categories; } +sub report_remove_internal_flag { + my $self = shift; + my $c = $self->{c}; + my $problem = $c->stash->{problem}; + $c->forward('/auth/check_csrf_token'); + $problem->non_public(0); + $problem->update; + $c->forward('/admin/log_edit', [ $problem->id, 'problem', 'Intern Flag entfernt' ]); + # Make sure the problem's time_spent is updated + $self->update_admin_log($c, $problem); +} + sub admin_report_edit { my $self = shift; my $c = $self->{c}; @@ -623,6 +613,10 @@ sub admin_report_edit { } } + if ( ($type eq 'super' || $type eq 'dm') && $c->get_param('stop_internal') ) { + $self->report_remove_internal_flag; + return $self->admin_report_edit_done; + } # Problem updates upon submission if ( ($type eq 'super' || $type eq 'dm') && $c->get_param('submit') ) { @@ -863,18 +857,12 @@ sub admin_report_edit { $c->go('index'); } - $c->stash->{updates} = [ $c->model('DB::Comment') - ->search( { problem_id => $problem->id }, { order_by => 'created' } ) - ->all ]; - - $self->stash_states($problem); - return 1; + return $self->admin_report_edit_done; } if ($type eq 'sdm') { - my $editable = $type eq 'sdm' && $body->id eq $problem->bodies_str; - $c->stash->{sdm_disabled} = $editable ? '' : 'disabled'; + my $editable = $body->id eq $problem->bodies_str; # Has cut-down edit template for adding update and sending back up only $c->stash->{template} = 'admin/report_edit-sdm.html'; @@ -905,6 +893,8 @@ sub admin_report_edit { # Make sure the problem's time_spent is updated $self->update_admin_log($c, $problem); $c->res->redirect( '/admin/summary' ); + } elsif ($editable && $c->get_param('stop_internal')) { + $self->report_remove_internal_flag; } elsif ($editable && $c->get_param('submit')) { $c->forward('/auth/check_csrf_token'); @@ -936,22 +926,25 @@ sub admin_report_edit { # If they clicked the no more updates button, we're done. if ($c->get_param('no_more_updates')) { - $problem->set_extra_metadata( subdiv_overdue => $self->overdue( $problem ) ); - $problem->bodies_str( $body->parent->id ); - $problem->whensent( undef ); - $self->set_problem_state($c, $problem, 'feedback pending'); + if ($problem->non_public) { + $problem->bodies_str( $body->parent->id ); + $self->set_problem_state($c, $problem, 'fixed - council'); + } else { + $problem->set_extra_metadata( subdiv_overdue => $self->overdue( $problem ) ); + $problem->bodies_str( $body->parent->id ); + $problem->whensent( undef ); + $self->set_problem_state($c, $problem, 'feedback pending'); + } $problem->update; $c->res->redirect( '/admin/summary' ); } } - $c->stash->{updates} = [ $c->model('DB::Comment') - ->search( { problem_id => $problem->id }, { order_by => 'created' } ) - ->all ]; - - $self->stash_states($problem); - return 1; + $c->stash->{sdm_disabled} = $editable ? '' : 'disabled'; + $c->stash->{sdm_disabled_internal} = $problem->non_public ? 'disabled' : ''; + $c->stash->{sdm_disabled_fixed} = $problem->is_fixed ? 'disabled' : ''; + return $self->admin_report_edit_done; } $self->stash_states($problem); @@ -959,6 +952,19 @@ sub admin_report_edit { } +sub admin_report_edit_done { + my $self = shift; + my $c = $self->{c}; + my $problem = $c->stash->{problem}; + $c->stash->{updates} = [ $c->model('DB::Comment') + ->search( { problem_id => $problem->id }, { order_by => 'created' } ) + ->all ]; + + $self->stash_states($problem); + return 1; +} + + sub admin_district_lookup { my ($self, $row) = @_; FixMyStreet::Geocode::Zurich::admin_district($row->local_coords); @@ -1053,6 +1059,7 @@ sub _admin_send_email { my ( $c, $template, $problem ) = @_; return unless $problem->get_extra_metadata('email_confirmed'); + return if $problem->non_public; my $to = $problem->name ? [ $problem->user->email, $problem->name ] @@ -1240,8 +1247,8 @@ sub admin_stats { sub export_as_csv { my ($self, $c, $params) = @_; - my $csv = $c->stash->{csv} = { - objects => $c->model('DB::Problem')->search_rs( + my $reporting = FixMyStreet::Reporting->new( + objects_rs => $c->model('DB::Problem')->search_rs( $params, { join => ['admin_log_entries', 'user'], @@ -1262,7 +1269,7 @@ sub export_as_csv { ] } ), - headers => [ + csv_headers => [ 'Report ID', 'Created', 'Sent to Agency', 'Last Updated', 'E', 'N', 'Category', 'Status', 'Closure Status', 'UserID', 'User email', 'User phone', 'User name', @@ -1270,7 +1277,7 @@ sub export_as_csv { 'Media URL', 'Interface Used', 'Council Response', 'Strasse', 'Mast-Nr.', 'Haus-Nr.', 'Hydranten-Nr.', ], - columns => [ + csv_columns => [ 'id', 'created', 'whensent',' lastupdate', 'local_coords_x', 'local_coords_y', 'category', 'state', 'closure_status', 'user_id', 'user_email', 'user_phone', 'user_name', @@ -1278,11 +1285,11 @@ sub export_as_csv { 'media_url', 'service', 'public_response', 'strasse', 'mast_nr',' haus_nr', 'hydranten_nr', ], - extra_data => sub { + csv_extra_data => sub { my $report = shift; my $body_name = ""; - if ( my $external_body = $report->body($c) ) { + if ( my $external_body = $report->body ) { $body_name = $external_body->name || '[Unknown body]'; } @@ -1325,8 +1332,8 @@ sub export_as_csv { }; }, filename => 'stats', - }; - $c->forward('/dashboard/generate_csv'); + ); + $reporting->generate_csv_http($c); } sub problem_confirm_email_extras { @@ -1389,4 +1396,13 @@ sub hook_report_filter_status { } @$status; } +# If report is made by a flagged user, mark as non-public +sub report_new_munge_before_insert { + my ($self, $report) = @_; + + if ($report->user->flagged) { + $report->non_public(1); + } +} + 1; |