diff options
Diffstat (limited to 'perllib/FixMyStreet/App/Controller')
17 files changed, 645 insertions, 213 deletions
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)$/; |