diff options
Diffstat (limited to 'perllib/FixMyStreet')
62 files changed, 2501 insertions, 1016 deletions
diff --git a/perllib/FixMyStreet/App.pm b/perllib/FixMyStreet/App.pm index 008aea595..82fcce508 100644 --- a/perllib/FixMyStreet/App.pm +++ b/perllib/FixMyStreet/App.pm @@ -232,8 +232,6 @@ sub setup_request { $c->model('DB::Problem')->set_restriction( $cobrand->site_key() ); - Memcached::set_namespace( FixMyStreet->config('FMS_DB_NAME') . ":" ); - FixMyStreet::Map::set_map_class( $cobrand->map_type || $c->get_param('map_override') ); # All pages need this, either loading it or prefetching it $c->stash->{map_js} = FixMyStreet::Map::map_javascript(); @@ -344,6 +342,24 @@ sub send_email { my $extra_stash_values = shift || {}; my $sender = $c->config->{DO_NOT_REPLY_EMAIL}; + my $email = $c->construct_email($template, $extra_stash_values) or return; + + my $result = 0; + try { + FixMyStreet::Email::Sender->send($email, { from => $sender }); + $result = $email; + } catch { + my $error = $_ || 'unknown error'; + $c->log->error("$error"); + }; + return $result; +} + +sub construct_email { + my ($c, $template, $extra_stash_values) = @_; + $extra_stash_values //= {}; + + my $sender = $c->config->{DO_NOT_REPLY_EMAIL}; my $sender_name = $c->cobrand->contact_name; # create the vars to pass to the email template @@ -354,6 +370,8 @@ sub send_email { %$extra_stash_values, additional_template_paths => \@include_path, }; + $vars->{site_name} = Utils::trim_text($c->view('Email')->render($c, 'site-name.txt', $vars)); + $vars->{signature} = $c->view('Email')->render($c, 'signature.txt', $vars); return if FixMyStreet::Email::is_abuser($c->model('DB')->schema, $vars->{to}); @@ -378,17 +396,7 @@ sub send_email { $data->{_html_} = $html_compiled if $html_compiled; $data->{_html_images_} = \@inline_images if @inline_images; - my $email = mySociety::Locale::in_gb_locale { FixMyStreet::Email::construct_email($data) }; - - my $result = 0; - try { - FixMyStreet::Email::Sender->send($email, { from => $sender }); - $result = $email; - } catch { - my $error = $_ || 'unknown error'; - $c->log->error("$error"); - }; - return $result; + return mySociety::Locale::in_gb_locale { FixMyStreet::Email::construct_email($data) }; } =head2 uri_with diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm index 85b6204fc..7d04f5ff9 100644 --- a/perllib/FixMyStreet/App/Controller/Admin.pm +++ b/perllib/FixMyStreet/App/Controller/Admin.pm @@ -12,9 +12,11 @@ use DateTime::Format::Strptime; use List::Util 'first'; use List::MoreUtils 'uniq'; use mySociety::ArrayUtils; +use Text::CSV; use FixMyStreet::SendReport; use FixMyStreet::SMS; +use Utils; =head1 NAME @@ -216,9 +218,14 @@ sub bodies : Path('bodies') : Args(0) { $c->forward('check_for_super_user'); $c->forward('/auth/check_csrf_token'); - my $params = $c->forward('body_params'); + my $values = $c->forward('body_params'); unless ( keys %{$c->stash->{body_errors}} ) { - my $body = $c->model('DB::Body')->create( $params ); + my $body = $c->model('DB::Body')->create( $values->{params} ); + if ($values->{extras}) { + $body->set_extra_metadata( $_ => $values->{extras}->{$_} ) + for keys %{$values->{extras}}; + $body->update; + } my @area_ids = $c->get_param_list('area_ids'); foreach (@area_ids) { $c->model('DB::BodyArea')->create( { body => $body, area_id => $_ } ); @@ -264,9 +271,15 @@ sub body_form_dropdowns : Private { } else { $areas = mySociety::MaPit::call('areas', $c->cobrand->area_types); } + + # Some cobrands may want to add extra areas at runtime beyond those + # available via MAPIT_WHITELIST or MAPIT_TYPES. This can be used for, + # e.g., parish councils on a particular council cobrand. + $areas = $c->cobrand->call_hook("add_extra_areas" => $areas) || $areas; + $c->stash->{areas} = [ sort { strcoll($a->{name}, $b->{name}) } values %$areas ]; - my @methods = map { $_ =~ s/FixMyStreet::SendReport:://; $_ } keys %{ FixMyStreet::SendReport->get_senders }; + my @methods = map { $_ =~ s/FixMyStreet::SendReport:://; $_ } sort keys %{ FixMyStreet::SendReport->get_senders }; $c->stash->{send_methods} = \@methods; } @@ -340,6 +353,7 @@ sub update_contacts : Private { } $c->forward('update_extra_fields', [ $contact ]); + $c->forward('contact_cobrand_extra_fields', [ $contact ]); if ( %errors ) { $c->stash->{updated} = _('Please correct the errors below'); @@ -387,9 +401,14 @@ sub update_contacts : Private { $c->forward('check_for_super_user'); $c->forward('/auth/check_csrf_token'); - my $params = $c->forward( 'body_params' ); + my $values = $c->forward( 'body_params' ); unless ( keys %{$c->stash->{body_errors}} ) { - $c->stash->{body}->update( $params ); + $c->stash->{body}->update( $values->{params} ); + if ($values->{extras}) { + $c->stash->{body}->set_extra_metadata( $_ => $values->{extras}->{$_} ) + for keys %{$values->{extras}}; + $c->stash->{body}->update; + } my @current = $c->stash->{body}->body_areas->all; my %current = map { $_->area_id => 1 } @current; my @area_ids = $c->get_param_list('area_ids'); @@ -444,6 +463,9 @@ sub body_params : Private { my %defaults = map { $_ => '' } @fields; %defaults = ( %defaults, send_comments => 0, + fetch_problems => 0, + convert_latlong => 0, + blank_updates_permitted => 0, suppress_alerts => 0, comment_user_id => undef, send_extended_statuses => 0, @@ -453,7 +475,10 @@ sub body_params : Private { ); my %params = map { $_ => $c->get_param($_) || $defaults{$_} } keys %defaults; $c->forward('check_body_params', [ \%params ]); - return \%params; + my @extras = qw/fetch_all_problems/; + %defaults = map { $_ => '' } @extras; + my %extras = map { $_ => $c->get_param($_) || $defaults{$_} } @extras; + return { params => \%params, extras => \%extras }; } sub check_body_params : Private { @@ -610,7 +635,7 @@ sub category : Chained('body') : PathPart('') { }, ); $c->stash->{history} = $history; - my @methods = map { $_ =~ s/FixMyStreet::SendReport:://; $_ } keys %{ FixMyStreet::SendReport->get_senders }; + my @methods = map { $_ =~ s/FixMyStreet::SendReport:://; $_ } sort keys %{ FixMyStreet::SendReport->get_senders }; $c->stash->{send_methods} = \@methods; return 1; @@ -619,6 +644,9 @@ sub category : Chained('body') : PathPart('') { sub reports : Path('reports') { my ( $self, $c ) = @_; + $c->stash->{edit_body_contacts} = 1 + if grep { $_ eq 'body' } keys %{$c->stash->{allowed_pages}}; + my $query = {}; if ( $c->cobrand->moniker eq 'zurich' ) { my $type = $c->stash->{admin_type}; @@ -641,6 +669,8 @@ sub reports : Path('reports') { my $p_page = $c->get_param('p') || 1; my $u_page = $c->get_param('u') || 1; + return if $c->cobrand->call_hook(report_search_query => $query, $p_page, $u_page, $order); + if (my $search = $c->get_param('search')) { $c->stash->{searched} = $search; @@ -761,10 +791,6 @@ sub reports : Path('reports') { $c->stash->{problems} = [ $problems->all ]; $c->stash->{problems_pager} = $problems->pager; } - - $c->stash->{edit_body_contacts} = 1 - if ( grep {$_ eq 'body'} keys %{$c->stash->{allowed_pages}}); - } sub update_user : Private { @@ -780,24 +806,10 @@ sub update_user : Private { return 0; } -sub report_edit : Path('report_edit') : Args(1) { - my ( $self, $c, $id ) = @_; - - my $problem = $c->cobrand->problems->search( { id => $id } )->first; - - $c->detach( '/page_error_404_not_found', [] ) - unless $problem; - - unless ( - $c->cobrand->moniker eq 'zurich' - || $c->user->has_permission_to(report_edit => $problem->bodies_str_ids) - ) { - $c->detach( '/page_error_403_access_denied', [] ); - } - - $c->stash->{problem} = $problem; +sub report_edit_display : Private { + my ( $self, $c ) = @_; - $c->forward('/auth/get_csrf_token'); + my $problem = $c->stash->{problem}; $c->stash->{page} = 'admin'; FixMyStreet::Map::display_map( @@ -814,27 +826,50 @@ sub report_edit : Path('report_edit') : Args(1) { : [], print_report => 1, ); +} - if (my $rotate_photo_param = $self->_get_rotate_photo_param($c)) { - $self->rotate_photo($c, $problem, @$rotate_photo_param); - if ( $c->cobrand->moniker eq 'zurich' ) { - # Clicking the photo rotation buttons should do nothing - # except for rotating the photo, so return the user - # to the report screen now. - $c->res->redirect( $c->uri_for( 'report_edit', $problem->id ) ); - return; - } else { - return 1; - } +sub report_edit : Path('report_edit') : Args(1) { + my ( $self, $c, $id ) = @_; + + my $problem = $c->cobrand->problems->search( { id => $id } )->first; + + $c->detach( '/page_error_404_not_found', [] ) + unless $problem; + + unless ( + $c->cobrand->moniker eq 'zurich' + || $c->user->has_permission_to(report_edit => $problem->bodies_str_ids) + ) { + $c->detach( '/page_error_403_access_denied', [] ); } - $c->forward('categories_for_point'); + $c->stash->{problem} = $problem; + if ( $problem->extra ) { + my @fields; + if ( my $fields = $problem->get_extra_fields ) { + for my $field ( @{$fields} ) { + my $name = $field->{description} ? + "$field->{description} ($field->{name})" : + "$field->{name}"; + push @fields, { name => $name, val => $field->{value} }; + } + } + my $extra = $problem->get_extra_metadata; + if ( $extra->{duplicates} ) { + push @fields, { name => 'Duplicates', val => join( ',', @{ $problem->get_extra_metadata('duplicates') } ) }; + delete $extra->{duplicates}; + } + for my $key ( keys %$extra ) { + push @fields, { name => $key, val => $extra->{$key} }; + } - if ( $c->cobrand->moniker eq 'zurich' ) { - my $done = $c->cobrand->admin_report_edit(); - return if $done; + $c->stash->{extra_fields} = \@fields; } + $c->forward('/auth/get_csrf_token'); + + $c->forward('categories_for_point'); + $c->forward('check_username_for_abuse', [ $problem->user ] ); $c->stash->{updates} = @@ -842,6 +877,16 @@ sub report_edit : Path('report_edit') : Args(1) { ->search( { problem_id => $problem->id }, { order_by => 'created' } ) ->all ]; + if (my $rotate_photo_param = $self->_get_rotate_photo_param($c)) { + $self->rotate_photo($c, $problem, @$rotate_photo_param); + $c->detach('report_edit_display'); + } + + if ( $c->cobrand->moniker eq 'zurich' ) { + my $done = $c->cobrand->admin_report_edit(); + $c->detach('report_edit_display') if $done; + } + if ( $c->get_param('resend') ) { $c->forward('/auth/check_csrf_token'); @@ -883,6 +928,12 @@ sub report_edit : Path('report_edit') : Args(1) { } $problem->set_inflated_columns(\%columns); + if ($c->get_param('closed_updates')) { + $problem->set_extra_metadata(closed_updates => 1); + } else { + $problem->unset_extra_metadata('closed_updates'); + } + $c->forward( '/admin/report_edit_category', [ $problem, $problem->state ne $old_state ] ); $c->forward('update_user', [ $problem ]); @@ -937,7 +988,7 @@ sub report_edit : Path('report_edit') : Args(1) { $problem->discard_changes; } - return 1; + $c->detach('report_edit_display'); } =head2 report_edit_category @@ -1010,11 +1061,18 @@ sub report_edit_location : Private { my ($lat, $lon) = map { Utils::truncate_coordinate($_) } $problem->latitude, $problem->longitude; if ( $c->stash->{latitude} != $lat || $c->stash->{longitude} != $lon ) { + # The two actions below change the stash, setting things up for e.g. a + # new report. But here we're only doing it in order to check the found + # bodies match; we don't want to overwrite the existing report data if + # this lookup is bad. So let's save the stash and restore it after the + # comparison. + my $safe_stash = { %{$c->stash} }; $c->forward('/council/load_and_check_areas', []); $c->forward('/report/new/setup_categories_and_bodies'); my %allowed_bodies = map { $_ => 1 } @{$problem->bodies_str_ids}; - my @new_bodies = @{$c->stash->{bodies_to_list}}; + my @new_bodies = keys %{$c->stash->{bodies_to_list}}; my $bodies_match = grep { exists( $allowed_bodies{$_} ) } @new_bodies; + $c->stash($safe_stash); return unless $bodies_match; $problem->latitude($c->stash->{latitude}); $problem->longitude($c->stash->{longitude}); @@ -1037,8 +1095,7 @@ sub categories_for_point : Private { # Remove the "Pick a category" option shift @{$c->stash->{category_options}} if @{$c->stash->{category_options}}; - $c->stash->{category_options_copy} = $c->stash->{category_options}; - $c->stash->{categories_hash} = { map { $_->{name} => 1 } @{$c->stash->{category_options}} }; + $c->stash->{categories_hash} = { map { $_->category => 1 } @{$c->stash->{category_options}} }; } sub templates : Path('templates') : Args(0) { @@ -1096,6 +1153,7 @@ sub template_edit : Path('templates') : Args(2) { id => $_->id, category => $_->category_display, active => $active_contacts{$_->id}, + email => $_->email, } } @live_contacts; $c->stash->{contacts} = \@all_contacts; @@ -1115,8 +1173,15 @@ sub template_edit : Path('templates') : Args(2) { $template->title( $c->get_param('title') ); $template->text( $c->get_param('text') ); $template->state( $c->get_param('state') ); + $template->external_status_code( $c->get_param('external_status_code') ); + + if ( $template->state && $template->external_status_code ) { + $c->stash->{errors} ||= {}; + $c->stash->{errors}->{state} = _("State and external status code cannot be used simultaneously."); + $c->stash->{errors}->{external_status_code} = _("State and external status code cannot be used simultaneously."); + } - $template->auto_response( $c->get_param('auto_response') && $template->state ? 1 : 0 ); + $template->auto_response( $c->get_param('auto_response') && ( $template->state || $template->external_status_code ) ? 1 : 0 ); if ($template->auto_response) { my @check_contact_ids = @new_contact_ids; # If the new template has not specific categories (i.e. it @@ -1128,7 +1193,10 @@ sub template_edit : Path('templates') : Args(2) { my $query = { 'auto_response' => 1, 'contact.id' => [ @check_contact_ids, undef ], - 'me.state' => $template->state, + -or => { + $template->state ? ('me.state' => $template->state) : (), + $template->external_status_code ? ('me.external_status_code' => $template->external_status_code) : (), + }, }; if ($template->in_storage) { $query->{'me.id'} = { '!=', $template->id }; @@ -1136,9 +1204,8 @@ sub template_edit : Path('templates') : Args(2) { if ($c->stash->{body}->response_templates->search($query, { join => { 'contact_response_templates' => 'contact' }, })->count) { - $c->stash->{errors} = { - auto_response => _("There is already an auto-response template for this category/state.") - }; + $c->stash->{errors} ||= {}; + $c->stash->{errors}->{auto_response} = _("There is already an auto-response template for this category/state."); } } @@ -1225,7 +1292,7 @@ sub users: Path('users') : Args(0) { sub update_edit : Path('update_edit') : Args(1) { my ( $self, $c, $id ) = @_; - my $update = $c->cobrand->updates->search({ id => $id })->first; + my $update = $c->cobrand->updates->search({ 'me.id' => $id })->first; $c->detach( '/page_error_404_not_found', [] ) unless $update; @@ -1612,6 +1679,61 @@ sub user_edit : Path('user_edit') : Args(1) { return 1; } +sub user_import : Path('user_import') { + my ( $self, $c, $id ) = @_; + + $c->forward('/auth/get_csrf_token'); + return unless $c->user_exists && $c->user->is_superuser; + + if ($c->req->method eq 'POST') { + $c->forward('/auth/check_csrf_token'); + $c->stash->{new_users} = []; + $c->stash->{existing_users} = []; + + my @all_permissions = map { keys %$_ } values %{ $c->cobrand->available_permissions }; + my %available_permissions = map { $_ => 1 } @all_permissions; + + my $csv = Text::CSV->new({ binary => 1}); + my $fh = $c->req->upload('csvfile')->fh; + $csv->getline($fh); # discard the header + while (my $row = $csv->getline($fh)) { + my ($name, $email, $from_body, $permissions) = @$row; + $email = lc Utils::trim_text($email); + my @permissions = split(/:/, $permissions); + + my $user = FixMyStreet::DB->resultset("User")->find_or_new({ email => $email, email_verified => 1 }); + if ($user->in_storage) { + push @{$c->stash->{existing_users}}, $user; + next; + } + + $user->name($name); + $user->from_body($from_body || undef); + $user->update_or_insert; + + my @user_permissions = grep { $available_permissions{$_} } @permissions; + foreach my $permission_type (@user_permissions) { + $user->user_body_permissions->find_or_create({ + body_id => $user->from_body->id, + permission_type => $permission_type, + }); + } + + push @{$c->stash->{new_users}}, $user; + } + + } +} + +sub contact_cobrand_extra_fields : Private { + my ( $self, $c, $contact ) = @_; + + my $extra_fields = $c->cobrand->call_hook('contact_extra_fields'); + foreach ( @$extra_fields ) { + $contact->set_extra_metadata( $_ => $c->get_param("extra[$_]") ); + } +} + sub user_cobrand_extra_fields : Private { my ( $self, $c ) = @_; @@ -1795,20 +1917,7 @@ sub user_hide_everywhere : Private { sub user_remove_account : Private { my ( $self, $c, $user ) = @_; $c->forward('user_logout_everywhere', [ $user ]); - $user->problems->update({ anonymous => 1, name => '', send_questionnaire => 0 }); - $user->comments->update({ anonymous => 1, name => '' }); - $user->alerts->update({ whendisabled => \'current_timestamp' }); - $user->password('', 1); - $user->update({ - email => 'removed-' . $user->id . '@' . FixMyStreet->config('EMAIL_DOMAIN'), - email_verified => 0, - name => '', - phone => '', - phone_verified => 0, - title => undef, - twitter_id => undef, - facebook_id => undef, - }); + $user->anonymize_account; $c->stash->{status_message} = _('That user’s personal details have been removed.'); } diff --git a/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm b/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm index bdeecc1a3..d965dd8f2 100644 --- a/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm +++ b/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm @@ -43,7 +43,7 @@ sub download : Path('download') : Args(0) { $c->detach( '/page_error_404_not_found', [] ); } - my $parser = DateTime::Format::Strptime->new( pattern => '%d/%m/%Y' ); + my $parser = DateTime::Format::Strptime->new( pattern => '%Y-%m-%d' ); my $start_date = $parser-> parse_datetime ( $c->get_param('start_date') ); my $end_date = $parser-> parse_datetime ( $c->get_param('end_date') ) ; my $one_day = DateTime::Duration->new( days => 1 ); diff --git a/perllib/FixMyStreet/App/Controller/Admin/Stats.pm b/perllib/FixMyStreet/App/Controller/Admin/Stats.pm index 2860b3531..5f82094d6 100644 --- a/perllib/FixMyStreet/App/Controller/Admin/Stats.pm +++ b/perllib/FixMyStreet/App/Controller/Admin/Stats.pm @@ -72,4 +72,31 @@ sub questionnaire : Local : Args(0) { return 1; } +sub refused : Local : Args(0) { + my ($self, $c) = @_; + + my $contacts = $c->model('DB::Contact')->not_deleted->search([ + { email => 'REFUSED' }, + { 'body.can_be_devolved' => 1, 'me.send_method' => 'Refused' }, + ], { prefetch => 'body' }); + my %bodies; + while (my $contact = $contacts->next) { + my $body = $contact->body; + $bodies{$body->id}{body} = $body unless $bodies{$body->id}{body}; + push @{$bodies{$body->id}{contacts}}, $contact; + } + + my $bodies = $c->model('DB::Body')->search({ send_method => 'Refused' }); + while (my $body = $bodies->next) { + $bodies{$body->id}{body} = $body; + $bodies{$body->id}{all} = 1; + } + + my @bodies; + foreach (sort { $bodies{$a}{body}->name cmp $bodies{$b}{body}->name } keys %bodies) { + push @bodies, $bodies{$_}; + } + $c->stash->{bodies} = \@bodies; +} + 1; diff --git a/perllib/FixMyStreet/App/Controller/Alert.pm b/perllib/FixMyStreet/App/Controller/Alert.pm index 9d522dbc9..1060c080b 100644 --- a/perllib/FixMyStreet/App/Controller/Alert.pm +++ b/perllib/FixMyStreet/App/Controller/Alert.pm @@ -369,7 +369,7 @@ sub process_user : Private { # return 1; # } # -# $alert_user->password( Utils::trim_text( $params{password_register} ) ); +# $alert_user->password( $params{password_register} ); } =head2 setup_coordinate_rss_feeds diff --git a/perllib/FixMyStreet/App/Controller/Around.pm b/perllib/FixMyStreet/App/Controller/Around.pm index ae7d83f55..8fed5c3aa 100644 --- a/perllib/FixMyStreet/App/Controller/Around.pm +++ b/perllib/FixMyStreet/App/Controller/Around.pm @@ -228,19 +228,18 @@ sub check_and_stash_category : Private { my @bodies = $c->model('DB::Body')->active->for_areas(keys %$all_areas)->all; my %bodies = map { $_->id => $_ } @bodies; - my @contacts = $c->model('DB::Contact')->not_deleted->search( + my @categories = $c->model('DB::Contact')->not_deleted->search( { body_id => [ keys %bodies ], }, { - columns => [ 'category' ], + columns => [ 'category', 'extra' ], order_by => [ 'category' ], distinct => 1 } )->all; - my @categories = map { { name => $_->category, value => $_->category_display } } @contacts; $c->stash->{filter_categories} = \@categories; - my %categories_mapped = map { $_->{name} => 1 } @categories; + my %categories_mapped = map { $_->category => 1 } @categories; my $categories = [ $c->get_param_list('filter_category', 1) ]; my %valid_categories = map { $_ => 1 } grep { $_ && $categories_mapped{$_} } @$categories; @@ -257,12 +256,16 @@ sub map_features : Private { return if $c->get_param('js'); # JS will request the same (or more) data client side + # Allow the cobrand to add in any additional query parameters + my $extra_params = $c->cobrand->call_hook('display_location_extra_params'); + my ( $on_map, $nearby, $distance ) = FixMyStreet::Map::map_features( $c, %$extra, categories => [ keys %{$c->stash->{filter_category}} ], states => $c->stash->{filter_problem_states}, order => $c->stash->{sort_order}, + extra => $extra_params, ); my @pins; @@ -305,6 +308,27 @@ sub ajax : Path('/ajax') { $c->forward('/reports/ajax', [ 'around/on_map_list_items.html' ]); } +sub location_closest_address : Path('/ajax/closest') { + my ( $self, $c ) = @_; + $c->res->content_type('application/json; charset=utf-8'); + + my $lat = $c->get_param('lat'); + my $lon = $c->get_param('lon'); + unless ($lat && $lon) { + $c->res->status(404); + $c->res->body(''); + return; + } + + my $closest = $c->cobrand->find_closest({ latitude => $lat, longitude => $lon }); + my $data = { + road => $closest->{address}{addressLine}, + full_address => $closest->{name}, + }; + + $c->res->body(encode_json($data)); +} + sub location_autocomplete : Path('/ajax/geocode') { my ( $self, $c ) = @_; $c->res->content_type('application/json; charset=utf-8'); diff --git a/perllib/FixMyStreet/App/Controller/Contact.pm b/perllib/FixMyStreet/App/Controller/Contact.pm index f2c3be47c..997009b87 100644 --- a/perllib/FixMyStreet/App/Controller/Contact.pm +++ b/perllib/FixMyStreet/App/Controller/Contact.pm @@ -87,12 +87,24 @@ sub determine_contact_type : Private { } elsif ($id) { $c->forward( '/report/load_problem_or_display_error', [ $id ] ); if ($update_id) { - my $update = $c->model('DB::Comment')->find( - { id => $update_id } - ); + my $update = $c->model('DB::Comment')->search( + { + id => $update_id, + problem_id => $id, + state => 'confirmed', + } + )->first; + + unless ($update) { + $c->detach( '/page_error_404_not_found', [ _('Unknown update ID') ] ); + } $c->stash->{update} = $update; } + + if ( $c->get_param("reject") && $c->user->has_permission_to(report_reject => $c->stash->{problem}->bodies_str_ids) ) { + $c->stash->{rejecting_report} = 1; + } } return 1; diff --git a/perllib/FixMyStreet/App/Controller/Council.pm b/perllib/FixMyStreet/App/Controller/Council.pm index 0e7553dc4..2e2dce0f7 100644 --- a/perllib/FixMyStreet/App/Controller/Council.pm +++ b/perllib/FixMyStreet/App/Controller/Council.pm @@ -49,6 +49,14 @@ sub load_and_check_areas : Private { $area_types = $c->cobrand->area_types; } + # Cobrand may wish to add area types to look up for a point at runtime. + # This can be used for, e.g., parish councils on a particular council + # cobrand. NB three-tier councils break the alerts pages, so don't run the + # hook if we're on an alerts page. + unless ($c->stash->{area_check_action} eq 'alert') { + $area_types = $c->cobrand->call_hook("add_extra_area_types" => $area_types) || $area_types; + } + my $all_areas; my %params; diff --git a/perllib/FixMyStreet/App/Controller/Dashboard.pm b/perllib/FixMyStreet/App/Controller/Dashboard.pm index 032c36e05..ffd8d76c1 100644 --- a/perllib/FixMyStreet/App/Controller/Dashboard.pm +++ b/perllib/FixMyStreet/App/Controller/Dashboard.pm @@ -103,7 +103,10 @@ sub index : Path : Args(0) { $c->stash->{bodies} = \@bodies; } - $c->stash->{start_date} = $c->get_param('start_date'); + my $days30 = DateTime->now(time_zone => FixMyStreet->time_zone || FixMyStreet->local_time_zone)->subtract(days => 30); + $days30->truncate( to => 'day' ); + + $c->stash->{start_date} = $c->get_param('start_date') || $days30->strftime('%Y-%m-%d'); $c->stash->{end_date} = $c->get_param('end_date'); $c->stash->{q_state} = $c->get_param('state') || ''; @@ -136,30 +139,14 @@ sub construct_rs_filter : Private { } my $dtf = $c->model('DB')->storage->datetime_parser; - my $date = DateTime->now( time_zone => FixMyStreet->local_time_zone )->subtract(days => 30); - $date->truncate( to => 'day' ); - - $where{'me.confirmed'} = { '>=', $dtf->format_datetime($date) }; - - my $start_date = $c->stash->{start_date}; - my $end_date = $c->stash->{end_date}; - if ($start_date or $end_date) { - my @parts; - if ($start_date) { - my $date = $dtf->parse_datetime($start_date); - push @parts, { '>=', $dtf->format_datetime( $date ) }; - } - if ($end_date) { - my $one_day = DateTime::Duration->new( days => 1 ); - my $date = $dtf->parse_datetime($end_date); - push @parts, { '<', $dtf->format_datetime( $date + $one_day ) }; - } - if (scalar @parts == 2) { - $where{'me.confirmed'} = [ -and => $parts[0], $parts[1] ]; - } else { - $where{'me.confirmed'} = $parts[0]; - } + my $start_date = $dtf->parse_datetime($c->stash->{start_date}); + $where{'me.confirmed'} = { '>=', $dtf->format_datetime($start_date) }; + + if (my $end_date = $c->stash->{end_date}) { + my $one_day = DateTime::Duration->new( days => 1 ); + $end_date = $dtf->parse_datetime($end_date) + $one_day; + $where{'me.confirmed'} = [ -and => $where{'me.confirmed'}, { '<', $dtf->format_datetime($end_date) } ]; } $c->stash->{params} = \%where; @@ -291,7 +278,7 @@ sub export_as_csv : Private { my $csv = $c->stash->{csv} = { problems => $c->stash->{problems_rs}->search_rs({}, { prefetch => 'comments', - order_by => 'me.confirmed' + order_by => ['me.confirmed', 'me.id'], }), headers => [ 'Report ID', @@ -346,6 +333,7 @@ sub export_as_csv : Private { } sort keys %where }, }; + $c->cobrand->call_hook("dashboard_export_add_columns"); $c->forward('generate_csv'); } @@ -358,6 +346,8 @@ Generates a CSV output, given a 'csv' stash hashref containing: * columns: an arrayref of the columns (looked up in the row's as_hashref, plus the following: user_name_display, acknowledged, fixed, closed, wards, local_coords_x, local_coords_y, url). +* extra_data: If present, a function that is passed the report and returns a +hashref of extra data to include that can be used by 'columns'. =cut @@ -371,21 +361,16 @@ sub generate_csv : Private { my $fixed_states = FixMyStreet::DB::Result::Problem->fixed_states; my $closed_states = FixMyStreet::DB::Result::Problem->closed_states; - my $wards = 0; - my $comments = 0; - foreach (@{$c->stash->{csv}->{columns}}) { - $wards = 1 if $_ eq 'wards'; - $comments = 1 if $_ eq 'acknowledged'; - } + my %asked_for = map { $_ => 1 } @{$c->stash->{csv}->{columns}}; my $problems = $c->stash->{csv}->{problems}; while ( my $report = $problems->next ) { - my $hashref = $report->as_hashref($c); + my $hashref = $report->as_hashref($c, \%asked_for); $hashref->{user_name_display} = $report->anonymous ? '(anonymous)' : $report->user->name; - if ($comments) { + if ($asked_for{acknowledged}) { for my $comment ($report->comments) { my $problem_state = $comment->problem_state or next; next unless $comment->state eq 'confirmed'; @@ -400,7 +385,7 @@ sub generate_csv : Private { } } - if ($wards) { + if ($asked_for{wards}) { $hashref->{wards} = join ', ', map { $c->stash->{children}->{$_}->{name} } grep {$c->stash->{children}->{$_} } @@ -411,6 +396,11 @@ sub generate_csv : Private { $report->local_coords; $hashref->{url} = join '', $c->cobrand->base_url_for_report($report), $report->url; + if (my $fn = $c->stash->{csv}->{extra_data}) { + my $extra = $fn->($report); + $hashref = { %$hashref, %$extra }; + } + $csv->combine( @{$hashref}{ @{$c->stash->{csv}->{columns}} diff --git a/perllib/FixMyStreet/App/Controller/Develop.pm b/perllib/FixMyStreet/App/Controller/Develop.pm new file mode 100755 index 000000000..0bc52883f --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Develop.pm @@ -0,0 +1,136 @@ +package FixMyStreet::App::Controller::Develop; +use Moose; +use namespace::autoclean; + +use File::Basename; + +BEGIN { extends 'Catalyst::Controller'; } + +=head1 NAME + +FixMyStreet::App::Controller::Develop - Catalyst Controller + +=head1 DESCRIPTION + +Developer-helping Catalyst Controller. + +=head1 METHODS + +=over 4 + +=item auto + +Makes sure this controller is only available when run in development. + +=cut + +sub auto : Private { + my ($self, $c) = @_; + $c->detach( '/page_error_404_not_found' ) unless $c->config->{STAGING_SITE}; + return 1; +} + +=item email_list + +Shows a list of links to preview HTML emails. + +=cut + +sub email_list : Path('/_dev/email') : Args(0) { + my ( $self, $c ) = @_; + + my @include_path = @{ $c->cobrand->path_to_email_templates($c->stash->{lang_code}) }; + push @include_path, $c->view('Email')->config->{INCLUDE_PATH}->[0]; + my %templates; + foreach (@include_path) { + $templates{$_} = 1 for grep { /^[^_]/ } map { s/\.html$//; basename $_ } glob "$_/*.html"; + } + + my %with_update = ('update-confirm' => 1, 'other-updated' => 1); + my %with_problem = ('alert-update' => 1, 'other-reported' => 1, + 'problem-confirm' => 1, 'problem-confirm-not-sending' => 1, + 'problem-moderated' => 1, 'questionnaire' => 1, 'submit' => 1); + + my $update = $c->model('DB::Comment')->first; + my $problem = $c->model('DB::Problem')->first; + + $c->stash->{templates} = []; + foreach (sort keys %templates) { + my $url = $c->uri_for('/_dev/email', $_); + $url .= "?problem=" . $problem->id if $problem && $with_problem{$_}; + $url .= "?update=" . $update->id if $update && $with_update{$_}; + push @{$c->stash->{templates}}, { name => $_, url => $url }; + } +} + +=item email_previewer + +Previews an HTML email template. A problem or update ID can be provided as a +query parameter, and other data is taken from the database. + +=back + +=cut + +sub email_previewer : Path('/_dev/email') : Args(1) { + my ( $self, $c, $template ) = @_; + + my $vars = {}; + if (my $id = $c->get_param('update')) { + $vars->{update} = $c->model('DB::Comment')->find($id); + $vars->{problem} = $vars->{report} = $vars->{update}->problem; + } elsif ($id = $c->get_param('problem')) { + $vars->{problem} = $vars->{report} = $c->model('DB::Problem')->find($id); + } + + # Special case needed variables + if ($template =~ /^alert-problem/) { + $vars->{area_name} = 'Area Name'; + $vars->{ward_name} = 'Ward Name'; + $vars->{data} = [ $c->model('DB::Problem')->search({}, { rows => 5 })->all ]; + } elsif ($template eq 'alert-update') { + $vars->{data} = []; + my $q = $c->model('DB::Comment')->search({}, { rows => 5 }); + while (my $u = $q->next) { + my $fn = sub { + return FixMyStreet::App::Model::PhotoSet->new({ + db_data => $u->photo, + })->get_image_data( num => 0, size => 'fp' ); + }; + push @{$vars->{data}}, { + item_photo => $u->photo, get_first_image_fp => $fn, item_text => $u->text, + item_name => $u->name, item_anonymous => $u->anonymous, confirmed => $u->confirmed }; + } + } elsif ($template eq 'questionnaire') { + $vars->{created} = 'N weeks'; + } + + my $email = $c->construct_email("$template.txt", $vars); + + # Look through the Email::MIME email for the text/html part, and any inline + # images. Turn the images into data: URIs. + my $html = ''; + my %images; + $email->walk_parts(sub { + my ($part) = @_; + return if $part->subparts; + if ($part->content_type =~ m[^image/]i) { + (my $cid = $part->header('Content-ID')) =~ s/[<>]//g; + (my $ct = $part->content_type) =~ s/;.*//; + $images{$cid} = "$ct;base64," . $part->body_raw; + } elsif ($part->content_type =~ m[text/html]i) { + $html = $part->body_str; + } + }); + + foreach (keys %images) { + $html =~ s/cid:([^"]*)/data:$images{$1}/g; + } + + $c->response->body($html); +} + +__PACKAGE__->meta->make_immutable; + +1; + diff --git a/perllib/FixMyStreet/App/Controller/FakeMapit.pm b/perllib/FixMyStreet/App/Controller/FakeMapit.pm index 0ec13ebfa..51975254a 100755 --- a/perllib/FixMyStreet/App/Controller/FakeMapit.pm +++ b/perllib/FixMyStreet/App/Controller/FakeMapit.pm @@ -2,7 +2,7 @@ package FixMyStreet::App::Controller::FakeMapit; use Moose; use namespace::autoclean; use JSON::MaybeXS; -use LWP::Simple; +use LWP::UserAgent; BEGIN { extends 'Catalyst::Controller'; } @@ -22,13 +22,25 @@ world is one area, with ID 161 and name "Everywhere". my $area = { "name" => "Everywhere", "type" => "ZZZ", "id" => 161 }; +has user_agent => ( + is => 'ro', + lazy => 1, + default => sub { + my $ua = LWP::UserAgent->new; + my $api_key = FixMyStreet->config('MAPIT_API_KEY'); + $ua->agent("FakeMapit proxy"); + $ua->default_header( 'X-Api-Key' => $api_key ) if $api_key; + return $ua; + } +); + # The user should have the web server proxying this, # but for development we can also do it on the server. sub proxy : Path('/mapit') { my ($self, $c) = @_; (my $path = $c->req->uri->path_query) =~ s{^/mapit/}{}; my $url = FixMyStreet->config('MAPIT_URL') . $path; - my $kml = LWP::Simple::get($url); + my $kml = $self->user_agent->get($url)->content; $c->response->body($kml); } diff --git a/perllib/FixMyStreet/App/Controller/My.pm b/perllib/FixMyStreet/App/Controller/My.pm index 1693766ba..883ccc0ce 100644 --- a/perllib/FixMyStreet/App/Controller/My.pm +++ b/perllib/FixMyStreet/App/Controller/My.pm @@ -159,11 +159,10 @@ sub setup_page_data : Private { my @categories = $c->stash->{problems_rs}->search({ state => [ FixMyStreet::DB::Result::Problem->visible_states() ], }, { - columns => [ 'category' ], + columns => [ 'category', 'extra' ], distinct => 1, order_by => [ 'category' ], } )->all; - @categories = map { { name => $_->category, value => $_->category_display } } @categories; $c->stash->{filter_categories} = \@categories; $c->stash->{page} = 'my'; diff --git a/perllib/FixMyStreet/App/Controller/Open311.pm b/perllib/FixMyStreet/App/Controller/Open311.pm index c7e4e5bee..83b9b8202 100644 --- a/perllib/FixMyStreet/App/Controller/Open311.pm +++ b/perllib/FixMyStreet/App/Controller/Open311.pm @@ -231,14 +231,15 @@ sub output_requests : Private { $problem->state( $statusmap{$problem->state} ); + my ($lat, $lon) = map { Utils::truncate_coordinate($_) } $problem->latitude, $problem->longitude; my $request = { 'service_request_id' => $id, 'title' => $problem->title, # Not in Open311 v2 'detail' => $problem->detail, # Not in Open311 v2 'description' => $problem->title .': ' . $problem->detail, - 'lat' => $problem->latitude, - 'long' => $problem->longitude, + 'lat' => $lat, + 'long' => $lon, 'status' => $problem->state, # 'status_notes' => {}, # Zurich has visible unconfirmed reports @@ -307,6 +308,7 @@ sub get_requests : Private { # Only provide access to the published reports my $states = FixMyStreet::DB::Result::Problem->visible_states(); delete $states->{unconfirmed}; + delete $states->{submitted}; my $criteria = { state => [ keys %$states ] }; @@ -409,6 +411,7 @@ sub get_request : Private { my $states = FixMyStreet::DB::Result::Problem->visible_states(); delete $states->{unconfirmed}; + delete $states->{submitted}; my $criteria = { state => [ keys %$states ], id => $id, @@ -419,6 +422,7 @@ sub get_request : Private { sub format_output : Private { my ( $self, $c, $hashref ) = @_; my $format = $c->stash->{format}; + $c->response->header('Access-Control-Allow-Origin' => '*'); if ('json' eq $format) { $c->res->content_type('application/json; charset=utf-8'); $c->res->body( encode_json($hashref) ); @@ -441,11 +445,10 @@ sub is_jurisdiction_id_ok : Private { # Input: DateTime object # Output: 2011-04-23T10:28:55+02:00 -# FIXME Need generic solution to find time zone sub w3date : Private { my $datestr = shift; return unless $datestr; - return DateTime::Format::W3CDTF->format_datetime($datestr); + return DateTime::Format::W3CDTF->format_datetime($datestr->truncate(to => 'second')); } =head1 AUTHOR diff --git a/perllib/FixMyStreet/App/Controller/Questionnaire.pm b/perllib/FixMyStreet/App/Controller/Questionnaire.pm index 1b338732b..58848f546 100755 --- a/perllib/FixMyStreet/App/Controller/Questionnaire.pm +++ b/perllib/FixMyStreet/App/Controller/Questionnaire.pm @@ -27,13 +27,13 @@ finds out if this user has answered the "ever reported" question before. =cut sub check_questionnaire : Private { - my ( $self, $c ) = @_; + my ( $self, $c, $unanswered ) = @_; my $questionnaire = $c->stash->{questionnaire}; my $problem = $questionnaire->problem; - if ( $questionnaire->whenanswered ) { + if ( $unanswered && $questionnaire->whenanswered ) { my $problem_url = $c->cobrand->base_url_for_report( $problem ) . $problem->url; my $contact_url = $c->uri_for( "/contact" ); my $message = sprintf(_("You have already answered this questionnaire. If you have a question, please <a href='%s'>get in touch</a>, or <a href='%s'>view your problem</a>.\n"), $contact_url, $problem_url); @@ -46,6 +46,11 @@ sub check_questionnaire : Private { $c->stash->{problem} = $problem; $c->stash->{answered_ever_reported} = $problem->user->answered_ever_reported; + $c->stash->{been_fixed} = $c->get_param('been_fixed') || ''; + + # In case they've already visited the questionnaire page, so take what was stored then + my $old_state = $c->stash->{old_state} = $questionnaire->old_state || $problem->state; + $c->stash->{was_fixed} = FixMyStreet::DB::Result::Problem->fixed_states()->{$old_state}; } =head2 submit @@ -144,35 +149,51 @@ sub submit_creator_fixed : Private { return 1; } -sub submit_standard : Private { +sub record_state_change : Private { my ( $self, $c ) = @_; - $c->forward( '/tokens/load_questionnaire', [ $c->get_param('token') ] ); - $c->forward( 'check_questionnaire' ); - $c->forward( 'process_questionnaire' ); + return unless $c->stash->{been_fixed}; my $problem = $c->stash->{problem}; - my $old_state = $problem->state; + my $old_state = $c->stash->{old_state}; my $new_state = ''; - $new_state = 'fixed - user' if $c->stash->{been_fixed} eq 'Yes' && + $new_state = 'fixed - user' if $c->stash->{been_fixed} eq 'Yes' && FixMyStreet::DB::Result::Problem->open_states()->{$old_state}; $new_state = 'fixed - user' if $c->stash->{been_fixed} eq 'Yes' && FixMyStreet::DB::Result::Problem->closed_states()->{$old_state}; $new_state = 'confirmed' if $c->stash->{been_fixed} eq 'No' && FixMyStreet::DB::Result::Problem->fixed_states()->{$old_state}; + $c->stash->{new_state} = $new_state; # Record state change, if there was one if ( $new_state ) { $problem->state( $new_state ); $problem->lastupdate( \'current_timestamp' ); - } - - # If it's not fixed and they say it's still not been fixed, record time update - if ( $c->stash->{been_fixed} eq 'No' && + } elsif ($problem->state ne $old_state) { + $problem->state( $old_state ); + $problem->lastupdate( \'current_timestamp' ); + } elsif ( $c->stash->{been_fixed} eq 'No' && FixMyStreet::DB::Result::Problem->open_states->{$old_state} ) { + # If it's not fixed and they say it's still not been fixed, record time update $problem->lastupdate( \'current_timestamp' ); } + $problem->update; + $c->stash->{questionnaire}->update({ + whenanswered => \'current_timestamp', + old_state => $old_state, + new_state => $c->stash->{been_fixed} eq 'Unknown' ? 'unknown' : ($new_state || $old_state), + }); +} + +sub submit_standard : Private { + my ( $self, $c ) = @_; + + $c->forward( '/tokens/load_questionnaire', [ $c->get_param('token') ] ); + $c->forward( 'check_questionnaire' ); + $c->forward( 'process_questionnaire' ); + $c->forward( 'record_state_change' ); + # Record questionnaire response my $reported = undef; $reported = 1 if $c->stash->{reported} eq 'Yes'; @@ -180,14 +201,13 @@ sub submit_standard : Private { my $q = $c->stash->{questionnaire}; $q->update( { - whenanswered => \'current_timestamp', ever_reported => $reported, - old_state => $old_state, - new_state => $c->stash->{been_fixed} eq 'Unknown' ? 'unknown' : ($new_state || $old_state), } ); + my $problem = $c->stash->{problem}; + # Record an update if they've given one, or if there's a state change - if ( $new_state || $c->stash->{update} ) { + if ( $c->stash->{new_state} || $c->stash->{update} ) { my $update = $c->stash->{update} || _('Questionnaire filled in by problem reporter'); $update = $c->model('DB::Comment')->new( { @@ -196,8 +216,8 @@ sub submit_standard : Private { user => $problem->user, text => $update, state => 'confirmed', - mark_fixed => $new_state eq 'fixed - user' ? 1 : 0, - mark_open => $new_state eq 'confirmed' ? 1 : 0, + mark_fixed => $c->stash->{new_state} eq 'fixed - user' ? 1 : 0, + mark_open => $c->stash->{new_state} eq 'confirmed' ? 1 : 0, lang => $c->stash->{lang_code}, cobrand => $c->cobrand->moniker, cobrand_data => '', @@ -205,6 +225,7 @@ sub submit_standard : Private { anonymous => $problem->anonymous, } ); + $update->set_extra_metadata( questionnaire_id => $q->id ); if ( my $fileid = $c->stash->{upload_fileid} ) { $update->photo( $fileid ); } @@ -216,14 +237,13 @@ sub submit_standard : Private { if ($c->stash->{been_fixed} eq 'No' || $c->stash->{been_fixed} eq 'Unknown') && $c->stash->{another} eq 'Yes'; $problem->update; - $c->stash->{new_state} = $new_state; $c->stash->{template} = 'questionnaire/completed.html'; } sub process_questionnaire : Private { my ( $self, $c ) = @_; - map { $c->stash->{$_} = $c->get_param($_) || '' } qw(been_fixed reported another update); + map { $c->stash->{$_} = $c->get_param($_) || '' } qw(reported another update); $c->stash->{update} = Utils::cleanup_text($c->stash->{update}, { allow_multiline => 1 }); @@ -240,7 +260,7 @@ sub process_questionnaire : Private { if ($c->stash->{been_fixed} eq 'No' || $c->stash->{been_fixed} eq 'Unknown') && !$c->stash->{another}; push @errors, _('Please provide some explanation as to why you\'re reopening this report') - if $c->stash->{been_fixed} eq 'No' && $c->stash->{problem}->is_fixed() && !$c->stash->{update}; + if $c->stash->{been_fixed} eq 'No' && $c->stash->{was_fixed} && !$c->stash->{update}; $c->forward('/photo/process_photo'); push @errors, $c->stash->{photo_error} @@ -258,7 +278,8 @@ sub process_questionnaire : Private { # Sent here from email token action. Simply load and display questionnaire. sub show : Private { my ( $self, $c ) = @_; - $c->forward( 'check_questionnaire' ); + $c->forward( 'check_questionnaire', [ 'unanswered' ] ); + $c->forward( 'record_state_change' ); $c->forward( 'display' ); } diff --git a/perllib/FixMyStreet/App/Controller/Report.pm b/perllib/FixMyStreet/App/Controller/Report.pm index b1cc5885a..799985f8e 100644 --- a/perllib/FixMyStreet/App/Controller/Report.pm +++ b/perllib/FixMyStreet/App/Controller/Report.pm @@ -76,7 +76,7 @@ sub _display : Private { $c->forward( 'load_updates' ); $c->forward( 'format_problem_for_display' ); - my $permissions = $c->stash->{_permissions} = $c->forward( 'check_has_permission_to', + my $permissions = $c->stash->{_permissions} ||= $c->forward( 'check_has_permission_to', [ qw/report_inspect report_edit_category report_edit_priority/ ] ); if (any { $_ } values %$permissions) { $c->stash->{template} = 'report/inspect.html'; @@ -121,14 +121,17 @@ sub load_problem_or_display_error : Private { $c->detach( '/page_error_404_not_found', [ _('Unknown problem ID') ] ) unless $c->cobrand->show_unconfirmed_reports ; } - elsif ( $problem->hidden_states->{ $problem->state } or - (($problem->get_extra_metadata('closure_status')||'') eq 'hidden')) { + elsif ( $problem->hidden_states->{ $problem->state } ) { $c->detach( '/page_error_410_gone', [ _('That report has been removed from FixMyStreet.') ] # ); } elsif ( $problem->non_public ) { - if ( !$c->user || $c->user->id != $problem->user->id ) { + # Creator, and inspection users can see non_public reports + $c->stash->{problem} = $problem; + my $permissions = $c->stash->{_permissions} = $c->forward( 'check_has_permission_to', + [ qw/report_inspect report_edit_category report_edit_priority/ ] ); + if ( !$c->user || ($c->user->id != $problem->user->id && !$permissions->{report_inspect}) ) { $c->detach( '/page_error_403_access_denied', [ sprintf(_('That report cannot be viewed on %s.'), $c->stash->{site_name}) ] @@ -163,17 +166,28 @@ sub load_updates : Private { { problem_id => $c->stash->{problem}->id, whenanswered => { '!=', undef }, - old_state => 'confirmed', new_state => 'confirmed', + old_state => [ -and => + { -in => [ FixMyStreet::DB::Result::Problem::closed_states, FixMyStreet::DB::Result::Problem::open_states ] }, + \'= new_state', + ] }, { order_by => 'whenanswered' } ); my @combined; + my %questionnaires_with_updates; while (my $update = $updates->next) { push @combined, [ $update->confirmed, $update ]; + if (my $qid = $update->get_extra_metadata('questionnaire_id')) { + $questionnaires_with_updates{$qid} = $update; + } } - while (my $update = $questionnaires->next) { - push @combined, [ $update->whenanswered, $update ]; + while (my $q = $questionnaires->next) { + if (my $update = $questionnaires_with_updates{$q->id}) { + $update->set_extra_metadata('open_from_questionnaire', 1); + next; + } + push @combined, [ $q->whenanswered, $q ]; } @combined = map { $_->[1] } sort { $a->[0] <=> $b->[0] } @combined; $c->stash->{updates} = \@combined; @@ -207,9 +221,15 @@ sub format_problem_for_display : Private { if ( $c->stash->{ajax} ) { $c->res->content_type('application/json; charset=utf-8'); + + # encode_json doesn't like DateTime objects, so strip them out + my $report_hashref = $c->cobrand->problem_as_hashref( $problem, $c ); + delete $report_hashref->{created}; + delete $report_hashref->{confirmed}; + my $content = encode_json( { - report => $c->cobrand->problem_as_hashref( $problem, $c ), + report => $report_hashref, updates => $c->cobrand->updates_as_hashref( $problem, $c ), } ); @@ -337,6 +357,8 @@ sub inspect : Private { my %update_params = (); if ($permissions->{report_inspect}) { + $problem->non_public($c->get_param('non_public') ? 1 : 0); + $problem->set_extra_metadata( traffic_information => $c->get_param('traffic_information') ); if ( my $info = $c->get_param('detailed_information') ) { @@ -380,7 +402,7 @@ sub inspect : Private { if ( $problem->state eq 'duplicate') { if (my $duplicate_of = $c->get_param('duplicate_of')) { $problem->set_duplicate_of($duplicate_of); - } elsif (not $c->get_param('public_update')) { + } elsif (not $c->get_param('include_update')) { $valid = 0; push @{ $c->stash->{errors} }, _('Please provide a duplicate ID or public update for this report.'); } @@ -404,7 +426,7 @@ sub inspect : Private { cobrand_data => $problem->cobrand_data, lang => $problem->lang, }; - $problem->user->create_alert($problem->id, $options); + $c->user->create_alert($problem->id, $options); } # If the state has been changed to action scheduled and they've said @@ -450,22 +472,30 @@ sub inspect : Private { } $problem->lastupdate( \'current_timestamp' ); $problem->update; - my $timestamp = \'current_timestamp'; - if (my $saved_at = $c->get_param('saved_at')) { - $timestamp = DateTime->from_epoch( epoch => $saved_at ); + if ($update_text || %update_params) { + my $timestamp = \'current_timestamp'; + if (my $saved_at = $c->get_param('saved_at')) { + # this comes in as a UTC epoch but the database expects everything + # to have the FMS timezone so we need to add the timezone otherwise + # dates come back out the database at time +/- timezone offset. + $timestamp = DateTime->from_epoch( + time_zone => FixMyStreet->time_zone || FixMyStreet->local_time_zone, + epoch => $saved_at + ); + } + my $name = $c->user->from_body ? $c->user->from_body->name : $c->user->name; + $problem->add_to_comments( { + text => $update_text, + created => $timestamp, + confirmed => $timestamp, + user_id => $c->user->id, + name => $name, + state => 'confirmed', + mark_fixed => 0, + anonymous => 0, + %update_params, + } ); } - my $name = $c->user->from_body ? $c->user->from_body->name : $c->user->name; - $problem->add_to_comments( { - text => $update_text, - created => $timestamp, - confirmed => $timestamp, - user_id => $c->user->id, - name => $name, - state => 'confirmed', - mark_fixed => 0, - anonymous => 0, - %update_params, - } ); my $redirect_uri; $problem->discard_changes; @@ -523,8 +553,10 @@ sub nearby_json : Private { # This is for the list template, this is a list on that page. $c->stash->{page} = 'report'; + my $extra_params = $c->cobrand->call_hook('display_location_extra_params'); + my $nearby = $c->model('DB::Nearby')->nearby( - $c, $dist, [ $p->id ], 5, $p->latitude, $p->longitude, [ $p->category ], undef + $c, $dist, [ $p->id ], 5, $p->latitude, $p->longitude, [ $p->category ], undef, $extra_params ); # Want to treat these as if they were on map $nearby = [ map { $_->problem } @$nearby ]; @@ -536,8 +568,8 @@ sub nearby_json : Private { } @$nearby; my $list_html = $c->render_fragment( - 'around/on_map_list_items.html', - { around_map => [], on_map => $nearby } + 'report/nearby.html', + { reports => $nearby } ); my $json = { pins => \@pins }; diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm index f9e07dd41..331067c1a 100644 --- a/perllib/FixMyStreet/App/Controller/Report/New.pm +++ b/perllib/FixMyStreet/App/Controller/Report/New.pm @@ -632,6 +632,8 @@ sub setup_categories_and_bodies : Private { my %bodies_to_list = (); # Bodies with categories assigned my @category_options = (); # categories to show my %category_extras = (); # extra fields to fill in for open311 + my %category_extras_hidden = + (); # whether all of a category's fields are hidden my %non_public_categories = (); # categories for which the reports are not public $c->stash->{unresponsive} = {}; @@ -664,10 +666,11 @@ sub setup_categories_and_bodies : Private { $bodies_to_list{ $contact->body_id } = $contact->body; unless ( $seen{$contact->category} ) { - push @category_options, { name => $contact->category, value => $contact->category_display, group => $contact->get_extra_metadata('group') || '' }; + push @category_options, $contact; my $metas = $contact->get_metadata_for_input; $category_extras{$contact->category} = $metas if @$metas; + $category_extras_hidden{$contact->category} = (grep { !$c->cobrand->category_extra_hidden($_) } @$metas) ? 0 : 1; my $body_send_method = $bodies{$contact->body_id}->send_method || ''; $c->stash->{unresponsive}{$contact->category} = $contact->body_id @@ -676,15 +679,15 @@ sub setup_categories_and_bodies : Private { $non_public_categories{ $contact->category } = 1 if $contact->non_public; } - $seen{$contact->category} = $contact->category_display; + $seen{$contact->category} = $contact; } if (@category_options) { # If there's an Other category present, put it at the bottom @category_options = ( - { name => _('-- Pick a category --'), value => _('-- Pick a category --'), group => '' }, - grep { $_->{name} ne _('Other') } @category_options ); - push @category_options, { name => _('Other'), value => $seen{_('Other')}, group => _('Other') } if $seen{_('Other')}; + { category => _('-- Pick a category --'), category_display => _('-- Pick a category --'), group => '' }, + grep { $_->category ne _('Other') } @category_options ); + push @category_options, $seen{_('Other')} if $seen{_('Other')}; } $c->cobrand->call_hook(munge_category_list => \@category_options, \@contacts, \%category_extras); @@ -692,11 +695,10 @@ sub setup_categories_and_bodies : Private { # put results onto stash for display $c->stash->{bodies} = \%bodies; $c->stash->{contacts} = \@contacts; - $c->stash->{bodies_to_list} = [ keys %bodies_to_list ]; - $c->stash->{bodies_to_list_names} = [ map { $_->name } values %bodies_to_list ]; - $c->stash->{bodies_to_list_urls} = [ map { $_->external_url } values %bodies_to_list ]; + $c->stash->{bodies_to_list} = \%bodies_to_list; $c->stash->{category_options} = \@category_options; $c->stash->{category_extras} = \%category_extras; + $c->stash->{category_extras_hidden} = \%category_extras_hidden; $c->stash->{non_public_categories} = \%non_public_categories; $c->stash->{extra_name_info} = $first_area->{id} == COUNCIL_ID_BROMLEY ? 1 : 0; @@ -709,7 +711,8 @@ sub setup_categories_and_bodies : Private { if ( $c->cobrand->call_hook('enable_category_groups') ) { my %category_groups = (); for my $category (@category_options) { - push @{$category_groups{$category->{group}}}, $category; + my $group = $category->{group} // $category->get_extra_metadata('group') // ''; + push @{$category_groups{$group}}, $category; } my @category_groups = (); @@ -827,7 +830,7 @@ sub process_user : Private { $c->forward('update_user', [ \%params ]); if ($params{password_register}) { $c->forward('/auth/test_password', [ $params{password_register} ]); - $report->user->password(Utils::trim_text($params{password_register})); + $report->user->password($params{password_register}); } return 1; @@ -870,6 +873,8 @@ sub process_report : Private { 'subcategory', # 'partial', # 'service', # + 'non_public', + 'single_body_only' ); # load the report @@ -897,6 +902,8 @@ sub process_report : Private { $report->anonymous( $params{may_show_name} ? 0 : 1 ); } + $report->non_public($params{non_public} ? 1 : 0); + # clean up text before setting $report->title( Utils::cleanup_text( $params{title} ) ); @@ -912,6 +919,7 @@ sub process_report : Private { # set these straight from the params $report->category( _ $params{category} ) if $params{category}; + $c->cobrand->call_hook(report_new_munge_category => $report); $report->subcategory( $params{subcategory} ); my $areas = $c->stash->{all_areas_mapit}; @@ -925,7 +933,7 @@ sub process_report : Private { return 1; } - my $bodies = $c->forward('contacts_to_bodies', [ $report->category ]); + my $bodies = $c->forward('contacts_to_bodies', [ $report->category, $params{single_body_only} ]); my $body_string = join(',', map { $_->id } @$bodies) || '-1'; $report->bodies_str($body_string); @@ -940,7 +948,7 @@ sub process_report : Private { if ( $c->stash->{non_public_categories}->{ $report->category } ) { $report->non_public( 1 ); } - } elsif ( @{ $c->stash->{bodies_to_list} } ) { + } elsif ( %{ $c->stash->{bodies_to_list} } ) { # There was an area with categories, but we've not been given one. Bail. $c->stash->{field_errors}->{category} = _('Please choose a category'); @@ -959,11 +967,19 @@ sub process_report : Private { my $value = $c->get_param($form_name) || ''; $c->stash->{field_errors}->{$form_name} = _('This information is required') if $field->{required} && !$value; + if ($field->{validator}) { + eval { + $value = $field->{validator}->($value); + }; + if ($@) { + $c->stash->{field_errors}->{$form_name} = $@; + } + } $report->set_extra_metadata( $form_name => $value ); } # set defaults that make sense - $report->state('unconfirmed'); + $report->state($c->cobrand->default_problem_state); # save the cobrand and language related information $report->cobrand( $c->cobrand->moniker ); @@ -974,10 +990,18 @@ sub process_report : Private { } sub contacts_to_bodies : Private { - my ($self, $c, $category) = @_; + my ($self, $c, $category, $single_body_only) = @_; my @contacts = grep { $_->category eq $category } @{$c->stash->{contacts}}; + # check that we've not indicated we only want to sent to a single body + # and if we find a matching one then only send to that. e.g. if we clicked + # on a TfL road on the map. + if ($single_body_only) { + my @contacts_filtered = grep { $_->body->name eq $single_body_only } @contacts; + @contacts = @contacts_filtered if scalar @contacts_filtered; + } + if ($c->stash->{unresponsive}{$category} || $c->stash->{unresponsive}{ALL} || !@contacts) { []; } else { @@ -999,7 +1023,7 @@ sub set_report_extras : Private { foreach my $contact (@$contacts) { my $metas = $contact->get_metadata_for_input; foreach my $field ( @$metas ) { - if ( lc( $field->{required} ) eq 'true' && !$c->cobrand->category_extra_hidden($field->{code})) { + if ( lc( $field->{required} ) eq 'true' && !$c->cobrand->category_extra_hidden($field)) { unless ( $c->get_param($param_prefix . $field->{code}) ) { $c->stash->{field_errors}->{ $field->{code} } = _('This information is required'); } @@ -1016,7 +1040,7 @@ sub set_report_extras : Private { my $metas = $extra_fields->get_extra_fields; $param_prefix = "extra[" . $extra_fields->id . "]"; foreach my $field ( @$metas ) { - if ( lc( $field->{required} ) eq 'true' && !$c->cobrand->category_extra_hidden($field->{code})) { + if ( lc( $field->{required} ) eq 'true' && !$c->cobrand->category_extra_hidden($field)) { unless ( $c->get_param($param_prefix . $field->{code}) ) { $c->stash->{field_errors}->{ $field->{code} } = _('This information is required'); } @@ -1296,9 +1320,17 @@ sub save_user_and_report : Private { if ( $c->cobrand->never_confirm_reports ) { $report->user->update_or_insert; $report->confirm(); - } elsif ( $c->forward('created_as_someone_else', [ $c->stash->{bodies} ]) ) { - # If created on behalf of someone else, we automatically confirm it, - # but we don't want to update the user account + # If created on behalf of someone else, we automatically confirm it, + # but we don't want to update the user account + } elsif ($c->stash->{contributing_as_another_user}) { + $report->set_extra_metadata( contributed_as => 'another_user'); + $report->set_extra_metadata( contributed_by => $c->user->id ); + $report->confirm(); + } elsif ($c->stash->{contributing_as_body}) { + $report->set_extra_metadata( contributed_as => 'body' ); + $report->confirm(); + } elsif ($c->stash->{contributing_as_anonymous_user}) { + $report->set_extra_metadata( contributed_as => 'anonymous_user' ); $report->confirm(); } elsif ( !$report->user->in_storage ) { # User does not exist. @@ -1328,6 +1360,8 @@ sub save_user_and_report : Private { $c->log->info($report->user->id . ' exists, but is not logged in for this report'); } + $c->cobrand->call_hook(report_new_munge_before_insert => $report); + $report->update_or_insert; # tidy up @@ -1338,11 +1372,6 @@ sub save_user_and_report : Private { return 1; } -sub created_as_someone_else : Private { - my ($self, $c, $bodies) = @_; - return $c->stash->{contributing_as_another_user} || $c->stash->{contributing_as_body} || $c->stash->{contributing_as_anonymous_user}; -} - =head2 generate_map Add the html needed to for the map to the stash. diff --git a/perllib/FixMyStreet/App/Controller/Report/Update.pm b/perllib/FixMyStreet/App/Controller/Report/Update.pm index 99eae8659..4a5b8db5d 100644 --- a/perllib/FixMyStreet/App/Controller/Report/Update.pm +++ b/perllib/FixMyStreet/App/Controller/Report/Update.pm @@ -156,7 +156,7 @@ sub process_user : Private { if ($params{password_register}) { $c->forward('/auth/test_password', [ $params{password_register} ]); - $update->user->password(Utils::trim_text($params{password_register})); + $update->user->password($params{password_register}); } return 1; @@ -240,6 +240,7 @@ This makes sure we only proceed to processing if we've had the form submitted sub check_form_submitted : Private { my ( $self, $c ) = @_; + return if $c->stash->{problem}->get_extra_metadata('closed_updates'); return $c->get_param('submit_update') || ''; } @@ -444,9 +445,17 @@ sub save_update : Private { if ( $c->cobrand->never_confirm_updates ) { $update->user->update_or_insert; $update->confirm(); - } elsif ( $c->forward('/report/new/created_as_someone_else', [ $update->problem->bodies_str ]) ) { - # If created on behalf of someone else, we automatically confirm it, - # but we don't want to update the user account + # If created on behalf of someone else, we automatically confirm it, + # but we don't want to update the user account + } elsif ($c->stash->{contributing_as_another_user}) { + $update->set_extra_metadata( contributed_as => 'another_user'); + $update->set_extra_metadata( contributed_by => $c->user->id ); + $update->confirm(); + } elsif ($c->stash->{contributing_as_body}) { + $update->set_extra_metadata( contributed_as => 'body' ); + $update->confirm(); + } elsif ($c->stash->{contributing_as_anonymous_user}) { + $update->set_extra_metadata( contributed_as => 'anonymous_user' ); $update->confirm(); } elsif ( !$update->user->in_storage ) { # User does not exist. diff --git a/perllib/FixMyStreet/App/Controller/Reports.pm b/perllib/FixMyStreet/App/Controller/Reports.pm index 7c3796c42..dc9e2c913 100644 --- a/perllib/FixMyStreet/App/Controller/Reports.pm +++ b/perllib/FixMyStreet/App/Controller/Reports.pm @@ -93,6 +93,7 @@ sub index : Path : Args(0) { } } else { my @bodies = $c->model('DB::Body')->active->translated->with_area_count->all_sorted; + @bodies = @{$c->cobrand->call_hook('reports_hook_restrict_bodies_list', \@bodies) || \@bodies }; $c->stash->{bodies} = \@bodies; } @@ -125,6 +126,17 @@ sub ward : Path : Args(2) { my @wards = split /\|/, $ward || ""; $c->forward( 'body_check', [ $body ] ); + # If viewing multiple wards, rewrite the url from + # /reports/Borsetshire?ward=North&ward=East + # to + # /reports/Borsetshire/North|East + my @ward_params = $c->get_param_list('ward'); + if ( @ward_params ) { + $c->stash->{wards} = [ map { { name => $_ } } (@wards, @ward_params) ]; + delete $c->req->params->{ward}; + $c->detach("redirect_body"); + } + my $body_short = $c->cobrand->short_name( $c->stash->{body} ); $c->stash->{body_url} = '/reports/' . $body_short; @@ -155,11 +167,10 @@ sub ward : Path : Args(2) { $c->stash->{stats} = $c->cobrand->get_report_stats(); my @categories = $c->stash->{body}->contacts->not_deleted->search( undef, { - columns => [ 'category' ], + columns => [ 'category', 'extra' ], distinct => 1, order_by => [ 'category' ], } )->all; - @categories = map { { name => $_->category, value => $_->category_display } } @categories; $c->stash->{filter_categories} = \@categories; $c->stash->{filter_category} = { map { $_ => 1 } $c->get_param_list('filter_category', 1) }; @@ -413,6 +424,8 @@ sub summary : Private { my ($self, $c) = @_; my $dashboard = $c->forward('load_dashboard_data'); + $c->log->info($c->user->email . ' viewed ' . $c->req->uri->path_query) if $c->user_exists; + eval { my $data = path(FixMyStreet->path_to('../data/all-reports-dashboard.json'))->slurp_utf8; $data = decode_json($data); @@ -487,8 +500,8 @@ sub export_summary_csv : Private { 'id', 'title', 'category', - 'created_pp', - 'confirmed_pp', + 'created', + 'confirmed', 'state', 'latitude', 'longitude', 'postcode', @@ -544,20 +557,27 @@ sub load_and_group_problems : Private { my $states = $c->stash->{filter_problem_states}; my $where = { - non_public => 0, state => [ keys %$states ] }; + + my $body = $c->stash->{body}; # Might be undef + + if ($c->user_exists && ($c->user->is_superuser || ($body && $c->user->has_permission_to('report_inspect', $body->id)))) { + # See all reports, no restriction + } else { + $where->{non_public} = 0; + } + my $filter = { order_by => $c->stash->{sort_order}, rows => $c->cobrand->reports_per_page, }; - if ($c->user_exists && $c->stash->{body}) { - my $bid = $c->stash->{body}->id; + if ($c->user_exists && $body) { my $prefetch = []; - if ($c->user->has_permission_to('planned_reports', $bid)) { + if ($c->user->has_permission_to('planned_reports', $body->id)) { push @$prefetch, 'user_planned_reports'; } - if ($c->user->has_permission_to('report_edit_priority', $bid) || $c->user->has_permission_to('report_inspect', $bid)) { + if ($c->user->has_permission_to('report_edit_priority', $body->id) || $c->user->has_permission_to('report_inspect', $body->id)) { push @$prefetch, 'response_priority'; } $prefetch = $prefetch->[0] if @$prefetch == 1; @@ -589,9 +609,9 @@ sub load_and_group_problems : Private { $where->{areas} = [ map { { 'like', '%,' . $_->{id} . ',%' } } @{$c->stash->{wards}} ]; - $problems = $problems->to_body($c->stash->{body}); - } elsif ($c->stash->{body}) { - $problems = $problems->to_body($c->stash->{body}); + $problems = $problems->to_body($body); + } elsif ($body) { + $problems = $problems->to_body($body); } if (my $bbox = $c->get_param('bbox')) { @@ -609,7 +629,7 @@ sub load_and_group_problems : Private { my ( %problems, @pins ); while ( my $problem = $problems->next ) { - if ( !$c->stash->{body} ) { + if ( !$body ) { add_row( $c, $problem, 0, \%problems, \@pins ); next; } @@ -623,7 +643,7 @@ sub load_and_group_problems : Private { # Add to bodies it was sent to my $bodies = $problem->bodies_str_ids; foreach ( @$bodies ) { - next if $_ != $c->stash->{body}->id; + next if $_ != $body->id; add_row( $c, $problem, $_, \%problems, \@pins ); } } @@ -697,9 +717,9 @@ sub stash_report_filter_status : Private { $filter_status{unshortlisted} = 1; } - if ($c->user and ($c->user->is_superuser or ( - $c->stash->{body} and $c->user->belongs_to_body($c->stash->{body}->id) - ))) { + my $body_user = $c->user_exists && $c->stash->{body} && $c->user->belongs_to_body($c->stash->{body}->id); + my $staff_user = $c->user_exists && ($c->user->is_superuser || $body_user); + if ($staff_user || $c->cobrand->call_hook('filter_show_all_states')) { $c->stash->{filter_states} = $c->cobrand->state_groups_inspect; foreach my $state (FixMyStreet::DB::Result::Problem->visible_states()) { if ($status{$state}) { @@ -727,6 +747,7 @@ sub stash_report_sort : Private { created => 'confirmed', comments => 'comment_count', ); + $types{created} = 'created' if $c->cobrand->moniker eq 'zurich'; my $sort = $c->get_param('sort') || $default; $sort = $default unless $sort =~ /^((updated|created)-(desc|asc)|comments-desc|shortlist)$/; diff --git a/perllib/FixMyStreet/Cobrand/BathNES.pm b/perllib/FixMyStreet/Cobrand/BathNES.pm new file mode 100644 index 000000000..81080bed9 --- /dev/null +++ b/perllib/FixMyStreet/Cobrand/BathNES.pm @@ -0,0 +1,260 @@ +package FixMyStreet::Cobrand::BathNES; +use parent 'FixMyStreet::Cobrand::Whitelabel'; + +use strict; +use warnings; + +use LWP::Simple; +use URI; +use Try::Tiny; +use JSON::MaybeXS; + +sub council_area_id { return 2551; } +sub council_area { return 'Bath and North East Somerset'; } +sub council_name { return 'Bath and North East Somerset Council'; } +sub council_url { return 'bathnes'; } + +sub contact_email { + my $self = shift; + return join( '@', 'councilconnect_rejections', 'bathnes.gov.uk' ); +} + +sub update_email { + my $self = shift; + return join( '@', 'highways', 'bathnes.gov.uk' ); +} + +sub admin_user_domain { 'bathnes.gov.uk' } + +sub base_url { + my $self = shift; + return $self->next::method() if FixMyStreet->config('STAGING_SITE'); + return 'https://fix.bathnes.gov.uk'; +} + +sub map_type { 'BathNES' } + +sub example_places { + return ( 'BA1 1JQ', "Lansdown Grove" ); +} + +sub get_geocoder { + return 'OSM'; # default of Bing gives poor results, let's try overriding. +} + +sub disambiguate_location { + my $self = shift; + my $string = shift; + + my $town = 'Bath and North East Somerset'; + + # The council have provided a list of common typos which we should correct: + my %replacements = ( + "broom" => "brougham", + "carnarvon" => "caernarvon", + "cornation" => "coronation", + "beafort" => "beaufort", + "beechan" => "beechen", + "malreword" => "malreward", + "canyerberry"=> "canterbury", + "clairemont"=> "claremont", + "salsbury"=> "salisbury", + "solsberry"=> "solsbury", + "lawn road" => "lorne", + "new road high littleton" => "danis house", + ); + + foreach my $original (keys %replacements) { + my $replacement = $replacements{$original}; + $string =~ s/$original/$replacement/ig; + } + + return { + %{ $self->SUPER::disambiguate_location() }, + town => $town, + centre => '51.3559192103294,-2.47522827137605', + span => '0.166437921041471,0.429359043406088', + bounds => [ 51.2730478766607, -2.70792015294201, 51.4394857977022, -2.27856110953593 ], + string => $string, + }; +} + +sub pin_colour { + my ( $self, $p, $context ) = @_; + return 'grey' if $p->state eq 'not responsible'; + return 'green' if $p->is_fixed || $p->is_closed; + return 'red' if $p->state eq 'confirmed'; + return 'yellow'; +} + +sub send_questionnaires { 0 } + +sub enable_category_groups { 1 } + +sub default_show_name { 0 } + +sub default_map_zoom { 3 } + +sub map_js_extra { + my ($self, $c) = @_; + + return unless $c->user_exists; + + my $banes_user = $c->user->from_body && $c->user->from_body->areas->{$self->council_area_id}; + if ( $banes_user || $c->user->is_superuser ) { + return ['/cobrands/bathnes/staff.js']; + } +} + +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 open311_config { + my ($self, $row, $h, $params) = @_; + + my $extra = $row->get_extra_fields; + push @$extra, + { 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 USRN + # 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 $usrn = $self->lookup_usrn($row)) { + push @$extra, + { name => 'site_code', + value => $usrn }; + } + } + + $row->set_extra_fields(@$extra); +} + +sub available_permissions { + my $self = shift; + + my $permissions = $self->SUPER::available_permissions(); + + $permissions->{Problems}->{report_reject} = "Reject reports"; + $permissions->{Dashboard}->{export_extra_columns} = "Extra columns in CSV export"; + + return $permissions; +} + +sub report_sent_confirmation_email { 1 } + +sub lookup_usrn { + my $self = shift; + my $row = shift; + + my $buffer = 5; # metres + my ($x, $y) = $row->local_coords; + my ($w, $s, $e, $n) = ($x-$buffer, $y-$buffer, $x+$buffer, $y+$buffer); + + my $uri = URI->new("https://isharemaps.bathnes.gov.uk/getows.ashx"); + $uri->query_form( + REQUEST => "GetFeature", + SERVICE => "WFS", + SRSNAME => "urn:ogc:def:crs:EPSG::27700", + TYPENAME => "AdoptedHighways", + VERSION => "1.1.0", + mapsource => "BathNES/WFS", + outputformat => "application/json", + BBOX => "$w,$s,$e,$n" + ); + + my $response = get($uri); + + my $j = JSON->new->utf8->allow_nonref; + try { + $j = $j->decode($response); + return $j->{features}->[0]->{properties}->{usrn}; + } catch { + # There was either no asset found, or an error with the WFS + # call - in either case let's just proceed without the USRN. + return; + } + +} + +sub enter_postcode_text { + my ($self) = @_; + return 'Enter a location in ' . $self->council_area; +} + +sub categories_restriction { + my ($self, $rs) = @_; + # Categories covering BANES have a mixture of Open311 and Email + # send methods. BANES only want specific categories to be visible on their + # cobrand, not the email categories from FMS.com. + # The FMS.com categories have a devolved send_method set to Email, so we can + # filter these out. + # NB. BANES have a 'Street Light Fault' category that has its + # send_method set to 'Email::BathNES' (to use a custom template) which must + # be show on the cobrand. + return $rs->search( { -or => [ + 'me.send_method' => undef, # Open311 categories + 'me.send_method' => '', # Open311 categories that have been edited in the admin + 'me.send_method' => 'Email::BathNES', # Street Light Fault + ] } ); +} + +sub dashboard_export_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", + "Reported As", + "Staff User", + "Attribute Data", + "Site Used", + ]; + + $c->stash->{csv}->{columns} = [ + @{ $c->stash->{csv}->{columns} }, + "user_email", + "user_phone", + "reported_as", + "staff_user", + "attribute_data", + "site_used", + ]; + + $c->stash->{csv}->{extra_data} = sub { + my $report = shift; + + my $reported_as = $report->get_extra_metadata('contributed_as') || ''; + my $staff_user = ''; + if ( my $contributed_by = $report->get_extra_metadata('contributed_by') ) { + $staff_user = $c->model('DB::User')->find({ id => $contributed_by })->email; + } + my $site_used = $report->service || $report->cobrand || ''; + my $attribute_data = join "; ", map { $_->{name} . " = " . $_->{value} } @{ $report->get_extra_fields }; + return { + user_email => $report->user->email || '', + user_phone => $report->user->phone || '', + reported_as => $reported_as, + staff_user => $staff_user, + attribute_data => $attribute_data, + site_used => $site_used, + }; + }; +} + +1; diff --git a/perllib/FixMyStreet/Cobrand/Bristol.pm b/perllib/FixMyStreet/Cobrand/Bristol.pm index 4648802bd..25dc5ab0a 100644 --- a/perllib/FixMyStreet/Cobrand/Bristol.pm +++ b/perllib/FixMyStreet/Cobrand/Bristol.pm @@ -20,7 +20,7 @@ sub example_places { } sub map_type { - 'OSM'; + 'Bristol'; } sub default_link_zoom { 6 } @@ -71,4 +71,10 @@ sub categories_restriction { return $rs->search( { 'me.send_method' => undef } ); } +sub open311_config { + my ($self, $row, $h, $params) = @_; + + $params->{always_send_email} = 1; +} + 1; diff --git a/perllib/FixMyStreet/Cobrand/Bromley.pm b/perllib/FixMyStreet/Cobrand/Bromley.pm index 2f47225a7..5d14d0b01 100644 --- a/perllib/FixMyStreet/Cobrand/Bromley.pm +++ b/perllib/FixMyStreet/Cobrand/Bromley.pm @@ -16,6 +16,8 @@ sub base_url { return 'https://fix.bromley.gov.uk'; } +sub default_show_name { 0 } + sub disambiguate_location { my $self = shift; my $string = shift; @@ -112,11 +114,19 @@ sub open311_config { my ($self, $row, $h, $params) = @_; my $extra = $row->get_extra_fields; + my $title = $row->title; + + foreach (@$extra) { + $title .= ' | ID: ' . $_->{value} if $_->{name} eq 'feature_id'; + $title .= ' | PROW ID: ' . $_->{value} if $_->{name} eq 'prow_reference'; + } + @$extra = grep { $_->{name} !~ /feature_id|prow_reference/ } @$extra; + push @$extra, { name => 'report_url', value => $h->{url} }, { name => 'report_title', - value => $row->title }, + value => $title }, { name => 'public_anonymity_required', value => $row->anonymous ? 'TRUE' : 'FALSE' }, { name => 'email_alerts_requested', diff --git a/perllib/FixMyStreet/Cobrand/Buckinghamshire.pm b/perllib/FixMyStreet/Cobrand/Buckinghamshire.pm new file mode 100644 index 000000000..a5e45d5a9 --- /dev/null +++ b/perllib/FixMyStreet/Cobrand/Buckinghamshire.pm @@ -0,0 +1,402 @@ +package FixMyStreet::Cobrand::Buckinghamshire; +use parent 'FixMyStreet::Cobrand::UKCouncils'; + +use strict; +use warnings; + +use LWP::Simple; +use URI; +use Try::Tiny; +use JSON::MaybeXS; + +sub council_area_id { return 2217; } +sub council_area { return 'Buckinghamshire'; } +sub council_name { return 'Buckinghamshire County Council'; } +sub council_url { return 'buckinghamshire'; } + +sub example_places { + return ( 'HP19 7QF', "Walton Road" ); +} + +sub base_url { + my $self = shift; + return $self->next::method() if FixMyStreet->config('STAGING_SITE'); + return 'https://fixmystreet.buckscc.gov.uk'; +} + +sub disambiguate_location { + my $self = shift; + my $string = shift; + + my $town = 'Buckinghamshire'; + + # The geocoder returns two results for 'Aylesbury', so force the better + # result to be used. + $town = "$town, HP20 2NH" if $string =~ /[\s]*aylesbury[\s]*/i; + + return { + %{ $self->SUPER::disambiguate_location() }, + town => $town, + centre => '51.7852948471218,-0.812140044990842', + span => '0.596065946222112,0.664092167105497', + bounds => [ 51.4854160129405, -1.1406945585036, 52.0814819591626, -0.476602391398098 ], + }; +} + +sub pin_colour { + my ( $self, $p, $context ) = @_; + return 'grey' if $p->state eq 'not responsible'; + return 'green' if $p->is_fixed || $p->is_closed; + return 'red' if $p->state eq 'confirmed'; + return 'yellow'; +} + +sub contact_email { + my $self = shift; + return join( '@', 'fixmystreetbs', 'email.buckscc.gov.uk' ); +} + +sub send_questionnaires { + return 0; +} + +sub open311_config { + my ($self, $row, $h, $params) = @_; + + my $extra = $row->get_extra_fields; + push @$extra, + { 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 }; + } + } + + $row->set_extra_fields(@$extra); +} + +sub map_type { 'Buckinghamshire' } + +sub default_map_zoom { 3 } + +sub enable_category_groups { 1 } + +# Enable adding/editing of parish councils in the admin +sub add_extra_areas { + my ($self, $areas) = @_; + + # This is a list of all Parish Councils within Buckinghamshire, + # taken from https://mapit.mysociety.org/area/2217/covers.json?type=CPC + my $parish_ids = [ + "135493", + "135494", + "148713", + "148714", + "53319", + "53360", + "53390", + "53404", + "53453", + "53486", + "53515", + "53542", + "53612", + "53822", + "53874", + "53887", + "53942", + "53991", + "54003", + "54014", + "54158", + "54174", + "54178", + "54207", + "54289", + "54305", + "54342", + "54355", + "54402", + "54465", + "54479", + "54493", + "54590", + "54615", + "54672", + "54691", + "54721", + "54731", + "54787", + "54846", + "54879", + "54971", + "55290", + "55326", + "55534", + "55638", + "55724", + "55775", + "55896", + "55900", + "55915", + "55945", + "55973", + "56007", + "56091", + "56154", + "56268", + "56350", + "56379", + "56418", + "56432", + "56498", + "56524", + "56592", + "56609", + "56641", + "56659", + "56664", + "56709", + "56758", + "56781", + "57099", + "57138", + "57330", + "57332", + "57366", + "57367", + "57507", + "57529", + "57582", + "57585", + "57666", + "57701", + "58166", + "58208", + "58229", + "58279", + "58312", + "58333", + "58405", + "58523", + "58659", + "58815", + "58844", + "58891", + "58965", + "58980", + "59003", + "59007", + "59012", + "59067", + "59144", + "59152", + "59179", + "59211", + "59235", + "59288", + "59353", + "59491", + "59518", + "59727", + "59763", + "59971", + "60027", + "60137", + "60321", + "60322", + "60438", + "60456", + "60462", + "60532", + "60549", + "60598", + "60622", + "60640", + "60731", + "60777", + "60806", + "60860", + "60954", + "61100", + "61102", + "61107", + "61142", + "61144", + "61167", + "61172", + "61249", + "61268", + "61269", + "61405", + "61445", + "61471", + "61479", + "61898", + "61902", + "61920", + "61964", + "62226", + "62267", + "62296", + "62311", + "62321", + "62431", + "62454", + "62640", + "62657", + "62938", + "63040", + "63053", + "63068", + "63470", + "63476", + "63501", + "63507", + "63517", + "63554", + "63715", + "63723" + ]; + my $ids_string = join ",", @{ $parish_ids }; + + my $extra_areas = mySociety::MaPit::call('areas', [ $ids_string ]); + + my %all_areas = ( + %$areas, + %$extra_areas + ); + return \%all_areas; +} + +# Make sure CPC areas are included in point lookups for new reports +sub add_extra_area_types { + my ($self, $types) = @_; + + my @types = ( + @$types, + 'CPC', + ); + return \@types; +} + +sub is_two_tier { 1 } + +sub should_skip_sending_update { + my ($self, $update ) = @_; + + # Bucks don't want to receive updates into Confirm that were made by anyone + # except the original problem reporter. + return $update->user_id != $update->problem->user_id; +} + +sub disable_phone_number_entry { 1 } + +sub report_sent_confirmation_email { 1 } + +sub is_council_with_case_management { 1 } + +# Try OSM for Bucks as it provides better disamiguation descriptions. +sub get_geocoder { 'OSM' } + +sub categories_restriction { + my ($self, $rs) = @_; + # Buckinghamshire is a two-tier council, but only want to display + # county-level categories on their cobrand. + return $rs->search( { 'body.id' => 2217 } ); +} + +sub lookup_site_code { + my $self = shift; + my $row = shift; + + my $buffer = 200; # metres + my ($x, $y) = $row->local_coords; + my ($w, $s, $e, $n) = ($x-$buffer, $y-$buffer, $x+$buffer, $y+$buffer); + + my $uri = URI->new("https://tilma.mysociety.org/mapserver/bucks"); + $uri->query_form( + REQUEST => "GetFeature", + SERVICE => "WFS", + SRSNAME => "urn:ogc:def:crs:EPSG::27700", + TYPENAME => "Whole_Street", + VERSION => "1.1.0", + outputformat => "geojson", + BBOX => "$w,$s,$e,$n" + ); + + my $response = get($uri); + + my $j = JSON->new->utf8->allow_nonref; + try { + $j = $j->decode($response); + } catch { + # There was either no asset found, or an error with the WFS + # call - in either case let's just proceed without the USRN. + return ''; + }; + + # We have a list of features, and we want to find the one closest to the + # report location. + my $site_code = ''; + my $nearest; + + # There are only certain features we care about, the rest can be ignored. + my @valid_types = ( "2", "3A", "3B", "4A", "4B", "HE", "HWOA", "HWSA", "P" ); + my %valid_types = map { $_ => 1 } @valid_types; + + for my $feature ( @{ $j->{features} } ) { + my $type = $feature->{properties}->{feature_ty}; + next unless $valid_types{$type}; + + # We shouldn't receive anything aside from these two geometry types, but belt and braces. + next unless $feature->{geometry}->{type} eq 'MultiLineString' || $feature->{geometry}->{type} eq 'LineString'; + + my @coordinates = @{ $feature->{geometry}->{coordinates} }; + if ( $feature->{geometry}->{type} eq 'MultiLineString') { + # The coordinates are stored as a list of lists, so flatten 'em out + @coordinates = map { @{ $_ } } @coordinates; + } + + # If any of this feature's points are closer than those we've seen so + # far then use the site_code from this feature. + for my $coords ( @coordinates ) { + my ($fx, $fy) = @$coords; + my $distance = $self->_distance($x, $y, $fx, $fy); + if ( !defined $nearest || $distance < $nearest ) { + $site_code = $feature->{properties}->{site_code}; + $nearest = $distance; + } + } + } + + return $site_code; +} + + +=head2 _distance + +Returns the cartesian distance between two coordinates. +This is not a general-purpose distance function, it's intended for use with +fairly nearby coordinates in EPSG:27700 where a spheroid doesn't need to be +taken into account. + +=cut +sub _distance { + my ($self, $ax, $ay, $bx, $by) = @_; + return sqrt( (($ax - $bx) ** 2) + (($ay - $by) ** 2) ); +} + +1; diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm index c6ca5c56b..816c5e315 100644 --- a/perllib/FixMyStreet/Cobrand/Default.pm +++ b/perllib/FixMyStreet/Cobrand/Default.pm @@ -5,6 +5,7 @@ use strict; use warnings; use FixMyStreet; use FixMyStreet::DB; +use FixMyStreet::Geocode::Address; use FixMyStreet::Geocode::Bing; use DateTime; use List::MoreUtils 'none'; @@ -514,34 +515,32 @@ sub geocoded_string_check { return 1; } =item find_closest Used by send-reports and similar to attach nearest things to the bottom of the -report. +report. This can be called with either a hash of lat/lon or a Problem. =cut sub find_closest { - my ( $self, $problem, $as_data ) = @_; + my ($self, $data) = @_; + $data = { problem => $data } if ref $data ne 'HASH'; + + my $problem = $data->{problem}; + my $lat = $problem ? $problem->latitude : $data->{latitude}; + my $lon = $problem ? $problem->longitude : $data->{longitude}; + my $j = $problem->geocode if $problem; - my $j = $problem->geocode; if (!$j) { - $j = FixMyStreet::Geocode::Bing::reverse( $problem->latitude, $problem->longitude, + $j = FixMyStreet::Geocode::Bing::reverse( $lat, $lon, disambiguate_location()->{bing_culture} ); - # cache the bing results for use in alerts - $problem->geocode( $j ); - $problem->update; - } - - my $data = $as_data ? {} : ''; - if ($j && $j->{resourceSets}[0]{resources}[0]{name}) { - my $str = $j->{resourceSets}[0]{resources}[0]{name}; - if ($as_data) { - $data->{road} = $str; - } else { - $data .= sprintf(_("Nearest road to the pin placed on the map (automatically generated by Bing Maps): %s"), - $str) . "\n\n"; + if ($problem) { + # cache the bing results for use in alerts + $problem->geocode( $j ); + $problem->update; } } - return $data; + return FixMyStreet::Geocode::Address->new($j->{resourceSets}[0]{resources}[0]) + if $j && $j->{resourceSets}[0]{resources}[0]{name}; + return {}; } =item find_closest_address_for_rss @@ -645,6 +644,7 @@ sub admin_pages { $pages->{flagged} = [ _('Flagged'), 7 ]; $pages->{states} = [ _('States'), 8 ]; $pages->{config} = [ _('Configuration'), 9]; + $pages->{user_import} = [ undef, undef ]; }; # And some that need special permissions if ( $user->has_body_permission_to('category_edit') ) { @@ -667,6 +667,7 @@ sub admin_pages { $pages->{responsepriority_edit} = [ undef, undef ]; }; if ( $user->has_body_permission_to('user_edit') ) { + $pages->{reports} = [ _('Reports'), 2 ]; $pages->{users} = [ _('Users'), 6 ]; $pages->{user_edit} = [ undef, undef ]; } @@ -727,7 +728,7 @@ sub available_permissions { # trusted => _("Trusted to make reports that don't need to be inspected"), }, _("Users") => { - user_edit => _("Edit other users' details"), + user_edit => _("Edit users' details/search for their reports"), user_manage_permissions => _("Edit other users' permissions"), user_assign_body => _("Grant access to the admin"), user_assign_areas => _("Assign users to areas"), # future use @@ -1067,6 +1068,8 @@ sub show_unconfirmed_reports { 0; } +sub default_problem_state { 'unconfirmed' } + sub state_groups_admin { my $rs = FixMyStreet::DB->resultset("State"); my @fixed = FixMyStreet::DB::Result::Problem->fixed_states; @@ -1159,50 +1162,6 @@ sub jurisdiction_id_example { return $self->moniker; } -=item body_details_data - -Returns a list of bodies to create with ensure_body. These -are mostly just passed to ->find_or_create, but there is some -pre-processing so that you can enter: - - area_id => 123, - parent => 'Big Town', - -instead of - - body_areas => [ { area_id => 123 } ], - parent => { name => 'Big Town' }, - -For example: - - return ( - { - name => 'Big Town', - }, - { - name => 'Small town', - parent => 'Big Town', - area_id => 1234, - }, - - -=cut - -sub body_details_data { - return (); -} - -=item contact_details_data - -Returns a list of contact_data to create with setup_contacts. -See Zurich for an example. - -=cut - -sub contact_details_data { - return () -} - =item lookup_by_ref_regex Returns a regex to match postcode form input against to determine if a lookup @@ -1221,7 +1180,7 @@ Return true if an Open311 service attribute should be a hidden field. sub category_extra_hidden { my ($self, $meta) = @_; - return 0; + return 0; } =item reputation_increment_states/reputation_decrement_states diff --git a/perllib/FixMyStreet/Cobrand/FiksGataMi.pm b/perllib/FixMyStreet/Cobrand/FiksGataMi.pm index ddae3010b..4b95dfeaf 100644 --- a/perllib/FixMyStreet/Cobrand/FiksGataMi.pm +++ b/perllib/FixMyStreet/Cobrand/FiksGataMi.pm @@ -69,6 +69,7 @@ sub geocoded_string_check { sub find_closest { my ( $self, $problem ) = @_; + $problem = $problem->{problem} if ref $problem eq 'HASH'; return FixMyStreet::Geocode::OSM::closest_road_text( $self, $problem->latitude, $problem->longitude ); } diff --git a/perllib/FixMyStreet/Cobrand/FixMyStreet.pm b/perllib/FixMyStreet/Cobrand/FixMyStreet.pm index 591234877..6c826ec01 100644 --- a/perllib/FixMyStreet/Cobrand/FixMyStreet.pm +++ b/perllib/FixMyStreet/Cobrand/FixMyStreet.pm @@ -136,7 +136,7 @@ sub about_hook { $c->stash->{form_name} = $c->get_param('name') || ''; $c->stash->{email} = $c->get_param('username') || ''; if ($c->user_exists) { - my $body = _user_to_body($c); + my $body = $c->user->from_body || _user_to_body($c); if ($body) { $c->stash->{body} = $body; $c->stash->{wards} = [ { name => 'summary' } ]; @@ -152,9 +152,7 @@ sub about_hook { $c->stash->{template} = 'auth/general.html'; $c->detach('/auth/general'); } else { - $c->stash->{no_body_found} = 1; - $c->set_param('em', $email); # What the contact form wants - $c->detach('/contact/submit'); + $c->stash->{error} = 'bad_email'; } } } diff --git a/perllib/FixMyStreet/Cobrand/FixaMinGata.pm b/perllib/FixMyStreet/Cobrand/FixaMinGata.pm index 07a4ef920..29e840dfa 100644 --- a/perllib/FixMyStreet/Cobrand/FixaMinGata.pm +++ b/perllib/FixMyStreet/Cobrand/FixaMinGata.pm @@ -3,10 +3,10 @@ use base 'FixMyStreet::Cobrand::Default'; use strict; use warnings; +use utf8; use Carp; use mySociety::MaPit; -use FixMyStreet::Geocode::FixaMinGata; use DateTime; sub country { @@ -23,10 +23,66 @@ sub enter_postcode_text { # Is also adding language parameter sub disambiguate_location { - return { + my $self = shift; + my $string = shift; + + my $out = { + %{ $self->SUPER::disambiguate_location() }, lang => 'sv', - country => 'se', # Is this the right format? /Rikard + country => 'se', }; + + $string = lc($string); + + if ($string eq 'lysekil') { + # Lysekil + $out->{bounds} = [ '58.4772', '11.3983', '58.1989', '11.5755' ]; + } elsif ($string eq 'tjörn') { + # Tjörn + $out->{bounds} = [ '58.0746', '11.4429', '57.9280', '11.7815' ]; + } elsif ($string eq 'varmdö') { + # Varmdö + $out->{bounds} = [ '59.4437', '18.3513', '59.1907', '18.7688' ]; + } elsif ($string eq 'öckerö') { + # Öckerö + $out->{bounds} = [ '57.7985', '11.5792', '57.6265', '11.7108' ]; + } + + return $out; +} + +sub geocoder_munge_results { + my ($self, $result) = @_; + + if ($result->{osm_id} == 1076755) { # Hammarö, Hammarö, Värmlands län, Svealand, Sweden + $result->{lat} = 59.3090; + $result->{lon} = 13.5297; + } + + if ($result->{osm_id} == 398625) { # Haninge, Landskapet Södermanland, Stockholms län, Svealand, Sweden + $result->{lat} = 59.1069; + $result->{lon} = 18.2085; + } + + if ($result->{osm_id} == 5831132) { # Nordmaling District, Nordmaling, Ångermanland, Västerbottens län, Norrland, 91433, Sweden + $result->{lat} = 63.5690; + $result->{lon} = 19.5028; + } + + if ($result->{osm_id} == 935430) { # Sotenäs, Västra Götalands län, Götaland, Sweden + $result->{lat} = 58.4219; + $result->{lon} = 11.3345; + } + + if ($result->{osm_id} == 935640) { # Tanum, Västra Götalands län, Götaland, Sweden + $result->{lat} = 58.7226; + $result->{lon} = 11.3242; + } + + if ($result->{osm_id} == 289344) { # Älvkarleby, Landskapet Uppland, Uppsala län, Svealand, Sweden + $result->{lat} = 60.5849; + $result->{lon} = 17.4545; + } } sub area_types { @@ -37,11 +93,9 @@ sub area_types { sub geocode_postcode { my ( $self, $s ) = @_; - # Most people write Swedish postcodes like this: - #+ XXX XX, so let's remove the space - # Is this the right place to do this? //Rikard - # This is the right place! // Jonas - $s =~ s/\ //g; # Rikard, remove space in postcode + # Most people write Swedish postcodes like this: + # XXX XX, so let's remove the space + $s =~ s/\ //g; if ($s =~ /^\d{5}$/) { my $location = mySociety::MaPit::call('postcode', $s); if ($location->{error}) { @@ -68,6 +122,7 @@ sub geocoded_string_check { sub find_closest { my ( $self, $problem ) = @_; + $problem = $problem->{problem} if ref $problem eq 'HASH'; return FixMyStreet::Geocode::OSM::closest_road_text( $self, $problem->latitude, $problem->longitude ); } @@ -108,13 +163,22 @@ sub filter_all_council_ids_list { return @all_councils_ids; # Är detta rätt? //Rikard } -# The pin is green is it's fixed, yellow if it's closed (but not fixed), and -# red otherwise. +# The pin is green is it's fixed or closed, yellow if it's in progress (not in a +# confirmed state), and red otherwise. sub pin_colour { my ( $self, $p, $context ) = @_; + return 'green' if $p->is_closed; return 'green' if $p->is_fixed; - return 'yellow' if $p->is_closed; + return 'yellow' if $p->is_in_progress; return 'red'; } +sub state_groups_inspect { + [ + [ _('Open'), [ 'confirmed', 'action scheduled', 'in progress', 'investigating' ] ], + [ _('Fixed'), [ 'fixed - council' ] ], + [ _('Closed'), [ 'duplicate', 'not responsible', 'unable to fix' ] ], + ] +} + 1; diff --git a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm index 00f099278..479d9c43b 100644 --- a/perllib/FixMyStreet/Cobrand/Oxfordshire.pm +++ b/perllib/FixMyStreet/Cobrand/Oxfordshire.pm @@ -67,6 +67,11 @@ sub problem_response_days { my $self = shift; my $p = shift; + return 'emergency' if $p->category eq 'Street lighting'; + + # Temporary, see https://github.com/mysociety/fixmystreetforcouncils/issues/291 + return 0; + return 10 if $p->category eq 'Bridges'; return 10 if $p->category eq 'Carriageway Defect'; # phone if urgent return 10 if $p->category eq 'Debris/Spillage'; @@ -87,7 +92,6 @@ sub problem_response_days { return 10 if $p->category eq 'Road traffic signs'; return 10 if $p->category eq 'Roads/highways'; return 10 if $p->category eq 'Skips and scaffolding'; - return 'emergency' if $p->category eq 'Street lighting'; return 10 if $p->category eq 'Traffic lights'; # phone if urgent return 10 if $p->category eq 'Traffic'; return 10 if $p->category eq 'Trees'; @@ -140,7 +144,7 @@ sub open311_config { push @$extra, { name => 'external_id', value => $row->id }; if ($h->{closest_address}) { - push @$extra, { name => 'closest_address', value => $h->{closest_address} } + push @$extra, { name => 'closest_address', value => "$h->{closest_address}" } } if ( $row->used_map || ( !$row->used_map && !$row->postcode ) ) { push @$extra, { name => 'northing', value => $h->{northing} }; diff --git a/perllib/FixMyStreet/Cobrand/Rutland.pm b/perllib/FixMyStreet/Cobrand/Rutland.pm new file mode 100644 index 000000000..6993b0964 --- /dev/null +++ b/perllib/FixMyStreet/Cobrand/Rutland.pm @@ -0,0 +1,55 @@ +package FixMyStreet::Cobrand::Rutland; +use base 'FixMyStreet::Cobrand::UKCouncils'; + +use strict; +use warnings; + +sub council_area_id { return 2600; } +sub council_area { return 'Rutland'; } +sub council_name { return 'Rutland County Council'; } +sub council_url { return 'rutland'; } + +sub open311_config { + my ($self, $row, $h, $params) = @_; + + my $extra = $row->get_extra_fields; + push @$extra, { name => 'external_id', value => $row->id }; + push @$extra, { name => 'title', value => $row->title }; + push @$extra, { name => 'description', value => $row->detail }; + + if ($h->{closest_address}) { + push @$extra, { name => 'closest_address', value => "$h->{closest_address}" } + } + $row->set_extra_fields( @$extra ); + + $params->{multi_photos} = 1; +} + +sub example_places { + return ( 'LE15 6HP', 'High Street', 'Oakham' ); +} + +sub disambiguate_location { + my $self = shift; + my $string = shift; + + return { + bounds => [52.524755166940075, -0.8217480325342802, 52.7597945702699, -0.4283542728893742] + }; +} + +sub pin_colour { + my ( $self, $p, $context ) = @_; + return 'green' if $p->is_fixed || $p->is_closed; + return 'yellow'; +} + +sub send_questionnaires { + return 0; +} + +sub ask_ever_reported { + return 0; +} + +1; diff --git a/perllib/FixMyStreet/Cobrand/UK.pm b/perllib/FixMyStreet/Cobrand/UK.pm index e1f5e565f..f99f29eb4 100644 --- a/perllib/FixMyStreet/Cobrand/UK.pm +++ b/perllib/FixMyStreet/Cobrand/UK.pm @@ -117,27 +117,23 @@ sub short_name { } sub find_closest { - my ( $self, $problem, $as_data ) = @_; - - my $data = $self->SUPER::find_closest($problem, $as_data); - - my $mapit_url = FixMyStreet->config('MAPIT_URL'); - my ($lat, $lon) = map { Utils::truncate_coordinate($_) } $problem->latitude, $problem->longitude; - my $url = $mapit_url . "nearest/4326/$lon,$lat"; - my $j = LWP::Simple::get($url); - if ($j) { - $j = JSON->new->utf8->allow_nonref->decode($j); - if ($j->{postcode}) { - if ($as_data) { - $data->{postcode} = $j->{postcode}; - } else { - $data .= sprintf(_("Nearest postcode to the pin placed on the map (automatically generated): %s (%sm away)"), - $j->{postcode}{postcode}, $j->{postcode}{distance}) . "\n\n"; - } - } + my ($self, $data) = @_; + + $data = { problem => $data } if ref $data ne 'HASH'; + + my $problem = $data->{problem}; + my $lat = $problem ? $problem->latitude : $data->{latitude}; + my $lon = $problem ? $problem->longitude : $data->{longitude}; + + my $closest = $self->SUPER::find_closest($data); + + ($lat, $lon) = map { Utils::truncate_coordinate($_) } $lat, $lon; + my $j = mySociety::MaPit::call('nearest', "4326/$lon,$lat"); + if ($j->{postcode}) { + $closest->{postcode} = $j->{postcode}; } - return $data; + return $closest; } sub reports_body_check { @@ -396,7 +392,8 @@ sub lookup_by_ref_regex { sub category_extra_hidden { my ($self, $meta) = @_; - return 1 if $meta eq 'usrn' || $meta eq 'asset_id'; + return 1 if $meta->{code} eq 'usrn' || $meta->{code} eq 'asset_id'; + return 1 if $meta->{automated} eq 'hidden_field'; return 0; } diff --git a/perllib/FixMyStreet/Cobrand/Zurich.pm b/perllib/FixMyStreet/Cobrand/Zurich.pm index f0308d6d7..b2a0e331d 100644 --- a/perllib/FixMyStreet/Cobrand/Zurich.pm +++ b/perllib/FixMyStreet/Cobrand/Zurich.pm @@ -10,7 +10,6 @@ use DateTime::Format::Pg; use strict; use warnings; -use feature 'say'; use utf8; =head1 NAME @@ -56,8 +55,7 @@ you already have, and the countres set so that they shouldn't in future. =cut sub setup_states { - FixMyStreet::DB::Result::Problem->visible_states_add('unconfirmed'); - FixMyStreet::DB::Result::Problem->visible_states_remove('investigating'); + FixMyStreet::DB::Result::Problem->visible_states_remove('not contactable'); } sub shorten_recency_if_new_greater_than_fixed { @@ -67,7 +65,7 @@ sub shorten_recency_if_new_greater_than_fixed { sub pin_colour { my ( $self, $p, $context ) = @_; return 'green' if $p->is_fixed || $p->is_closed; - return 'red' if $p->state eq 'unconfirmed' || $p->state eq 'confirmed'; + return 'red' if $p->state eq 'submitted' || $p->state eq 'confirmed'; return 'yellow'; } @@ -98,35 +96,11 @@ sub prettify_dt { return Utils::prettify_dt( $dt, 'zurich' ); } -# problem already has a concept of is_fixed/is_closed, but Zurich has different -# workflow for this here. -# -# TODO: look at more elegant way of doing this, for example having ::DB::Problem -# consider cobrand specific state config? - -sub zurich_closed_states { - my $states = { - 'fixed - council' => 1, - 'closed' => 1, # extern - 'hidden' => 1, - 'investigating' => 1, # wish - 'unable to fix' => 1, # jurisdiction unknown - 'partial' => 1, # not contactable - }; - - return wantarray ? keys %{ $states } : $states; -} - -sub problem_is_closed { - my ($self, $problem) = @_; - return exists $self->zurich_closed_states->{ $problem->state } ? 1 : 0; -} - sub zurich_public_response_states { my $states = { 'fixed - council' => 1, - 'closed' => 1, # extern - 'unable to fix' => 1, # jurisdiction unknown + 'external' => 1, + 'wish' => 1, }; return wantarray ? keys %{ $states } : $states; @@ -134,9 +108,9 @@ sub zurich_public_response_states { sub zurich_user_response_states { my $states = { + 'jurisdiction unknown' => 1, 'hidden' => 1, - 'investigating' => 1, # wish - 'partial' => 1, # not contactable + 'not contactable' => 1, }; return wantarray ? keys %{ $states } : $states; @@ -160,43 +134,33 @@ sub problem_as_hashref { my $hashref = $problem->as_hashref( $ctx ); - if ( $problem->state eq 'unconfirmed' ) { - for my $var ( qw( photo detail state state_t is_fixed meta ) ) { + if ( $problem->state eq 'submitted' ) { + for my $var ( qw( photo is_fixed meta ) ) { delete $hashref->{ $var }; } $hashref->{detail} = _('This report is awaiting moderation.'); $hashref->{title} = _('This report is awaiting moderation.'); - $hashref->{state} = 'submitted'; - $hashref->{state_t} = _('Submitted'); $hashref->{banner_id} = 'closed'; } else { + if ( $problem->state eq 'confirmed' || $problem->state eq 'external' ) { + $hashref->{banner_id} = 'closed'; + } elsif ( $problem->is_fixed || $problem->is_closed ) { + $hashref->{banner_id} = 'fixed'; + } else { + $hashref->{banner_id} = 'progress'; + } + if ( $problem->state eq 'confirmed' ) { $hashref->{state} = 'open'; $hashref->{state_t} = _('Open'); - $hashref->{banner_id} = 'closed'; - } elsif ( $problem->state eq 'closed' ) { - $hashref->{state} = 'extern'; # is this correct? - $hashref->{banner_id} = 'closed'; - $hashref->{state_t} = _('Extern'); - } elsif ( $problem->state eq 'unable to fix' ) { - $hashref->{state} = 'jurisdiction unknown'; # is this correct? - $hashref->{state_t} = _('Jurisdiction Unknown'); - $hashref->{banner_id} = 'fixed'; # green - } elsif ( $problem->state eq 'partial' ) { - $hashref->{state} = 'not contactable'; # is this correct? - $hashref->{state_t} = _('Not contactable'); - # no banner_id as hidden - } elsif ( $problem->state eq 'investigating' ) { - $hashref->{state} = 'wish'; # is this correct? - $hashref->{state_t} = _('Wish'); + } elsif ( $problem->state eq 'wish' ) { + $hashref->{state_t} = _('Closed'); } elsif ( $problem->is_fixed ) { $hashref->{state} = 'closed'; - $hashref->{banner_id} = 'fixed'; $hashref->{state_t} = _('Closed'); - } elsif ( $problem->state eq 'in progress' || $problem->state eq 'planned' ) { + } elsif ( $problem->state eq 'feedback pending' ) { $hashref->{state} = 'in progress'; - $hashref->{state_t} = _('In progress'); - $hashref->{banner_id} = 'progress'; + $hashref->{state_t} = FixMyStreet::DB->resultset("State")->display('in progress'); } } @@ -210,13 +174,13 @@ sub updates_as_hashref { my $hashref = {}; - if ( $problem->state eq 'fixed - council' || $problem->state eq 'closed' ) { + if ($self->problem_has_public_response($problem)) { $hashref->{update_pp} = $self->prettify_dt( $problem->lastupdate ); - if ( $problem->state eq 'fixed - council' ) { + if ( $problem->state ne 'external' ) { $hashref->{details} = FixMyStreet::App::View::Web::add_links( $problem->get_extra_metadata('public_response') || '' ); - } elsif ( $problem->state eq 'closed' ) { + } else { $hashref->{details} = sprintf( _('Assigned to %s'), $problem->body($ctx)->name ); } } @@ -229,6 +193,7 @@ sub updates_as_hashref { # boolean whether that indexed photo can be shown. sub allow_photo_display { my ( $self, $r, $num ) = @_; + return unless $r; my $publish_photo; if (blessed $r) { $publish_photo = $r->get_extra_metadata('publish_photo'); @@ -250,10 +215,6 @@ sub allow_photo_display { return $i + 1; } -sub show_unconfirmed_reports { - 1; -} - sub get_body_sender { my ( $self, $body, $category ) = @_; return { method => 'Zurich' }; @@ -324,25 +285,23 @@ sub overdue { return 0 unless $w; # call with previous state - if ( $problem->state eq 'unconfirmed' ) { + if ( $problem->state eq 'submitted' ) { # One working day $w = add_days( $w, 1 ); return $w < DateTime->now() ? 1 : 0; - } elsif ( $problem->state eq 'confirmed' || $problem->state eq 'in progress' || $problem->state eq 'planned' ) { + } 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 ); return $w < DateTime->now() ? 1 : 0; # call with new state - } elsif ( $self->problem_is_closed($problem) ) { + } else { # States which affect the closed_overdue statistic # Five working days from moderation (so 6 from creation) $w = add_days( $w, 6 ); return $w < DateTime->now() ? 1 : 0; - } else { - return 0; } } @@ -430,6 +389,13 @@ sub admin_pages { 'users' => [_('Users'), 3], 'user_edit' => [undef, undef], }; + + # There are some pages that only super users can see + if ($self->{c}->user->is_superuser) { + $pages->{states} = [ _('States'), 8 ]; + $pages->{config} = [ _('Configuration'), 9]; + }; + return $pages if $type eq 'super'; } @@ -471,14 +437,14 @@ sub admin { $order .= ' desc' if $dir; # XXX No multiples or missing bodies - $c->stash->{unconfirmed} = $c->cobrand->problems->search({ - state => [ 'unconfirmed', 'confirmed' ], + $c->stash->{submitted} = $c->cobrand->problems->search({ + state => [ 'submitted', 'confirmed' ], bodies_str => $c->stash->{body}->id, }, { order_by => $order, }); $c->stash->{approval} = $c->cobrand->problems->search({ - state => 'planned', + state => 'feedback pending', bodies_str => $c->stash->{body}->id, }, { order_by => $order, @@ -486,7 +452,7 @@ sub admin { my $page = $c->get_param('p') || 1; $c->stash->{other} = $c->cobrand->problems->search({ - state => { -not_in => [ 'unconfirmed', 'confirmed', 'planned' ] }, + state => { -not_in => [ 'submitted', 'confirmed', 'feedback pending' ] }, bodies_str => \@all, }, { order_by => $order, @@ -512,7 +478,7 @@ sub admin { order_by => $order } ); $c->stash->{reports_unpublished} = $c->cobrand->problems->search( { - state => 'planned', + state => 'feedback pending', bodies_str => $body->parent->id, }, { order_by => $order @@ -529,6 +495,15 @@ sub admin { } } +sub category_options { + my ($self, $c) = @_; + my @categories = $c->model('DB::Contact')->not_deleted->all; + $c->stash->{category_options} = [ map { { + category => $_->category, category_display => $_->category, + abbreviation => $_->get_extra_metadata('abbreviation'), + } } @categories ]; +} + sub admin_report_edit { my $self = shift; my $c = $self->{c}; @@ -539,6 +514,8 @@ sub admin_report_edit { if ($type ne 'super') { my %allowed_bodies = map { $_->id => 1 } ( $body->bodies->all, $body ); + # SDMs can see parent reports but not edit them + $allowed_bodies{$body->parent->id} = 1 if $type eq 'sdm'; $c->detach( '/page_error_404_not_found' ) unless $allowed_bodies{$problem->bodies_str}; } @@ -550,8 +527,7 @@ sub admin_report_edit { $c->stash->{bodies} = \@bodies; # Can change category to any other - my @categories = $c->model('DB::Contact')->not_deleted->all; - $c->stash->{category_options} = [ map { { name => $_->category, value => $_->category } } @categories ]; + $self->category_options($c); } elsif ($type eq 'dm') { @@ -565,8 +541,7 @@ sub admin_report_edit { $c->stash->{bodies} = \@bodies; # Can change category to any other - my @categories = $c->model('DB::Contact')->not_deleted->all; - $c->stash->{category_options} = [ map { { name => $_->category, value => $_->category } } @categories ]; + $self->category_options($c); } @@ -636,10 +611,9 @@ sub admin_report_edit { my $state = $c->get_param('state') || ''; my $oldstate = $problem->state; - my $closure_states = $self->zurich_closed_states; - delete $closure_states->{'fixed - council'}; # may not be needed? + my $closure_states = { map { $_ => 1 } FixMyStreet::DB::Result::Problem->closed_states(), FixMyStreet::DB::Result::Problem->hidden_states() }; - my $old_closure_state = $problem->get_extra_metadata('closure_status'); + my $old_closure_state = $problem->get_extra_metadata('closure_status') || ''; # update the public update from DM if (my $update = $c->get_param('status_update')) { @@ -661,21 +635,20 @@ sub admin_report_edit { $internal_note_text = "Weitergeleitet von $old_cat an $new_cat"; $self->update_admin_log($c, $problem, "Changed category from $old_cat to $new_cat"); $redirect = 1 if $cat->body_id ne $body->id; - } elsif ( $closure_states->{$state} and - ( $oldstate ne 'planned' ) - || (($old_closure_state ||'') ne $state)) + } elsif ( $oldstate ne $state and $closure_states->{$state} and + $oldstate ne 'feedback pending' || $old_closure_state ne $state) { # for these states - # - closed (Extern) - # - investigating (Wish) + # - external + # - wish # - hidden - # - partial (Not contactable) - # - unable to fix (Jurisdiction unknown) - # we divert to planned (Rueckmeldung ausstehend) and set closure_status to the requested state + # - not contactable + # - jurisdiction unknown + # we divert to feedback pending (Rueckmeldung ausstehend) and set closure_status to the requested state # From here, the DM can reply to the user, triggering the setting of problem to correct state $problem->set_extra_metadata( closure_status => $state ); - $self->set_problem_state($c, $problem, 'planned'); - $state = 'planned'; + $self->set_problem_state($c, $problem, 'feedback pending'); + $state = 'feedback pending'; $problem->set_extra_metadata_if_undefined( moderated_overdue => $self->overdue( $problem ) ); } elsif ( my $subdiv = $c->get_param('body_subdivision') ) { @@ -688,18 +661,18 @@ sub admin_report_edit { } else { if ($state) { - if ($oldstate eq 'unconfirmed' and $state ne 'unconfirmed') { + if ($oldstate eq 'submitted' and $state ne 'submitted') { # only set this for the first state change $problem->set_extra_metadata_if_undefined( moderated_overdue => $self->overdue( $problem ) ); } $self->set_problem_state($c, $problem, $state) unless $closure_states->{$state}; - # we'll defer to 'planned' clause below to change the state + # we'll defer to 'feedback pending' clause below to change the state } } - if ($problem->state eq 'planned') { + if ($problem->state eq 'feedback pending') { # Rueckmeldung ausstehend # override $state from the metadata set above $state = $problem->get_extra_metadata('closure_status') || ''; @@ -712,7 +685,7 @@ sub admin_report_edit { $moderated++; $closed++; } - elsif ($state =~/^(closed|investigating)$/) { # Extern | Wish + elsif ($state =~/^(external|wish)$/) { $moderated++; # Nested if instead of `and` because in these cases, we *don't* # want to close unless we have body_external (so we don't want @@ -725,7 +698,7 @@ sub admin_report_edit { if ($problem->external_body && $c->get_param('publish_response')) { $problem->whensent( undef ); $self->set_problem_state($c, $problem, $state); - my $template = ($state eq 'investigating') ? 'problem-wish.txt' : 'problem-external.txt'; + my $template = ($state eq 'wish') ? 'problem-wish.txt' : 'problem-external.txt'; _admin_send_email( $c, $template, $problem ); $redirect = 0; $closed++; @@ -771,7 +744,7 @@ sub admin_report_edit { # send external_message if provided and state is *now* Wish|Extern # e.g. was already, or was set in the Rueckmeldung ausstehend clause above. if ( my $external_message = $c->get_param('external_message') - and $problem->state =~ /^(closed|investigating)$/) + and $problem->state =~ /^(external|wish)$/) { my $external = $problem->external_body; my $external_body = $c->model('DB::Body')->find($external) @@ -783,7 +756,7 @@ sub admin_report_edit { $problem->add_to_comments( { text => ( sprintf '(%s %s) %s', - $state eq 'closed' ? + $state eq 'external' ? _('Forwarded to external body') : _('Forwarded wish to external body'), $external_body->name, @@ -842,10 +815,13 @@ sub admin_report_edit { if ($type eq 'sdm') { + my $editable = $type eq 'sdm' && $body->id eq $problem->bodies_str; + $c->stash->{sdm_disabled} = $editable ? '' : 'disabled'; + # Has cut-down edit template for adding update and sending back up only $c->stash->{template} = 'admin/report_edit-sdm.html'; - if ($c->get_param('send_back') or $c->get_param('not_contactable')) { + if ($editable && $c->get_param('send_back') or $c->get_param('not_contactable')) { # SDM can send back a report either to be assigned to a different # subdivision, or because the customer was not contactable. # We handle these in the same way but with different statuses. @@ -857,8 +833,8 @@ sub admin_report_edit { $problem->bodies_str( $body->parent->id ); if ($not_contactable) { # we can't directly set state, but mark the closure_status for DM to confirm. - $self->set_problem_state($c, $problem, 'planned'); - $problem->set_extra_metadata( closure_status => 'partial'); + $self->set_problem_state($c, $problem, 'feedback pending'); + $problem->set_extra_metadata( closure_status => 'not contactable'); } else { $self->set_problem_state($c, $problem, 'confirmed'); @@ -871,7 +847,7 @@ 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 ($c->get_param('submit')) { + } elsif ($editable && $c->get_param('submit')) { $c->forward('/auth/check_csrf_token'); my $db_update = 0; @@ -905,7 +881,7 @@ sub admin_report_edit { $problem->set_extra_metadata( subdiv_overdue => $self->overdue( $problem ) ); $problem->bodies_str( $body->parent->id ); $problem->whensent( undef ); - $self->set_problem_state($c, $problem, 'planned'); + $self->set_problem_state($c, $problem, 'feedback pending'); $problem->update; $c->res->redirect( '/admin/summary' ); } @@ -933,61 +909,52 @@ sub stash_states { my @states = ( { # Erfasst - state => 'unconfirmed', - trans => _('Submitted'), - unconfirmed => 1, + state => 'submitted', + submitted => 1, hidden => 1, }, { # Aufgenommen state => 'confirmed', - trans => _('Open'), - unconfirmed => 1, + submitted => 1, }, { # Unsichtbar (hidden) state => 'hidden', - trans => _('Hidden'), - unconfirmed => 1, + submitted => 1, hidden => 1, }, { # Extern - state => 'closed', - trans => _('Extern'), + state => 'external', }, { # Zustaendigkeit unbekannt - state => 'unable to fix', - trans => _('Jurisdiction unknown'), + state => 'jurisdiction unknown', }, { - # Wunsch (hidden) - state => 'investigating', - trans => _('Wish'), + # Wunsch + state => 'wish', }, { # Nicht kontaktierbar (hidden) - state => 'partial', - trans => _('Not contactable'), + state => 'not contactable', }, ); - my %state_trans = map { $_->{state} => $_->{trans} } @states; my $state = $problem->state; # Rueckmeldung ausstehend may also indicate the status it's working towards. push @states, do { - if ($state eq 'planned' and my $closure_status = $problem->get_extra_metadata('closure_status')) { + if ($state eq 'feedback pending' and my $closure_status = $problem->get_extra_metadata('closure_status')) { { state => $closure_status, - trans => sprintf '%s (%s)', _('Planned'), $state_trans{$closure_status}, + trans => sprintf 'Rückmeldung ausstehend (%s)', FixMyStreet::DB->resultset("State")->display($closure_status), }; } else { { - state => 'planned', - trans => _('Planned'), + state => 'feedback pending', }; } }; @@ -995,25 +962,22 @@ sub stash_states { if ($state eq 'in progress') { push @states, { state => 'in progress', - trans => _('In progress'), }; } elsif ($state eq 'fixed - council') { push @states, { state => 'fixed - council', - trans => _('Closed'), }; } - elsif ($state =~/^(hidden|unconfirmed)$/) { + elsif ($state =~/^(hidden|submitted)$/) { @states = grep { $_->{$state} } @states; } $c->stash->{states} = \@states; - $c->stash->{states_trans} = { map { $_->{state} => $_->{trans} } @states }; # [% states_trans.${problem.state} %] # stash details about the public response $c->stash->{default_public_response} = "\nFreundliche Grüsse\n\nIhre Stadt Zürich\n"; $c->stash->{show_publish_response} = - ($problem->state eq 'planned'); + ($problem->state eq 'feedback pending'); } =head2 _admin_send_email @@ -1045,7 +1009,7 @@ sub _admin_send_email { sub munge_sendreport_params { my ($self, $row, $h, $params) = @_; - if ($row->state =~ /^(closed|investigating)$/) { + if ($row->state =~ /^(external|wish)$/) { # we attach images to reports sent to external bodies my $photoset = $row->get_photoset(); my $num = $photoset->num_images @@ -1115,138 +1079,34 @@ sub admin_stats { my $self = shift; my $c = $self->{c}; - my %date_params; + my %optional_params; my $ym = $c->get_param('ym'); my ($m, $y) = $ym ? ($ym =~ /^(\d+)\.(\d+)$/) : (); $c->stash->{ym} = $ym; if ($y && $m) { $c->stash->{start_date} = DateTime->new( year => $y, month => $m, day => 1 ); $c->stash->{end_date} = $c->stash->{start_date} + DateTime::Duration->new( months => 1 ); - $date_params{created} = { + $optional_params{created} = { '>=', DateTime::Format::Pg->format_datetime($c->stash->{start_date}), '<', DateTime::Format::Pg->format_datetime($c->stash->{end_date}), }; } + my $cat = $c->stash->{category} = $c->get_param('category'); + $optional_params{category} = $cat if $cat; + my %params = ( - %date_params, + %optional_params, state => [ FixMyStreet::DB::Result::Problem->visible_states() ], ); if ( $c->get_param('export') ) { - my $problems = $c->model('DB::Problem')->search( - {%date_params}, - { - join => 'admin_log_entries', - distinct => 1, - columns => [ - 'id', 'created', - 'latitude', 'longitude', - 'cobrand', 'category', - 'state', 'user_id', - 'external_body', - 'title', 'detail', - 'photo', - 'whensent', 'lastupdate', - 'service', - 'extra', - { sum_time_spent => { sum => 'admin_log_entries.time_spent' } }, - ] - } - ); - my @fields = ( - 'Report ID', - 'Created', - 'Sent to Agency', - 'Last Updated', - 'E', - 'N', - 'Category', - 'Status', - 'Closure Status', - 'UserID', - 'External Body', - 'Time Spent', - 'Title', - 'Detail', - 'Media URL', - 'Interface Used', - 'Council Response', - 'Strasse', - 'Mast-Nr.', - 'Haus-Nr.', - 'Hydranten-Nr.', - ); - - my $body = ""; - require Text::CSV; - my $csv = Text::CSV->new({ binary => 1 }); - - if ($csv->combine(@fields)) { - $body .= $csv->string . "\n"; - } - else { - $body .= sprintf "{{error emitting CSV line: %s}}\n", $csv->error_diag; - } - - while ( my $report = $problems->next ) { - my $external_body; - my $body_name = ""; - if ( $external_body = $report->body($c) ) { - $body_name = $external_body->name || '[Unknown body]'; - } - - my $detail = $report->detail; - my $public_response = $report->get_extra_metadata('public_response') || ''; - my $metas = $report->get_extra_fields(); - my %extras; - foreach my $field (@$metas) { - $extras{$field->{name}} = $field->{value}; - } - - # replace newlines with HTML <br/> element - $detail =~ s{\r?\n}{ <br/> }g; - $public_response =~ s{\r?\n}{ <br/> }g if $public_response; - - # Assemble photo URL, if report has a photo - my $photo_to_display = $c->cobrand->allow_photo_display($report); - my $media_url = (@{$report->photos} && $photo_to_display) - ? $c->cobrand->base_url . $report->photos->[$photo_to_display-1]->{url} - : ''; - - my @columns = ( - $report->id, - $report->created, - $report->whensent, - $report->lastupdate, - $report->local_coords, $report->category, - $report->state, - $report->get_extra_metadata('closure_status') || '', - $report->user_id, - $body_name, - $report->get_column('sum_time_spent') || 0, - $report->title, - $detail, - $media_url, - $report->service || 'Web interface', - $public_response, - $extras{'strasse'} || '', - $extras{'mast_nr'} || '', - $extras{'haus_nr'} || '', - $extras{'hydranten_nr'} || '' - ); - if ($csv->combine(@columns)) { - $body .= $csv->string . "\n"; - } - else { - $body .= sprintf "{{error emitting CSV line: %s}}\n", $csv->error_diag; - } - } - $c->res->content_type('text/csv; charset=utf-8'); - $c->res->header('Content-Disposition' => 'attachment; filename=stats.csv'); - $c->res->body($body); + return $self->export_as_csv($c, \%optional_params); } + # Can change category to any other + $self->category_options($c); + # Total reports (non-hidden) my $total = $c->model('DB::Problem')->search( \%params )->count; # Device for apps (iOS/Android) @@ -1256,17 +1116,17 @@ sub admin_stats { group_by => [ 'service' ], }); # Reports solved - my $solved = $c->model('DB::Problem')->search( { state => 'fixed - council', %date_params } )->count; + my $solved = $c->model('DB::Problem')->search( { state => 'fixed - council', %optional_params } )->count; # Reports marked as spam - my $hidden = $c->model('DB::Problem')->search( { state => 'hidden', %date_params } )->count; + my $hidden = $c->model('DB::Problem')->search( { state => 'hidden', %optional_params } )->count; # Reports assigned to third party - my $closed = $c->model('DB::Problem')->search( { state => 'closed', %date_params } )->count; + my $external = $c->model('DB::Problem')->search( { state => 'external', %optional_params } )->count; # Reports moderated within 1 day - my $moderated = $c->model('DB::Problem')->search( { extra => { like => '%moderated_overdue,I1:0%' }, %date_params } )->count; + my $moderated = $c->model('DB::Problem')->search( { extra => { like => '%moderated_overdue,I1:0%' }, %optional_params } )->count; # Reports solved within 5 days (sent back from subdiv) my $subdiv_dealtwith = $c->model('DB::Problem')->search( { extra => { like => '%subdiv_overdue,I1:0%' }, %params } )->count; - # Reports solved within 5 days (marked as 'fixed - council', 'closed', or 'hidden' - my $fixed_in_time = $c->model('DB::Problem')->search( { extra => { like => '%closed_overdue,I1:0%' }, %date_params } )->count; + # Reports solved within 5 days (marked as 'fixed - council', 'external', or 'hidden' + my $fixed_in_time = $c->model('DB::Problem')->search( { extra => { like => '%closed_overdue,I1:0%' }, %optional_params } )->count; # Reports per category my $per_category = $c->model('DB::Problem')->search( \%params, { select => [ 'category', { count => 'id' } ], @@ -1295,7 +1155,7 @@ sub admin_stats { reports_total => $total, reports_solved => $solved, reports_spam => $hidden, - reports_assigned => $closed, + reports_assigned => $external, reports_moderated => $moderated, reports_dealtwith => $fixed_in_time, reports_category_changed => $changed, @@ -1310,6 +1170,99 @@ sub admin_stats { return 1; } +sub export_as_csv { + my ($self, $c, $params) = @_; + $c->model('DB')->schema->storage->sql_maker->quote_char('"'); + my $csv = $c->stash->{csv} = { + problems => $c->model('DB::Problem')->search_rs( + $params, + { + join => ['admin_log_entries', 'user'], + distinct => 1, + columns => [ + 'id', 'created', + 'latitude', 'longitude', + 'cobrand', 'category', + 'state', 'user_id', + 'external_body', + 'title', 'detail', + 'photo', + 'whensent', 'lastupdate', + 'service', + 'extra', + { sum_time_spent => { sum => 'admin_log_entries.time_spent' } }, + 'name', 'user.id', 'user.email', 'user.phone', 'user.name', + ] + } + ), + headers => [ + 'Report ID', 'Created', 'Sent to Agency', 'Last Updated', + 'E', 'N', 'Category', 'Status', 'Closure Status', + 'UserID', 'User email', 'User phone', 'User name', + 'External Body', 'Time Spent', 'Title', 'Detail', + 'Media URL', 'Interface Used', 'Council Response', + 'Strasse', 'Mast-Nr.', 'Haus-Nr.', 'Hydranten-Nr.', + ], + columns => [ + 'id', 'created', 'whensent',' lastupdate', 'local_coords_x', + 'local_coords_y', 'category', 'state', 'closure_status', + 'user_id', 'user_email', 'user_phone', 'user_name', + 'body_name', 'sum_time_spent', 'title', 'detail', + 'media_url', 'service', 'public_response', + 'strasse', 'mast_nr',' haus_nr', 'hydranten_nr', + ], + extra_data => sub { + my $report = shift; + + my $body_name = ""; + if ( my $external_body = $report->body($c) ) { + $body_name = $external_body->name || '[Unknown body]'; + } + + my $detail = $report->detail; + my $public_response = $report->get_extra_metadata('public_response') || ''; + my $metas = $report->get_extra_fields(); + my %extras; + foreach my $field (@$metas) { + $extras{$field->{name}} = $field->{value}; + } + + # replace newlines with HTML <br/> element + $detail =~ s{\r?\n}{ <br/> }g; + $public_response =~ s{\r?\n}{ <br/> }g if $public_response; + + # Assemble photo URL, if report has a photo + my $photo_to_display = $c->cobrand->allow_photo_display($report); + my $media_url = (@{$report->photos} && $photo_to_display) + ? $c->cobrand->base_url . $report->photos->[$photo_to_display-1]->{url} + : ''; + + return { + whensent => $report->whensent, + lastupdate => $report->lastupdate, + user_id => $report->user_id, + user_email => $report->user->email || '', + user_phone => $report->user->phone || '', + user_name => $report->name, + closure_status => $report->get_extra_metadata('closure_status') || '', + body_name => $body_name, + sum_time_spent => $report->get_column('sum_time_spent') || 0, + detail => $detail, + media_url => $media_url, + service => $report->service || 'Web interface', + public_response => $public_response, + strasse => $extras{'strasse'} || '', + mast_nr => $extras{'mast_nr'} || '', + haus_nr => $extras{'haus_nr'} || '', + hydranten_nr => $extras{'hydranten_nr'} || '' + }; + }, + filename => 'stats', + }; + $c->forward('/dashboard/generate_csv'); + $c->model('DB')->schema->storage->sql_maker->quote_char(''); +} + sub problem_confirm_email_extras { my ($self, $report) = @_; my $confirmed_reports = $report->user->problems->search({ @@ -1319,102 +1272,46 @@ sub problem_confirm_email_extras { $self->{c}->stash->{email_confirmed} = $confirmed_reports; } -sub body_details_data { - return ( - { - name => 'Stadt Zurich' - }, - { - name => 'Elektrizitäwerk Stadt Zürich', - parent => 'Stadt Zurich', - area_id => 423017, - }, - { - name => 'ERZ Entsorgung + Recycling Zürich', - parent => 'Stadt Zurich', - area_id => 423017, - }, - { - name => 'Fachstelle Graffiti', - parent => 'Stadt Zurich', - area_id => 423017, - }, - { - name => 'Grün Stadt Zürich', - parent => 'Stadt Zurich', - area_id => 423017, - }, - { - name => 'Tiefbauamt Stadt Zürich', - parent => 'Stadt Zurich', - area_id => 423017, - }, - { - name => 'Dienstabteilung Verkehr', - parent => 'Stadt Zurich', - area_id => 423017, - }, - ); -} +sub reports_per_page { return 20; } -sub contact_details_data { - return ( - { - category => 'Beleuchtung/Uhren', - body_name => 'Elektrizitätswerk Stadt Zürich', - fields => [ - { - code => 'strasse', - description => 'Strasse', - datatype => 'string', - required => 'yes', - }, - { - code => 'haus_nr', - description => 'Haus-Nr.', - datatype => 'string', - }, - { - code => 'mast_nr', - description => 'Mast-Nr.', - datatype => 'string', - } - ], - }, - { - category => 'Brunnen/Hydranten', - # body_name ??? - fields => [ - { - code => 'hydranten_nr', - description => 'Hydranten-Nr.', - datatype => 'string', - }, - ], - }, - { - category => "Grünflächen/Spielplätze", - body_name => 'Grün Stadt Zürich', - rename_from => "Tiere/Grünflächen", - }, - { - category => 'Spielplatz/Sitzbank', - body_name => 'Grün Stadt Zürich', - delete => 1, - }, +sub singleton_bodies_str { 1 } + +sub contact_extra_fields { [ 'abbreviation' ] }; + +sub default_problem_state { 'submitted' } + +sub db_state_migration { + my $rs = FixMyStreet::DB->resultset('State'); + + # Create new states needed + $rs->create({ label => 'submitted', type => 'open', name => 'Erfasst' }); + $rs->create({ label => 'feedback pending', type => 'open', name => 'Rückmeldung ausstehend' }); + $rs->create({ label => 'wish', type => 'closed', name => 'Wunsch' }); + $rs->create({ label => 'external', type => 'closed', name => 'Extern' }); + $rs->create({ label => 'jurisdiction unknown', type => 'closed', name => 'Zuständigkeit unbekannt' }); + $rs->create({ label => 'not contactable', type => 'closed', name => 'Nicht kontaktierbar' }); + + # And update used current ones to have correct name + $rs->find({ label => 'in progress' })->update({ name => 'In Bearbeitung' }); + $rs->find({ label => 'fixed' })->update({ name => 'Beantwortet' }); + + # Move reports to correct new state + my %state_move = ( + unconfirmed => 'submitted', + closed => 'external', + investigating => 'wish', + 'unable to fix' => 'jurisdiction unknown', + planned => 'feedback pending', + partial => 'not contactable', ); -} + foreach (keys %state_move) { + FixMyStreet::DB->resultset('Problem')->search({ state => $_ })->update({ state => $state_move{$_} }); + } -sub contact_details_data_body_default { - my ($self) = @_; - # temporary measure to assign un-bodied contacts to parent - # (this isn't at all how things will be setup in live, but is - # handy during dev.) - return $self->{c}->model('DB::Body')->find({ name => 'Stadt Zurich' }); + # Delete unused standard states from the database + for ('action scheduled', 'duplicate', 'not responsible', 'internal referral', 'planned', 'investigating', 'unable to fix') { + $rs->find({ label => $_ })->delete; + } } -sub reports_per_page { return 20; } - -sub singleton_bodies_str { 1 } - 1; diff --git a/perllib/FixMyStreet/DB/Factories.pm b/perllib/FixMyStreet/DB/Factories.pm index 0e99608e1..56cff280b 100644 --- a/perllib/FixMyStreet/DB/Factories.pm +++ b/perllib/FixMyStreet/DB/Factories.pm @@ -1,5 +1,48 @@ +use strict; +use warnings; +use v5.14; + use FixMyStreet::DB; +package FixMyStreet::DB::Factories; + +use Path::Tiny; +my $db; +my $opt; + +END { + if ($db) { + $opt->commit ? $db->txn_commit : $db->txn_rollback; + } +} +sub setup { + my $cls = shift; + + $opt = shift; + $db = FixMyStreet::DB->schema->storage; + $db->txn_begin; + if (!$opt->commit) { + say "NOT COMMITTING TO DATABASE"; + } + + if ($opt->empty) { + path(FixMyStreet->path_to('web/photo'))->remove_tree({ keep_root => 1 }); + $db->dbh->do(q{ +DO +$func$ +BEGIN + EXECUTE + (SELECT 'TRUNCATE TABLE ' || string_agg(quote_ident(tablename), ', ') || ' RESTART IDENTITY CASCADE ' + FROM pg_tables WHERE schemaname='public'); +END +$func$; +}) or die $!; + $db->dbh->do( scalar FixMyStreet->path_to('db/fixture.sql')->slurp ) or die $!; + $db->dbh->do( scalar FixMyStreet->path_to('db/generate_secret.sql')->slurp ) or die $!; + say "Emptied database"; + } +} + package FixMyStreet::DB::Factory::Base; use parent "DBIx::Class::Factory"; @@ -19,6 +62,10 @@ sub find_or_create { package FixMyStreet::DB::Factory::Problem; use parent "DBIx::Class::Factory"; +use Path::Tiny; +use DateTime::Format::Pg; +use FixMyStreet; +use FixMyStreet::App::Model::PhotoSet; __PACKAGE__->resultset(FixMyStreet::DB->resultset("Problem")); @@ -43,6 +90,94 @@ __PACKAGE__->fields({ category => 'Other', }); +sub data { + my $self = shift; + + my %titles = ( + 'Potholes' => ['Deep pothole', 'Small pothole', 'Pothole in cycle lane', 'Pothole on busy pavement', 'Large pothole', 'Sinking manhole'], + 'Street lighting' => ['Faulty light', 'Street light not working', 'Lights out in tunnel', 'Light not coming on', 'Light not going off'], + 'Graffiti' => ['Graffiti', 'Graffiti', 'Offensive graffiti', 'Graffiti on the bridge', 'Remove graffiti'], + 'Other' => ['Loose drain cover', 'Flytipping on country lane', 'Vehicle blocking footpath', 'Hedge encroaching pavement', 'Full litter bins'], + ); + my %photos = ( + 'Potholes' => [ '33717571655_46dfc6f65f_z.jpg', '37855543925_9dbbbecf41_z.jpg', '19119222668_a3c866d7c8_z.jpg', '12049724866_404b066875_z.jpg', '3705226606_eac71cf195_z.jpg', '6304445383_bd216ca892_z.jpg' ], + 'Street lighting' => ['38110448864_fd71227247_z.jpg', '27050321819_ac123400eb_z.jpg', '35732107202_b790c61f63_z.jpg', '31889115854_01cdf38b0d_z.jpg', undef ], + 'Graffiti' => ['12205918375_f37f0b27a9_z.jpg', '8895442578_376a9b0be0_z.jpg', '22998854352_17555b7536_z.jpg', '22593395257_3d48f23bfa_z.jpg', '20515339175_f4ed9fc1d9_z.jpg' ], + 'Other' => ['14347396807_20737504f7_z.jpg', '14792525771_167bc20e3d_z.jpg', undef, '36296226976_a83a118ff8_z.jpg', '23222004240_273977b2b2_z.jpg'], + ); + my %descriptions = ( + 'Potholes' => [ + '6” deep pothole in the very centre of the Bristol road; cars are swerving to avoid it. Please treat this as a matter of urgency.', + 'It’s small but it’s a trip hazard. Right where people cross over to get into the school or church. About 3” across but will become larger if not attended to.', + 'Just went over my handlebars as I didn’t see this pothole on Banbury road, just before the traffic lights. Dread to think what might have happened if the traffic had been busier.', + 'I work in the cafe at 34 Clarington Avenue and we’ve had four people come in having tripped over in the last seven days. The pothole’s right outside the key-cutting shop, just near the alleyway.', + 'This has been here, next to the side of the road, for a month', + 'A manhole on the junction of Etherington Road is sinking into the road surface. Not only is it an accident waiting to happen but it’s making a terrible noise every time a car passes over it.', + ], + 'Street lighting' => [ + 'I saw a workman attempting to fix this streetlight over a week ago, and ever since then it’s come on in the daytime and gone off as soon as it gets dark. Come and sort it out please!', + 'Every Tuesday night I have to walk across the carpark outside the station at around 9pm. Not a problem in summer but now the nights are drawing in I feel very unsafe. Please get the streetlight by the exit fixed as I’m sure I can’t be the only woman feeling vulnerable.', + 'My toddler is too scared to go in now, as soon as you’re more than a few paces in it’s absolutely pitch black with no hope of seeing any puddles or worse on the floor. I think this needs seeing to as a priority. Thank you.', + 'I think the lights in the multi storey carpark are motion sensitive but I’ve actually never seen them come on. Maybe the bulb needs replacing??', + 'This streetlight is right outside my bedroom window. It is on 24 hours a day, even in blazing sunlight. Apart from the fact that it’s a waste of electricity, it makes my bedroom feel like an interrogation chamber. Please come and fix it.', + ], + 'Graffiti' => [ + 'Someone has scrawled a really offensive piece of graffiti (are they called ‘tags’??) on the side of the town hall. You might want to see about getting it cleaned off. Wouldn’t want my own children to see that, I’m sure others feel the same.', + 'Can’t see the timetable at the bus shelter cos some idiot’s covered it all in red spray paint. Honestly. Kids of today.', + 'Not gonna write down what it depicts cos I suspect that’d get caught in your profanity filter lol. But please do come and paint over this monstrosity before it causes an accident.', + 'That same guy that’s graffitied all over town has gone and done the same on the passenger bridge over the tracks, you can see it as you come into the station. Ugly bit of garbage graffiti. Bit of a poor first impression for the town eh.', + 'What’s the procedure for requesting a bit of graffiti be removed? There’s been a huge scrawl on the wall outside the club for months. Nice sentiment maybe but really brings the tone of the area down.', + ], + 'Other' => [ + 'Surprised me so much when I crossed the road I nearly took a tumble! Glad I didn’t fall in, this really needs securing now.', + 'Some unmentionable has driven down Larker’s Lane and left a huge heap of old rubbish on the verge. Talk about ruining the view! Such a beautiful spot and these lowlifes come and dump their junk. Probably trying to avoid paying the tip.', + 'Well someone on foot can just about squeeze through but good luck if you’ve got a pushchair or god forbid a wheelchair. Think someone’s abandoned this car; it hasn’t moved in weeks.', + 'Awful trying to walk past after a rain shower, well any time really.', + 'I think these need seeing to more frequently, they’re always full to overflowing by midday.', + ], + ); + + return { + titles => \%titles, + descriptions => \%descriptions, + photos => \%photos, + }; +} + +sub create_problem { + my $self = shift; + my $params = shift; + + my $data = $self->data; + my $category = $params->{category}; + my $inaccurate_km = 0.01; + + my $titles = $data->{titles}{$category}; + my $descs = $data->{descriptions}{$category}; + my $rand = int(rand(@$titles)); + + my $photo; + if (my $file = $data->{photos}{$category}->[$rand]) { + my $files = [ $file ]; + if ($category eq 'Graffiti') { + push @$files, $data->{photos}{$category}->[int(rand(@$titles))]; + } + $files = [ map { path(FixMyStreet->path_to("t/images/$_"))->slurp_raw } @$files ]; + my $photoset = FixMyStreet::App::Model::PhotoSet->new({ + data_items => $files, + }); + $photo = $photoset->data; + } + + $params->{latitude} += rand(2 * $inaccurate_km) - $inaccurate_km; + $params->{longitude} += rand(3 * $inaccurate_km) - 1.5 * $inaccurate_km, + $params->{title} = $titles->[$rand]; + $params->{detail} = $descs->[$rand]; + $params->{photo_id} = $photo; + $params->{confirmed} = DateTime::Format::Pg->format_datetime($params->{confirmed}); + return $self->create($params); +} + ####################### package FixMyStreet::DB::Factory::Body; diff --git a/perllib/FixMyStreet/DB/Result/Body.pm b/perllib/FixMyStreet/DB/Result/Body.pm index 07bea276c..74a38f225 100644 --- a/perllib/FixMyStreet/DB/Result/Body.pm +++ b/perllib/FixMyStreet/DB/Result/Body.pm @@ -44,6 +44,14 @@ __PACKAGE__->add_columns( { data_type => "boolean", default_value => \"false", is_nullable => 0 }, "external_url", { data_type => "text", is_nullable => 1 }, + "fetch_problems", + { data_type => "boolean", default_value => \"false", is_nullable => 0 }, + "blank_updates_permitted", + { data_type => "boolean", default_value => \"false", is_nullable => 1 }, + "convert_latlong", + { data_type => "boolean", default_value => \"false", is_nullable => 0 }, + "extra", + { data_type => "text", is_nullable => 1 }, ); __PACKAGE__->set_primary_key("id"); __PACKAGE__->has_many( @@ -118,13 +126,17 @@ __PACKAGE__->has_many( ); -# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-02-13 15:11:11 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:BOJANVwg3kR/1VjDq0LykA +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2018-04-05 14:29:33 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:HV8IM2C1ErrpvXoRTZ1B1Q + +__PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn"); +__PACKAGE__->rabx_column('extra'); use Moo; use namespace::clean; -with 'FixMyStreet::Roles::Translatable'; +with 'FixMyStreet::Roles::Translatable', + 'FixMyStreet::Roles::Extra'; sub url { my ( $self, $c, $args ) = @_; diff --git a/perllib/FixMyStreet/DB/Result/Contact.pm b/perllib/FixMyStreet/DB/Result/Contact.pm index f9cbf1c44..c544f084a 100644 --- a/perllib/FixMyStreet/DB/Result/Contact.pm +++ b/perllib/FixMyStreet/DB/Result/Contact.pm @@ -97,7 +97,11 @@ sub category_display { sub get_metadata_for_input { my $self = shift; my $id_field = $self->id_field; - my @metadata = grep { $_->{code} !~ /^(easting|northing|closest_address|$id_field)$/ } @{$self->get_extra_fields}; + my @metadata = @{$self->get_extra_fields}; + # First, ones we always want to ignore (hard-coded, old system) + @metadata = grep { $_->{code} !~ /^(easting|northing|closest_address|$id_field)$/ } @metadata; + # Also ignore any we have with a 'server_set' automated attribute + @metadata = grep { !$_->{automated} || $_->{automated} ne 'server_set' } @metadata; # Just in case the extra data is in an old parsed format foreach (@metadata) { diff --git a/perllib/FixMyStreet/DB/Result/Problem.pm b/perllib/FixMyStreet/DB/Result/Problem.pm index c73f7efca..f67e0b0f8 100644 --- a/perllib/FixMyStreet/DB/Result/Problem.pm +++ b/perllib/FixMyStreet/DB/Result/Problem.pm @@ -341,7 +341,7 @@ around service => sub { sub title_safe { my $self = shift; - return _('Awaiting moderation') if $self->cobrand eq 'zurich' && $self->state eq 'unconfirmed'; + return _('Awaiting moderation') if $self->cobrand eq 'zurich' && $self->state eq 'submitted'; return $self->title; } @@ -509,6 +509,18 @@ sub tokenised_url { return "/M/". $token->token; } +=head2 is_hidden + +Returns 1 if the problem is in an hidden state otherwise 0. + +=cut + +sub is_hidden { + my $self = shift; + + return exists $self->hidden_states->{ $self->state } ? 1 : 0; +} + =head2 is_open Returns 1 if the problem is in a open state otherwise 0. @@ -641,22 +653,27 @@ sub body { my $body; if ($problem->external_body) { if ($problem->cobrand eq 'zurich') { - $body = $c->model('DB::Body')->find({ id => $problem->external_body }); + my $cache = $problem->result_source->schema->cache; + return $cache->{bodies}{$problem->external_body} //= $c->model('DB::Body')->find({ id => $problem->external_body }); } else { $body = $problem->external_body; } } else { my $bodies = $problem->bodies; - $body = join( _(' and '), - map { - my $name = $_->name; - if ($c and FixMyStreet->config('AREA_LINKS_FROM_PROBLEMS')) { - '<a href="' . $_->url($c) . '">' . $name . '</a>'; - } else { - $name; - } - } values %$bodies - ); + my @body_names = sort map { + my $name = $_->name; + if ($c and FixMyStreet->config('AREA_LINKS_FROM_PROBLEMS')) { + '<a href="' . $_->url($c) . '">' . $name . '</a>'; + } else { + $name; + } + } values %$bodies; + if ( scalar @body_names > 2 ) { + $body = join( ', ', splice @body_names, 0, -1); + $body = join( ',' . _(' and '), ($body, $body_names[-1])); + } else { + $body = join( _(' and '), @body_names); + } } return $body; } @@ -906,12 +923,11 @@ sub add_send_method { } sub as_hashref { - my $self = shift; - my $c = shift; + my ($self, $c, $cols) = @_; my $state_t = FixMyStreet::DB->resultset("State")->display($self->state); - return { + my $out = { id => $self->id, title => $self->title, category => $self->category, @@ -923,16 +939,17 @@ sub as_hashref { state => $self->state, state_t => $state_t, used_map => $self->used_map, - is_fixed => $self->fixed_states->{ $self->state } ? 1 : 0, - photos => [ map { $_->{url} } @{$self->photos} ], - meta => $self->confirmed ? $self->meta_line( $c ) : '', - ($self->confirmed ? ( - confirmed => $self->confirmed, - confirmed_pp => $c->cobrand->prettify_dt( $self->confirmed ), - ) : ()), - created => $self->created, - created_pp => $c->cobrand->prettify_dt( $self->created ), + created => $self->created, }; + $out->{is_fixed} = $self->fixed_states->{ $self->state } ? 1 : 0 if !$cols || $cols->{is_fixed}; + $out->{photos} = [ map { $_->{url} } @{$self->photos} ] if !$cols || $cols->{photos}; + $out->{meta} = $self->confirmed ? $self->meta_line( $c ) : '' if !$cols || $cols->{meta}; + $out->{created_pp} = $c->cobrand->prettify_dt( $self->created ) if !$cols || $cols->{created_pp}; + if ($self->confirmed) { + $out->{confirmed} = $self->confirmed if !$cols || $cols->{confirmed}; + $out->{confirmed_pp} = $c->cobrand->prettify_dt( $self->confirmed ) if !$cols || $cols->{confirmed_pp}; + } + return $out; } =head2 latest_moderation_log_entry diff --git a/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm b/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm index 5a2029eb1..73e0d898e 100644 --- a/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm +++ b/perllib/FixMyStreet/DB/Result/ResponseTemplate.pm @@ -35,6 +35,8 @@ __PACKAGE__->add_columns( { data_type => "boolean", default_value => \"false", is_nullable => 0 }, "state", { data_type => "text", is_nullable => 1 }, + "external_status_code", + { data_type => "text", is_nullable => 1 }, ); __PACKAGE__->set_primary_key("id"); __PACKAGE__->add_unique_constraint("response_templates_body_id_title_key", ["body_id", "title"]); @@ -52,8 +54,8 @@ __PACKAGE__->has_many( ); -# Created by DBIx::Class::Schema::Loader v0.07035 @ 2016-12-01 15:10:52 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ySPzQpFwJNki8XBjCNiqZQ +# Created by DBIx::Class::Schema::Loader v0.07048 @ 2018-03-22 11:18:36 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:p0+/jFma6H9eZ3MZAJQRaQ __PACKAGE__->many_to_many( contacts => 'contact_response_templates', 'contact' ); diff --git a/perllib/FixMyStreet/DB/Result/Session.pm b/perllib/FixMyStreet/DB/Result/Session.pm index 4713c99eb..a478c5444 100644 --- a/perllib/FixMyStreet/DB/Result/Session.pm +++ b/perllib/FixMyStreet/DB/Result/Session.pm @@ -24,5 +24,24 @@ __PACKAGE__->set_primary_key("id"); # Created by DBIx::Class::Schema::Loader v0.07017 @ 2012-03-08 17:19:55 # DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:MVmCn4gLQWXTDIIaDHiVmA -# You can replace this text with custom code or comments, and it will be preserved on regeneration +use Storable; +use MIME::Base64; + +sub id_code { + my $self = shift; + my $id = $self->id; + $id =~ s/^session://; + $id =~ s/\s+$//; + return $id; +} + +sub user { + my $self = shift; + return unless $self->session_data; + my $data = Storable::thaw(MIME::Base64::decode($self->session_data)); + return unless $data->{__user}; + my $user = $self->result_source->schema->resultset("User")->find($data->{__user}{id}); + return $user; +} + 1; diff --git a/perllib/FixMyStreet/DB/Result/User.pm b/perllib/FixMyStreet/DB/Result/User.pm index db68236bf..8b539f85d 100644 --- a/perllib/FixMyStreet/DB/Result/User.pm +++ b/perllib/FixMyStreet/DB/Result/User.pm @@ -20,10 +20,14 @@ __PACKAGE__->add_columns( }, "email", { data_type => "text", is_nullable => 1 }, + "email_verified", + { data_type => "boolean", default_value => \"false", is_nullable => 0 }, "name", { data_type => "text", is_nullable => 1 }, "phone", { data_type => "text", is_nullable => 1 }, + "phone_verified", + { data_type => "boolean", default_value => \"false", is_nullable => 0 }, "password", { data_type => "text", default_value => "", is_nullable => 0 }, "from_body", @@ -42,10 +46,20 @@ __PACKAGE__->add_columns( { data_type => "integer", is_nullable => 1 }, "extra", { data_type => "text", is_nullable => 1 }, - "email_verified", - { data_type => "boolean", default_value => \"false", is_nullable => 0 }, - "phone_verified", - { data_type => "boolean", default_value => \"false", is_nullable => 0 }, + "created", + { + data_type => "timestamp", + default_value => \"current_timestamp", + is_nullable => 0, + original => { default_value => \"now()" }, + }, + "last_active", + { + data_type => "timestamp", + default_value => \"current_timestamp", + is_nullable => 0, + original => { default_value => \"now()" }, + }, ); __PACKAGE__->set_primary_key("id"); __PACKAGE__->add_unique_constraint("users_facebook_id_key", ["facebook_id"]); @@ -105,8 +119,8 @@ __PACKAGE__->has_many( ); -# Created by DBIx::Class::Schema::Loader v0.07035 @ 2017-09-19 18:02:17 -# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:OKHKCSahWD3Ov6ulj+2f/w +# Created by DBIx::Class::Schema::Loader v0.07035 @ 2018-05-23 18:54:36 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:/V7+Ygv/t6VX8dDhNGN16w # These are not fully unique constraints (they only are when the *_verified # is true), but this is managed in ResultSet::User's find() wrapper. @@ -442,6 +456,25 @@ sub adopt { $other->delete; } +sub anonymize_account { + my $self = shift; + + $self->problems->update({ anonymous => 1, name => '', send_questionnaire => 0 }); + $self->comments->update({ anonymous => 1, name => '' }); + $self->alerts->update({ whendisabled => \'current_timestamp' }); + $self->password('', 1); + $self->update({ + email => 'removed-' . $self->id . '@' . FixMyStreet->config('EMAIL_DOMAIN'), + email_verified => 0, + name => '', + phone => '', + phone_verified => 0, + title => undef, + twitter_id => undef, + facebook_id => undef, + }); +} + # Planned reports / shortlist # Override the default auto-created function as we only want one live entry so @@ -511,4 +544,11 @@ has categories => ( }, ); +sub set_last_active { + my $self = shift; + my $time = shift; + $self->unset_extra_metadata('inactive_email_sent'); + $self->last_active($time or \'current_timestamp'); +} + 1; diff --git a/perllib/FixMyStreet/DB/ResultSet/DefectType.pm b/perllib/FixMyStreet/DB/ResultSet/DefectType.pm index b2ef77f7c..5b1247129 100644 --- a/perllib/FixMyStreet/DB/ResultSet/DefectType.pm +++ b/perllib/FixMyStreet/DB/ResultSet/DefectType.pm @@ -4,7 +4,6 @@ use base 'DBIx::Class::ResultSet'; use strict; use warnings; use Moo; -use HTML::Entities; with('FixMyStreet::Roles::ContactExtra'); @@ -16,10 +15,10 @@ sub map_extras { my ($rs, @ts) = @_; return map { my $meta = $_->get_extra_metadata(); - my %extra = map { $_ => encode_entities($meta->{$_}) } keys %$meta; + my %extra = map { $_ => $meta->{$_} } keys %$meta; { id => $_->id, - name => encode_entities($_->name), + name => $_->name, extra => \%extra } } @ts; diff --git a/perllib/FixMyStreet/DB/ResultSet/Nearby.pm b/perllib/FixMyStreet/DB/ResultSet/Nearby.pm index 6e5e0220f..b075e3664 100644 --- a/perllib/FixMyStreet/DB/ResultSet/Nearby.pm +++ b/perllib/FixMyStreet/DB/ResultSet/Nearby.pm @@ -10,22 +10,26 @@ sub to_body { } sub nearby { - my ( $rs, $c, $dist, $ids, $limit, $mid_lat, $mid_lon, $categories, $states ) = @_; + my ( $rs, $c, $dist, $ids, $limit, $mid_lat, $mid_lon, $categories, $states, $extra_params ) = @_; unless ( $states ) { $states = FixMyStreet::DB::Result::Problem->visible_states(); } my $params = { - non_public => 0, state => [ keys %$states ], }; $params->{id} = { -not_in => $ids } if $ids; $params->{category} = $categories if $categories && @$categories; + FixMyStreet::DB::ResultSet::Problem->non_public_if_possible($params, $c); + $rs = $c->cobrand->problems_restriction($rs); + # Add in any optional extra query parameters + $params = { %$params, %$extra_params } if $extra_params; + my $attrs = { prefetch => 'problem', bind => [ $mid_lat, $mid_lon, $dist ], diff --git a/perllib/FixMyStreet/DB/ResultSet/Problem.pm b/perllib/FixMyStreet/DB/ResultSet/Problem.pm index 3f083c073..ef078ed08 100644 --- a/perllib/FixMyStreet/DB/ResultSet/Problem.pm +++ b/perllib/FixMyStreet/DB/ResultSet/Problem.pm @@ -15,15 +15,41 @@ sub set_restriction { $site_key = $key; } -sub to_body { - my ($rs, $bodies, $join) = @_; - return $rs unless $bodies; +sub body_query { + my ($rs, $bodies) = @_; unless (ref $bodies eq 'ARRAY') { $bodies = [ map { ref $_ ? $_->id : $_ } $bodies ]; } + \[ "regexp_split_to_array(bodies_str, ',') && ?", [ {} => $bodies ] ] +} + +# Edits PARAMS in place to either hide non_public reports, or show them +# if user is superuser (all) or inspector (correct body) +sub non_public_if_possible { + my ($rs, $params, $c) = @_; + if ($c->user_exists) { + if ($c->user->is_superuser) { + # See all reports, no restriction + } elsif ($c->user->has_body_permission_to('report_inspect')) { + $params->{'-or'} = [ + non_public => 0, + $rs->body_query($c->user->from_body->id), + ]; + } else { + $params->{non_public} = 0; + } + } else { + $params->{non_public} = 0; + } +} + +sub to_body { + my ($rs, $bodies, $join) = @_; + return $rs unless $bodies; $join = { join => 'problem' } if $join; $rs = $rs->search( - \[ "regexp_split_to_array(bodies_str, ',') && ?", [ {} => $bodies ] ], + # This isn't using $rs->body_query because $rs might be Problem, Comment, or Nearby + FixMyStreet::DB::ResultSet::Problem->body_query($bodies), $join ); return $rs; @@ -92,8 +118,8 @@ sub _recent { my $key = $photos ? 'recent_photos' : 'recent'; $key .= ":$site_key:$num"; - # unconfirmed might be returned for e.g. Zurich, but would mean in moderation, so no photo - my @states = grep { $_ ne 'unconfirmed' } FixMyStreet::DB::Result::Problem->visible_states(); + # submitted might be returned for e.g. Zurich, but would mean in moderation, so no photo + my @states = grep { $_ ne 'submitted' } FixMyStreet::DB::Result::Problem->visible_states(); my $query = { non_public => 0, state => \@states, @@ -106,34 +132,25 @@ sub _recent { }; my $probs; - my $new = 0; - if (defined $lat) { - my $dist2 = $dist; # Create a copy of the variable to stop it being stringified into a locale in the next line! - $key .= ":$lat:$lon:$dist2"; - $probs = Memcached::get($key); - unless ($probs) { - $attrs->{bind} = [ $lat, $lon, $dist ]; - $attrs->{join} = 'nearby'; - $probs = [ mySociety::Locale::in_gb_locale { - $rs->search( $query, $attrs )->all; - } ]; - $new = 1; - } + if (defined $lat) { # No caching + $attrs->{bind} = [ $lat, $lon, $dist ]; + $attrs->{join} = 'nearby'; + $probs = [ mySociety::Locale::in_gb_locale { + $rs->search( $query, $attrs )->all; + } ]; } else { $probs = Memcached::get($key); - unless ($probs) { + if ($probs) { + # Need to reattach schema so that confirmed column gets reinflated. + $probs->[0]->result_source->schema( $rs->result_source->schema ) if $probs->[0]; + # Catch any cached ones since hidden + $probs = [ grep { ! $_->is_hidden } @$probs ]; + } else { $probs = [ $rs->search( $query, $attrs )->all ]; - $new = 1; + Memcached::set($key, $probs, 3600); } } - if ( $new ) { - Memcached::set($key, $probs, 3600); - } else { - # Need to reattach schema so that confirmed column gets reinflated. - $probs->[0]->result_source->schema( $rs->result_source->schema ) if $probs->[0]; - } - return $probs; } @@ -151,13 +168,17 @@ sub around_map { } my $q = { - non_public => 0, state => [ keys %{$p{states}} ], latitude => { '>=', $p{min_lat}, '<', $p{max_lat} }, longitude => { '>=', $p{min_lon}, '<', $p{max_lon} }, }; $q->{category} = $p{categories} if $p{categories} && @{$p{categories}}; + $rs->non_public_if_possible($q, $c); + + # Add in any optional extra query parameters + $q = { %$q, %{$p{extra}} } if $p{extra}; + my $problems = mySociety::Locale::in_gb_locale { $rs->search( $q, $attr )->include_comment_counts->page($p{page}); }; diff --git a/perllib/FixMyStreet/DB/ResultSet/ResponsePriority.pm b/perllib/FixMyStreet/DB/ResultSet/ResponsePriority.pm index 89bb4dfd7..96f7cf7a0 100644 --- a/perllib/FixMyStreet/DB/ResultSet/ResponsePriority.pm +++ b/perllib/FixMyStreet/DB/ResultSet/ResponsePriority.pm @@ -4,7 +4,6 @@ use base 'DBIx::Class::ResultSet'; use strict; use warnings; use Moo; -use HTML::Entities; with('FixMyStreet::Roles::ContactExtra'); @@ -14,7 +13,7 @@ sub join_table { sub map_extras { my ($rs, @ts) = @_; - return map { { id => $_->id, name => encode_entities($_->name) } } @ts; + return map { { id => $_->id, name => $_->name } } @ts; } 1; diff --git a/perllib/FixMyStreet/DB/ResultSet/ResponseTemplate.pm b/perllib/FixMyStreet/DB/ResultSet/ResponseTemplate.pm index aa070daa3..46fcba153 100644 --- a/perllib/FixMyStreet/DB/ResultSet/ResponseTemplate.pm +++ b/perllib/FixMyStreet/DB/ResultSet/ResponseTemplate.pm @@ -2,7 +2,6 @@ package FixMyStreet::DB::ResultSet::ResponseTemplate; use base 'DBIx::Class::ResultSet'; use Moo; -use HTML::Entities; with('FixMyStreet::Roles::ContactExtra'); @@ -17,8 +16,8 @@ sub name_column { sub map_extras { my ($rs, @ts) = @_; return map { - my $out = { id => encode_entities($_->text), name => encode_entities($_->title) }; - $out->{state} = encode_entities($_->state) if $_->state; + my $out = { id => $_->text, name => $_->title }; + $out->{state} = $_->state if $_->state; $out; } @ts; } diff --git a/perllib/FixMyStreet/Geocode/Address.pm b/perllib/FixMyStreet/Geocode/Address.pm new file mode 100644 index 000000000..522091f62 --- /dev/null +++ b/perllib/FixMyStreet/Geocode/Address.pm @@ -0,0 +1,28 @@ +package FixMyStreet::Geocode::Address; + +use strict; +use warnings; + +use overload '""' => \&as_string, fallback => 1; + +sub new { + my ($class, $data) = @_; + my $self = { %$data }; + bless $self, $class; +} + +sub as_string { + my $self = shift; + + my $data = sprintf(_("Nearest road to the pin placed on the map (automatically generated by Bing Maps): %s"), + $self->{name}) . "\n\n"; + + if ($self->{postcode}) { + $data .= sprintf(_("Nearest postcode to the pin placed on the map (automatically generated): %s (%sm away)"), + $self->{postcode}{postcode}, $self->{postcode}{distance}) . "\n\n"; + } + + return $data; +} + +1; diff --git a/perllib/FixMyStreet/Geocode/Bing.pm b/perllib/FixMyStreet/Geocode/Bing.pm index a846f3348..9e425441a 100644 --- a/perllib/FixMyStreet/Geocode/Bing.pm +++ b/perllib/FixMyStreet/Geocode/Bing.pm @@ -20,6 +20,8 @@ sub string { my ( $s, $c ) = @_; my $params = $c->cobrand->disambiguate_location($s); + # Allow cobrand to fixup the user input + $s = $params->{string} if $params->{string}; $s = FixMyStreet::Geocode::escape($s); $s .= '+' . $params->{town} if $params->{town} and $s !~ /$params->{town}/i; diff --git a/perllib/FixMyStreet/Geocode/FixaMinGata.pm b/perllib/FixMyStreet/Geocode/FixaMinGata.pm deleted file mode 100644 index 3ad98b148..000000000 --- a/perllib/FixMyStreet/Geocode/FixaMinGata.pm +++ /dev/null @@ -1,176 +0,0 @@ -# FixMyStreet:Geocode::FixaMinGata -# OpenStreetmap forward and reverse geocoding for FixMyStreet. -# -# Copyright (c) 2011 Petter Reinholdtsen. Some rights reserved. -# Email: pere@hungry.com - -# This module is a slightly derived version of OSM.pm. - -# As of January 2014, the FixaMinGata developers are considering to make further -# changes related to OSM, so it's probably best to keep this module separate -# from the OSM module for now. - -package FixMyStreet::Geocode::FixaMinGata; - -use warnings; -use strict; - -use LWP::Simple; -use Memcached; -use XML::Simple; -use Utils; - -my $osmapibase = "http://www.openstreetmap.org/api/"; -my $nominatimbase = "http://nominatim.openstreetmap.org/"; - -# string STRING CONTEXT -# Looks up on Nominatim, and caches, a user-inputted location. -# Returns array of (LAT, LON, ERROR), where ERROR is either undef, a string, or -# an array of matches if there are more than one. The information in the query -# may be used to disambiguate the location in cobranded versions of the site. -sub string { - my ( $s, $c ) = @_; - - my $params = $c->cobrand->disambiguate_location($s); - - $s = FixMyStreet::Geocode::escape($s); - # $s .= '+' . $params->{town} if $params->{town} and $s !~ /$params->{town}/i; - - my $url = "${nominatimbase}search?"; - my %query_params = ( - q => $s, - format => 'json', - addressdetails => 1, - limit => 20, - #'accept-language' => '', - email => 'info' . chr(64) . 'morus.se', - ); - # $query_params{viewbox} = $params->{bounds}[1] . ',' . $params->{bounds}[2] . ',' . $params->{bounds}[3] . ',' . $params->{bounds}[0] - # if $params->{bounds}; - $query_params{countrycodes} = $params->{country} - if $params->{country}; - $url .= join('&', map { "$_=$query_params{$_}" } keys %query_params); - - my $js = FixMyStreet::Geocode::cache('osm', $url); - if (!$js) { - return { error => _('Sorry, we could not find that location.') }; - } - - my ( %locations, $error, @valid_locations, $latitude, $longitude ); - foreach (@$js) { - next if $_->{class} eq "boundary"; - my @s = split(/,/, $_->{display_name}); - my $address = join(",", @s[0,1,2]); - $locations{$address} = [$_->{lat}, $_->{lon}]; - } - - foreach my $key (keys %locations) { - ( $latitude, $longitude ) = - map { Utils::truncate_coordinate($_) } - ($locations{$key}[0], $locations{$key}[1]); - push (@$error, { - address => $key, - latitude => $latitude, - longitude => $longitude - }); - push (@valid_locations, $_); - } - - return { latitude => $latitude, longitude => $longitude } if scalar @valid_locations == 1; - return { error => $error }; -} - -sub reverse_geocode { - my ($latitude, $longitude, $zoom) = @_; - my $url = - "${nominatimbase}reverse?format=xml&zoom=$zoom&lat=$latitude&lon=$longitude"; - my $key = "OSM:reverse_geocode:$url"; - my $result = Memcached::get($key); - unless ($result) { - my $j = LWP::Simple::get($url); - if ($j) { - Memcached::set($key, $j, 3600); - my $ref = XMLin($j); - return $ref; - } else { - print STDERR "No reply from $url\n"; - } - return undef; - } - return XMLin($result); -} - -sub _osmxml_to_hash { - my ($xml, $type) = @_; - my $ref = XMLin($xml); - my %tags; - if ('ARRAY' eq ref $ref->{$type}->{tag}) { - map { $tags{$_->{'k'}} = $_->{'v'} } @{$ref->{$type}->{tag}}; - return \%tags; - } else { - return undef; - } -} - -sub get_object_tags { - my ($type, $id) = @_; - my $url = "${osmapibase}0.6/$type/$id"; - my $key = "OSM:get_object_tags:$url"; - my $result = Memcached::get($key); - unless ($result) { - my $j = LWP::Simple::get($url); - if ($j) { - Memcached::set($key, $j, 3600); - return _osmxml_to_hash($j, $type); - } else { - print STDERR "No reply from $url\n"; - } - return undef; - } - return _osmxml_to_hash($result, $type); -} - -# A better alternative might be -# http://www.geonames.org/maps/osm-reverse-geocoder.html#findNearbyStreetsOSM -sub get_nearest_road_tags { - my ( $cobrand, $latitude, $longitude ) = @_; - my $inforef = reverse_geocode($latitude, $longitude, 16); - if (exists $inforef->{result}->{osm_type} - && 'way' eq $inforef->{result}->{osm_type}) { - my $osmtags = get_object_tags('way', - $inforef->{result}->{osm_id}); - unless ( exists $osmtags->{operator} ) { - $osmtags->{operatorguess} = $cobrand->guess_road_operator( $osmtags ); - } - return $osmtags; - } - return undef; -} - -sub closest_road_text { - my ( $cobrand, $latitude, $longitude ) = @_; - my $str = ''; - my $osmtags = get_nearest_road_tags( $cobrand, $latitude, $longitude ); - if ($osmtags) { - my ($name, $ref) = ('',''); - $name = $osmtags->{name} if exists $osmtags->{name}; - $ref = " ($osmtags->{ref})" if exists $osmtags->{ref}; - if ($name || $ref) { - $str .= _('The following information about the nearest road might be inaccurate or irrelevant, if the problem is close to several roads or close to a road without a name registered in OpenStreetMap.') . "\n\n"; - $str .= sprintf(_("Nearest named road to the pin placed on the map (automatically generated using OpenStreetMap): %s%s"), - $name, $ref) . "\n\n"; - - if (my $operator = $osmtags->{operator}) { - $str .= sprintf(_("Road operator for this named road (from OpenStreetMap): %s"), - $operator) . "\n\n"; - } elsif ($operator = $osmtags->{operatorguess}) { - $str .= sprintf(_("Road operator for this named road (derived from road reference number and type): %s"), - $operator) . "\n\n"; - } - } - } - return $str; -} - -1; - diff --git a/perllib/FixMyStreet/Geocode/Google.pm b/perllib/FixMyStreet/Geocode/Google.pm index 162101953..ad1881541 100644 --- a/perllib/FixMyStreet/Geocode/Google.pm +++ b/perllib/FixMyStreet/Geocode/Google.pm @@ -19,6 +19,8 @@ sub string { my ( $s, $c ) = @_; my $params = $c->cobrand->disambiguate_location($s); + # Allow cobrand to fixup the user input + $s = $params->{string} if $params->{string}; my $components = ""; diff --git a/perllib/FixMyStreet/Geocode/OSM.pm b/perllib/FixMyStreet/Geocode/OSM.pm index 020be3c2a..4d57007c5 100644 --- a/perllib/FixMyStreet/Geocode/OSM.pm +++ b/perllib/FixMyStreet/Geocode/OSM.pm @@ -26,6 +26,8 @@ sub string { my ( $s, $c ) = @_; my $params = $c->cobrand->disambiguate_location($s); + # Allow cobrand to fixup the user input + $s = $params->{string} if $params->{string}; $s = FixMyStreet::Geocode::escape($s); $s .= '+' . $params->{town} if $params->{town} and $s !~ /$params->{town}/i; @@ -39,6 +41,8 @@ sub string { ); $query_params{viewbox} = $params->{bounds}[1] . ',' . $params->{bounds}[2] . ',' . $params->{bounds}[3] . ',' . $params->{bounds}[0] if $params->{bounds}; + $query_params{bounded} = 1 + if $params->{bounds}; $query_params{countrycodes} = $params->{country} if $params->{country}; $url .= join('&', map { "$_=$query_params{$_}" } sort keys %query_params); @@ -50,11 +54,13 @@ sub string { my ( $error, @valid_locations, $latitude, $longitude ); foreach (@$js) { + $c->cobrand->call_hook(geocoder_munge_results => $_); ( $latitude, $longitude ) = map { Utils::truncate_coordinate($_) } ( $_->{lat}, $_->{lon} ); push (@$error, { address => $_->{display_name}, + icon => $_->{icon}, latitude => $latitude, longitude => $longitude }); diff --git a/perllib/FixMyStreet/Integrations/ExorRDI.pm b/perllib/FixMyStreet/Integrations/ExorRDI.pm index dc865e1ad..ce59df9be 100644 --- a/perllib/FixMyStreet/Integrations/ExorRDI.pm +++ b/perllib/FixMyStreet/Integrations/ExorRDI.pm @@ -113,9 +113,9 @@ sub construct { my $location = "${eastings}E ${northings}N"; $location = "[DID NOT USE MAP] $location" unless $report->used_map; - my $closest_address = $cobrand->find_closest($report, 1); + my $closest_address = $cobrand->find_closest($report); if (%$closest_address) { - $location .= " Nearest road: $closest_address->{road}." if $closest_address->{road}; + $location .= " Nearest road: $closest_address->{name}." if $closest_address->{name}; $location .= " Nearest postcode: $closest_address->{postcode}{postcode}." if $closest_address->{postcode}; } diff --git a/perllib/FixMyStreet/Map.pm b/perllib/FixMyStreet/Map.pm index 91c198913..f5d4c1db6 100644 --- a/perllib/FixMyStreet/Map.pm +++ b/perllib/FixMyStreet/Map.pm @@ -104,7 +104,7 @@ sub map_features { my $limit = 20; my @ids = map { $_->id } @$on_map; $nearby = $c->model('DB::Nearby')->nearby( - $c, $dist, \@ids, $limit, @p{"latitude", "longitude", "categories", "states"} + $c, $dist, \@ids, $limit, @p{"latitude", "longitude", "categories", "states", "extra"} ); } diff --git a/perllib/FixMyStreet/Map/BathNES.pm b/perllib/FixMyStreet/Map/BathNES.pm new file mode 100644 index 000000000..9c9c3c11d --- /dev/null +++ b/perllib/FixMyStreet/Map/BathNES.pm @@ -0,0 +1,18 @@ +# FixMyStreet:Map::BathNES +# More JavaScript, for street assets + +package FixMyStreet::Map::BathNES; +use base 'FixMyStreet::Map::OSM'; + +use strict; + +sub map_javascript { [ + '/vendor/OpenLayers/OpenLayers.bathnes.js', + '/vendor/OpenLayers.Projection.OrdnanceSurvey.js', + '/js/map-OpenLayers.js', + '/js/map-OpenStreetMap.js', + '/cobrands/fixmystreet/assets.js', + '/cobrands/bathnes/js.js', +] } + +1;
\ No newline at end of file diff --git a/perllib/FixMyStreet/Map/Bromley.pm b/perllib/FixMyStreet/Map/Bromley.pm index 1310c2a5a..22e4147f6 100644 --- a/perllib/FixMyStreet/Map/Bromley.pm +++ b/perllib/FixMyStreet/Map/Bromley.pm @@ -10,10 +10,11 @@ use base 'FixMyStreet::Map::FMS'; use strict; sub map_javascript { [ - '/vendor/OpenLayers/OpenLayers.fixmystreet.js', + '/vendor/OpenLayers/OpenLayers.buckinghamshire.js', '/js/map-OpenLayers.js', '/js/map-bing-ol.js', '/js/map-fms.js', + '/cobrands/fixmystreet/assets.js', '/cobrands/bromley/map.js', ] } diff --git a/perllib/FixMyStreet/Map/Buckinghamshire.pm b/perllib/FixMyStreet/Map/Buckinghamshire.pm new file mode 100644 index 000000000..b6d86d4b9 --- /dev/null +++ b/perllib/FixMyStreet/Map/Buckinghamshire.pm @@ -0,0 +1,19 @@ +# FixMyStreet:Map::Buckinghamshire +# More JavaScript, for street assets + +package FixMyStreet::Map::Buckinghamshire; +use base 'FixMyStreet::Map::OSM'; + +use strict; + +sub map_javascript { [ + '/vendor/OpenLayers/OpenLayers.buckinghamshire.js', + '/vendor/OpenLayers.Projection.OrdnanceSurvey.js', + '/js/map-OpenLayers.js', + '/js/map-OpenStreetMap.js', + '/cobrands/fixmystreet-uk-councils/roadworks.js', + '/cobrands/fixmystreet/assets.js', + '/cobrands/buckinghamshire/js.js', +] } + +1; diff --git a/perllib/FixMyStreet/Map/Zurich.pm b/perllib/FixMyStreet/Map/Zurich.pm index 8b4a3d931..6d9a309ff 100644 --- a/perllib/FixMyStreet/Map/Zurich.pm +++ b/perllib/FixMyStreet/Map/Zurich.pm @@ -22,20 +22,20 @@ sub tile_parameters { my $self = shift; my $params = { urls => [ - 'https://www.gis.stadt-zuerich.ch/maps/rest/services/tiled95/LuftbildHybrid/MapServer/WMTS/tile', - 'https://www.gis.stadt-zuerich.ch/maps/rest/services/tiled95/Stadtplan3D/MapServer/WMTS/tile' + 'http://www.ogc.stadt-zuerich.ch/mapproxy/wmts/', + 'http://www.ogc.stadt-zuerich.ch/mapproxy/wmts/', ], layer_names => [ 'LuftbildHybrid', 'Stadtplan3D' ], wmts_version => '1.0.0', layer_style => 'default', - matrix_set => 'default028mm', - suffix => '.jpg', # appended to tile URLs + matrix_set => 'stzh', + suffix => '.jpeg', # appended to tile URLs size => 512, # pixels dpi => 96, inches_per_unit => 39.3701, # BNG uses metres projection => 'EPSG:2056', - origin_x => -27386400.0, - origin_y => 31814500.0, + origin_x => 2672499.0, + origin_y => 1256999.0, }; return $params; } diff --git a/perllib/FixMyStreet/Script/ArchiveOldEnquiries.pm b/perllib/FixMyStreet/Script/ArchiveOldEnquiries.pm index 5d1d45379..03bc511a0 100644 --- a/perllib/FixMyStreet/Script/ArchiveOldEnquiries.pm +++ b/perllib/FixMyStreet/Script/ArchiveOldEnquiries.pm @@ -14,10 +14,6 @@ use FixMyStreet::Email; my $opts = { commit => 0, - body => '2237', - cobrand => 'oxfordshire', - closure_cutoff => "2015-01-01 00:00:00", - email_cutoff => "2016-01-01 00:00:00", }; sub query { @@ -84,11 +80,7 @@ sub archive { }); printf("Closing %d old reports, without sending emails: ", $problems_to_close->count); - - if ( $opts->{commit} ) { - $problems_to_close->update({ state => 'closed', send_questionnaire => 0 }); - } - + close_problems($problems_to_close); printf("done.\n") } @@ -132,10 +124,46 @@ sub send_email_and_close { unless ( $email_error ) { printf("done.\n Closing reports: "); - - $problems->update({ state => 'closed', send_questionnaire => 0 }); + close_problems($problems); printf("done.\n"); } else { printf("error! Not closing reports for this user.\n") } } + +sub close_problems { + return unless $opts->{commit}; + + my $problems = shift; + while (my $problem = $problems->next) { + my $timestamp = \'current_timestamp'; + my $comment = $problem->add_to_comments( { + text => '', + created => $timestamp, + confirmed => $timestamp, + user_id => $opts->{user}, + name => _('an administrator'), + mark_fixed => 0, + anonymous => 0, + state => 'confirmed', + problem_state => 'closed', + extra => { is_superuser => 1 }, + } ); + $problem->update({ state => 'closed', send_questionnaire => 0 }); + + # Stop any alerts being sent out about this closure. + my @alerts = FixMyStreet::DB->resultset('Alert')->search( { + alert_type => 'new_updates', + parameter => $problem->id, + confirmed => 1, + } ); + + for my $alert (@alerts) { + my $alerts_sent = FixMyStreet::DB->resultset('AlertSent')->find_or_create( { + alert_id => $alert->id, + parameter => $comment->id, + } ); + } + + } +} diff --git a/perllib/FixMyStreet/Script/Inactive.pm b/perllib/FixMyStreet/Script/Inactive.pm new file mode 100644 index 000000000..0468d2a52 --- /dev/null +++ b/perllib/FixMyStreet/Script/Inactive.pm @@ -0,0 +1,174 @@ +package FixMyStreet::Script::Inactive; + +use v5.14; +use warnings; + +use Moo; +use CronFns; +use FixMyStreet; +use FixMyStreet::Cobrand; +use FixMyStreet::DB; +use FixMyStreet::Email; + +has anonymize => ( is => 'ro' ); +has close => ( is => 'ro' ); +has email => ( is => 'ro' ); +has verbose => ( is => 'ro' ); +has dry_run => ( is => 'ro' ); + +sub BUILDARGS { + my ($cls, %args) = @_; + $args{dry_run} = delete $args{'dry-run'}; + return \%args; +} + +has cobrand => ( + is => 'lazy', + default => sub { + my $base_url = FixMyStreet->config('BASE_URL'); + my $site = CronFns::site($base_url); + my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker($site)->new; + $cobrand->set_lang_and_domain(undef, 1); + $cobrand; + }, +); + +has anonymous_user => ( + is => 'lazy', + default => sub { + FixMyStreet::DB->resultset("User")->find_or_create({ + email => 'removed-automatically@' . FixMyStreet->config('EMAIL_DOMAIN'), + }); + } +); + +sub users { + my $self = shift; + + say "DRY RUN" if $self->dry_run; + $self->anonymize_users; + $self->email_inactive_users if $self->email; +} + +sub reports { + my $self = shift; + + say "DRY RUN" if $self->dry_run; + $self->anonymize_reports if $self->anonymize; + $self->close_updates if $self->close; +} + +sub close_updates { + my $self = shift; + + my $problems = FixMyStreet::DB->resultset("Problem")->search({ + lastupdate => { '<', interval($self->close) }, + state => [ FixMyStreet::DB::Result::Problem->closed_states(), FixMyStreet::DB::Result::Problem->fixed_states() ], + extra => [ undef, { -not_like => '%closed_updates%' } ], + }); + + while (my $problem = $problems->next) { + say "Closing updates on problem #" . $problem->id if $self->verbose; + next if $self->dry_run; + $problem->set_extra_metadata( closed_updates => 1 ); + $problem->update; + } +} + +sub anonymize_reports { + my $self = shift; + + # Need to look though them all each time, in case any new updates/alerts + my $problems = FixMyStreet::DB->resultset("Problem")->search({ + lastupdate => { '<', interval($self->anonymize) }, + state => [ + FixMyStreet::DB::Result::Problem->closed_states(), + FixMyStreet::DB::Result::Problem->fixed_states(), + FixMyStreet::DB::Result::Problem->hidden_states(), + ], + }); + + while (my $problem = $problems->next) { + say "Anonymizing problem #" . $problem->id if $self->verbose; + next if $self->dry_run; + + # Remove personal data from the report + $problem->update({ + user => $self->anonymous_user, + name => '', + anonymous => 1, + send_questionnaire => 0, + }) if $problem->user != $self->anonymous_user; + + # Remove personal data from the report's updates + $problem->comments->search({ + user_id => { '!=' => $self->anonymous_user->id }, + })->update({ + user_id => $self->anonymous_user->id, + name => '', + anonymous => 1, + }); + + # Remove alerts - could just delete, but of interest how many there were, perhaps? + FixMyStreet::DB->resultset('Alert')->search({ + user_id => { '!=' => $self->anonymous_user->id }, + alert_type => 'new_updates', + parameter => $problem->id, + })->update({ + user_id => $self->anonymous_user->id, + whendisabled => \'current_timestamp', + }); + } +} + +sub anonymize_users { + my $self = shift; + + my $users = FixMyStreet::DB->resultset("User")->search({ + last_active => { '<', interval($self->anonymize) }, + }); + + while (my $user = $users->next) { + say "Anonymizing user #" . $user->id if $self->verbose; + next if $self->dry_run; + $user->anonymize_account; + } +} + +sub email_inactive_users { + my $self = shift; + + my $users = FixMyStreet::DB->resultset("User")->search({ + last_active => [ -and => { '<', interval($self->email) }, + { '>=', interval($self->anonymize) } ], + }); + while (my $user = $users->next) { + next if $user->get_extra_metadata('inactive_email_sent'); + + say "Emailing user #" . $user->id if $self->verbose; + next if $self->dry_run; + FixMyStreet::Email::send_cron( + $user->result_source->schema, + 'inactive-account.txt', + { + email_from => $self->email, + anonymize_from => $self->anonymize, + user => $user, + url => $self->cobrand->base_url_with_lang . '/my', + }, + { To => [ $user->email, $user->name ] }, + undef, 0, $self->cobrand, + ); + + $user->set_extra_metadata('inactive_email_sent', 1); + $user->update; + } +} + +sub interval { + my $interval = shift; + my $s = "current_timestamp - '$interval months'::interval"; + return \$s; +} + +1; diff --git a/perllib/FixMyStreet/Script/Reports.pm b/perllib/FixMyStreet/Script/Reports.pm index 8e4a4aec1..578d966d6 100644 --- a/perllib/FixMyStreet/Script/Reports.pm +++ b/perllib/FixMyStreet/Script/Reports.pm @@ -29,7 +29,7 @@ sub send(;$) { my $site = $site_override || CronFns::site($base_url); my $states = [ FixMyStreet::DB::Result::Problem::open_states() ]; - $states = [ 'unconfirmed', 'confirmed', 'in progress', 'planned', 'closed', 'investigating' ] if $site eq 'zurich'; + $states = [ 'submitted', 'confirmed', 'in progress', 'feedback pending', 'external', 'wish' ] if $site eq 'zurich'; my $unsent = $rs->search( { state => $states, whensent => undef, @@ -78,6 +78,7 @@ sub send(;$) { my $email_base_url = $cobrand->base_url_for_report($row); my %h = map { $_ => $row->$_ } qw/id title detail name category latitude longitude used_map/; $h{report} = $row; + $h{cobrand} = $cobrand; map { $h{$_} = $row->user->$_ || '' } qw/email phone/; $h{confirmed} = DateTime::Format::Pg->format_datetime( $row->confirmed->truncate (to => 'second' ) ) if $row->confirmed; @@ -88,6 +89,8 @@ sub send(;$) { 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 . $row->photos->[0]->{url_full}; + my @all_images = map { $email_base_url . $_->{url_full} } @{ $row->photos }; + $h{all_image_urls} = \@all_images; } else { $h{has_photo} = ''; $h{image_url} = ''; @@ -178,16 +181,8 @@ sub send(;$) { 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); @@ -300,6 +295,9 @@ sub _send_report_sent_email { # Don't send 'report sent' text return unless $row->user->email_verified; + my $contributed_as = $row->get_extra_metadata('contributed_as') || ''; + return if $contributed_as eq 'body' || $contributed_as eq 'anonymous_user'; + FixMyStreet::Email::send_cron( $row->result_source->schema, 'confirm_report_sent.txt', @@ -308,7 +306,7 @@ sub _send_report_sent_email { To => $row->user->email, From => [ $cobrand->contact_email, $cobrand->contact_name ], }, - $cobrand->contact_email, + undef, $nomail, $cobrand, $row->lang, diff --git a/perllib/FixMyStreet/Script/UpdateAllReports.pm b/perllib/FixMyStreet/Script/UpdateAllReports.pm index d6f3eb64b..21d8d28a0 100755 --- a/perllib/FixMyStreet/Script/UpdateAllReports.pm +++ b/perllib/FixMyStreet/Script/UpdateAllReports.pm @@ -6,23 +6,24 @@ use warnings; use FixMyStreet; use FixMyStreet::Cobrand; use FixMyStreet::DB; +use CronFns; use List::MoreUtils qw(zip); use List::Util qw(sum); +my $site = CronFns::site(FixMyStreet->config('BASE_URL')); + my $fourweeks = 4*7*24*60*60; # Age problems from when they're confirmed, except on Zurich # where they appear as soon as they're created. my $age_column = 'confirmed'; -if ( FixMyStreet->config('BASE_URL') =~ /zurich|zueri/ ) { - $age_column = 'created'; -} +$age_column = 'created' if $site eq 'zurich'; my $dtf = FixMyStreet::DB->schema->storage->datetime_parser; -my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker('default')->new; -FixMyStreet::DB->schema->cobrand($cobrand); +my $cobrand_cls = FixMyStreet::Cobrand->get_class_for_moniker($site)->new; +FixMyStreet::DB->schema->cobrand($cobrand_cls); sub generate { my $include_areas = shift; @@ -34,7 +35,7 @@ sub generate { }, { columns => [ - 'id', 'bodies_str', 'state', 'areas', 'cobrand', + 'id', 'bodies_str', 'state', 'areas', 'cobrand', 'category', { duration => { extract => "epoch from current_timestamp-lastupdate" } }, { age => { extract => "epoch from current_timestamp-$age_column" } }, ] @@ -43,13 +44,26 @@ sub generate { $problems = $problems->cursor; # Raw DB cursor for speed my ( %fixed, %open ); - my @cols = ( 'id', 'bodies_str', 'state', 'areas', 'cobrand', 'duration', 'age' ); + my %stats = ( + fixed => \%fixed, + open => \%open, + ); + my @cols = ( 'id', 'bodies_str', 'state', 'areas', 'cobrand', 'category', 'duration', 'age' ); while ( my @problem = $problems->next ) { my %problem = zip @cols, @problem; - my @bodies; - my @areas; + my @bodies = split( /,/, $problem{bodies_str} ); my $cobrand = $problem{cobrand}; + + if (my $type = $cobrand_cls->call_hook(dashboard_categorize_problem => \%problem)) { + foreach my $body ( @bodies ) { + $stats{$type}{$body}++; + $stats{$cobrand}{$type}{$body}++; + } + next; + } + my $duration_str = ( $problem{duration} > 2 * $fourweeks ) ? 'old' : 'new'; + my $type = ( $problem{duration} > 2 * $fourweeks ) ? 'unknown' : ($problem{age} > $fourweeks ? 'older' : 'new'); @@ -57,9 +71,6 @@ sub generate { FixMyStreet::DB::Result::Problem->fixed_states()->{$problem{state}} || FixMyStreet::DB::Result::Problem->closed_states()->{$problem{state}}; - # Add to bodies it was sent to - @bodies = split( /,/, $problem{bodies_str} ); - foreach my $body ( @bodies ) { if ( $problem_fixed ) { # Fixed problems are either old or new @@ -73,7 +84,7 @@ sub generate { } if ( $include_areas ) { - @areas = grep { $_ } split( /,/, $problem{areas} ); + my @areas = grep { $_ } split( /,/, $problem{areas} ); foreach my $area ( @areas ) { if ( $problem_fixed ) { $fixed{areas}{$area}{$duration_str}++; @@ -84,10 +95,7 @@ sub generate { } } - return { - fixed => \%fixed, - open => \%open, - }; + return \%stats; } sub end_period { diff --git a/perllib/FixMyStreet/SendReport/Email/BathNES.pm b/perllib/FixMyStreet/SendReport/Email/BathNES.pm new file mode 100644 index 000000000..786d36d1e --- /dev/null +++ b/perllib/FixMyStreet/SendReport/Email/BathNES.pm @@ -0,0 +1,16 @@ +package FixMyStreet::SendReport::Email::BathNES; + +use Moo; + +BEGIN { extends 'FixMyStreet::SendReport::Email'; } + +sub get_template { + my ( $self, $row ) = @_; + if ( $row->category eq 'Street Light Fault' ) { + return 'bathnes/submit-street-light-fault.txt'; + } else { + return 'submit.txt'; + } +} + +1; diff --git a/perllib/FixMyStreet/SendReport/Open311.pm b/perllib/FixMyStreet/SendReport/Open311.pm index ecda0bca1..84aa851ed 100644 --- a/perllib/FixMyStreet/SendReport/Open311.pm +++ b/perllib/FixMyStreet/SendReport/Open311.pm @@ -28,6 +28,7 @@ sub send { send_notpinpointed => 0, use_service_as_deviceid => 0, extended_description => 1, + multi_photos => 0, ); my $cobrand = $body->get_cobrand_handler || $row->get_cobrand_logged; @@ -47,13 +48,15 @@ sub send { if ($_->{code} eq $id_field) { push @$extra, { name => $id_field, value => $row->id }; } elsif ($_->{code} eq 'closest_address' && $h->{closest_address}) { - push @$extra, { name => $_->{code}, value => $h->{$_->{code}} }; + push @$extra, { name => $_->{code}, value => "$h->{closest_address}" }; } elsif ($_->{code} =~ /^(easting|northing)$/) { # NB If there's ever a cobrand with always_send_latlong=0 and # send_notpinpointed=0 then this line will need changing to # consider the send_notpinpointed check, as per the # '#NOTPINPOINTED#' code in perllib/Open311.pm. - if ( $row->used_map || $open311_params{always_send_latlong} ) { + if ( $row->used_map || $open311_params{always_send_latlong} || ( + !$row->used_map && !$row->postcode && $open311_params{send_notpinpointed} + ) ) { push @$extra, { name => $_->{code}, value => $h->{$_->{code}} }; } } diff --git a/perllib/FixMyStreet/SendReport/Zurich.pm b/perllib/FixMyStreet/SendReport/Zurich.pm index b38981d94..59adfd688 100644 --- a/perllib/FixMyStreet/SendReport/Zurich.pm +++ b/perllib/FixMyStreet/SendReport/Zurich.pm @@ -44,15 +44,15 @@ sub get_template { my ( $self, $row ) = @_; my $template; - if ( $row->state eq 'unconfirmed' || $row->state eq 'confirmed' ) { + if ( $row->state eq 'submitted' || $row->state eq 'confirmed' ) { $template = 'submit.txt'; } elsif ( $row->state eq 'in progress' ) { $template = 'submit-in-progress.txt'; - } elsif ( $row->state eq 'planned' ) { + } elsif ( $row->state eq 'feedback pending' ) { $template = 'submit-feedback-pending.txt'; - } elsif ( $row->state eq 'investigating' ) { + } elsif ( $row->state eq 'wish' ) { $template = 'submit-external-wish.txt'; - } elsif ( $row->state eq 'closed' ) { + } elsif ( $row->state eq 'external' ) { $template = 'submit-external.txt'; if ( $row->extra->{third_personal} ) { $template = 'submit-external-personal.txt'; diff --git a/perllib/FixMyStreet/TestAppProve.pm b/perllib/FixMyStreet/TestAppProve.pm index d549b0148..0329bcfde 100644 --- a/perllib/FixMyStreet/TestAppProve.pm +++ b/perllib/FixMyStreet/TestAppProve.pm @@ -33,6 +33,55 @@ END { cleanup(); } +my $pg; + +sub spin_up_database { + warn "Spinning up a Pg cluster/database...\n"; + $pg = Test::PostgreSQL->new( + run_psql_args => '-1Xq -v ON_ERROR_STOP=1', # No -b on 9.1 + ); + # On 9.1, must run each file separately + $pg->run_psql_scripts('db/schema.sql'); + $pg->run_psql_scripts('db/fixture.sql'); + $pg->run_psql_scripts('db/generate_secret.sql'); + + warn sprintf "# Connected to %s\n", $pg->dsn; + + return { + FMS_DB_PORT => $pg->port, + FMS_DB_NAME => 'test', + FMS_DB_USER => 'postgres', + FMS_DB_HOST => 'localhost', + FMS_DB_PASS => '', + }; +} + +sub get_config { + my $cls = shift; + my $extra_config = shift; + my $config_file = delete $extra_config->{config_file}; + my $db_config_file = delete $extra_config->{db_config_file}; + + my $config = YAML::Load( path($config_file)->slurp ); + if ($db_config_file) { + my $db_config = YAML::Load( path($db_config_file)->slurp ); + $config->{FMS_DB_PORT} = $db_config->{FMS_DB_PORT}; + $config->{FMS_DB_NAME} = $db_config->{FMS_DB_NAME}; + $config->{FMS_DB_USER} = $db_config->{FMS_DB_USER}; + $config->{FMS_DB_HOST} = $db_config->{FMS_DB_HOST}; + $config->{FMS_DB_PASS} = $db_config->{FMS_DB_PASS}; + } else { + my $new_db_config = $cls->spin_up_database(); + $config = { %$config, %$new_db_config }; + } + + $config = { %$config, %$extra_config }; + + my $config_out = "general-test-autogenerated.$$.yml"; + path("conf/$config_out")->spew( YAML::Dump($config) ); + return $config_out; +} + sub run { my ($class, @args) = @_; local @ARGV = @args; @@ -53,42 +102,7 @@ sub run { 'state=s@' => \@state, ); - my $config = YAML::Load( path($config_file)->slurp ); - my $pg; - if ($db_config_file) { - my $db_config = YAML::Load( path($db_config_file)->slurp ); - $config->{FMS_DB_PORT} = $db_config->{FMS_DB_PORT}; - $config->{FMS_DB_NAME} = $db_config->{FMS_DB_NAME}; - $config->{FMS_DB_USER} = $db_config->{FMS_DB_USER}; - $config->{FMS_DB_HOST} = $db_config->{FMS_DB_HOST}; - $config->{FMS_DB_PASS} = $db_config->{FMS_DB_PASS}; - } - else { - warn "Spinning up a Pg cluster/database...\n"; - $pg = Test::PostgreSQL->new(); - - warn sprintf "# Connected to %s\n", $pg->dsn; - - my $dbh = DBI->connect($pg->dsn); - - my $tmpwarn = $SIG{__WARN__}; - $SIG{__WARN__} = - sub { print STDERR @_ if $_[0] !~ m/NOTICE: CREATE TABLE/; }; - $dbh->do( path('db/schema.sql')->slurp ) or die $!; - $dbh->do( path('db/fixture.sql')->slurp ) or die $!; - $dbh->do( path('db/generate_secret.sql')->slurp ) or die $!; - $SIG{__WARN__} = $tmpwarn; - - $config->{FMS_DB_PORT} = $pg->port; - $config->{FMS_DB_NAME} = 'test'; - $config->{FMS_DB_USER} = 'postgres'; - $config->{FMS_DB_HOST} = 'localhost'; - $config->{FMS_DB_PASS} = ''; - } - - my $config_out = "general-test-autogenerated.$$.yml"; - path("conf/$config_out")->spew( YAML::Dump($config) ); - + my $config_out = $class->get_config({ config_file => $config_file, db_config_file => $db_config_file }); local $ENV{FMS_OVERRIDE_CONFIG} = $config_out; my $prove = App::Prove->new; diff --git a/perllib/FixMyStreet/TestMech.pm b/perllib/FixMyStreet/TestMech.pm index ac2ac023d..c5b72a7cf 100644 --- a/perllib/FixMyStreet/TestMech.pm +++ b/perllib/FixMyStreet/TestMech.pm @@ -247,7 +247,7 @@ sub get_text_body_from_email { } sub get_link_from_email { - my ($mech, $email, $multiple) = @_; + my ($mech, $email, $multiple, $mismatch) = @_; unless ($email) { $email = $mech->get_email; $mech->clear_emails_ok; @@ -261,7 +261,7 @@ sub get_link_from_email { if (@links) { # Must be an HTML part now, first two links are in header my @html_links = $part->body =~ m{https?://[^"]+}g; - is $links[0], $html_links[2], 'HTML link matches text link'; + is $links[0], $html_links[2], 'HTML link matches text link' unless $mismatch; } else { @links = $part->body =~ m{https?://\S+}g; ok @links, "Found links in email '@links'"; |