diff options
author | Marius Halden <marius.h@lden.org> | 2017-05-28 21:31:42 +0200 |
---|---|---|
committer | Marius Halden <marius.h@lden.org> | 2017-05-28 21:31:42 +0200 |
commit | 987124b09a32248414faf4d0d6615d43b29ac6f6 (patch) | |
tree | a549db8af723c981d3b346e855f25d6fd5ff8aa7 /perllib/FixMyStreet/App | |
parent | dbf56159e44c1560a413022451bf1a1c4cb22a52 (diff) | |
parent | a085b63ce09f87e83b75cda9b9cd08aadfe75d61 (diff) |
Merge tag 'v2.0.4' into fiksgatami-dev
Diffstat (limited to 'perllib/FixMyStreet/App')
21 files changed, 757 insertions, 177 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm index bbdf449aa..1f3307710 100644 --- a/perllib/FixMyStreet/App/Controller/Admin.pm +++ b/perllib/FixMyStreet/App/Controller/Admin.pm @@ -7,7 +7,7 @@ BEGIN { extends 'Catalyst::Controller'; } use Path::Class; use POSIX qw(strftime strcoll); use Digest::SHA qw(sha1_hex); -use mySociety::EmailUtil qw(is_valid_email); +use mySociety::EmailUtil qw(is_valid_email is_valid_email_list); use mySociety::ArrayUtils; use DateTime::Format::Strptime; use List::Util 'first'; @@ -78,7 +78,7 @@ sub index : Path : Args(0) { $c->forward('stats_by_state'); my @unsent = $c->cobrand->problems->search( { - state => [ 'confirmed' ], + state => [ FixMyStreet::DB::Result::Problem::open_states() ], whensent => undef, bodies_str => { '!=', undef }, } )->all; @@ -357,10 +357,11 @@ sub update_contacts : Private { } ); - my $email = $self->trim( $c->get_param('email') ); + my $email = $c->get_param('email'); + $email =~ s/\s+//g; my $send_method = $c->get_param('send_method') || $contact->send_method || $contact->body->send_method || ""; unless ( $send_method eq 'Open311' ) { - $errors{email} = _('Please enter a valid email') unless is_valid_email($email) || $email eq 'REFUSED'; + $errors{email} = _('Please enter a valid email') unless is_valid_email_list($email) || $email eq 'REFUSED'; } $contact->email( $email ); @@ -732,7 +733,7 @@ sub report_edit : Path('report_edit') : Args(1) { } } - $c->stash->{categories} = $c->forward('categories_for_point'); + $c->forward('categories_for_point'); if ( $c->cobrand->moniker eq 'zurich' ) { my $done = $c->cobrand->admin_report_edit(); @@ -789,11 +790,9 @@ sub report_edit : Path('report_edit') : Args(1) { $c->forward( '/admin/report_edit_category', [ $problem ] ); - if ( $c->get_param('email') ne $problem->user->email ) { - my $user = $c->model('DB::User')->find_or_create( - { email => $c->get_param('email') } - ); - + my $email = lc $c->get_param('email'); + if ( $email ne $problem->user->email ) { + my $user = $c->model('DB::User')->find_or_create({ email => $email }); $user->insert unless $user->in_storage; $problem->user( $user ); } @@ -910,7 +909,8 @@ sub categories_for_point : Private { # Remove the "Pick a category" option shift @{$c->stash->{category_options}} if @{$c->stash->{category_options}}; - return $c->stash->{category_options}; + $c->stash->{categories} = $c->stash->{category_options}; + $c->stash->{categories_hash} = { map { $_ => 1 } @{$c->stash->{category_options}} }; } sub templates : Path('templates') : Args(0) { @@ -978,6 +978,7 @@ sub template_edit : Path('templates') : Args(2) { } else { $template->title( $c->get_param('title') ); $template->text( $c->get_param('text') ); + $template->state( $c->get_param('state') ); $template->auto_response( $c->get_param('auto_response') ? 1 : 0 ); $template->update_or_insert; @@ -1005,10 +1006,9 @@ sub load_template_body : Private { my ($self, $c, $body_id) = @_; my $zurich_user = $c->user->from_body && $c->cobrand->moniker eq 'zurich'; - my $has_permission = $c->user->has_body_permission_to('template_edit') && - $c->user->from_body->id eq $body_id; + my $has_permission = $c->user->has_body_permission_to('template_edit', $body_id); - unless ( $c->user->is_superuser || $zurich_user || $has_permission ) { + unless ( $zurich_user || $has_permission ) { $c->detach( '/page_error_404_not_found', [] ); } @@ -1117,8 +1117,9 @@ sub update_edit : Path('update_edit') : Args(1) { # $update->name can be null which makes ne unhappy my $name = $update->name || ''; + my $email = lc $c->get_param('email'); if ( $c->get_param('name') ne $name - || $c->get_param('email') ne $update->user->email + || $email ne $update->user->email || $c->get_param('anonymous') ne $update->anonymous || $c->get_param('text') ne $update->text ) { $edited = 1; @@ -1138,11 +1139,8 @@ sub update_edit : Path('update_edit') : Args(1) { $update->anonymous( $c->get_param('anonymous') ); $update->state( $new_state ); - if ( $c->get_param('email') ne $update->user->email ) { - my $user = - $c->model('DB::User') - ->find_or_create( { email => $c->get_param('email') } ); - + if ( $email ne $update->user->email ) { + my $user = $c->model('DB::User')->find_or_create({ email => $email }); $user->insert unless $user->in_storage; $update->user($user); } @@ -1207,7 +1205,7 @@ sub user_add : Path('user_edit') : Args(0) { my $user = $c->model('DB::User')->find_or_create( { name => $c->get_param('name'), - email => $c->get_param('email'), + email => lc $c->get_param('email'), phone => $c->get_param('phone') || undef, from_body => $c->get_param('body') || undef, flagged => $c->get_param('flagged') || 0, @@ -1217,13 +1215,13 @@ sub user_add : Path('user_edit') : Args(0) { key => 'users_email_key' } ); $c->stash->{user} = $user; + $c->forward('user_cobrand_extra_fields'); + $user->update; $c->forward( 'log_edit', [ $user->id, 'user', 'edit' ] ); - $c->stash->{status_message} = - '<p><em>' . _('Updated!') . '</em></p>'; - - return 1; + $c->flash->{status_message} = _("Updated!"); + $c->res->redirect( $c->uri_for( 'user_edit', $user->id ) ); } sub user_edit : Path('user_edit') : Args(1) { @@ -1234,7 +1232,7 @@ sub user_edit : Path('user_edit') : Args(1) { my $user = $c->cobrand->users->find( { id => $id } ); $c->detach( '/page_error_404_not_found', [] ) unless $user; - unless ( $c->user->is_superuser || $c->user->has_body_permission_to('user_edit') || $c->cobrand->moniker eq 'zurich' ) { + unless ( $c->user->has_body_permission_to('user_edit') || $c->cobrand->moniker eq 'zurich' ) { $c->detach('/page_error_403_access_denied', []); } @@ -1247,12 +1245,18 @@ sub user_edit : Path('user_edit') : Args(1) { $c->forward('fetch_all_bodies'); $c->forward('fetch_body_areas', [ $user->from_body ]) if $user->from_body; + if ( defined $c->flash->{status_message} ) { + $c->stash->{status_message} = + '<p><em>' . $c->flash->{status_message} . '</em></p>'; + } + if ( $c->get_param('submit') ) { $c->forward('/auth/check_csrf_token'); my $edited = 0; - if ( $user->email ne $c->get_param('email') || + my $email = lc $c->get_param('email'); + if ( $user->email ne $email || $user->name ne $c->get_param('name') || ($user->phone || "") ne $c->get_param('phone') || ($user->from_body && $c->get_param('body') && $user->from_body->id ne $c->get_param('body')) || @@ -1262,7 +1266,8 @@ sub user_edit : Path('user_edit') : Args(1) { } $user->name( $c->get_param('name') ); - $user->email( $c->get_param('email') ); + my $original_email = $user->email; + $user->email( $email ); $user->phone( $c->get_param('phone') ) if $c->get_param('phone'); $user->flagged( $c->get_param('flagged') || 0 ); # Only superusers can grant superuser status @@ -1278,6 +1283,8 @@ sub user_edit : Path('user_edit') : Args(1) { $user->from_body( undef ); } + $c->forward('user_cobrand_extra_fields'); + # Has the user's from_body changed since we fetched areas (if we ever did)? # If so, we need to re-fetch areas so the UI is up to date. if ( $user->from_body && $user->from_body->id ne $c->stash->{fetched_areas_body_id} ) { @@ -1361,19 +1368,24 @@ sub user_edit : Path('user_edit') : Args(1) { return if %{$c->stash->{field_errors}}; my $existing_user = $c->model('DB::User')->search({ email => $user->email, id => { '!=', $user->id } })->first; - if ($existing_user) { + my $existing_user_cobrand = $c->cobrand->users->search({ email => $user->email, id => { '!=', $user->id } })->first; + if ($existing_user_cobrand) { $existing_user->adopt($user); $c->forward( 'log_edit', [ $id, 'user', 'merge' ] ); $c->res->redirect( $c->uri_for( 'user_edit', $existing_user->id ) ); } else { + if ($existing_user) { + # Tried to change email to an existing one lacking permission + # so make sure it's switched back + $user->email($original_email); + } $user->update; if ($edited) { $c->forward( 'log_edit', [ $id, 'user', 'edit' ] ); } + $c->flash->{status_message} = _("Updated!"); + $c->res->redirect( $c->uri_for( 'user_edit', $user->id ) ); } - - $c->stash->{status_message} = - '<p><em>' . _('Updated!') . '</em></p>'; } if ( $user->from_body ) { @@ -1395,6 +1407,15 @@ sub user_edit : Path('user_edit') : Args(1) { return 1; } +sub user_cobrand_extra_fields : Private { + my ( $self, $c ) = @_; + + my @extra_fields = @{ $c->cobrand->call_hook('user_extra_fields') || [] }; + foreach ( @extra_fields ) { + $c->stash->{user}->set_extra_metadata( $_ => $c->get_param("extra[$_]") ); + } +} + sub flagged : Path('flagged') : Args(0) { my ( $self, $c ) = @_; @@ -1465,7 +1486,7 @@ sub stats : Path('stats') : Args(0) { $selected_body = $c->user->from_body->id; } - if ( $c->cobrand->moniker eq 'seesomething' || $c->cobrand->moniker eq 'zurich' ) { + if ( $c->cobrand->moniker eq 'zurich' ) { return $c->cobrand->admin_stats(); } @@ -1612,7 +1633,7 @@ accordingly sub ban_user : Private { my ( $self, $c ) = @_; - my $email = $c->get_param('email'); + my $email = lc $c->get_param('email'); return unless $email; @@ -1639,7 +1660,7 @@ Sets the flag on a user with the given email sub flag_user : Private { my ( $self, $c ) = @_; - my $email = $c->get_param('email'); + my $email = lc $c->get_param('email'); return unless $email; @@ -1667,7 +1688,7 @@ Remove the flag on a user with the given email sub remove_user_flag : Private { my ( $self, $c ) = @_; - my $email = $c->get_param('email'); + my $email = lc $c->get_param('email'); return unless $email; diff --git a/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm b/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm new file mode 100644 index 000000000..bcfeb3dd8 --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Admin/DefectTypes.pm @@ -0,0 +1,113 @@ +package FixMyStreet::App::Controller::Admin::DefectTypes; +use Moose; +use namespace::autoclean; +use mySociety::ArrayUtils; + +BEGIN { extends 'Catalyst::Controller'; } + + +sub begin : Private { + my ( $self, $c ) = @_; + + $c->forward('/admin/begin'); +} + +sub index : Path : Args(0) { + my ( $self, $c ) = @_; + + my $user = $c->user; + + if ($user->is_superuser) { + $c->forward('/admin/fetch_all_bodies'); + } elsif ( $user->from_body ) { + $c->forward('load_user_body', [ $user->from_body->id ]); + $c->res->redirect( $c->uri_for( '', $c->stash->{body}->id ) ); + } else { + $c->detach( '/page_error_404_not_found' ); + } +} + +sub list : Path : Args(1) { + my ($self, $c, $body_id) = @_; + + $c->forward('load_user_body', [ $body_id ]); + + my @defect_types = $c->stash->{body}->defect_types->search( + undef, + { + order_by => 'name' + } + ); + + $c->stash->{defect_types} = \@defect_types; +} + +sub edit : Path : Args(2) { + my ( $self, $c, $body_id, $defect_type_id ) = @_; + + $c->forward('load_user_body', [ $body_id ]); + + my $defect_type; + if ($defect_type_id eq 'new') { + $defect_type = $c->stash->{body}->defect_types->new({}); + } + else { + $defect_type = $c->stash->{body}->defect_types->find( $defect_type_id ) + or $c->detach( '/page_error_404_not_found' ); + } + + $c->forward('/admin/fetch_contacts'); + my @contacts = $defect_type->contacts->all; + my @live_contacts = $c->stash->{live_contacts}->all; + my %active_contacts = map { $_->id => 1 } @contacts; + my @all_contacts = map { { + id => $_->id, + category => $_->category, + active => $active_contacts{$_->id}, + } } @live_contacts; + $c->stash->{contacts} = \@all_contacts; + + if ($c->req->method eq 'POST') { + $defect_type->name( $c->get_param('name') ); + $defect_type->description( $c->get_param('description') ); + + my @extra_fields = @{ $c->cobrand->call_hook('defect_type_extra_fields') || [] }; + foreach ( @extra_fields ) { + $defect_type->set_extra_metadata( $_ => $c->get_param("extra[$_]") ); + } + + $defect_type->update_or_insert; + my @live_contact_ids = map { $_->id } @live_contacts; + my @new_contact_ids = $c->get_param_list('categories'); + @new_contact_ids = @{ mySociety::ArrayUtils::intersection(\@live_contact_ids, \@new_contact_ids) }; + $defect_type->contact_defect_types->search({ + contact_id => { '!=' => \@new_contact_ids }, + })->delete; + foreach my $contact_id (@new_contact_ids) { + $defect_type->contact_defect_types->find_or_create({ + contact_id => $contact_id, + }); + } + + $c->res->redirect( $c->uri_for( '', $c->stash->{body}->id ) ); + } + + $c->stash->{defect_type} = $defect_type; +} + +sub load_user_body : Private { + my ($self, $c, $body_id) = @_; + + my $has_permission = $c->user->has_body_permission_to('defect_type_edit', $body_id); + + unless ( $has_permission ) { + $c->detach( '/page_error_404_not_found' ); + } + + $c->stash->{body} = $c->model('DB::Body')->find($body_id) + or $c->detach( '/page_error_404_not_found' ); +} + +__PACKAGE__->meta->make_immutable; + +1; diff --git a/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm b/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm new file mode 100644 index 000000000..201742c81 --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Admin/ExorDefects.pm @@ -0,0 +1,230 @@ +package FixMyStreet::App::Controller::Admin::ExorDefects; +use Moose; +use namespace::autoclean; + +use Text::CSV; +use DateTime; +use mySociety::Random qw(random_bytes); + +BEGIN { extends 'Catalyst::Controller'; } + + +sub begin : Private { + my ( $self, $c ) = @_; + + $c->forward('/admin/begin'); +} + +sub index : Path : Args(0) { + my ( $self, $c ) = @_; + + foreach (qw(error_message start_date end_date user_id)) { + if ( defined $c->flash->{$_} ) { + $c->stash->{$_} = $c->flash->{$_}; + } + } + + my @inspectors = $c->cobrand->users->search({ + 'user_body_permissions.permission_type' => 'report_inspect' + }, { + join => 'user_body_permissions', + distinct => 1, + } + )->all; + $c->stash->{inspectors} = \@inspectors; + + # Default start/end date is today + my $now = DateTime->now( time_zone => + FixMyStreet->time_zone || FixMyStreet->local_time_zone ); + $c->stash->{start_date} ||= $now; + $c->stash->{end_date} ||= $now; + +} + +sub download : Path('download') : Args(0) { + my ( $self, $c ) = @_; + + if ( !$c->cobrand->can('exor_rdi_link_id') ) { + # This only works on the Oxfordshire cobrand currently. + $c->detach( '/page_error_404_not_found', [] ); + } + + my $parser = DateTime::Format::Strptime->new( pattern => '%d/%m/%Y' ); + 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 ); + + my %params = ( + -and => [ + state => [ 'action scheduled' ], + external_id => { '!=' => undef }, + 'admin_log_entries.action' => 'inspected', + 'admin_log_entries.whenedited' => { '>=', $start_date }, + 'admin_log_entries.whenedited' => { '<=', $end_date + $one_day }, + ] + ); + + my $user; + if ( $c->get_param('user_id') ) { + my $uid = $c->get_param('user_id'); + $params{'admin_log_entries.user_id'} = $uid; + $user = $c->model('DB::User')->find( { id => $uid } ); + } + + my $problems = $c->cobrand->problems->search( + \%params, + { + join => 'admin_log_entries', + distinct => 1, + } + ); + + if ( !$problems->count ) { + if ( defined $user ) { + $c->flash->{error_message} = _("No inspections by that inspector in the selected date range."); + } else { + $c->flash->{error_message} = _("No inspections in the selected date range."); + } + $c->flash->{start_date} = $start_date; + $c->flash->{end_date} = $end_date; + $c->flash->{user_id} = $user->id if $user; + $c->res->redirect( $c->uri_for( '' ) ); + } + + # A single RDI file might contain inspections from multiple inspectors, so + # we need to group inspections by inspector within G records. + my $inspectors = {}; + my $inspector_initials = {}; + while ( my $report = $problems->next ) { + my $user = $report->inspection_log_entry->user; + $inspectors->{$user->id} ||= []; + push @{ $inspectors->{$user->id} }, $report; + unless ( $inspector_initials->{$user->id} ) { + $inspector_initials->{$user->id} = $user->get_extra_metadata('initials'); + } + } + + my $csv = Text::CSV->new({ binary => 1, eol => "" }); + + my $p_count = 0; + my $link_id = $c->cobrand->exor_rdi_link_id; + + # RDI first line is always the same + $csv->combine("1", "1.8", "1.0.0.0", "ENHN", ""); + my @body = ($csv->string); + + my $i = 0; + foreach my $inspector_id (keys %$inspectors) { + my $inspections = $inspectors->{$inspector_id}; + my $initials = $inspector_initials->{$inspector_id}; + + $csv->combine( + "G", # start of an area/sequence + $link_id, # area/link id, fixed value for our purposes + "","", # must be empty + $initials || "XX", # inspector initials + $start_date->strftime("%y%m%d"), # date of inspection yymmdd + "0700", # time of inspection hhmm, set to static value for now + "D", # inspection variant, should always be D + "INS", # inspection type, always INS + "N", # Area of the county - north (N) or south (S) + "", "", "", "" # empty fields + ); + push @body, $csv->string; + + $csv->combine( + "H", # initial inspection type + "MC" # minor carriageway (changes depending on activity code) + ); + push @body, $csv->string; + + foreach my $report (@$inspections) { + my ($eastings, $northings) = $report->local_coords; + my $description = sprintf("%s %s", $report->external_id || "", $report->get_extra_metadata('detailed_information') || ""); + my $activity_code = $report->defect_type ? + $report->defect_type->get_extra_metadata('activity_code') + : 'MC'; + my $traffic_information = $report->get_extra_metadata('traffic_information') ? + 'TM ' . $report->get_extra_metadata('traffic_information') + : 'TM none'; + + $csv->combine( + "I", # beginning of defect record + $activity_code, # activity code - minor carriageway, also FC (footway) + "", # empty field, can also be A (seen on MC) or B (seen on FC) + sprintf("%03d", ++$i), # randomised sequence number + "${eastings}E ${northings}N", # defect location field, which we don't capture from inspectors + $report->inspection_log_entry->whenedited->strftime("%H%M"), # defect time raised + "","","","","","","", # empty fields + $traffic_information, + $description, # defect description + ); + push @body, $csv->string; + + my $defect_type = $report->defect_type ? + $report->defect_type->get_extra_metadata('defect_code') + : 'SFP2'; + $csv->combine( + "J", # georeferencing record + $defect_type, # defect type - SFP2: sweep and fill <1m2, POT2 also seen + $report->response_priority ? + $report->response_priority->external_id : + "2", # priority of defect + "","", # empty fields + $eastings, # eastings + $northings, # northings + "","","","","" # empty fields + ); + push @body, $csv->string; + + $csv->combine( + "M", # bill of quantities record + "resolve", # permanent repair + "","", # empty fields + "/CMC", # /C + activity code + "", "" # empty fields + ); + push @body, $csv->string; + } + + # end this group of defects with a P record + $csv->combine( + "P", # end of area/sequence + 0, # always 0 + 999999, # charging code, always 999999 in OCC + ); + push @body, $csv->string; + $p_count++; + } + + # end the RDI file with an X record + my $record_count = $i; + $csv->combine( + "X", # end of inspection record + $p_count, + $p_count, + $record_count, # number of I records + $record_count, # number of J records + 0, 0, 0, # always zero + $record_count, # number of M records + 0, # always zero + $p_count, + 0, 0, 0 # error counts, always zero + ); + push @body, $csv->string; + + my $start = $start_date->strftime("%Y%m%d"); + my $end = $end_date->strftime("%Y%m%d"); + my $filename = sprintf("exor_defects-%s-%s.rdi", $start, $end); + if ( $user ) { + my $initials = $user->get_extra_metadata("initials") || ""; + $filename = sprintf("exor_defects-%s-%s-%s.rdi", $start, $end, $initials); + } + $c->res->content_type('text/csv; charset=utf-8'); + $c->res->header('content-disposition' => "attachment; filename=$filename"); + # The RDI format is very weird CSV - each line must be wrapped in + # double quotes. + $c->res->body( join "", map { "\"$_\"\r\n" } @body ); +} + +1;
\ No newline at end of file diff --git a/perllib/FixMyStreet/App/Controller/Admin/ResponsePriorities.pm b/perllib/FixMyStreet/App/Controller/Admin/ResponsePriorities.pm index 032e593c6..bae0f71a7 100644 --- a/perllib/FixMyStreet/App/Controller/Admin/ResponsePriorities.pm +++ b/perllib/FixMyStreet/App/Controller/Admin/ResponsePriorities.pm @@ -70,6 +70,7 @@ sub edit : Path : Args(2) { $priority->deleted( $c->get_param('deleted') ? 1 : 0 ); $priority->name( $c->get_param('name') ); $priority->description( $c->get_param('description') ); + $priority->external_id( $c->get_param('external_id') ); $priority->update_or_insert; my @live_contact_ids = map { $_->id } @live_contacts; @@ -92,10 +93,9 @@ sub edit : Path : Args(2) { sub load_user_body : Private { my ($self, $c, $body_id) = @_; - my $has_permission = $c->user->has_body_permission_to('responsepriority_edit') && - $c->user->from_body->id eq $body_id; + my $has_permission = $c->user->has_body_permission_to('responsepriority_edit', $body_id); - unless ( $c->user->is_superuser || $has_permission ) { + unless ( $has_permission ) { $c->detach( '/page_error_404_not_found' ); } diff --git a/perllib/FixMyStreet/App/Controller/Around.pm b/perllib/FixMyStreet/App/Controller/Around.pm index b4f94bb35..1fe35d0a3 100644 --- a/perllib/FixMyStreet/App/Controller/Around.pm +++ b/perllib/FixMyStreet/App/Controller/Around.pm @@ -182,7 +182,7 @@ sub display_location : Private { my ( $on_map_all, $on_map, $nearby, $distance ) = FixMyStreet::Map::map_features( $c, latitude => $latitude, longitude => $longitude, - interval => $interval, categories => $c->stash->{filter_category}, + interval => $interval, categories => [ keys %{$c->stash->{filter_category}} ], states => $c->stash->{filter_problem_states}, order => $c->stash->{sort_order}, ); @@ -264,8 +264,8 @@ sub check_and_stash_category : Private { my %categories_mapped = map { $_ => 1 } @categories; my $categories = [ $c->get_param_list('filter_category', 1) ]; - my @valid_categories = grep { $_ && $categories_mapped{$_} } @$categories; - $c->stash->{filter_category} = \@valid_categories; + my %valid_categories = map { $_ => 1 } grep { $_ && $categories_mapped{$_} } @$categories; + $c->stash->{filter_category} = \%valid_categories; } =head2 /ajax @@ -291,6 +291,8 @@ sub ajax : Path('/ajax') { # assume this is not cacheable - may need to be more fine-grained later $c->res->header( 'Cache_Control' => 'max-age=0' ); + $c->stash->{page} = 'around'; # Needed by _item.html + # how far back should we go? my $all_pins = $c->get_param('all_pins') ? 1 : undef; my $interval = $all_pins ? undef : $c->cobrand->on_map_default_max_pin_age; diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm index 40cd163cf..4efa7abb8 100644 --- a/perllib/FixMyStreet/App/Controller/Auth.pm +++ b/perllib/FixMyStreet/App/Controller/Auth.pm @@ -70,6 +70,7 @@ sub sign_in : Private { my ( $self, $c, $email ) = @_; $email ||= $c->get_param('email') || ''; + $email = lc $email; my $password = $c->get_param('password_sign_in') || ''; my $remember_me = $c->get_param('remember_me') || 0; @@ -103,7 +104,7 @@ sub sign_in : Private { Email the user the details they need to sign in. Don't check for an account - if there isn't one we can create it when they come back with a token (which -contains the email addresss). +contains the email address). =cut @@ -222,7 +223,7 @@ sub token : Path('/M') : Args(1) { $c->authenticate( { email => $user->email }, 'no_password' ); # send the user to their page - $c->detach( 'redirect_on_signin', [ $data->{r} ] ); + $c->detach( 'redirect_on_signin', [ $data->{r}, $data->{p} ] ); } =head2 facebook_sign_in @@ -271,9 +272,8 @@ sub facebook_callback: Path('/auth/Facebook') : Args(0) { $access_token = $fb->get_access_token(code => $c->get_param('code')); }; if ($@) { - ($c->stash->{message} = $@) =~ s/at [^ ]*Auth.pm.*//; - $c->stash->{template} = 'errors/generic.html'; - $c->detach; + (my $message = $@) =~ s/at [^ ]*Auth.pm.*//; + $c->detach('/page_error_500_internal_error', [ $message ]); } # save this token in session @@ -339,9 +339,8 @@ sub twitter_callback: Path('/auth/Twitter') : Args(0) { $twitter->request_access_token(verifier => $verifier); }; if ($@) { - ($c->stash->{message} = $@) =~ s/at [^ ]*Auth.pm.*//; - $c->stash->{template} = 'errors/generic.html'; - $c->detach; + (my $message = $@) =~ s/at [^ ]*Auth.pm.*//; + $c->detach('/page_error_500_internal_error', [ $message ]); } my $info = $twitter->verify_credentials(); @@ -412,13 +411,36 @@ Used after signing in to take the person back to where they were. sub redirect_on_signin : Private { - my ( $self, $c, $redirect ) = @_; - $redirect = 'my' unless $redirect; - $redirect = 'my' if $redirect =~ /^admin/ && !$c->user->is_superuser; + my ( $self, $c, $redirect, $params ) = @_; + unless ( $redirect ) { + $c->detach('redirect_to_categories') if $c->user->from_body && scalar @{ $c->user->categories }; + $redirect = 'my'; + } + $redirect = 'my' if $redirect =~ /^admin/ && !$c->cobrand->admin_allow_user($c->user); if ( $c->cobrand->moniker eq 'zurich' ) { $redirect = 'admin' if $c->user->from_body; } - $c->res->redirect( $c->uri_for( "/$redirect" ) ); + if (defined $params) { + $c->res->redirect( $c->uri_for( "/$redirect", $params ) ); + } else { + $c->res->redirect( $c->uri_for( "/$redirect" ) ); + } +} + +=head2 redirect_to_categories + +Redirects the user to their body's reports page, prefiltered to whatever +categories this user has been assigned to. + +=cut + +sub redirect_to_categories : Private { + my ( $self, $c ) = @_; + + my $categories = join(',', @{ $c->user->categories }); + my $body_short = $c->cobrand->short_name( $c->user->from_body ); + + $c->res->redirect( $c->uri_for( "/reports/" . $body_short, { filter_category => $categories } ) ); } =head2 redirect @@ -518,17 +540,17 @@ sub check_csrf_token : Private { $token =~ s/ /+/g; my ($time) = $token =~ /^(\d+)-[0-9a-zA-Z+\/]+$/; $c->stash->{csrf_time} = $time; + my $gen_token = $c->forward('get_csrf_token'); + delete $c->stash->{csrf_time}; $c->detach('no_csrf_token') unless $time && $time > time() - 3600 - && $token eq $c->forward('get_csrf_token'); - delete $c->stash->{csrf_time}; + && $token eq $gen_token; } sub no_csrf_token : Private { my ($self, $c) = @_; - $c->stash->{message} = _('Unknown error'); - $c->stash->{template} = 'errors/generic.html'; + $c->detach('/page_error_400_bad_request', []); } =head2 sign_out diff --git a/perllib/FixMyStreet/App/Controller/Dashboard.pm b/perllib/FixMyStreet/App/Controller/Dashboard.pm index 9189b28e5..fbe5a2dc9 100644 --- a/perllib/FixMyStreet/App/Controller/Dashboard.pm +++ b/perllib/FixMyStreet/App/Controller/Dashboard.pm @@ -57,9 +57,9 @@ sub example : Local : Args(0) { } }; if ($@) { - $c->stash->{message} = _("There was a problem showing this page. Please try again later.") . ' ' . + my $message = _("There was a problem showing this page. Please try again later.") . ' ' . sprintf(_('The error was: %s'), $@); - $c->stash->{template} = 'errors/generic.html'; + $c->detach('/page_error_500_internal_error', [ $message ]); } } diff --git a/perllib/FixMyStreet/App/Controller/Moderate.pm b/perllib/FixMyStreet/App/Controller/Moderate.pm index 94e6cd62a..74f2e6b31 100644 --- a/perllib/FixMyStreet/App/Controller/Moderate.pm +++ b/perllib/FixMyStreet/App/Controller/Moderate.pm @@ -83,6 +83,11 @@ sub moderate_report : Chained('report') : PathPart('') : Args(0) { $c->detach( 'report_moderate_audit', \@types ) } +sub moderating_user_name { + my $user = shift; + return $user->from_body ? $user->from_body->name : 'a FixMyStreet administrator'; +} + sub report_moderate_audit : Private { my ($self, $c, @types) = @_; @@ -95,7 +100,7 @@ sub report_moderate_audit : Private { $c->model('DB::AdminLog')->create({ action => 'moderation', user => $user, - admin_user => $user->name, + admin_user => moderating_user_name($user), object_id => $problem->id, object_type => 'problem', reason => (sprintf '%s (%s)', $reason, $types_csv), @@ -249,7 +254,7 @@ sub update_moderate_audit : Private { $c->model('DB::AdminLog')->create({ action => 'moderation', user => $user, - admin_user => $user->name, + admin_user => moderating_user_name($user), object_id => $comment->id, object_type => 'update', reason => (sprintf '%s (%s)', $reason, $types_csv), diff --git a/perllib/FixMyStreet/App/Controller/My.pm b/perllib/FixMyStreet/App/Controller/My.pm index 51f1687ee..77711f807 100644 --- a/perllib/FixMyStreet/App/Controller/My.pm +++ b/perllib/FixMyStreet/App/Controller/My.pm @@ -3,6 +3,7 @@ use Moose; use namespace::autoclean; use JSON::MaybeXS; +use List::MoreUtils qw(first_index); BEGIN { extends 'Catalyst::Controller'; } @@ -30,8 +31,11 @@ sub begin : Private { sub my : Path : Args(0) { my ( $self, $c ) = @_; + $c->forward('/auth/get_csrf_token'); + $c->stash->{problems_rs} = $c->cobrand->problems->search( { user_id => $c->user->id }); + $c->forward('/reports/stash_report_sort', [ 'created-desc' ]); $c->forward('get_problems'); if ($c->get_param('ajax')) { $c->detach('/reports/ajax', [ 'my/_problem-list.html' ]); @@ -43,21 +47,58 @@ sub my : Path : Args(0) { sub planned : Local : Args(0) { my ( $self, $c ) = @_; + $c->forward('/auth/get_csrf_token'); + $c->detach('/page_error_403_access_denied', []) unless $c->user->has_body_permission_to('planned_reports'); $c->stash->{problems_rs} = $c->user->active_planned_reports; + $c->forward('planned_reorder'); + $c->forward('/reports/stash_report_sort', [ 'shortlist' ]); $c->forward('get_problems'); + if ($c->get_param('ajax')) { + $c->stash->{shortlist} = $c->stash->{sort_key} eq 'shortlist'; + $c->detach('/reports/ajax', [ 'my/_problem-list.html' ]); + } $c->forward('setup_page_data'); } +sub planned_reorder : Private { + my ($self, $c) = @_; + + my @extra = grep { /^shortlist-(up|down|\d+)$/ } keys %{$c->req->params}; + return unless @extra; + my ($reorder) = $extra[0] =~ /^shortlist-(up|down|\d+)$/; + + my @shortlist = sort by_shortlisted $c->stash->{problems_rs}->all; + + # Find where moving problem ID is + my $id = $c->get_param('id') || return; + my $curr_index = first_index { $_->id == $id } @shortlist; + return unless $curr_index > -1; + + if ($reorder eq 'up' && $curr_index > 0) { + @shortlist[$curr_index-1,$curr_index] = @shortlist[$curr_index,$curr_index-1]; + } elsif ($reorder eq 'down' && $curr_index < @shortlist-1) { + @shortlist[$curr_index,$curr_index+1] = @shortlist[$curr_index+1,$curr_index]; + } elsif ($reorder >= 0 && $reorder <= @shortlist-1) { # Must be an index to move it + @shortlist[$curr_index,$reorder] = @shortlist[$reorder,$curr_index]; + } + + # Store new ordering + my $i = 1; + foreach (@shortlist) { + $_->set_extra_metadata('order', $i++); + $_->update; + } +} + sub get_problems : Private { my ($self, $c) = @_; my $p_page = $c->get_param('p') || 1; $c->forward( '/reports/stash_report_filter_status' ); - $c->forward('/reports/stash_report_sort', [ 'created-desc' ]); my $pins = []; my $problems = []; @@ -70,12 +111,15 @@ sub get_problems : Private { my $categories = [ $c->get_param_list('filter_category', 1) ]; if ( @$categories ) { $params->{category} = $categories; - $c->stash->{filter_category} = $categories; + $c->stash->{filter_category} = { map { $_ => 1 } @$categories }; } + my $rows = 50; + $rows = 5000 if $c->stash->{sort_key} eq 'shortlist'; # Want all reports + my $rs = $c->stash->{problems_rs}->search( $params, { order_by => $c->stash->{sort_order}, - rows => 50 + rows => $rows, } )->include_comment_counts->page( $p_page ); while ( my $problem = $rs->next ) { @@ -83,6 +127,9 @@ sub get_problems : Private { push @$pins, $problem->pin_data($c, 'my', private => 1); push @$problems, $problem; } + + @$problems = sort by_shortlisted @$problems if $c->stash->{sort_key} eq 'shortlist'; + $c->stash->{problems_pager} = $rs->pager; $c->stash->{problems} = $problems; $c->stash->{pins} = $pins; @@ -134,27 +181,45 @@ sub planned_change : Path('planned/change') { my ($self, $c) = @_; $c->forward('/auth/check_csrf_token'); + $c->go('planned') if grep { /^shortlist-(up|down|\d+)$/ } keys %{$c->req->params}; + my $id = $c->get_param('id'); $c->forward( '/report/load_problem_or_display_error', [ $id ] ); - my $change = $c->get_param('change'); + my $add = $c->get_param('shortlist-add'); + my $remove = $c->get_param('shortlist-remove'); $c->detach('/page_error_403_access_denied', []) - unless $change && $change =~ /add|remove/; + unless $add || $remove; - if ($change eq 'add') { + if ($add) { $c->user->add_to_planned_reports($c->stash->{problem}); - } elsif ($change eq 'remove') { + } elsif ($remove) { $c->user->remove_from_planned_reports($c->stash->{problem}); } if ($c->get_param('ajax')) { $c->res->content_type('application/json; charset=utf-8'); - $c->res->body(encode_json({ outcome => $change })); + $c->res->body(encode_json({ outcome => $add ? 'add' : 'remove' })); } else { $c->res->redirect( $c->uri_for_action('report/display', $id) ); } } +sub by_shortlisted { + my $a_order = $a->get_extra_metadata('order') || 0; + my $b_order = $b->get_extra_metadata('order') || 0; + if ($a_order && $b_order) { + $a_order <=> $b_order; + } elsif ($a_order) { + -1; # Want non-ordered to come last + } elsif ($b_order) { + 1; # Want non-ordered to come last + } else { + # Default to order added to planned reports + $a->user_planned_reports->first->id <=> $b->user_planned_reports->first->id; + } +} + __PACKAGE__->meta->make_immutable; 1; diff --git a/perllib/FixMyStreet/App/Controller/Offline.pm b/perllib/FixMyStreet/App/Controller/Offline.pm new file mode 100644 index 000000000..dceccc81f --- /dev/null +++ b/perllib/FixMyStreet/App/Controller/Offline.pm @@ -0,0 +1,47 @@ +package FixMyStreet::App::Controller::Offline; +use Moose; +use namespace::autoclean; + +BEGIN { extends 'Catalyst::Controller'; } + +=head1 NAME + +FixMyStreet::App::Controller::Offline - Catalyst Controller + +=head1 DESCRIPTION + +Offline pages Catalyst Controller. +On staging site, appcache only for people who want it. + +=head1 METHODS + +=cut + +sub have_appcache : Private { + my ($self, $c) = @_; + return $c->user_exists && $c->user->has_body_permission_to('planned_reports') + && !FixMyStreet->staging_flag('enable_appcache', 0); +} + +sub manifest : Path("/offline/appcache.manifest") { + my ($self, $c) = @_; + unless ($c->forward('have_appcache')) { + $c->response->status(404); + $c->response->body('NOT FOUND'); + } + $c->res->content_type('text/cache-manifest; charset=utf-8'); + $c->res->header(Cache_Control => 'no-cache, no-store'); +} + +sub appcache : Path("/offline/appcache") { + my ($self, $c) = @_; + $c->detach('/page_error_404_not_found', []) if keys %{$c->req->params}; + unless ($c->forward('have_appcache')) { + $c->response->status(404); + $c->response->body('NOT FOUND'); + } +} + +__PACKAGE__->meta->make_immutable; + +1; diff --git a/perllib/FixMyStreet/App/Controller/Open311.pm b/perllib/FixMyStreet/App/Controller/Open311.pm index 98e5f42b2..bc08593de 100644 --- a/perllib/FixMyStreet/App/Controller/Open311.pm +++ b/perllib/FixMyStreet/App/Controller/Open311.pm @@ -233,44 +233,42 @@ sub output_requests : Private { 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 ], - 'status' => [ $problem->state ], -# 'status_notes' => [ {} ], - 'requested_datetime' => [ w3date($problem->confirmed) ], - 'updated_datetime' => [ w3date($problem->lastupdate) ], -# 'expected_datetime' => [ {} ], -# 'address' => [ {} ], -# 'address_id' => [ {} ], - 'service_code' => [ $problem->category ], - 'service_name' => [ $problem->category ], -# 'service_notice' => [ {} ], -# 'zipcode' => [ {} ], - 'interface_used' => [ $problem->service ], # Not in Open311 v2 + '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, + 'status' => $problem->state, +# 'status_notes' => {}, + 'requested_datetime' => w3date($problem->confirmed), + 'updated_datetime' => w3date($problem->lastupdate), +# 'expected_datetime' => {}, +# 'address' => {}, +# 'address_id' => {}, + 'service_code' => $problem->category, + 'service_name' => $problem->category, +# 'service_notice' => {}, +# 'zipcode' => {}, + 'interface_used' => $problem->service, # Not in Open311 v2 }; if ( $c->cobrand->moniker eq 'zurich' ) { - $request->{service_notice} = [ - $problem->get_extra_metadata('public_response') - ]; + $request->{service_notice} = $problem->get_extra_metadata('public_response'); } else { # FIXME Not according to Open311 v2 - $request->{agency_responsible} = $problem->bodies; + my @body_names = map { $_->name } values %{$problem->bodies}; + $request->{agency_responsible} = {'recipient' => [ @body_names ] }; } if ( !$problem->anonymous ) { # Not in Open311 v2 - $request->{'requestor_name'} = [ $problem->name ]; + $request->{'requestor_name'} = $problem->name; } if ( $problem->whensent ) { # Not in Open311 v2 - $request->{'agency_sent_datetime'} = - [ w3date($problem->whensent) ]; + $request->{'agency_sent_datetime'} = w3date($problem->whensent); } # Extract number of updates @@ -279,25 +277,18 @@ sub output_requests : Private { )->count; if ($updates) { # Not in Open311 v2 - $request->{'comment_count'} = [ $updates ]; + $request->{'comment_count'} = $updates; } my $display_photos = $c->cobrand->allow_photo_display($problem); if ($display_photos && $problem->photo) { my $url = $c->cobrand->base_url(); my $imgurl = $url . $problem->photos->[0]->{url_full}; - $request->{'media_url'} = [ $imgurl ]; + $request->{'media_url'} = $imgurl; } push(@problemlist, $request); } - foreach my $request (@problemlist) { - if ($request->{agency_responsible}) { - my @body_names = map { $_->name } values %{$request->{agency_responsible}} ; - $request->{agency_responsible} = - [ {'recipient' => [ @body_names ] } ]; - } - } $c->forward( 'format_output', [ { 'requests' => [ { 'request' => \@problemlist @@ -432,7 +423,7 @@ sub format_output : Private { $c->res->body( encode_json($hashref) ); } elsif ('xml' eq $format) { $c->res->content_type('application/xml; charset=utf-8'); - $c->res->body( XMLout($hashref, RootName => undef) ); + $c->res->body( XMLout($hashref, RootName => undef, NoAttr => 1 ) ); } else { $c->detach( 'error', [ sprintf(_('Invalid format %s specified.'), $format) diff --git a/perllib/FixMyStreet/App/Controller/Questionnaire.pm b/perllib/FixMyStreet/App/Controller/Questionnaire.pm index 017a552db..1b338732b 100755 --- a/perllib/FixMyStreet/App/Controller/Questionnaire.pm +++ b/perllib/FixMyStreet/App/Controller/Questionnaire.pm @@ -36,9 +36,8 @@ sub check_questionnaire : Private { if ( $questionnaire->whenanswered ) { my $problem_url = $c->cobrand->base_url_for_report( $problem ) . $problem->url; my $contact_url = $c->uri_for( "/contact" ); - $c->stash->{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); - $c->stash->{template} = 'errors/generic.html'; - $c->detach; + 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); + $c->detach('/page_error_400_bad_request', [ $message ]); } unless ( $problem->is_visible ) { @@ -86,8 +85,8 @@ Display couldn't locate problem error message sub missing_problem : Private { my ( $self, $c ) = @_; - $c->stash->{message} = _("I'm afraid we couldn't locate your problem in the database.\n"); - $c->stash->{template} = 'errors/generic.html'; + my $message = _("I'm afraid we couldn't locate your problem in the database.\n"); + $c->detach('/page_error_400_bad_request', [ $message ]); } sub submit_creator_fixed : Private { diff --git a/perllib/FixMyStreet/App/Controller/Report.pm b/perllib/FixMyStreet/App/Controller/Report.pm index 5a1cfbe54..ad2702460 100644 --- a/perllib/FixMyStreet/App/Controller/Report.pm +++ b/perllib/FixMyStreet/App/Controller/Report.pm @@ -275,7 +275,8 @@ sub delete :Local :Args(1) { $p->user->update_reputation(-1); $c->model('DB::AdminLog')->create( { - admin_user => $c->user->email, + user => $c->user->obj, + admin_user => $c->user->from_body->name, object_type => 'problem', action => 'state_change', object_id => $id, @@ -305,7 +306,7 @@ sub inspect : Private { my $problem = $c->stash->{problem}; my $permissions = $c->stash->{_permissions}; - $c->stash->{categories} = $c->forward('/admin/categories_for_point'); + $c->forward('/admin/categories_for_point'); $c->stash->{report_meta} = { map { $_->{name} => $_ } @{ $c->stash->{problem}->get_extra_fields() } }; my %category_body = map { $_->category => $_->body_id } map { $_->contacts->all } values %{$problem->bodies}; @@ -343,12 +344,15 @@ sub inspect : Private { $problem->set_extra_metadata( $_ => $c->get_param($_) ); } - if ( $c->get_param('save_inspected') ) { + if ( $c->get_param('defect_type') ) { + $problem->defect_type($problem->defect_types->find($c->get_param('defect_type'))); + } else { + $problem->defect_type(undef); + } + + if ( $c->get_param('include_update') ) { $update_text = Utils::cleanup_text( $c->get_param('public_update'), { allow_multiline => 1 } ); - if ($update_text) { - $problem->set_extra_metadata( inspected => 1 ); - $reputation_change = 1; - } else { + if (!$update_text) { $valid = 0; $c->stash->{errors} ||= []; push @{ $c->stash->{errors} }, _('Please provide a public update for this report.'); @@ -374,6 +378,16 @@ sub inspect : Private { } if ( $problem->state ne $old_state ) { $c->forward( '/admin/log_edit', [ $problem->id, 'problem', 'state_change' ] ); + + # If the state has been changed by an inspector, consider the + # report to be inspected. + unless ($problem->get_extra_metadata('inspected')) { + $problem->set_extra_metadata( inspected => 1 ); + $c->forward( '/admin/log_edit', [ $problem->id, 'problem', 'inspected' ] ); + my $state = $problem->state; + $reputation_change = 1 if $c->cobrand->reputation_increment_states->{$state}; + $reputation_change = -1 if $c->cobrand->reputation_decrement_states->{$state}; + } } } @@ -408,12 +422,17 @@ sub inspect : Private { $problem->lastupdate( \'current_timestamp' ); $problem->update; if ( defined($update_text) ) { + my $timestamp = \'current_timestamp'; + if (my $saved_at = $c->get_param('saved_at')) { + $timestamp = DateTime->from_epoch( 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 => \'current_timestamp', - confirmed => \'current_timestamp', + created => $timestamp, + confirmed => $timestamp, user_id => $c->user->id, - name => $c->user->from_body->name, + name => $name, state => 'confirmed', mark_fixed => 0, anonymous => 0, @@ -429,6 +448,12 @@ sub inspect : Private { } else { $redirect_uri = $c->uri_for( $problem->url ); } + + # Or if inspector, redirect back to shortlist + if ($c->user->has_body_permission_to('planned_reports')) { + $redirect_uri = $c->uri_for_action('my/planned'); + } + $c->log->debug( "Redirecting to: " . $redirect_uri ); $c->res->redirect( $redirect_uri ); } diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm index e2569d2e9..2a68b170e 100644 --- a/perllib/FixMyStreet/App/Controller/Report/New.pm +++ b/perllib/FixMyStreet/App/Controller/Report/New.pm @@ -83,6 +83,14 @@ sub report_new : Path : Args(0) { $c->forward('initialize_report'); $c->forward('/auth/get_csrf_token'); + my @shortlist = grep { /^shortlist-(add|remove)-(\d+)$/ } keys %{$c->req->params}; + if (@shortlist) { + my ($cmd, $id) = $shortlist[0] =~ /^shortlist-(add|remove)-(\d+)$/; + $c->req->params->{id} = $id; + $c->req->params->{"shortlist-$cmd"} = 1; + $c->detach('/my/planned_change'); + } + # work out the location for this report and do some checks # Also show map if we're just updating the filters return $c->forward('redirect_to_around') @@ -651,8 +659,7 @@ sub setup_categories_and_bodies : Private { push @category_options, _('Other') if $seen{_('Other')}; } - $c->cobrand->munge_category_list(\@category_options, \@contacts, \%category_extras) - if $c->cobrand->can('munge_category_list'); + $c->cobrand->call_hook(munge_category_list => \@category_options, \@contacts, \%category_extras); # put results onto stash for display $c->stash->{bodies} = \%bodies; @@ -895,7 +902,7 @@ sub contacts_to_bodies : Private { if ($c->stash->{unresponsive}{$category} || $c->stash->{unresponsive}{ALL}) { []; } else { - if ( $c->cobrand->can('singleton_bodies_str') && $c->cobrand->singleton_bodies_str ) { + if ( $c->cobrand->call_hook('singleton_bodies_str') ) { # Cobrands like Zurich can only ever have a single body: 'x', because some functionality # relies on string comparison against bodies_str. [ $contacts[0]->body ]; @@ -1025,9 +1032,7 @@ sub send_problem_confirm_email : Private { $template = 'problem-confirm-not-sending.txt' unless $report->bodies_str; $c->stash->{token_url} = $c->uri_for_email( '/P', $token->token ); - if ($c->cobrand->can('problem_confirm_email_extras')) { - $c->cobrand->problem_confirm_email_extras($report); - } + $c->cobrand->call_hook(problem_confirm_email_extras => $report); $c->send_email( $template, { to => [ $report->name ? [ $report->user->email, $report->name ] : $report->user->email ], diff --git a/perllib/FixMyStreet/App/Controller/Reports.pm b/perllib/FixMyStreet/App/Controller/Reports.pm index 813c2052d..ed851f71f 100644 --- a/perllib/FixMyStreet/App/Controller/Reports.pm +++ b/perllib/FixMyStreet/App/Controller/Reports.pm @@ -76,13 +76,12 @@ sub index : Path : Args(0) { $c->stash->{open} = $j->{open}; }; if ($@) { - $c->stash->{message} = _("There was a problem showing the All Reports page. Please try again later."); + my $message = _("There was a problem showing the All Reports page. Please try again later."); if ($c->config->{STAGING_SITE}) { - $c->stash->{message} .= '</p><p>Perhaps the bin/update-all-reports script needs running. Use: bin/update-all-reports</p><p>' + $message .= '</p><p>Perhaps the bin/update-all-reports script needs running. Use: bin/update-all-reports</p><p>' . sprintf(_('The error was: %s'), $@); } - $c->stash->{template} = 'errors/generic.html'; - return; + $c->detach('/page_error_500_internal_error', [ $message ]); } # Down here so that error pages aren't cached. @@ -109,6 +108,8 @@ Show the summary page for a particular ward. sub ward : Path : Args(2) { my ( $self, $c, $body, $ward ) = @_; + $c->forward('/auth/get_csrf_token'); + $c->forward( 'body_check', [ $body ] ); $c->forward( 'ward_check', [ $ward ] ) if $ward; @@ -136,7 +137,7 @@ sub ward : Path : Args(2) { } )->all; @categories = map { $_->category } @categories; $c->stash->{filter_categories} = \@categories; - $c->stash->{filter_category} = [ $c->get_param_list('filter_category', 1) ]; + $c->stash->{filter_category} = { map { $_ => 1 } $c->get_param_list('filter_category', 1) }; my $pins = $c->stash->{pins}; @@ -377,6 +378,25 @@ sub load_and_group_problems : Private { non_public => 0, state => [ keys %$states ] }; + my $filter = { + order_by => $c->stash->{sort_order}, + rows => $c->cobrand->reports_per_page, + }; + + if (defined $c->stash->{filter_status}{shortlisted}) { + $where->{'me.id'} = { '=', \"user_planned_reports.report_id"}; + $where->{'user_planned_reports.removed'} = undef; + $filter->{join} = 'user_planned_reports'; + } elsif (defined $c->stash->{filter_status}{unshortlisted}) { + my $shortlisted_ids = $c->cobrand->problems->search({ + 'me.id' => { '=', \"user_planned_reports.report_id"}, + 'user_planned_reports.removed' => undef, + }, { + join => 'user_planned_reports', + columns => ['me.id'], + })->as_query; + $where->{'me.id'} = { -not_in => $shortlisted_ids }; + } my $not_open = [ FixMyStreet::DB::Result::Problem::fixed_states(), FixMyStreet::DB::Result::Problem::closed_states() ]; if ( $type eq 'new' ) { @@ -410,13 +430,17 @@ sub load_and_group_problems : Private { $problems = $problems->to_body($c->stash->{body}); } + if (my $bbox = $c->get_param('bbox')) { + my ($min_lon, $min_lat, $max_lon, $max_lat) = split /,/, $bbox; + $where->{latitude} = { '>=', $min_lat, '<', $max_lat }; + $where->{longitude} = { '>=', $min_lon, '<', $max_lon }; + } + $problems = $problems->search( $where, - { - order_by => $c->stash->{sort_order}, - rows => $c->cobrand->reports_per_page, - } + $filter )->include_comment_counts->page( $page ); + $c->stash->{pager} = $problems->pager; my ( %problems, @pins ); @@ -499,6 +523,19 @@ sub stash_report_filter_status : Private { %filter_problem_states = %$s; } + if ($status{shortlisted}) { + $filter_status{shortlisted} = 1; + } + + if ($status{unshortlisted}) { + $filter_status{unshortlisted} = 1; + } + + if (keys %filter_problem_states == 0) { + my $s = FixMyStreet::DB::Result::Problem->open_states(); + %filter_problem_states = (%filter_problem_states, %$s); + } + $c->stash->{filter_problem_states} = \%filter_problem_states; $c->stash->{filter_status} = \%filter_status; return 1; @@ -514,13 +551,17 @@ sub stash_report_sort : Private { ); my $sort = $c->get_param('sort') || $default; - $sort = $default unless $sort =~ /^((updated|created)-(desc|asc)|comments-desc)$/; + $sort = $default unless $sort =~ /^((updated|created)-(desc|asc)|comments-desc|shortlist)$/; + $c->stash->{sort_key} = $sort; + + # Going to do this sorting code-side + $sort = 'created-desc' if $sort eq 'shortlist'; + $sort =~ /^(updated|created|comments)-(desc|asc)$/; my $order_by = $types{$1} || $1; my $dir = $2; $order_by = { -desc => $order_by } if $dir eq 'desc'; - $c->stash->{sort_key} = $sort; $c->stash->{sort_order} = $order_by; return 1; } @@ -572,4 +613,3 @@ Licensed under the Affero GPL. __PACKAGE__->meta->make_immutable; 1; - diff --git a/perllib/FixMyStreet/App/Controller/Root.pm b/perllib/FixMyStreet/App/Controller/Root.pm index 3d4c6a1ba..4f098dfc3 100644 --- a/perllib/FixMyStreet/App/Controller/Root.pm +++ b/perllib/FixMyStreet/App/Controller/Root.pm @@ -58,6 +58,7 @@ sub index : Path : Args(0) { return; } + $c->forward('/auth/get_csrf_token'); } =head2 default @@ -103,9 +104,25 @@ sub page_error_410_gone : Private { sub page_error_403_access_denied : Private { my ( $self, $c, $error_msg ) = @_; + $c->detach('page_error', [ $error_msg || _("Sorry, you don't have permission to do that."), 403 ]); +} + +sub page_error_400_bad_request : Private { + my ( $self, $c, $error_msg ) = @_; + $c->forward('/auth/get_csrf_token'); + $c->detach('page_error', [ $error_msg, 400 ]); +} + +sub page_error_500_internal_error : Private { + my ( $self, $c, $error_msg ) = @_; + $c->detach('page_error', [ $error_msg, 500 ]); +} + +sub page_error : Private { + my ($self, $c, $error_msg, $code) = @_; $c->stash->{template} = 'errors/generic.html'; - $c->stash->{message} = $error_msg || _("Sorry, you don't have permission to do that."); - $c->response->status(403); + $c->stash->{message} = $error_msg || _('Unknown error'); + $c->response->status($code); } =head2 end diff --git a/perllib/FixMyStreet/App/Controller/Tokens.pm b/perllib/FixMyStreet/App/Controller/Tokens.pm index da017c57f..a1b0c57ba 100644 --- a/perllib/FixMyStreet/App/Controller/Tokens.pm +++ b/perllib/FixMyStreet/App/Controller/Tokens.pm @@ -348,6 +348,7 @@ sub token_too_old : Private { my ( $self, $c ) = @_; $c->stash->{token_not_found} = 1; $c->stash->{template} = 'auth/token.html'; + $c->response->status(400); } __PACKAGE__->meta->make_immutable; diff --git a/perllib/FixMyStreet/App/Model/EmailSend.pm b/perllib/FixMyStreet/App/Model/EmailSend.pm deleted file mode 100644 index 93751d4a6..000000000 --- a/perllib/FixMyStreet/App/Model/EmailSend.pm +++ /dev/null @@ -1,19 +0,0 @@ -package FixMyStreet::App::Model::EmailSend; -use base 'Catalyst::Model::Factory'; - -use strict; -use warnings; - -=head1 NAME - -FixMyStreet::App::Model::EmailSend - -=head1 DESCRIPTION - -Catalyst Model wrapper around FixMyStreet::EmailSend - -=cut - -__PACKAGE__->config( - class => 'FixMyStreet::EmailSend', -); diff --git a/perllib/FixMyStreet/App/Model/PhotoSet.pm b/perllib/FixMyStreet/App/Model/PhotoSet.pm index 46e1fb630..8fcc1700e 100644 --- a/perllib/FixMyStreet/App/Model/PhotoSet.pm +++ b/perllib/FixMyStreet/App/Model/PhotoSet.pm @@ -67,14 +67,7 @@ has upload_dir => ( is => 'ro', lazy => 1, default => sub { - my $self = shift; - my $cache_dir = path(FixMyStreet->config('UPLOAD_DIR'))->absolute(FixMyStreet->path_to()); - $cache_dir->mkpath; - unless ( -d $cache_dir && -w $cache_dir ) { - warn "Can't find/write to photo cache directory '$cache_dir'"; - return; - } - $cache_dir; + path(FixMyStreet->config('UPLOAD_DIR'))->absolute(FixMyStreet->path_to()); }, ); @@ -191,12 +184,7 @@ has ids => ( # Arrayref of $fileid tuples (always, so post upload/raw data proc $type ||= 'jpeg'; if ($fileid && length($fileid) == 40) { my $file = $self->get_file($fileid, $type); - if ($file->exists) { - $file->basename; - } else { - warn "File $part doesn't exist"; - (); - } + $file->basename; } else { # A bad hash, probably a bot spamming with bad data. (); diff --git a/perllib/FixMyStreet/App/Response.pm b/perllib/FixMyStreet/App/Response.pm new file mode 100644 index 000000000..16ebf995f --- /dev/null +++ b/perllib/FixMyStreet/App/Response.pm @@ -0,0 +1,27 @@ +# This package exists to try and work around a big bug in Edge: +# https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8572187/ + +package FixMyStreet::App::Response; +use Moose; +extends 'Catalyst::Response'; + +around 'redirect' => sub { + my $orig = shift; + my $self = shift; + my ($location, $status) = @_; + + return $self->$orig() unless @_; # getter + + my $agent = $self->_context->request->user_agent; + return $self->$orig(@_) unless $agent =~ /Edge\/14/; # Only care about Edge + + # Instead of a redirect, output HTML that redirects + $self->body(<<END +<meta http-equiv="refresh" content="0; url=$location"> +Please follow this link: <a href="$location">$location</a> +END + ); + return $location; +}; + +1; diff --git a/perllib/FixMyStreet/App/View/Web.pm b/perllib/FixMyStreet/App/View/Web.pm index f0bcad0be..496463700 100644 --- a/perllib/FixMyStreet/App/View/Web.pm +++ b/perllib/FixMyStreet/App/View/Web.pm @@ -140,21 +140,22 @@ sub escape_js { my %version_hash; sub version { - my ( $self, $c, $file ) = @_; + my ( $self, $c, $file, $url ) = @_; + $url ||= $file; _version_get_mtime($file); if ($version_hash{$file} && $file =~ /\.js$/) { # See if there's an auto.min.js version and use that instead if there is (my $file_min = $file) =~ s/\.js$/.auto.min.js/; _version_get_mtime($file_min); - $file = $file_min if $version_hash{$file_min} >= $version_hash{$file}; + $url = $file = $file_min if $version_hash{$file_min} >= $version_hash{$file}; } my $admin = $self->template->context->stash->{admin} ? FixMyStreet->config('ADMIN_BASE_URL') : ''; - return "$admin$file?$version_hash{$file}"; + return "$admin$url?$version_hash{$file}"; } sub _version_get_mtime { my $file = shift; - unless ($version_hash{$file} && !FixMyStreet->config('STAGING_SITE')) { + unless (defined $version_hash{$file} && !FixMyStreet->config('STAGING_SITE')) { my $path = FixMyStreet->path_to('web', $file); $version_hash{$file} = ( stat( $path ) )[9] || 0; } |